From cbdfd3859b7d89fe521efab757019609acc3d0bd Mon Sep 17 00:00:00 2001 From: Pierre Millot Date: Thu, 7 Jul 2022 15:20:35 +0200 Subject: [PATCH] feat(java): convert from gson to jackson (#771) --- .github/actions/setup/action.yml | 2 +- .../algoliasearch-core/build.gradle | 6 +- .../src/main/java/com/algolia/ApiClient.java | 25 +- .../java/com/algolia/utils/HttpRequester.java | 20 +- .../src/main/java/com/algolia/utils/JSON.java | 480 ------------------ .../java/com/algolia/utils/JSONBuilder.java | 28 + .../java/com/algolia/utils/Requester.java | 4 +- .../cts/tests/ParametersWithDataType.java | 32 +- .../java/com/algolia/playground/Search.java | 4 +- scripts/pre-gen/removeExistingCodegen.ts | 10 - .../{libraries/okhttp-gson => }/api.mustache | 6 +- templates/java/model.mustache | 13 +- templates/java/modelEnum.mustache | 53 +- templates/java/modelInnerEnum.mustache | 51 +- templates/java/oneof_interface.mustache | 107 +++- templates/java/pojo.mustache | 102 +--- templates/java/tests/build.mustache | 2 +- templates/java/tests/generateParams.mustache | 2 +- .../java/tests/requests/requests.mustache | 39 +- .../search/batchDictionaryEntries.json | 34 ++ 20 files changed, 258 insertions(+), 762 deletions(-) delete mode 100644 clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/JSON.java create mode 100644 clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/JSONBuilder.java rename templates/java/{libraries/okhttp-gson => }/api.mustache (99%) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 81ba1b6306..215b0bdd76 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -37,7 +37,7 @@ runs: - name: Download Java formatter if: inputs.type != 'minimal' shell: bash - run: curl -L "https://github.com/google/google-java-format/releases/download/v1.13.0/google-java-format-1.13.0-all-deps.jar" > /tmp/java-formatter.jar + run: curl --retry 3 -L "https://github.com/google/google-java-format/releases/download/v1.13.0/google-java-format-1.13.0-all-deps.jar" > /tmp/java-formatter.jar # JavaScript for monorepo and tooling - name: Install Node diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/build.gradle b/clients/algoliasearch-client-java-2/algoliasearch-core/build.gradle index 86cb737ac8..9e03fd7613 100644 --- a/clients/algoliasearch-client-java-2/algoliasearch-core/build.gradle +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/build.gradle @@ -12,8 +12,10 @@ dependencies { implementation 'com.google.code.findbugs:jsr305:3.0.2' api 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0' - api 'com.google.code.gson:gson:2.9.0' - implementation 'io.gsonfire:gson-fire:1.8.5' + implementation 'com.fasterxml.jackson.core:jackson-core:2.13.3' + api 'com.fasterxml.jackson.core:jackson-annotations:2.13.3' + api 'com.fasterxml.jackson.core:jackson-databind:2.13.3' + implementation 'org.openapitools:jackson-databind-nullable:0.2.1' } tasks.withType(JavaCompile) { diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/ApiClient.java b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/ApiClient.java index 6782bda1a3..075c84fff6 100644 --- a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/ApiClient.java +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/ApiClient.java @@ -3,9 +3,11 @@ import com.algolia.exceptions.*; import com.algolia.utils.*; import com.algolia.utils.retry.StatefulHost; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.lang.reflect.Type; import java.net.URLEncoder; import java.time.LocalDate; import java.time.OffsetDateTime; @@ -24,6 +26,7 @@ public abstract class ApiClient { private String contentType; private Requester requester; + private ObjectMapper json; public ApiClient(String appId, String apiKey, String clientName, String version, ClientOptions options) { if (appId == null || appId.length() == 0) { @@ -54,6 +57,8 @@ public ApiClient(String appId, String apiKey, String clientName, String version, } else { this.requester = new HttpRequester(); } + + this.json = new JSONBuilder().build(); } private void refreshUserAgent() { @@ -189,13 +194,13 @@ public ApiClient setHosts(List hosts) { * @param param Parameter * @return String representation of the parameter */ - public String parameterToString(Object param) { + public String parameterToString(Object param) throws UnsupportedOperationException { if (param == null) { return ""; } else if (param instanceof Date || param instanceof OffsetDateTime || param instanceof LocalDate) { - // Serialize to json string and remove the " enclosing characters - String jsonStr = JSON.serialize(param); - return jsonStr.substring(1, jsonStr.length() - 1); + // note: date comes as string for now, we should never have to serialize one + // maybe we could accept them as Date object and in that case use jackson serialization + throw new UnsupportedOperationException("Date must come as string (already serialized)"); } else if (param instanceof Collection) { StringJoiner b = new StringJoiner(","); for (Object o : (Collection) param) { @@ -233,7 +238,11 @@ public RequestBody serialize(Object obj) throws AlgoliaRuntimeException { String content; if (obj != null) { - content = JSON.serialize(obj); + try { + content = json.writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new AlgoliaRuntimeException(e); + } } else { content = null; } @@ -246,9 +255,9 @@ public RequestBody serialize(Object obj) throws AlgoliaRuntimeException { * * @param Type * @param returnType Return type - * @see #execute(Call, Type) + * @see #execute(Call, TypeReference) */ - public CompletableFuture executeAsync(Call call, final Type returnType) { + public CompletableFuture executeAsync(Call call, final TypeReference returnType) { final CompletableFuture future = new CompletableFuture<>(); call.enqueue( new Callback() { diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/HttpRequester.java b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/HttpRequester.java index b1970682c7..38607522aa 100644 --- a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/HttpRequester.java +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/HttpRequester.java @@ -3,8 +3,10 @@ import com.algolia.exceptions.*; import com.algolia.utils.retry.RetryStrategy; import com.algolia.utils.retry.StatefulHost; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import java.io.IOException; -import java.lang.reflect.Type; import java.util.List; import java.util.concurrent.TimeUnit; import okhttp3.Call; @@ -20,6 +22,7 @@ public class HttpRequester implements Requester { private OkHttpClient httpClient; private HttpLoggingInterceptor loggingInterceptor; private LogLevel level; + private ObjectMapper json; public HttpRequester() { this.retryStrategy = new RetryStrategy(); @@ -34,6 +37,8 @@ public HttpRequester() { builder.retryOnConnectionFailure(false); httpClient = builder.build(); + + this.json = new JSONBuilder().build(); } @Override @@ -42,7 +47,7 @@ public Call newCall(Request request) { } @Override - public T handleResponse(Response response, Type returnType) throws AlgoliaRuntimeException { + public T handleResponse(Response response, TypeReference returnType) throws AlgoliaRuntimeException { if (response.isSuccessful()) { if (returnType == null || response.code() == 204) { // returning null if the returnType is not defined, or the status code is 204 (No Content) @@ -69,12 +74,12 @@ public T handleResponse(Response response, Type returnType) throws AlgoliaRu } } - private T deserialize(Response response, Type returnType) throws AlgoliaRuntimeException { + private T deserialize(Response response, TypeReference returnType) throws AlgoliaRuntimeException { if (response == null || returnType == null) { return null; } - if ("byte[]".equals(returnType.toString())) { + if ("byte[]".equals(returnType.getType().getTypeName())) { // Handle binary response (byte array). try { return (T) response.body().bytes(); @@ -98,8 +103,11 @@ private T deserialize(Response response, Type returnType) throws AlgoliaRunt if (contentType == null) { contentType = "application/json"; } - - return JSON.deserialize(respBody, returnType); + try { + return (T) json.readValue(respBody, returnType); + } catch (JsonProcessingException e) { + throw new AlgoliaRuntimeException(e); + } } @Override diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/JSON.java b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/JSON.java deleted file mode 100644 index 2939ce8491..0000000000 --- a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/JSON.java +++ /dev/null @@ -1,480 +0,0 @@ -package com.algolia.utils; - -import com.google.gson.FieldNamingPolicy; -import com.google.gson.FieldNamingStrategy; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.InstanceCreator; -import com.google.gson.JsonParseException; -import com.google.gson.JsonSyntaxException; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.annotations.SerializedName; -import com.google.gson.internal.$Gson$Types; -import com.google.gson.internal.ConstructorConstructor; -import com.google.gson.internal.Excluder; -import com.google.gson.internal.LinkedTreeMap; -import com.google.gson.internal.ObjectConstructor; -import com.google.gson.internal.Primitives; -import com.google.gson.internal.bind.MapTypeAdapterFactory; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import io.gsonfire.GsonFireBuilder; -import java.io.IOException; -import java.io.StringWriter; -import java.lang.reflect.Field; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import okio.ByteString; - -public class JSON { - - private static Gson gson; - private static final ByteArrayAdapter byteArrayAdapter = new ByteArrayAdapter(); - private static final RetainFieldMapFactory mapAdapter = new RetainFieldMapFactory(); - - static { - gson = createGson().registerTypeAdapter(byte[].class, byteArrayAdapter).registerTypeAdapterFactory(mapAdapter).create(); - } - - public static GsonBuilder createGson() { - GsonFireBuilder fireBuilder = new GsonFireBuilder(); - return fireBuilder.createGsonBuilder(); - } - - // Suppress default constructor for noninstantiability - private JSON() { - throw new AssertionError(); - } - - /** - * Get Gson. - * - * @return Gson - */ - public static Gson getGson() { - return gson; - } - - /** - * Set Gson. - * - * @param gson Gson - * @return JSON - */ - public static void setGson(Gson gon) { - gson = gon; - } - - /** - * 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 - */ - public static T deserialize(String body, Type returnType) { - try { - 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 != null && returnType.equals(String.class)) { - return (T) body; - } else { - throw (e); - } - } - } - - public static T tryDeserialize(JsonReader reader, Type returnType) { - try { - return gson.fromJson(reader, returnType); - } catch (JsonParseException e) { - return null; - } - } -} - -/** Gson TypeAdapter for Byte Array type */ -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 { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - String bytesAsBase64 = in.nextString(); - ByteString byteString = ByteString.decodeBase64(bytesAsBase64); - return byteString != null ? byteString.toByteArray() : new byte[0]; - } -} - -// https://stackoverflow.com/questions/21458468/gson-wont-properly-serialise-a-class-that-extends-hashmap -class RetainFieldMapFactory implements TypeAdapterFactory { - - FieldNamingPolicy fieldNamingPolicy = FieldNamingPolicy.IDENTITY; - ConstructorConstructor constructorConstructor = new ConstructorConstructor(Collections.>emptyMap(), true); - MapTypeAdapterFactory defaultMapFactory = new MapTypeAdapterFactory(constructorConstructor, false); - ReflectiveFilterMapFieldFactory defaultObjectFactory = new ReflectiveFilterMapFieldFactory( - constructorConstructor, - fieldNamingPolicy, - Excluder.DEFAULT - ); - - @Override - public TypeAdapter create(Gson gson, TypeToken type) { - final TypeAdapter mapAdapter = defaultMapFactory.create(gson, type); - if (mapAdapter != null) { - return (TypeAdapter) new RetainFieldMapAdapter(mapAdapter, defaultObjectFactory.create(gson, type)); - } - return mapAdapter; - } - - class RetainFieldMapAdapter extends TypeAdapter> { - - TypeAdapter> mapAdapter; - ReflectiveTypeAdapterFactory.Adapter> objectAdapter; - - RetainFieldMapAdapter(TypeAdapter mapAdapter, ReflectiveTypeAdapterFactory.Adapter objectAdapter) { - this.mapAdapter = mapAdapter; - this.objectAdapter = objectAdapter; - } - - @Override - public void write(final JsonWriter out, Map value) throws IOException { - if (value == null) { - out.nullValue(); - return; - } - // 1.write object - StringWriter sw = new StringWriter(); - objectAdapter.write(new JsonWriter(sw), value); - - // 2.convert object to a map - Map objectMap = mapAdapter.fromJson(sw.toString()); - - // 3.overwrite fields in object to a copy map - value = new LinkedHashMap(value); - value.putAll(objectMap); - - // 4.write the copy map - mapAdapter.write(out, value); - } - - @Override - public Map read(JsonReader in) throws IOException { - // 1.create map, all key-value retain in map - Map map = mapAdapter.read(in); - - // 2.create object from created map - Map object = objectAdapter.fromJsonTree(mapAdapter.toJsonTree(map)); - - // 3.remove fields in object from map - for (String field : objectAdapter.boundFields.keySet()) { - map.remove(field); - } - // 4.put map to object - object.putAll(map); - return object; - } - } - - static class ReflectiveFilterMapFieldFactory extends ReflectiveTypeAdapterFactory { - - public ReflectiveFilterMapFieldFactory( - ConstructorConstructor constructorConstructor, - FieldNamingStrategy fieldNamingPolicy, - Excluder excluder - ) { - super(constructorConstructor, fieldNamingPolicy, excluder); - } - - @Override - protected boolean shouldFindFieldInClass(Class willFindClass, Class originalRaw) { - Class[] endClasses = new Class[] { - Object.class, - HashMap.class, - LinkedHashMap.class, - LinkedTreeMap.class, - Hashtable.class, - TreeMap.class, - ConcurrentHashMap.class, - IdentityHashMap.class, - WeakHashMap.class, - EnumMap.class, - }; - for (Class c : endClasses) { - if (willFindClass == c) return false; - } - - return super.shouldFindFieldInClass(willFindClass, originalRaw); - } - } - - /** - * below code copy from {@link com.google.gson.internal.bind.ReflectiveTypeAdapterFactory} (little - * modify, in source this class is final) Type adapter that reflects over the fields and methods - * of a class. - */ - static class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { - - private final ConstructorConstructor constructorConstructor; - private final FieldNamingStrategy fieldNamingPolicy; - private final Excluder excluder; - - public ReflectiveTypeAdapterFactory( - ConstructorConstructor constructorConstructor, - FieldNamingStrategy fieldNamingPolicy, - Excluder excluder - ) { - this.constructorConstructor = constructorConstructor; - this.fieldNamingPolicy = fieldNamingPolicy; - this.excluder = excluder; - } - - public boolean excludeField(Field f, boolean serialize) { - return (!excluder.excludeClass(f.getType(), serialize) && !excluder.excludeField(f, serialize)); - } - - private String getFieldName(Field f) { - SerializedName serializedName = f.getAnnotation(SerializedName.class); - return serializedName == null ? fieldNamingPolicy.translateName(f) : serializedName.value(); - } - - public Adapter create(Gson gson, final TypeToken type) { - Class raw = type.getRawType(); - - if (!Object.class.isAssignableFrom(raw)) { - return null; // it's a primitive! - } - - ObjectConstructor constructor = constructorConstructor.get(type); - return new Adapter(constructor, getBoundFields(gson, type, raw)); - } - - private ReflectiveTypeAdapterFactory.BoundField createBoundField( - final Gson context, - final Field field, - final String name, - final TypeToken fieldType, - boolean serialize, - boolean deserialize - ) { - final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType()); - - // special casing primitives here saves ~5% on Android... - return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) { - final TypeAdapter typeAdapter = context.getAdapter(fieldType); - - @SuppressWarnings({ "unchecked", "rawtypes" }) // the type adapter and field type always agree - @Override - void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException { - Object fieldValue = field.get(value); - TypeAdapter t = new TypeAdapterRuntimeTypeWrapper(context, this.typeAdapter, fieldType.getType()); - t.write(writer, fieldValue); - } - - @Override - void read(JsonReader reader, Object value) throws IOException, IllegalAccessException { - Object fieldValue = typeAdapter.read(reader); - if (fieldValue != null || !isPrimitive) { - field.set(value, fieldValue); - } - } - }; - } - - private Map getBoundFields(Gson context, TypeToken type, Class raw) { - Map result = new LinkedHashMap(); - if (raw.isInterface()) { - return result; - } - - Type declaredType = type.getType(); - Class originalRaw = type.getRawType(); - while (shouldFindFieldInClass(raw, originalRaw)) { - Field[] fields = raw.getDeclaredFields(); - for (Field field : fields) { - boolean serialize = excludeField(field, true); - boolean deserialize = excludeField(field, false); - if (!serialize && !deserialize) { - continue; - } - field.setAccessible(true); - Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType()); - BoundField boundField = createBoundField(context, field, getFieldName(field), TypeToken.get(fieldType), serialize, deserialize); - BoundField previous = result.put(boundField.name, boundField); - if (previous != null) { - throw new IllegalArgumentException(declaredType + " declares multiple JSON fields named " + previous.name); - } - } - type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass())); - raw = type.getRawType(); - } - return result; - } - - protected boolean shouldFindFieldInClass(Class willFindClass, Class originalRaw) { - return willFindClass != Object.class; - } - - abstract static class BoundField { - - final String name; - final boolean serialized; - final boolean deserialized; - - protected BoundField(String name, boolean serialized, boolean deserialized) { - this.name = name; - this.serialized = serialized; - this.deserialized = deserialized; - } - - abstract void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException; - - abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException; - } - - public static final class Adapter extends TypeAdapter { - - private final ObjectConstructor constructor; - private final Map boundFields; - - private Adapter(ObjectConstructor constructor, Map boundFields) { - this.constructor = constructor; - this.boundFields = boundFields; - } - - @Override - public T read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - T instance = constructor.construct(); - - try { - in.beginObject(); - while (in.hasNext()) { - String name = in.nextName(); - BoundField field = boundFields.get(name); - if (field == null || !field.deserialized) { - in.skipValue(); - } else { - field.read(in, instance); - } - } - } catch (IllegalStateException e) { - throw new JsonSyntaxException(e); - } catch (IllegalAccessException e) { - throw new AssertionError(e); - } - in.endObject(); - return instance; - } - - @Override - public void write(JsonWriter out, T value) throws IOException { - if (value == null) { - out.nullValue(); - return; - } - - out.beginObject(); - try { - for (BoundField boundField : boundFields.values()) { - if (boundField.serialized) { - out.name(boundField.name); - boundField.write(out, value); - } - } - } catch (IllegalAccessException e) { - throw new AssertionError(); - } - out.endObject(); - } - } - } - - static class TypeAdapterRuntimeTypeWrapper extends TypeAdapter { - - private final Gson context; - private final TypeAdapter delegate; - private final Type type; - - TypeAdapterRuntimeTypeWrapper(Gson context, TypeAdapter delegate, Type type) { - this.context = context; - this.delegate = delegate; - this.type = type; - } - - @Override - public T read(JsonReader in) throws IOException { - return delegate.read(in); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public void write(JsonWriter out, T value) throws IOException { - // Order of preference for choosing type adapters - // First preference: a type adapter registered for the runtime type - // Second preference: a type adapter registered for the declared type - // Third preference: reflective type adapter for the runtime type (if it is a - // sub class of the declared type) - // Fourth preference: reflective type adapter for the declared type - - TypeAdapter chosen = delegate; - Type runtimeType = getRuntimeTypeIfMoreSpecific(type, value); - if (runtimeType != type) { - TypeAdapter runtimeTypeAdapter = context.getAdapter(TypeToken.get(runtimeType)); - if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) { - // The user registered a type adapter for the runtime type, so we will use that - chosen = runtimeTypeAdapter; - } else if (!(delegate instanceof ReflectiveTypeAdapterFactory.Adapter)) { - // The user registered a type adapter for Base class, so we prefer it over the - // reflective type adapter for the runtime type - chosen = delegate; - } else { - // Use the type adapter for runtime type - chosen = runtimeTypeAdapter; - } - } - chosen.write(out, value); - } - - /** Finds a compatible runtime type if it is more specific */ - private Type getRuntimeTypeIfMoreSpecific(Type type, Object value) { - if (value != null && (type == Object.class || type instanceof TypeVariable || type instanceof Class)) { - type = value.getClass(); - } - return type; - } - } -} diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/JSONBuilder.java b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/JSONBuilder.java new file mode 100644 index 0000000000..dd6f7201e7 --- /dev/null +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/JSONBuilder.java @@ -0,0 +1,28 @@ +package com.algolia.utils; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.json.JsonMapper; + +public class JSONBuilder { + + private boolean failOnUnknown = false; + + public JSONBuilder() {} + + public JSONBuilder failOnUnknown(boolean failOnUnknown) { + this.failOnUnknown = failOnUnknown; + return this; + } + + public ObjectMapper build() { + ObjectMapper mapper = JsonMapper.builder().disable(MapperFeature.ALLOW_COERCION_OF_SCALARS).build(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, this.failOnUnknown); + mapper.enable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); + mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); + return mapper; + } +} diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/Requester.java b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/Requester.java index 904d8df9b4..f96527d5eb 100644 --- a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/Requester.java +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/Requester.java @@ -2,7 +2,7 @@ import com.algolia.exceptions.AlgoliaRuntimeException; import com.algolia.utils.retry.StatefulHost; -import java.lang.reflect.Type; +import com.fasterxml.jackson.core.type.TypeReference; import java.util.List; import okhttp3.Call; import okhttp3.Request; @@ -11,7 +11,7 @@ public interface Requester { public Call newCall(Request request); - public T handleResponse(Response response, Type returnType) throws AlgoliaRuntimeException; + public T handleResponse(Response response, TypeReference returnType) throws AlgoliaRuntimeException; /** * Enable/disable debugging for this API client. diff --git a/generators/src/main/java/com/algolia/codegen/cts/tests/ParametersWithDataType.java b/generators/src/main/java/com/algolia/codegen/cts/tests/ParametersWithDataType.java index 8f6f1f6da8..8728a69c11 100644 --- a/generators/src/main/java/com/algolia/codegen/cts/tests/ParametersWithDataType.java +++ b/generators/src/main/java/com/algolia/codegen/cts/tests/ParametersWithDataType.java @@ -199,6 +199,7 @@ private Map createDefaultOutput() { testOutput.put("isEnum", false); testOutput.put("isSimpleObject", false); testOutput.put("oneOfModel", false); + testOutput.put("isAdditionalProperty", false); return testOutput; } @@ -304,18 +305,27 @@ private void handleModel( } } if (varSpec == null) { - throw new CTSException( - "Parameter '" + - entry.getKey() + - "' not found in '" + - paramName + - "'. You might have a type conflict in the spec for '" + - baseType + - "'" - ); + if (spec.getAdditionalPropertiesIsAnyType()) { + // we hit an additionalProperties, infer it's type + CodegenParameter additionalPropertiesSpec = new CodegenParameter(); + additionalPropertiesSpec.dataType = inferDataType(entry.getValue(), additionalPropertiesSpec, null); + Map value = traverseParams(entry.getKey(), entry.getValue(), additionalPropertiesSpec, paramName, suffix + 1); + value.put("isAdditionalProperty", true); + values.add(value); + } else { + throw new CTSException( + "Parameter '" + + entry.getKey() + + "' not found in '" + + paramName + + "'. You might have a type conflict in the spec for '" + + baseType + + "'" + ); + } + } else { + values.add(traverseParams(entry.getKey(), entry.getValue(), varSpec, paramName, suffix + 1)); } - - values.add(traverseParams(entry.getKey(), entry.getValue(), varSpec, paramName, suffix + 1)); } testOutput.put("isObject", true); testOutput.put("value", values); diff --git a/playground/java/src/main/java/com/algolia/playground/Search.java b/playground/java/src/main/java/com/algolia/playground/Search.java index 0f0fa4464f..f725cad3b8 100644 --- a/playground/java/src/main/java/com/algolia/playground/Search.java +++ b/playground/java/src/main/java/com/algolia/playground/Search.java @@ -11,7 +11,7 @@ class Actor { - String name; + public String name; Actor(String name) { this.name = name; @@ -54,7 +54,7 @@ public static void main(String[] args) { SearchMethodParams searchMethodParams = new SearchMethodParams(); List requests = new ArrayList<>(); - requests.add(SearchQuery.of(new SearchForHits().setIndexName(indexName).setQuery(query))); + requests.add(SearchQuery.of(new SearchForHits().setIndexName(indexName).setQuery(query).addAttributesToSnippet("title").addAttributesToSnippet("alternative_titles"))); searchMethodParams.setRequests(requests); CompletableFuture result = client.searchAsync(searchMethodParams); diff --git a/scripts/pre-gen/removeExistingCodegen.ts b/scripts/pre-gen/removeExistingCodegen.ts index fe10ca45dd..8318006ec6 100644 --- a/scripts/pre-gen/removeExistingCodegen.ts +++ b/scripts/pre-gen/removeExistingCodegen.ts @@ -22,16 +22,6 @@ export async function removeExistingCodegen({ switch (language) { case 'java': - if (client === 'query-suggestions') { - // eslint-disable-next-line no-warning-comments - // TODO: temporary solution, remove in next PR - await fsp.rm( - toAbsolutePath( - path.resolve('..', output, baseModelFolder, 'querySuggestions') - ), - { force: true, recursive: true } - ); - } clientModel = client.replace('-', ''); clientApi = `${clientName}*.java`; break; diff --git a/templates/java/libraries/okhttp-gson/api.mustache b/templates/java/api.mustache similarity index 99% rename from templates/java/libraries/okhttp-gson/api.mustache rename to templates/java/api.mustache index d906448fd4..9bcaf9a884 100644 --- a/templates/java/libraries/okhttp-gson/api.mustache +++ b/templates/java/api.mustache @@ -2,7 +2,7 @@ package {{package}}; import {{invokerPackage}}.ApiClient; -import com.google.gson.reflect.TypeToken; +import com.fasterxml.jackson.core.type.TypeReference; import okhttp3.Call; import okhttp3.Request; @@ -17,7 +17,6 @@ import java.util.function.IntUnaryOperator; import java.util.EnumSet; import java.util.Random; import java.util.Collections; -import java.lang.reflect.Type; {{^fullJavaUtil}} import java.util.ArrayList; import java.util.HashMap; @@ -234,8 +233,7 @@ public class {{classname}} extends ApiClient { {{/headerParams}} Call call = this.buildCall(requestPath, "{{httpMethod}}", queryParameters, bodyObj, headers, requestOptions, {{#vendorExtensions.x-use-read-transporter}}true{{/vendorExtensions.x-use-read-transporter}}{{^vendorExtensions.x-use-read-transporter}}false{{/vendorExtensions.x-use-read-transporter}}); - Type returnType = new TypeToken<{{{returnType}}}>() {}.getType(); - return this.executeAsync(call, returnType); + return this.executeAsync(call, new TypeReference<{{{returnType}}}>() {}); } {{! This case only sets `requestOptions` as optional }} diff --git a/templates/java/model.mustache b/templates/java/model.mustache index 13a53736c1..5b1b77df91 100644 --- a/templates/java/model.mustache +++ b/templates/java/model.mustache @@ -7,18 +7,7 @@ import {{import}}; {{#serializableModel}} import java.io.Serializable; {{/serializableModel}} -{{#jsonb}} -import java.lang.reflect.Type; -import javax.json.bind.annotation.JsonbTypeDeserializer; -import javax.json.bind.annotation.JsonbTypeSerializer; -import javax.json.bind.serializer.DeserializationContext; -import javax.json.bind.serializer.JsonbDeserializer; -import javax.json.bind.serializer.JsonbSerializer; -import javax.json.bind.serializer.SerializationContext; -import javax.json.stream.JsonGenerator; -import javax.json.stream.JsonParser; -import javax.json.bind.annotation.JsonbProperty; -{{/jsonb}} +import com.fasterxml.jackson.annotation.*; {{#models}} {{#model}} diff --git a/templates/java/modelEnum.mustache b/templates/java/modelEnum.mustache index 0a84a70316..6ed179c82a 100644 --- a/templates/java/modelEnum.mustache +++ b/templates/java/modelEnum.mustache @@ -1,21 +1,9 @@ -{{#gson}} -import java.io.IOException; -import com.google.gson.TypeAdapter; -import com.google.gson.annotations.JsonAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; -{{/gson}} +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; /** * {{{description}}}{{^description}}Gets or Sets {{{name}}}{{/description}} */ -{{#gson}} -@JsonAdapter({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.Adapter.class) -{{/gson}} -{{#jsonb}} -@JsonbTypeSerializer({{datatypeWithEnum}}.Serializer.class) -@JsonbTypeDeserializer({{datatypeWithEnum}}.Deserializer.class) -{{/jsonb}} {{>additionalEnumTypeAnnotations}}public enum {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} { {{#allowableValues}}{{#enumVars}} {{#enumDescription}}/** @@ -29,6 +17,7 @@ import com.google.gson.stream.JsonWriter; this.value = value; } + @JsonValue public {{{dataType}}} getValue() { return value; } @@ -38,6 +27,7 @@ import com.google.gson.stream.JsonWriter; return String.valueOf(value); } + @JsonCreator public static {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) { for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { if (b.value.equals(value)) { @@ -46,39 +36,4 @@ import com.google.gson.stream.JsonWriter; } {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/isNullable}} } -{{#gson}} - - public static class Adapter extends TypeAdapter<{{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}> { - @Override - public void write(final JsonWriter jsonWriter, final {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} enumeration) throws IOException { - jsonWriter.value(enumeration.getValue()); - } - - @Override - public {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} read(final JsonReader jsonReader) throws IOException { - {{^isNumber}}{{{dataType}}}{{/isNumber}}{{#isNumber}}String{{/isNumber}} value = jsonReader.{{#isNumber}}nextString(){{/isNumber}}{{#isInteger}}nextInt(){{/isInteger}}{{^isNumber}}{{^isInteger}}next{{{dataType}}}(){{/isInteger}}{{/isNumber}}; - return {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.fromValue({{#isNumber}}new BigDecimal({{/isNumber}}value{{#isNumber}}){{/isNumber}}); - } - } -{{/gson}} -{{#jsonb}} - public static final class Deserializer implements JsonbDeserializer<{{datatypeWithEnum}}> { - @Override - public {{datatypeWithEnum}} deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) { - for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { - if (String.valueOf(b.value).equals(parser.getString())) { - return b; - } - } - {{#useNullForUnknownEnumValue}}return null;{{/useNullForUnknownEnumValue}}{{^useNullForUnknownEnumValue}}throw new IllegalArgumentException("Unexpected value '" + parser.getString() + "'");{{/useNullForUnknownEnumValue}} - } - } - - public static final class Serializer implements JsonbSerializer<{{datatypeWithEnum}}> { - @Override - public void serialize({{datatypeWithEnum}} obj, JsonGenerator generator, SerializationContext ctx) { - generator.write(obj.value); - } - } -{{/jsonb}} } diff --git a/templates/java/modelInnerEnum.mustache b/templates/java/modelInnerEnum.mustache index 1b1a4badeb..bb81fd8b36 100644 --- a/templates/java/modelInnerEnum.mustache +++ b/templates/java/modelInnerEnum.mustache @@ -1,17 +1,6 @@ /** * {{{description}}}{{^description}}Gets or Sets {{{name}}}{{/description}} */ -{{#gson}} - @JsonAdapter({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.Adapter.class) -{{/gson}} -{{#jsonb}} - @JsonbTypeSerializer({{datatypeWithEnum}}.Serializer.class) - @JsonbTypeDeserializer({{datatypeWithEnum}}.Deserializer.class) -{{/jsonb}} -{{#withXml}} - @XmlType(name="{{datatypeWithEnum}}") - @XmlEnum({{dataType}}.class) -{{/withXml}} {{>additionalEnumTypeAnnotations}}public enum {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} { {{#allowableValues}} {{#enumVars}} @@ -20,9 +9,6 @@ * {{{.}}} */ {{/enumDescription}} - {{#withXml}} - @XmlEnumValue({{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}{{{value}}}{{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}) - {{/withXml}} {{{name}}}({{{value}}}){{^-last}}, {{/-last}}{{#-last}};{{/-last}} {{/enumVars}} @@ -34,6 +20,7 @@ this.value = value; } + @JsonValue public {{{dataType}}} getValue() { return value; } @@ -43,6 +30,7 @@ return String.valueOf(value); } + @JsonCreator public static {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) { for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { if (b.value.equals(value)) { @@ -51,39 +39,4 @@ } {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/isNullable}} } -{{#gson}} - - public static class Adapter extends TypeAdapter<{{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}> { - @Override - public void write(final JsonWriter jsonWriter, final {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} enumeration) throws IOException { - jsonWriter.value(enumeration.getValue()); - } - - @Override - public {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} read(final JsonReader jsonReader) throws IOException { - {{^isNumber}}{{{dataType}}}{{/isNumber}}{{#isNumber}}String{{/isNumber}} value = {{#isFloat}}(float){{/isFloat}} jsonReader.{{#isNumber}}nextString(){{/isNumber}}{{#isInteger}}nextInt(){{/isInteger}}{{^isNumber}}{{^isInteger}}{{#isFloat}}nextDouble{{/isFloat}}{{^isFloat}}next{{{dataType}}}{{/isFloat}}(){{/isInteger}}{{/isNumber}}; - return {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.fromValue({{#isNumber}}new BigDecimal({{/isNumber}}value{{#isNumber}}){{/isNumber}}); - } - } -{{/gson}} -{{#jsonb}} - public static final class Deserializer implements JsonbDeserializer<{{datatypeWithEnum}}> { - @Override - public {{datatypeWithEnum}} deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) { - for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { - if (String.valueOf(b.value).equals(parser.getString())) { - return b; - } - } - {{#useNullForUnknownEnumValue}}return null;{{/useNullForUnknownEnumValue}}{{^useNullForUnknownEnumValue}}throw new IllegalArgumentException("Unexpected value '" + parser.getString() + "'");{{/useNullForUnknownEnumValue}} - } - } - - public static final class Serializer implements JsonbSerializer<{{datatypeWithEnum}}> { - @Override - public void serialize({{datatypeWithEnum}} obj, JsonGenerator generator, SerializationContext ctx) { - generator.write(obj.value); - } - } -{{/jsonb}} } diff --git a/templates/java/oneof_interface.mustache b/templates/java/oneof_interface.mustache index 605dcfb163..eecd6fa511 100644 --- a/templates/java/oneof_interface.mustache +++ b/templates/java/oneof_interface.mustache @@ -1,20 +1,22 @@ import com.algolia.utils.CompoundType; import com.algolia.utils.JSON; - -import com.google.gson.TypeAdapter; -import com.google.gson.reflect.TypeToken; -import com.google.gson.annotations.JsonAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import java.io.IOException; import java.util.List; -@JsonAdapter({{classname}}.Adapter.class) /** * {{{description}}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}} * @deprecated{{/isDeprecated}} */{{#isDeprecated}} @Deprecated{{/isDeprecated}} +@JsonDeserialize(using = {{classname}}.{{classname}}Deserializer.class) +@JsonSerialize(using = {{classname}}.{{classname}}Serializer.class) {{>additionalModelTypeAnnotations}} public abstract class {{classname}} implements CompoundType { {{#vendorExtensions.x-one-of-list}} @@ -24,32 +26,89 @@ public abstract class {{classname}} implements CompoundType { {{/vendorExtensions.x-one-of-list}} - public static class Adapter extends TypeAdapter<{{classname}}> { - @Override - public void write( - final JsonWriter out, - final {{classname}} oneOf) throws IOException { - TypeAdapter runtimeTypeAdapter = (TypeAdapter) JSON.getGson() - .getAdapter(TypeToken.get(oneOf.getInsideValue().getClass())); - runtimeTypeAdapter.write(out, oneOf.getInsideValue()); + public static class {{classname}}Serializer extends StdSerializer<{{classname}}> { + public {{classname}}Serializer(Class<{{classname}}> t) { + super(t); + } + + public {{classname}}Serializer() { + this(null); } @Override - public {{classname}} read(final JsonReader jsonReader) - throws IOException { - {{#vendorExtensions.x-one-of-list}} - {{{type}}} {{#lambda.lowercase}}{{name}}{{/lambda.lowercase}} = JSON.tryDeserialize(jsonReader, new TypeToken<{{{type}}}>() {}.getType()); - if({{#lambda.lowercase}}{{name}}{{/lambda.lowercase}} != null) { - return {{classname}}.of{{#vendorExtensions.x-one-of-explicit-name}}{{{name}}}{{/vendorExtensions.x-one-of-explicit-name}}({{#lambda.lowercase}}{{name}}{{/lambda.lowercase}}); + public void serialize({{classname}} value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { + jgen.writeObject(value.getInsideValue()); + } + } + + public static class {{classname}}Deserializer extends StdDeserializer<{{classname}}> { + public {{classname}}Deserializer() { + this({{classname}}.class); } - {{/vendorExtensions.x-one-of-list}} - return null; + + public {{classname}}Deserializer(Class vc) { + super(vc); + } + + @Override + public {{classname}} deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + JsonNode tree = jp.readValueAsTree(); + {{{classname}}} deserialized = null; + + int match = 0; + JsonToken token = tree.traverse(jp.getCodec()).nextToken(); + String currentType = ""; + {{#vendorExtensions.x-one-of-list}} + // deserialize {{{type}}} + try { + boolean attemptParsing = true; + currentType = "{{{type}}}"; + if (((currentType.equals("Integer") || currentType.equals("Long")) && token == JsonToken.VALUE_NUMBER_INT) | + ((currentType.equals("Float") || currentType.equals("Double")) && token == JsonToken.VALUE_NUMBER_FLOAT) | + (currentType.equals("Boolean") && (token == JsonToken.VALUE_FALSE || token == JsonToken.VALUE_TRUE)) | + (currentType.equals("String") && token == JsonToken.VALUE_STRING) | + (currentType.startsWith("List<") && token == JsonToken.START_ARRAY) + {{#isNullable}} + | (token == JsonToken.VALUE_NULL) + {{/isNullable}}) { + deserialized = {{{classname}}}.of{{#vendorExtensions.x-one-of-explicit-name}}{{{name}}}{{/vendorExtensions.x-one-of-explicit-name}}(({{{type}}}) tree.traverse(jp.getCodec()).readValueAs(new TypeReference<{{{type}}}>() {})); + match++; + } else if (token == JsonToken.START_OBJECT) { + try { + deserialized = {{{classname}}}.of{{#vendorExtensions.x-one-of-explicit-name}}{{{name}}}{{/vendorExtensions.x-one-of-explicit-name}}(({{{type}}}) tree.traverse(jp.getCodec()).readValueAs(new TypeReference<{{{type}}}>() {})); + match++; + } catch(IOException e) { + // do nothing + } + } + } catch (Exception e) { + // deserialization failed, continue + System.err.println("Failed to deserialize oneOf {{{type}}} (error: " + e.getMessage() + ") (type: " + currentType + ")"); + } + + {{/vendorExtensions.x-one-of-list}} + if (match == 1) { + return deserialized; + } + throw new IOException(String.format("Failed deserialization for {{classname}}: %d classes match result, expected 1", match)); + } + + /** + * Handle deserialization of the 'null' value. + */ + @Override + public {{classname}} getNullValue(DeserializationContext ctxt) throws JsonMappingException { + {{#isNullable}} + return null; + {{/isNullable}} + {{^isNullable}} + throw new JsonMappingException(ctxt.getParser(), "{{classname}} cannot be null"); + {{/isNullable}} } } } {{#vendorExtensions.x-one-of-list}} -@JsonAdapter({{classname}}.Adapter.class) class {{classname}}{{name}} extends {{classname}} { private final {{{type}}} insideValue; diff --git a/templates/java/pojo.mustache b/templates/java/pojo.mustache index 0d5a4a44db..d2cfc26c80 100644 --- a/templates/java/pojo.mustache +++ b/templates/java/pojo.mustache @@ -4,7 +4,7 @@ */{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{>additionalModelTypeAnnotations}} -public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{ +public class {{classname}} { {{#serializableModel}} private static final long serialVersionUID = 1L; @@ -20,9 +20,7 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{ {{/mostInnerItems}} {{/isContainer}} {{/isEnum}} - {{#gson}} - @SerializedName("{{baseName}}") - {{/gson}} + @JsonProperty("{{baseName}}") {{#isContainer}} private {{{datatypeWithEnum}}} {{name}}{{#required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/required}}; {{/isContainer}} @@ -31,27 +29,20 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{ {{/isContainer}} {{/vars}} - {{#parcelableModel}} - public {{classname}}() { {{#parent}} - super(); - {{/parent}} - {{#gson}} - {{#discriminator}} - this.{{{discriminatorName}}} = this.getClass().getSimpleName(); - {{/discriminator}} - {{/gson}} - } - {{/parcelableModel}} - {{^parcelableModel}} - {{#gson}} - {{#discriminator}} - public {{classname}}() { - this.{{{discriminatorName}}} = this.getClass().getSimpleName(); - } - {{/discriminator}} - {{/gson}} - {{/parcelableModel}} +private Map additionalProperties = new HashMap<>(); + +@JsonAnyGetter +public Map getAdditionalProperties() { + return this.additionalProperties; +} + +@JsonAnySetter +public {{classname}} setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + return this; +} +{{/parent}} {{#vars}} {{^isReadOnly}} @@ -117,9 +108,6 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{ {{^required}} @javax.annotation.Nullable {{/required}} -{{#jsonb}} - @JsonbProperty("{{baseName}}") -{{/jsonb}} public {{{datatypeWithEnum}}} {{getter}}() { return {{name}}; } @@ -180,64 +168,4 @@ public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{ } return o.toString().replace("\n", "\n "); } - -{{#parcelableModel}} - - public void writeToParcel(Parcel out, int flags) { -{{#model}} -{{#isArray}} - out.writeList(this); -{{/isArray}} -{{^isArray}} -{{#parent}} - super.writeToParcel(out, flags); -{{/parent}} -{{#vars}} - out.writeValue({{name}}); -{{/vars}} -{{/isArray}} -{{/model}} - } - - {{classname}}(Parcel in) { -{{#isArray}} - in.readTypedList(this, {{arrayModelType}}.CREATOR); -{{/isArray}} -{{^isArray}} -{{#parent}} - super(in); -{{/parent}} -{{#vars}} -{{#isPrimitiveType}} - {{name}} = ({{{datatypeWithEnum}}})in.readValue(null); -{{/isPrimitiveType}} -{{^isPrimitiveType}} - {{name}} = ({{{datatypeWithEnum}}})in.readValue({{complexType}}.class.getClassLoader()); -{{/isPrimitiveType}} -{{/vars}} -{{/isArray}} - } - - public int describeContents() { - return 0; - } - - public static final Parcelable.Creator<{{classname}}> CREATOR = new Parcelable.Creator<{{classname}}>() { - public {{classname}} createFromParcel(Parcel in) { -{{#model}} -{{#isArray}} - {{classname}} result = new {{classname}}(); - result.addAll(in.readArrayList({{arrayModelType}}.class.getClassLoader())); - return result; -{{/isArray}} -{{^isArray}} - return new {{classname}}(in); -{{/isArray}} -{{/model}} - } - public {{classname}}[] newArray(int size) { - return new {{classname}}[size]; - } - }; -{{/parcelableModel}} } diff --git a/templates/java/tests/build.mustache b/templates/java/tests/build.mustache index 99f796c6fc..8e03e278d9 100644 --- a/templates/java/tests/build.mustache +++ b/templates/java/tests/build.mustache @@ -10,7 +10,7 @@ dependencies { testImplementation 'com.algolia:algoliasearch-client-java:{{packageVersion}}' testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' testImplementation 'org.skyscreamer:jsonassert:1.5.0' - testImplementation 'com.google.code.gson:gson:2.9.0' + testImplementation 'com.fasterxml.jackson.core:jackson-core:2.13.3' } group = 'com.algolia' diff --git a/templates/java/tests/generateParams.mustache b/templates/java/tests/generateParams.mustache index 9043a74b55..f78ae6018d 100644 --- a/templates/java/tests/generateParams.mustache +++ b/templates/java/tests/generateParams.mustache @@ -25,7 +25,7 @@ {{#isObject}} {{{objectName}}} {{{key}}}{{suffix}} = new {{{objectName}}}(); { - {{#value}}{{> generateParams}}{{parent}}{{parentSuffix}}.set{{#lambda.titlecase}}{{{key}}}{{/lambda.titlecase}}({{> maybeConvertOneOf}});{{/value}} + {{#value}}{{> generateParams}}{{#isAdditionalProperty}}{{parent}}{{parentSuffix}}.setAdditionalProperty("{{{key}}}", {{> maybeConvertOneOf}});{{/isAdditionalProperty}}{{^isAdditionalProperty}}{{parent}}{{parentSuffix}}.set{{#lambda.titlecase}}{{{key}}}{{/lambda.titlecase}}({{> maybeConvertOneOf}});{{/isAdditionalProperty}}{{/value}} } {{/isObject}} {{#isFreeFormObject}} diff --git a/templates/java/tests/requests/requests.mustache b/templates/java/tests/requests/requests.mustache index e58494eb3b..4da8bcf84b 100644 --- a/templates/java/tests/requests/requests.mustache +++ b/templates/java/tests/requests/requests.mustache @@ -3,6 +3,7 @@ package com.algolia.methods.requests; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.fail; import java.util.*; @@ -10,9 +11,11 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.BeforeAll; -import com.google.gson.reflect.TypeToken; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.core.JsonProcessingException; -import com.algolia.utils.JSON; +import com.algolia.utils.JSONBuilder; import com.algolia.utils.ClientOptions; import com.algolia.utils.RequestOptions; import com.algolia.model.{{import}}.*; @@ -28,9 +31,11 @@ import com.algolia.EchoResponse; class {{client}}RequestsTests { private {{client}} client; private EchoInterceptor echo; + private ObjectMapper json; @BeforeAll void init() { + json = new JSONBuilder().failOnUnknown(true).build(); HttpRequester requester = new HttpRequester(); echo = new EchoInterceptor(); requester.addInterceptor(echo.getEchoInterceptor()); @@ -69,21 +74,29 @@ class {{client}}RequestsTests { {{/request.body}} {{#request.queryParameters}} - Map expectedQuery = JSON.deserialize("{{#lambda.escapeQuotes}}{{{request.queryParameters}}}{{/lambda.escapeQuotes}}", new TypeToken>() {}.getType()); - Map actualQuery = req.queryParameters; - - assertEquals(expectedQuery.size(), actualQuery.size()); - for (Map.Entry p : actualQuery.entrySet()) { - assertEquals(expectedQuery.get(p.getKey()), p.getValue()); + try { + Map expectedQuery = json.readValue("{{#lambda.escapeQuotes}}{{{request.queryParameters}}}{{/lambda.escapeQuotes}}", new TypeReference>() {}); + Map actualQuery = req.queryParameters; + + assertEquals(expectedQuery.size(), actualQuery.size()); + for (Map.Entry p : actualQuery.entrySet()) { + assertEquals(expectedQuery.get(p.getKey()), p.getValue()); + } + } catch(JsonProcessingException e) { + fail("failed to parse queryParameters json"); } {{/request.queryParameters}} {{#request.headers}} - Map expectedHeaders = JSON.deserialize("{{#lambda.escapeQuotes}}{{{request.headers}}}{{/lambda.escapeQuotes}}", new TypeToken>() {}.getType()); - Map actualHeaders = req.headers; - - for (Map.Entry p : expectedHeaders.entrySet()) { - assertEquals(actualHeaders.get(p.getKey()), p.getValue()); + try { + Map expectedHeaders = json.readValue("{{#lambda.escapeQuotes}}{{{request.headers}}}{{/lambda.escapeQuotes}}", new TypeReference>() {}); + Map actualHeaders = req.headers; + + for (Map.Entry p : expectedHeaders.entrySet()) { + assertEquals(actualHeaders.get(p.getKey()), p.getValue()); + } + } catch(JsonProcessingException e) { + fail("failed to parse headers json"); } {{/request.headers}} } diff --git a/tests/CTS/methods/requests/search/batchDictionaryEntries.json b/tests/CTS/methods/requests/search/batchDictionaryEntries.json index 7f3a1a5bf7..e86a1ea040 100644 --- a/tests/CTS/methods/requests/search/batchDictionaryEntries.json +++ b/tests/CTS/methods/requests/search/batchDictionaryEntries.json @@ -132,5 +132,39 @@ ] } } + }, + { + "testName": "get batchDictionaryEntries results additional properties", + "parameters": { + "dictionaryName": "compounds", + "batchDictionaryEntriesParams": { + "requests": [ + { + "action": "addEntry", + "body": { + "objectID": "1", + "language": "en", + "additional": "try me" + } + } + ] + } + }, + "request": { + "path": "/1/dictionaries/compounds/batch", + "method": "POST", + "body": { + "requests": [ + { + "action": "addEntry", + "body": { + "objectID": "1", + "language": "en", + "additional": "try me" + } + } + ] + } + } } ]