diff --git a/driver/src/test/java/com/arangodb/serde/JacksonInterferenceTest.java b/driver/src/test/java/com/arangodb/serde/JacksonInterferenceTest.java new file mode 100644 index 000000000..13f925fc9 --- /dev/null +++ b/driver/src/test/java/com/arangodb/serde/JacksonInterferenceTest.java @@ -0,0 +1,237 @@ +package com.arangodb.serde; + +import com.arangodb.serde.jackson.*; +import com.arangodb.serde.jackson.json.JacksonJsonSerdeProvider; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +class JacksonInterferenceTest { + + private final ObjectMapper mapper = new ObjectMapper(); + private final ArangoSerde serde = new JacksonJsonSerdeProvider().create(); + + private FooField fooField; + private FooProp fooProp; + + static class FooField { + @Id + public String myId; + @Key + public String myKey; + @Rev + public String myRev; + @From + public String myFrom; + @To + public String myTo; + } + + static class FooProp { + public String myId; + public String myKey; + public String myRev; + public String myFrom; + public String myTo; + + @Id + public String getMyId() { + return myId; + } + + @Id + public void setMyId(String myId) { + this.myId = myId; + } + + @Key + public String getMyKey() { + return myKey; + } + + @Key + public void setMyKey(String myKey) { + this.myKey = myKey; + } + + @Rev + public String getMyRev() { + return myRev; + } + + @Rev + public void setMyRev(String myRev) { + this.myRev = myRev; + } + + @From + public String getMyFrom() { + return myFrom; + } + + @From + public void setMyFrom(String myFrom) { + this.myFrom = myFrom; + } + + @To + public String getMyTo() { + return myTo; + } + + @To + public void setMyTo(String myTo) { + this.myTo = myTo; + } + } + + @BeforeEach + void init() { + fooField = new FooField(); + fooProp = new FooProp(); + + fooField.myId = "myId"; + fooProp.myId = "myId"; + + fooField.myKey = "myKey"; + fooProp.myKey = "myKey"; + + fooField.myRev = "myRev"; + fooProp.myRev = "myRev"; + + fooField.myFrom = "myFrom"; + fooProp.myFrom = "myFrom"; + + fooField.myTo = "myTo"; + fooProp.myTo = "myTo"; + } + + @Test + void serializeField() { + // id + testSerialize(fooField, "myId", fooField.myId, this::jacksonSerialize); + testSerialize(fooField, "_id", fooField.myId, this::serdeSerialize); + // key + testSerialize(fooField, "myKey", fooField.myKey, this::jacksonSerialize); + testSerialize(fooField, "_key", fooField.myKey, this::serdeSerialize); + // rev + testSerialize(fooField, "myRev", fooField.myRev, this::jacksonSerialize); + testSerialize(fooField, "_rev", fooField.myRev, this::serdeSerialize); + // from + testSerialize(fooField, "myFrom", fooField.myFrom, this::jacksonSerialize); + testSerialize(fooField, "_from", fooField.myFrom, this::serdeSerialize); + // to + testSerialize(fooField, "myTo", fooField.myTo, this::jacksonSerialize); + testSerialize(fooField, "_to", fooField.myTo, this::serdeSerialize); + } + + @Test + void serializeProp() { + // id + testSerialize(fooProp, "myId", fooProp.myId, this::jacksonSerialize); + testSerialize(fooProp, "_id", fooProp.myId, this::serdeSerialize); + // key + testSerialize(fooProp, "myKey", fooProp.myKey, this::jacksonSerialize); + testSerialize(fooProp, "_key", fooProp.myKey, this::serdeSerialize); + // rev + testSerialize(fooProp, "myRev", fooProp.myRev, this::jacksonSerialize); + testSerialize(fooProp, "_rev", fooProp.myRev, this::serdeSerialize); + // from + testSerialize(fooProp, "myFrom", fooProp.myFrom, this::jacksonSerialize); + testSerialize(fooProp, "_from", fooProp.myFrom, this::serdeSerialize); + // to + testSerialize(fooProp, "myTo", fooProp.myTo, this::jacksonSerialize); + testSerialize(fooProp, "_to", fooProp.myTo, this::serdeSerialize); + } + + @Test + void deserializeField() throws IOException { + // id + testDeserialize("myId", FooField.class, foo -> foo.myId, this::jacksonDeserialize); + testDeserialize("_id", FooField.class, foo -> foo.myId, this::serdeDeserialize); + // key + testDeserialize("myKey", FooField.class, foo -> foo.myKey, this::jacksonDeserialize); + testDeserialize("_key", FooField.class, foo -> foo.myKey, this::serdeDeserialize); + // rev + testDeserialize("myRev", FooField.class, foo -> foo.myRev, this::jacksonDeserialize); + testDeserialize("_rev", FooField.class, foo -> foo.myRev, this::serdeDeserialize); + // from + testDeserialize("myFrom", FooField.class, foo -> foo.myFrom, this::jacksonDeserialize); + testDeserialize("_from", FooField.class, foo -> foo.myFrom, this::serdeDeserialize); + // to + testDeserialize("myTo", FooField.class, foo -> foo.myTo, this::jacksonDeserialize); + testDeserialize("_to", FooField.class, foo -> foo.myTo, this::serdeDeserialize); + } + + @Test + void deserializeProp() throws IOException { + // id + testDeserialize("myId", FooProp.class, FooProp::getMyId, this::jacksonDeserialize); + testDeserialize("_id", FooProp.class, FooProp::getMyId, this::serdeDeserialize); + // key + testDeserialize("myKey", FooProp.class, FooProp::getMyKey, this::jacksonDeserialize); + testDeserialize("_key", FooProp.class, FooProp::getMyKey, this::serdeDeserialize); + // rev + testDeserialize("myRev", FooProp.class, FooProp::getMyRev, this::jacksonDeserialize); + testDeserialize("_rev", FooProp.class, FooProp::getMyRev, this::serdeDeserialize); + // from + testDeserialize("myFrom", FooProp.class, FooProp::getMyFrom, this::jacksonDeserialize); + testDeserialize("_from", FooProp.class, FooProp::getMyFrom, this::serdeDeserialize); + // to + testDeserialize("myTo", FooProp.class, FooProp::getMyTo, this::jacksonDeserialize); + testDeserialize("_to", FooProp.class, FooProp::getMyTo, this::serdeDeserialize); + } + + void testSerialize(Object data, String fieldName, String expectedValue, Function serializer) { + JsonNode jn = serializer.apply(data).get(fieldName); + assertThat(jn).isNotNull(); + assertThat(jn.textValue()).isEqualTo(expectedValue); + } + + void testDeserialize(String fieldName, Class clazz, Function getter, + BiFunction, T> deserializer) throws IOException { + String fieldValue = UUID.randomUUID().toString(); + ObjectNode on = JsonNodeFactory.instance.objectNode().put(fieldName, fieldValue); + byte[] bytes = mapper.writeValueAsBytes(on); + T deser = deserializer.apply(bytes, clazz); + assertThat(getter.apply(deser)).isEqualTo(fieldValue); + } + + private JsonNode jacksonSerialize(Object data) { + try { + return mapper.readTree(mapper.writeValueAsBytes(data)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private JsonNode serdeSerialize(Object data) { + try { + return mapper.readTree(serde.serialize(data)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private T jacksonDeserialize(byte[] bytes, Class clazz) { + try { + return mapper.readValue(bytes, clazz); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private T serdeDeserialize(byte[] bytes, Class clazz) { + return serde.deserialize(bytes, clazz); + } +} diff --git a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/From.java b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/From.java index b84ee1fb0..8a5b12021 100644 --- a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/From.java +++ b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/From.java @@ -1,9 +1,5 @@ package com.arangodb.serde.jackson; -import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -14,8 +10,5 @@ */ @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) -@JacksonAnnotationsInside -@JsonProperty("_from") -@JsonInclude(JsonInclude.Include.NON_NULL) public @interface From { } diff --git a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/Id.java b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/Id.java index ee1033993..da57af859 100644 --- a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/Id.java +++ b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/Id.java @@ -1,9 +1,5 @@ package com.arangodb.serde.jackson; -import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -14,8 +10,5 @@ */ @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) -@JacksonAnnotationsInside -@JsonProperty("_id") -@JsonInclude(JsonInclude.Include.NON_NULL) public @interface Id { } diff --git a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/Key.java b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/Key.java index 94177c44d..a066db02b 100644 --- a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/Key.java +++ b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/Key.java @@ -1,9 +1,5 @@ package com.arangodb.serde.jackson; -import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -14,8 +10,5 @@ */ @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) -@JacksonAnnotationsInside -@JsonProperty("_key") -@JsonInclude(JsonInclude.Include.NON_NULL) public @interface Key { } diff --git a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/Rev.java b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/Rev.java index 764927d6f..71dcac153 100644 --- a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/Rev.java +++ b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/Rev.java @@ -1,9 +1,5 @@ package com.arangodb.serde.jackson; -import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -14,8 +10,5 @@ */ @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) -@JacksonAnnotationsInside -@JsonProperty("_rev") -@JsonInclude(JsonInclude.Include.NON_NULL) public @interface Rev { } diff --git a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/To.java b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/To.java index 7510e2e53..8886a1ef4 100644 --- a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/To.java +++ b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/To.java @@ -1,9 +1,5 @@ package com.arangodb.serde.jackson; -import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -14,8 +10,5 @@ */ @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) -@JacksonAnnotationsInside -@JsonProperty("_to") -@JsonInclude(JsonInclude.Include.NON_NULL) public @interface To { } diff --git a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/internal/ArangoSerdeAnnotationIntrospector.java b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/internal/ArangoSerdeAnnotationIntrospector.java new file mode 100644 index 000000000..d91fb31f6 --- /dev/null +++ b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/internal/ArangoSerdeAnnotationIntrospector.java @@ -0,0 +1,60 @@ +package com.arangodb.serde.jackson.internal; + +import com.arangodb.serde.jackson.*; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyName; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +class ArangoSerdeAnnotationIntrospector extends JacksonAnnotationIntrospector { + private static final JsonInclude JSON_INCLUDE_NON_NULL = JsonIncludeNonNull.class.getAnnotation(JsonInclude.class); + private static final Map, String> MAPPINGS; + private static final Class[] ANNOTATIONS; + + static { + MAPPINGS = new HashMap<>(); + MAPPINGS.put(Id.class, "_id"); + MAPPINGS.put(Key.class, "_key"); + MAPPINGS.put(Rev.class, "_rev"); + MAPPINGS.put(From.class, "_from"); + MAPPINGS.put(To.class, "_to"); + ANNOTATIONS = MAPPINGS.keySet().toArray(new Class[0]); + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + private static class JsonIncludeNonNull { + } + + @Override + public PropertyName findNameForSerialization(Annotated a) { + return Optional.ofNullable(findMapping(a)).orElseGet(() -> super.findNameForSerialization(a)); + } + + @Override + public PropertyName findNameForDeserialization(Annotated a) { + return Optional.ofNullable(findMapping(a)).orElseGet(() -> super.findNameForDeserialization(a)); + } + + private PropertyName findMapping(Annotated a) { + for (Map.Entry, String> e : MAPPINGS.entrySet()) { + if (_hasAnnotation(a, e.getKey())) { + return PropertyName.construct(e.getValue()); + } + } + return null; + } + + @Override + public JsonInclude.Value findPropertyInclusion(Annotated a) { + if (_hasOneOf(a, ANNOTATIONS)) { + return new JsonInclude.Value(JSON_INCLUDE_NON_NULL); + } else { + return super.findPropertyInclusion(a); + } + } +} diff --git a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/internal/JacksonSerdeImpl.java b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/internal/JacksonSerdeImpl.java index 43f3f66be..ab26a0a3c 100644 --- a/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/internal/JacksonSerdeImpl.java +++ b/jackson-serde-json/src/main/java/com/arangodb/serde/jackson/internal/JacksonSerdeImpl.java @@ -18,6 +18,7 @@ public final class JacksonSerdeImpl implements JacksonSerde { public JacksonSerdeImpl(final ObjectMapper mapper) { this.mapper = mapper; mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.setAnnotationIntrospector(new ArangoSerdeAnnotationIntrospector()); } @Override