diff --git a/Makefile b/Makefile
index c0c7cadf0..3721bf1c4 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ openapi-generator-cli:=java -jar $(openapi-generator-jar)
generator:=java
library:=okhttp-gson
-services:=payments
+services:=payments checkout
models:=src/main/java/com/adyen/model
output:=target/out
@@ -41,11 +41,11 @@ $(services): target/spec $(openapi-generator-jar)
--skip-validate-spec \
--model-package $(subst /,.,com.adyen.model.$@) \
--library $(library) \
- --global-property models \
--global-property modelDocs=false \
--global-property modelTests=false \
--additional-properties=dateLibrary=legacy
mv $(output)/$(models)/$@ $(models)/$@
+ mv $(output)/$(models)/JSON.java $(models)/$@
# Checkout spec (and patch version)
diff --git a/README.md b/README.md
index b3b742ee9..e6c0419b6 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ You can use Maven and add this dependency to your project's POM:
com.adyen
adyen-java-api-library
- 18.1.1
+ 18.1.2
```
diff --git a/pom.xml b/pom.xml
index f96e276e6..96760ae50 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
com.adyen
adyen-java-api-library
jar
- 18.1.1
+ 18.1.2
Adyen Java API Library
Adyen API Client Library for Java
https://github.com/adyen/adyen-java-api-library
@@ -25,7 +25,7 @@
UTF-8
UTF-8
- 1.6.5
+ 1.6.8
scm:git:git@github.com:Adyen/adyen-java-api-library.git
@@ -191,7 +191,7 @@
com.fasterxml.jackson.core
jackson-databind
- 2.13.3
+ 2.13.4.2
org.apache.httpcomponents.client5
@@ -224,12 +224,27 @@
io.swagger.core.v3
swagger-models
- 2.2.2
+ 2.2.4
io.swagger.core.v3
swagger-annotations
- 2.2.2
+ 2.2.4
+
+
+ javax.ws.rs
+ javax.ws.rs-api
+ 2.1.1
+
+
+ io.gsonfire
+ gson-fire
+ 1.8.5
+
+
+ com.squareup.okio
+ okio
+ 3.2.0
diff --git a/src/main/java/com/adyen/Client.java b/src/main/java/com/adyen/Client.java
index d960aeb0b..c2fd16420 100644
--- a/src/main/java/com/adyen/Client.java
+++ b/src/main/java/com/adyen/Client.java
@@ -47,7 +47,7 @@ public class Client {
public static final String MARKETPAY_NOTIFICATION_API_VERSION = "v6";
public static final String MARKETPAY_HOP_API_VERSION = "v6";
public static final String LIB_NAME = "adyen-java-api-library";
- public static final String LIB_VERSION = "18.1.1";
+ public static final String LIB_VERSION = "18.1.2";
public static final String CHECKOUT_ENDPOINT_TEST = "https://checkout-test.adyen.com/checkout";
public static final String CHECKOUT_ENDPOINT_LIVE_SUFFIX = "-checkout-live.adyenpayments.com/checkout";
public static final String CHECKOUT_ENDPOINT_CERT_LIVE = "https://checkoutcert-live-%s.adyen.com/checkout";
diff --git a/src/main/java/com/adyen/model/ThreeDSecureData.java b/src/main/java/com/adyen/model/ThreeDSecureData.java
index ba35ab0b2..b4e445d0b 100644
--- a/src/main/java/com/adyen/model/ThreeDSecureData.java
+++ b/src/main/java/com/adyen/model/ThreeDSecureData.java
@@ -20,6 +20,7 @@
*/
package com.adyen.model;
+import com.adyen.serializer.ByteArrayToStringAdapter;
import com.fasterxml.jackson.annotation.JsonValue;
import com.google.gson.TypeAdapter;
import com.google.gson.annotations.JsonAdapter;
@@ -84,6 +85,7 @@ public AuthenticationResponseEnum read(final JsonReader jsonReader) throws IOExc
private AuthenticationResponseEnum authenticationResponse = null;
@SerializedName("cavv")
+ @JsonAdapter(ByteArrayToStringAdapter.class)
private byte[] cavv = null;
@SerializedName("cavvAlgorithm")
@@ -203,12 +205,14 @@ public DirectoryResponseEnum read(final JsonReader jsonReader) throws IOExceptio
private String threeDSVersion = null;
@SerializedName("tokenAuthenticationVerificationValue")
+ @JsonAdapter(ByteArrayToStringAdapter.class)
private byte[] tokenAuthenticationVerificationValue = null;
@SerializedName("transStatusReason")
private String transStatusReason = null;
@SerializedName("xid")
+ @JsonAdapter(ByteArrayToStringAdapter.class)
private byte[] xid = null;
public ThreeDSecureData authenticationResponse(AuthenticationResponseEnum authenticationResponse) {
diff --git a/src/main/java/com/adyen/terminal/security/NexoCrypto.java b/src/main/java/com/adyen/terminal/security/NexoCrypto.java
index 42342cbdd..de3236614 100644
--- a/src/main/java/com/adyen/terminal/security/NexoCrypto.java
+++ b/src/main/java/com/adyen/terminal/security/NexoCrypto.java
@@ -41,12 +41,16 @@
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-import java.util.Random;
+import java.security.SecureRandom;
+import java.security.Provider;
import static com.adyen.model.terminal.security.NexoDerivedKey.NEXO_IV_LENGTH;
public class NexoCrypto {
+ private static SecureRandom secureRandom = new SecureRandom();
+ private static final Provider PROVIDER = secureRandom.getProvider();
+
public SaleToPOISecuredMessage encrypt(
String saleToPoiMessageJson, MessageHeader messageHeader, SecurityKey securityKey) throws Exception {
validateSecurityKey(securityKey);
@@ -145,11 +149,16 @@ private void validateHmac(byte[] receivedHmac, byte[] decryptedMessage, NexoDeri
}
/**
- * Generate a random iv nonce
+ * Generate a random iv nonce with cryptographically strongest non blocking RNG
*/
private byte[] generateRandomIvNonce() {
byte[] ivNonce = new byte[NEXO_IV_LENGTH];
- new Random().nextBytes(ivNonce);
+ try {
+ secureRandom = SecureRandom.getInstance("NativePRNGNonBlocking", PROVIDER);
+ } catch (Exception NoSuchAlgorithmException) {
+ secureRandom = new SecureRandom();
+ }
+ secureRandom.nextBytes(ivNonce);
return ivNonce;
}
-}
+}
\ No newline at end of file
diff --git a/src/test/java/com/adyen/PaymentTest.java b/src/test/java/com/adyen/PaymentTest.java
index 32b6bd8c6..6dde271fb 100644
--- a/src/test/java/com/adyen/PaymentTest.java
+++ b/src/test/java/com/adyen/PaymentTest.java
@@ -20,22 +20,13 @@
*/
package com.adyen;
+import com.adyen.constants.ApiConstants;
import com.adyen.constants.ApiConstants.AdditionalData;
import com.adyen.constants.ApiConstants.RefusalReason;
import com.adyen.httpclient.AdyenHttpClient;
+import com.adyen.httpclient.ClientInterface;
import com.adyen.httpclient.HTTPClientException;
-import com.adyen.model.Address;
-import com.adyen.model.AuthenticationResultRequest;
-import com.adyen.model.AuthenticationResultResponse;
-import com.adyen.model.FraudCheckResult;
-import com.adyen.model.Name;
-import com.adyen.model.PaymentRequest;
-import com.adyen.model.PaymentRequest3d;
-import com.adyen.model.PaymentRequest3ds2;
-import com.adyen.model.PaymentResult;
-import com.adyen.model.RequestOptions;
-import com.adyen.model.ThreeDS2ResultRequest;
-import com.adyen.model.ThreeDS2ResultResponse;
+import com.adyen.model.*;
import com.adyen.model.applicationinfo.ApplicationInfo;
import com.adyen.model.applicationinfo.MerchantDevice;
import com.adyen.service.Payment;
@@ -43,6 +34,7 @@
import org.junit.Test;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.HashMap;
@@ -51,18 +43,12 @@
import static com.adyen.constants.ApiConstants.SelectedBrand.BOLETO_SANTANDER;
import static com.adyen.model.PaymentResult.ResultCodeEnum.RECEIVED;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
/**
* Tests for /authorise and /authorise3d
@@ -424,4 +410,18 @@ public void TestGetAuthenticationResultErrorNotAllowed() throws IOException {
assertEquals(403, e.getError().getStatus());
}
}
+
+ @Test
+ public void TestByteArrayToJSONString() throws Exception {
+ Client client = createMockClientFromFile("mocks/authorise-success.json");
+ Payment payment = new Payment(client);
+ PaymentRequest paymentRequest = new PaymentRequest();
+ paymentRequest.mpiData(new ThreeDSecureData().cavv("AQIDBAUGBwgJCgsMDQ4PEBESExQ=".getBytes()));
+
+ payment.authorise(paymentRequest);
+
+ String expected = "\"mpiData\":{\"cavv\":\"AQIDBAUGBwgJCgsMDQ4PEBESExQ=\"}";
+ ClientInterface http = client.getHttpClient();
+ verify(http).request(anyString(), contains(expected), any(), eq(false), isNull(), any());
+ }
}
diff --git a/templates/libraries/okhttp-gson/AbstractOpenApiSchema.mustache b/templates/libraries/okhttp-gson/AbstractOpenApiSchema.mustache
new file mode 100644
index 000000000..0f85519ea
--- /dev/null
+++ b/templates/libraries/okhttp-gson/AbstractOpenApiSchema.mustache
@@ -0,0 +1,134 @@
+{{>licenseInfo}}
+
+package {{modelPackage}};
+
+import java.util.Objects;
+import java.lang.reflect.Type;
+import java.util.Map;
+import javax.ws.rs.core.GenericType;
+
+/**
+ * Abstract class for oneOf,anyOf schemas defined in OpenAPI spec
+ */
+public abstract class AbstractOpenApiSchema {
+
+ // store the actual instance of the schema/object
+ private Object instance;
+
+ // is nullable
+ private Boolean isNullable;
+
+ // schema type (e.g. oneOf, anyOf)
+ private final String schemaType;
+
+ public AbstractOpenApiSchema(String schemaType, Boolean isNullable) {
+ this.schemaType = schemaType;
+ this.isNullable = isNullable;
+ }
+
+ /**
+ * Get the list of oneOf/anyOf composed schemas allowed to be stored in this object
+ *
+ * @return an instance of the actual schema/object
+ */
+ public abstract Map getSchemas();
+
+ /**
+ * Get the actual instance
+ *
+ * @return an instance of the actual schema/object
+ */
+ //@JsonValue
+ public Object getActualInstance() {return instance;}
+
+ /**
+ * Set the actual instance
+ *
+ * @param instance the actual instance of the schema/object
+ */
+ public void setActualInstance(Object instance) {this.instance = instance;}
+
+ /**
+ * Get the instant recursively when the schemas defined in oneOf/anyof happen to be oneOf/anyOf schema as well
+ *
+ * @return an instance of the actual schema/object
+ */
+ public Object getActualInstanceRecursively() {
+ return getActualInstanceRecursively(this);
+ }
+
+ private Object getActualInstanceRecursively(AbstractOpenApiSchema object) {
+ if (object.getActualInstance() == null) {
+ return null;
+ } else if (object.getActualInstance() instanceof AbstractOpenApiSchema) {
+ return getActualInstanceRecursively((AbstractOpenApiSchema)object.getActualInstance());
+ } else {
+ return object.getActualInstance();
+ }
+ }
+
+ /**
+ * Get the schema type (e.g. anyOf, oneOf)
+ *
+ * @return the schema type
+ */
+ public String getSchemaType() {
+ return schemaType;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ").append(getClass()).append(" {\n");
+ sb.append(" instance: ").append(toIndentedString(instance)).append("\n");
+ sb.append(" isNullable: ").append(toIndentedString(isNullable)).append("\n");
+ sb.append(" schemaType: ").append(toIndentedString(schemaType)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ AbstractOpenApiSchema a = (AbstractOpenApiSchema) o;
+ return Objects.equals(this.instance, a.instance) &&
+ Objects.equals(this.isNullable, a.isNullable) &&
+ Objects.equals(this.schemaType, a.schemaType);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(instance, isNullable, schemaType);
+ }
+
+ /**
+ * Is nullable
+ *
+ * @return true if it's nullable
+ */
+ public Boolean isNullable() {
+ if (Boolean.TRUE.equals(isNullable)) {
+ return Boolean.TRUE;
+ } else {
+ return Boolean.FALSE;
+ }
+ }
+
+{{>libraries/jersey2/additional_properties}}
+
+}
diff --git a/templates/libraries/okhttp-gson/JSON.mustache b/templates/libraries/okhttp-gson/JSON.mustache
new file mode 100644
index 000000000..437b75f86
--- /dev/null
+++ b/templates/libraries/okhttp-gson/JSON.mustache
@@ -0,0 +1,534 @@
+{{>licenseInfo}}
+
+package {{modelPackage}};
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.internal.bind.util.ISO8601Utils;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import com.google.gson.JsonElement;
+import io.gsonfire.GsonFireBuilder;
+import io.gsonfire.TypeSelector;
+{{#joda}}
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.DateTimeFormatterBuilder;
+import org.joda.time.format.ISODateTimeFormat;
+{{/joda}}
+
+import okio.ByteString;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Type;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Map;
+import java.util.HashMap;
+
+/*
+ * A JSON utility class
+ *
+ * NOTE: in the future, this class may be converted to static, which may break
+ * backward-compatibility
+ */
+public class JSON {
+ private static Gson gson;
+ private static boolean isLenientOnJson = false;
+ private static DateTypeAdapter dateTypeAdapter = new DateTypeAdapter();
+ private static SqlDateTypeAdapter sqlDateTypeAdapter = new SqlDateTypeAdapter();
+ {{#joda}}
+ private static DateTimeTypeAdapter dateTimeTypeAdapter = new DateTimeTypeAdapter();
+ private static LocalDateTypeAdapter localDateTypeAdapter = new LocalDateTypeAdapter();
+ {{/joda}}
+ {{#jsr310}}
+ private static OffsetDateTimeTypeAdapter offsetDateTimeTypeAdapter = new OffsetDateTimeTypeAdapter();
+ private static LocalDateTypeAdapter localDateTypeAdapter = new LocalDateTypeAdapter();
+ {{/jsr310}}
+ private static ByteArrayAdapter byteArrayAdapter = new ByteArrayAdapter();
+
+ @SuppressWarnings("unchecked")
+ public static GsonBuilder createGson() {
+ GsonFireBuilder fireBuilder = new GsonFireBuilder()
+ {{#models}}
+ {{#model}}
+ {{#discriminator}}
+ .registerTypeSelector({{modelPackage}}.{{classname}}.class, new TypeSelector<{{modelPackage}}.{{classname}}>() {
+ @Override
+ public Class extends {{modelPackage}}.{{classname}}> getClassForElement(JsonElement readElement) {
+ Map classByDiscriminatorValue = new HashMap();
+ {{#mappedModels}}
+ classByDiscriminatorValue.put("{{mappingName}}"{{^discriminatorCaseSensitive}}.toUpperCase(Locale.ROOT){{/discriminatorCaseSensitive}}, {{modelPackage}}.{{modelName}}.class);
+ {{/mappedModels}}
+ classByDiscriminatorValue.put("{{name}}"{{^discriminatorCaseSensitive}}.toUpperCase(Locale.ROOT){{/discriminatorCaseSensitive}}, {{modelPackage}}.{{classname}}.class);
+ return getClassByDiscriminator(classByDiscriminatorValue,
+ getDiscriminatorValue(readElement, "{{{propertyBaseName}}}"));
+ }
+ })
+ {{/discriminator}}
+ {{/model}}
+ {{/models}}
+ ;
+ GsonBuilder builder = fireBuilder.createGsonBuilder();
+ {{#disableHtmlEscaping}}
+ builder.disableHtmlEscaping();
+ {{/disableHtmlEscaping}}
+ return builder;
+ }
+
+ private static String getDiscriminatorValue(JsonElement readElement, String discriminatorField) {
+ JsonElement element = readElement.getAsJsonObject().get(discriminatorField);
+ if (null == element) {
+ throw new IllegalArgumentException("missing discriminator field: <" + discriminatorField + ">");
+ }
+ return element.getAsString();
+ }
+
+ /**
+ * Returns the Java class that implements the OpenAPI schema for the specified discriminator value.
+ *
+ * @param classByDiscriminatorValue The map of discriminator values to Java classes.
+ * @param discriminatorValue The value of the OpenAPI discriminator in the input data.
+ * @return The Java class that implements the OpenAPI schema
+ */
+ private static Class getClassByDiscriminator(Map classByDiscriminatorValue, String discriminatorValue) {
+ Class clazz = (Class) classByDiscriminatorValue.get(discriminatorValue{{^discriminatorCaseSensitive}}.toUpperCase(Locale.ROOT){{/discriminatorCaseSensitive}});
+ if (null == clazz) {
+ throw new IllegalArgumentException("cannot determine model class of name: <" + discriminatorValue + ">");
+ }
+ return clazz;
+ }
+
+ {
+ GsonBuilder gsonBuilder = createGson();
+ gsonBuilder.registerTypeAdapter(Date.class, dateTypeAdapter);
+ gsonBuilder.registerTypeAdapter(java.sql.Date.class, sqlDateTypeAdapter);
+ {{#joda}}
+ gsonBuilder.registerTypeAdapter(DateTime.class, dateTimeTypeAdapter);
+ gsonBuilder.registerTypeAdapter(LocalDate.class, localDateTypeAdapter);
+ {{/joda}}
+ {{#jsr310}}
+ gsonBuilder.registerTypeAdapter(OffsetDateTime.class, offsetDateTimeTypeAdapter);
+ gsonBuilder.registerTypeAdapter(LocalDate.class, localDateTypeAdapter);
+ {{/jsr310}}
+ gsonBuilder.registerTypeAdapter(byte[].class, byteArrayAdapter);
+ {{#models}}
+ {{#model}}
+ {{^isEnum}}
+ {{^hasChildren}}
+ gsonBuilder.registerTypeAdapterFactory(new {{modelPackage}}.{{{classname}}}.CustomTypeAdapterFactory());
+ {{/hasChildren}}
+ {{/isEnum}}
+ {{/model}}
+ {{/models}}
+ gson = gsonBuilder.create();
+ }
+
+ /**
+ * Get Gson.
+ *
+ * @return Gson
+ */
+ public static Gson getGson() {
+ return gson;
+ }
+
+ /**
+ * Set Gson.
+ *
+ * @param gson Gson
+ */
+ public static void setGson(Gson gson) {
+ JSON.gson = gson;
+ }
+
+ public static void setLenientOnJson(boolean lenientOnJson) {
+ isLenientOnJson = lenientOnJson;
+ }
+
+ /**
+ * Serialize the given Java object into JSON string.
+ *
+ * @param obj Object
+ * @return String representation of the JSON
+ */
+ public static String serialize(Object obj) {
+ return gson.toJson(obj);
+ }
+
+ /**
+ * Deserialize the given JSON string to Java object.
+ *
+ * @param Type
+ * @param body The JSON string
+ * @param returnType The type to deserialize into
+ * @return The deserialized Java object
+ */
+ @SuppressWarnings("unchecked")
+ public static T deserialize(String body, Type returnType) {
+ try {
+ if (isLenientOnJson) {
+ JsonReader jsonReader = new JsonReader(new StringReader(body));
+ // see https://google-gson.googlecode.com/svn/trunk/gson/docs/javadocs/com/google/gson/stream/JsonReader.html#setLenient(boolean)
+ jsonReader.setLenient(true);
+ return gson.fromJson(jsonReader, returnType);
+ } else {
+ return gson.fromJson(body, returnType);
+ }
+ } catch (JsonParseException e) {
+ // Fallback processing when failed to parse JSON form response body:
+ // return the response body string directly for the String return type;
+ if (returnType.equals(String.class)) {
+ return (T) body;
+ } else {
+ throw (e);
+ }
+ }
+ }
+
+ /**
+ * Gson TypeAdapter for Byte Array type
+ */
+ public static class ByteArrayAdapter extends TypeAdapter {
+
+ @Override
+ public void write(JsonWriter out, byte[] value) throws IOException {
+ if (value == null) {
+ out.nullValue();
+ } else {
+ out.value(ByteString.of(value).base64());
+ }
+ }
+
+ @Override
+ public byte[] read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String bytesAsBase64 = in.nextString();
+ ByteString byteString = ByteString.decodeBase64(bytesAsBase64);
+ return byteString.toByteArray();
+ }
+ }
+ }
+
+ {{#joda}}
+ /**
+ * Gson TypeAdapter for Joda DateTime type
+ */
+ public static class DateTimeTypeAdapter extends TypeAdapter {
+
+ private DateTimeFormatter formatter;
+
+ public DateTimeTypeAdapter() {
+ this(new DateTimeFormatterBuilder()
+ .append(ISODateTimeFormat.dateTime().getPrinter(), ISODateTimeFormat.dateOptionalTimeParser().getParser())
+ .toFormatter());
+ }
+
+ public DateTimeTypeAdapter(DateTimeFormatter formatter) {
+ this.formatter = formatter;
+ }
+
+ public void setFormat(DateTimeFormatter dateFormat) {
+ this.formatter = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, DateTime date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ out.value(formatter.print(date));
+ }
+ }
+
+ @Override
+ public DateTime read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ return formatter.parseDateTime(date);
+ }
+ }
+ }
+
+ /**
+ * Gson TypeAdapter for Joda LocalDate type
+ */
+ public static class LocalDateTypeAdapter extends TypeAdapter {
+
+ private DateTimeFormatter formatter;
+
+ public LocalDateTypeAdapter() {
+ this(ISODateTimeFormat.date());
+ }
+
+ public LocalDateTypeAdapter(DateTimeFormatter formatter) {
+ this.formatter = formatter;
+ }
+
+ public void setFormat(DateTimeFormatter dateFormat) {
+ this.formatter = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, LocalDate date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ out.value(formatter.print(date));
+ }
+ }
+
+ @Override
+ public LocalDate read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ return formatter.parseLocalDate(date);
+ }
+ }
+ }
+
+ public static void setDateTimeFormat(DateTimeFormatter dateFormat) {
+ dateTimeTypeAdapter.setFormat(dateFormat);
+ }
+
+ public static void setLocalDateFormat(DateTimeFormatter dateFormat) {
+ localDateTypeAdapter.setFormat(dateFormat);
+ }
+
+ {{/joda}}
+ {{#jsr310}}
+ /**
+ * Gson TypeAdapter for JSR310 OffsetDateTime type
+ */
+ public static class OffsetDateTimeTypeAdapter extends TypeAdapter {
+
+ private DateTimeFormatter formatter;
+
+ public OffsetDateTimeTypeAdapter() {
+ this(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+ }
+
+ public OffsetDateTimeTypeAdapter(DateTimeFormatter formatter) {
+ this.formatter = formatter;
+ }
+
+ public void setFormat(DateTimeFormatter dateFormat) {
+ this.formatter = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, OffsetDateTime date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ out.value(formatter.format(date));
+ }
+ }
+
+ @Override
+ public OffsetDateTime read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ if (date.endsWith("+0000")) {
+ date = date.substring(0, date.length()-5) + "Z";
+ }
+ return OffsetDateTime.parse(date, formatter);
+ }
+ }
+ }
+
+ /**
+ * Gson TypeAdapter for JSR310 LocalDate type
+ */
+ public static class LocalDateTypeAdapter extends TypeAdapter {
+
+ private DateTimeFormatter formatter;
+
+ public LocalDateTypeAdapter() {
+ this(DateTimeFormatter.ISO_LOCAL_DATE);
+ }
+
+ public LocalDateTypeAdapter(DateTimeFormatter formatter) {
+ this.formatter = formatter;
+ }
+
+ public void setFormat(DateTimeFormatter dateFormat) {
+ this.formatter = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, LocalDate date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ out.value(formatter.format(date));
+ }
+ }
+
+ @Override
+ public LocalDate read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ return LocalDate.parse(date, formatter);
+ }
+ }
+ }
+
+ public static void setOffsetDateTimeFormat(DateTimeFormatter dateFormat) {
+ offsetDateTimeTypeAdapter.setFormat(dateFormat);
+ }
+
+ public static void setLocalDateFormat(DateTimeFormatter dateFormat) {
+ localDateTypeAdapter.setFormat(dateFormat);
+ }
+
+ {{/jsr310}}
+ /**
+ * Gson TypeAdapter for java.sql.Date type
+ * If the dateFormat is null, a simple "yyyy-MM-dd" format will be used
+ * (more efficient than SimpleDateFormat).
+ */
+ public static class SqlDateTypeAdapter extends TypeAdapter {
+
+ private DateFormat dateFormat;
+
+ public SqlDateTypeAdapter() {}
+
+ public SqlDateTypeAdapter(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ public void setFormat(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, java.sql.Date date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ String value;
+ if (dateFormat != null) {
+ value = dateFormat.format(date);
+ } else {
+ value = date.toString();
+ }
+ out.value(value);
+ }
+ }
+
+ @Override
+ public java.sql.Date read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ try {
+ if (dateFormat != null) {
+ return new java.sql.Date(dateFormat.parse(date).getTime());
+ }
+ return new java.sql.Date(ISO8601Utils.parse(date, new ParsePosition(0)).getTime());
+ } catch (ParseException e) {
+ throw new JsonParseException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Gson TypeAdapter for java.util.Date type
+ * If the dateFormat is null, ISO8601Utils will be used.
+ */
+ public static class DateTypeAdapter extends TypeAdapter {
+
+ private DateFormat dateFormat;
+
+ public DateTypeAdapter() {}
+
+ public DateTypeAdapter(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ public void setFormat(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, Date date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ String value;
+ if (dateFormat != null) {
+ value = dateFormat.format(date);
+ } else {
+ value = ISO8601Utils.format(date, true);
+ }
+ out.value(value);
+ }
+ }
+
+ @Override
+ public Date read(JsonReader in) throws IOException {
+ try {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ try {
+ if (dateFormat != null) {
+ return dateFormat.parse(date);
+ }
+ return ISO8601Utils.parse(date, new ParsePosition(0));
+ } catch (ParseException e) {
+ throw new JsonParseException(e);
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ throw new JsonParseException(e);
+ }
+ }
+ }
+
+ public static void setDateFormat(DateFormat dateFormat) {
+ dateTypeAdapter.setFormat(dateFormat);
+ }
+
+ public static void setSqlDateFormat(DateFormat dateFormat) {
+ sqlDateTypeAdapter.setFormat(dateFormat);
+ }
+}
diff --git a/templates/libraries/okhttp-gson/oneof_model.mustache b/templates/libraries/okhttp-gson/oneof_model.mustache
new file mode 100644
index 000000000..8748d06be
--- /dev/null
+++ b/templates/libraries/okhttp-gson/oneof_model.mustache
@@ -0,0 +1,250 @@
+import javax.ws.rs.core.GenericType;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+
+import {{package}}.JSON;
+
+{{>additionalModelTypeAnnotations}}{{>xmlAnnotation}}
+public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-implements}}, {{{.}}}{{/vendorExtensions.x-implements}} {
+ private static final Logger log = Logger.getLogger({{classname}}.class.getName());
+
+ public static class CustomTypeAdapterFactory implements TypeAdapterFactory {
+ @SuppressWarnings("unchecked")
+ @Override
+ public TypeAdapter create(Gson gson, TypeToken type) {
+ if (!{{classname}}.class.isAssignableFrom(type.getRawType())) {
+ return null; // this class only serializes '{{classname}}' and its subtypes
+ }
+ final TypeAdapter elementAdapter = gson.getAdapter(JsonElement.class);
+ {{#oneOf}}
+ final TypeAdapter<{{.}}> adapter{{.}} = gson.getDelegateAdapter(this, TypeToken.get({{.}}.class));
+ {{/oneOf}}
+
+ return (TypeAdapter) new TypeAdapter<{{classname}}>() {
+ @Override
+ public void write(JsonWriter out, {{classname}} value) throws IOException {
+ if (value == null || value.getActualInstance() == null) {
+ elementAdapter.write(out, null);
+ return;
+ }
+
+ {{#oneOf}}
+ // check if the actual instance is of the type `{{.}}`
+ if (value.getActualInstance() instanceof {{.}}) {
+ JsonObject obj = adapter{{.}}.toJsonTree(({{.}})value.getActualInstance()).getAsJsonObject();
+ elementAdapter.write(out, obj);
+ return;
+ }
+
+ {{/oneOf}}
+ throw new IOException("Failed to serialize as the type doesn't match oneOf schemas: {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}");
+ }
+
+ @Override
+ public {{classname}} read(JsonReader in) throws IOException {
+ Object deserialized = null;
+ JsonObject jsonObject = elementAdapter.read(in).getAsJsonObject();
+
+ {{#useOneOfDiscriminatorLookup}}
+ {{#discriminator}}
+ // use discriminator value for faster oneOf lookup
+ {{classname}} new{{classname}} = new {{classname}}();
+ if (jsonObject.get("{{{propertyBaseName}}}") == null) {
+ log.log(Level.WARNING, "Failed to lookup discriminator value for {{classname}} as `{{{propertyBaseName}}}` was not found in the payload or the payload is empty.");
+ } else {
+ // look up the discriminator value in the field `{{{propertyBaseName}}}`
+ switch (jsonObject.get("{{{propertyBaseName}}}").getAsString()) {
+ {{#mappedModels}}
+ case "{{{mappingName}}}":
+ deserialized = adapter{{modelName}}.fromJsonTree(jsonObject);
+ new{{classname}}.setActualInstance(deserialized);
+ return new{{classname}};
+ {{/mappedModels}}
+ default:
+ log.log(Level.WARNING, String.format("Failed to lookup discriminator value `%s` for {{classname}}. Possible values:{{#mappedModels}} {{{mappingName}}}{{/mappedModels}}", jsonObject.get("{{{propertyBaseName}}}").getAsString()));
+ }
+ }
+
+ {{/discriminator}}
+ {{/useOneOfDiscriminatorLookup}}
+ int match = 0;
+ ArrayList errorMessages = new ArrayList<>();
+ TypeAdapter actualAdapter = elementAdapter;
+
+ {{#oneOf}}
+ // deserialize {{{.}}}
+ try {
+ // validate the JSON object to see if any exception is thrown
+ {{.}}.validateJsonObject(jsonObject);
+ actualAdapter = adapter{{.}};
+ match++;
+ log.log(Level.FINER, "Input data matches schema '{{{.}}}'");
+ } catch (Exception e) {
+ // deserialization failed, continue
+ errorMessages.add(String.format("Deserialization for {{{.}}} failed with `%s`.", e.getMessage()));
+ log.log(Level.FINER, "Input data does not match schema '{{{.}}}'", e);
+ }
+
+ {{/oneOf}}
+ if (match == 1) {
+ {{classname}} ret = new {{classname}}();
+ ret.setActualInstance(actualAdapter.fromJsonTree(jsonObject));
+ return ret;
+ }
+
+ throw new IOException(String.format("Failed deserialization for {{classname}}: %d classes match result, expected 1. Detailed failure message for oneOf schemas: %s. JSON: %s", match, errorMessages, jsonObject.toString()));
+ }
+ }.nullSafe();
+ }
+ }
+
+ // store a list of schema names defined in oneOf
+ public static final Map schemas = new HashMap();
+
+ public {{classname}}() {
+ super("oneOf", {{#isNullable}}Boolean.TRUE{{/isNullable}}{{^isNullable}}Boolean.FALSE{{/isNullable}});
+ }
+
+ {{#oneOf}}
+ public {{classname}}({{{.}}} o) {
+ super("oneOf", {{#isNullable}}Boolean.TRUE{{/isNullable}}{{^isNullable}}Boolean.FALSE{{/isNullable}});
+ setActualInstance(o);
+ }
+
+ {{/oneOf}}
+ static {
+ {{#oneOf}}
+ schemas.put("{{{.}}}", new GenericType<{{{.}}}>() {
+ });
+ {{/oneOf}}
+ }
+
+ @Override
+ public Map getSchemas() {
+ return {{classname}}.schemas;
+ }
+
+ /**
+ * Set the instance that matches the oneOf child schema, check
+ * the instance parameter is valid against the oneOf child schemas:
+ * {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}
+ *
+ * It could be an instance of the 'oneOf' schemas.
+ * The oneOf child schemas may themselves be a composed schema (allOf, anyOf, oneOf).
+ */
+ @Override
+ public void setActualInstance(Object instance) {
+ {{#isNullable}}
+ if (instance == null) {
+ super.setActualInstance(instance);
+ return;
+ }
+
+ {{/isNullable}}
+ {{#oneOf}}
+ if (instance instanceof {{{.}}}) {
+ super.setActualInstance(instance);
+ return;
+ }
+
+ {{/oneOf}}
+ throw new RuntimeException("Invalid instance type. Must be {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}");
+ }
+
+ /**
+ * Get the actual instance, which can be the following:
+ * {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}
+ *
+ * @return The actual instance ({{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}})
+ */
+ @Override
+ public Object getActualInstance() {
+ return super.getActualInstance();
+ }
+
+ {{#oneOf}}
+ /**
+ * Get the actual instance of `{{{.}}}`. If the actual instance is not `{{{.}}}`,
+ * the ClassCastException will be thrown.
+ *
+ * @return The actual instance of `{{{.}}}`
+ * @throws ClassCastException if the instance is not `{{{.}}}`
+ */
+ public {{{.}}} get{{{.}}}() throws ClassCastException {
+ return ({{{.}}})super.getActualInstance();
+ }
+
+ {{/oneOf}}
+
+ /**
+ * Validates the JSON Object and throws an exception if issues found
+ *
+ * @param jsonObj JSON Object
+ * @throws IOException if the JSON Object is invalid with respect to {{classname}}
+ */
+ public static void validateJsonObject(JsonObject jsonObj) throws IOException {
+ // validate oneOf schemas one by one
+ int validCount = 0;
+ ArrayList errorMessages = new ArrayList<>();
+ {{#oneOf}}
+ // validate the json string with {{{.}}}
+ try {
+ {{{.}}}.validateJsonObject(jsonObj);
+ validCount++;
+ } catch (Exception e) {
+ errorMessages.add(String.format("Deserialization for {{{.}}} failed with `%s`.", e.getMessage()));
+ // continue to the next one
+ }
+ {{/oneOf}}
+ if (validCount != 1) {
+ throw new IOException(String.format("The JSON string is invalid for {{classname}} with oneOf schemas: {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}. %d class(es) match the result, expected 1. Detailed failure message for oneOf schemas: %s. JSON: %s", validCount, errorMessages, jsonObj.toString()));
+ }
+ }
+
+ /**
+ * Create an instance of {{classname}} given an JSON string
+ *
+ * @param jsonString JSON string
+ * @return An instance of {{classname}}
+ * @throws IOException if the JSON string is invalid with respect to {{classname}}
+ */
+ public static {{{classname}}} fromJson(String jsonString) throws IOException {
+ return JSON.getGson().fromJson(jsonString, {{{classname}}}.class);
+ }
+
+ /**
+ * Convert an instance of {{classname}} to an JSON string
+ *
+ * @return JSON string
+ */
+ public String toJson() {
+ return JSON.getGson().toJson(this);
+ }
+}
diff --git a/templates/libraries/okhttp-gson/pojo.mustache b/templates/libraries/okhttp-gson/pojo.mustache
index 8ae82cc72..eec760d55 100644
--- a/templates/libraries/okhttp-gson/pojo.mustache
+++ b/templates/libraries/okhttp-gson/pojo.mustache
@@ -16,6 +16,8 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
+import {{package}}.JSON;
+
/**
* {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}}
* @deprecated{{/isDeprecated}}
@@ -488,10 +490,22 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
{{/isArray}}
{{^isContainer}}
{{#isString}}
+ {{^isEnum}}
+ // validate the {{#isRequired}}required{{/isRequired}}{{^isRequired}}optional{{/isRequired}} field {{{baseName}}}
if ({{^isRequired}}jsonObj.get("{{{baseName}}}") != null && {{/isRequired}}!jsonObj.get("{{{baseName}}}").isJsonPrimitive()) {
throw new IllegalArgumentException(String.format("Expected the field `{{{baseName}}}` to be a primitive type in the JSON string but got `%s`", jsonObj.get("{{{baseName}}}").toString()));
}
+ {{/isEnum}}
{{/isString}}
+ {{#isEnum}}
+ // ensure the field {{{baseName}}} can be parsed to an enum value
+ if ({{^isRequired}}jsonObj.get("{{{baseName}}}") != null) {
+ if({{/isRequired}}!jsonObj.get("{{{baseName}}}").isJsonPrimitive()) {
+ throw new IllegalArgumentException(String.format("Expected the field `{{{baseName}}}` to be a primitive type in the JSON string but got `%s`", jsonObj.get("{{{baseName}}}").toString()));
+ }
+ {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.fromValue(jsonObj.get("{{{baseName}}}").getAsString());
+ }
+ {{/isEnum}}
{{#isModel}}
{{#isRequired}}
// validate the required field `{{{baseName}}}`
@@ -517,7 +531,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
{{modelName}}.validateJsonObject(jsonObj);
break;
{{/mappedModels}}
- default:
+ default:
throw new IllegalArgumentException(String.format("The value of the `{{{propertyBaseName}}}` field `%s` does not match any key defined in the discriminator's mapping.", discriminatorValue));
}
{{/discriminator}}
@@ -597,4 +611,23 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtens
}
{{/hasChildren}}
+ /**
+ * Create an instance of {{classname}} given an JSON string
+ *
+ * @param jsonString JSON string
+ * @return An instance of {{classname}}
+ * @throws IOException if the JSON string is invalid with respect to {{classname}}
+ */
+ public static {{{classname}}} fromJson(String jsonString) throws IOException {
+ return JSON.getGson().fromJson(jsonString, {{{classname}}}.class);
+ }
+
+ /**
+ * Convert an instance of {{classname}} to an JSON string
+ *
+ * @return JSON string
+ */
+ public String toJson() {
+ return JSON.getGson().toJson(this);
+ }
}