Repository: johnzon Updated Branches: refs/heads/master 52f0aab8e -> 2ca9baf80
JOHNZON-143 add @JohnzonDeduplicateObjects support This allows to enable deduplicateObjects via an annotation on the root object. See JOHNZON-135 for the underlying functionality. Project: http://git-wip-us.apache.org/repos/asf/johnzon/repo Commit: http://git-wip-us.apache.org/repos/asf/johnzon/commit/2ca9baf8 Tree: http://git-wip-us.apache.org/repos/asf/johnzon/tree/2ca9baf8 Diff: http://git-wip-us.apache.org/repos/asf/johnzon/diff/2ca9baf8 Branch: refs/heads/master Commit: 2ca9baf80a47c698a32dbbd294d27bc2ded5a3cd Parents: 52f0aab Author: Mark Struberg <[email protected]> Authored: Thu Nov 2 14:22:00 2017 +0100 Committer: Mark Struberg <[email protected]> Committed: Thu Nov 2 14:22:00 2017 +0100 ---------------------------------------------------------------------- .../mapper/JohnzonDeduplicateObjects.java | 39 +++++++++ .../java/org/apache/johnzon/mapper/Mapper.java | 24 ++++-- .../johnzon/mapper/MappingGeneratorImpl.java | 20 +++-- .../johnzon/mapper/MappingParserImpl.java | 35 +++++--- .../org/apache/johnzon/mapper/Mappings.java | 15 ++++ .../johnzon/mapper/CircularObjectsTest.java | 84 ++++++++++++++++++++ 6 files changed, 193 insertions(+), 24 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/johnzon/blob/2ca9baf8/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/JohnzonDeduplicateObjects.java ---------------------------------------------------------------------- diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/JohnzonDeduplicateObjects.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/JohnzonDeduplicateObjects.java new file mode 100644 index 0000000..cf95596 --- /dev/null +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/JohnzonDeduplicateObjects.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.johnzon.mapper; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark an Object to leverage object deduplication without having + * to explicitly enable it in the Mapper or JsonB Builder. + * + * The feature only gets activated if this annotation is available + * on the root object to be serialised/deserialised. + * + * @see MapperBuilder#setDeduplicateObjects(boolean) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface JohnzonDeduplicateObjects { + boolean value() default true; +} http://git-wip-us.apache.org/repos/asf/johnzon/blob/2ca9baf8/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java ---------------------------------------------------------------------- diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java index 7d411c3..29bf3fd 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java @@ -86,7 +86,7 @@ public class Mapper implements Closeable { public <T> void writeArray(final Collection<T> object, final Writer stream) { JsonGenerator generator = generatorFactory.createGenerator(stream(stream)); - writeObject(object, generator, null, new JsonPointerTracker(null, "/")); + writeObject(object, generator, null, config.isDeduplicateObjects() ? new JsonPointerTracker(null, "/") : null); } public <T> void writeIterable(final Iterable<T> object, final OutputStream stream) { @@ -126,16 +126,30 @@ public class Mapper implements Closeable { } final JsonGenerator generator = generatorFactory.createGenerator(stream(stream)); - writeObject(object, generator, null, config.isDeduplicateObjects() ? new JsonPointerTracker(null, "/") : null); + + writeObject(object, generator, null, isDeduplicateObjects(object.getClass()) ? new JsonPointerTracker(null, "/") : null); + } + + private boolean isDeduplicateObjects(Class<?> rootType) { + Boolean dedup = null; + Mappings.ClassMapping classMapping = mappings.findOrCreateClassMapping(rootType); + if (classMapping != null) { + dedup = classMapping.isDeduplicateObjects(); + } + if (dedup == null) { + dedup = config.isDeduplicateObjects(); + } + + return dedup; } public void writeObject(final Object object, final OutputStream stream) { final JsonGenerator generator = generatorFactory.createGenerator(stream(stream), config.getEncoding()); - writeObject(object, generator, null, config.isDeduplicateObjects() ? new JsonPointerTracker(null, "/") : null); + writeObject(object, generator, null, isDeduplicateObjects(object.getClass()) ? new JsonPointerTracker(null, "/") : null); } private void writeObject(final Object object, final JsonGenerator generator, final Collection<String> ignored, JsonPointerTracker jsonPointer) { - MappingGeneratorImpl mappingGenerator = new MappingGeneratorImpl(config, generator, mappings); + MappingGeneratorImpl mappingGenerator = new MappingGeneratorImpl(config, generator, mappings, jsonPointer != null); Throwable originalException = null; try { @@ -234,7 +248,7 @@ public class Mapper implements Closeable { private <T> T mapObject(final Type clazz, final JsonReader reader) { - return new MappingParserImpl(config, mappings, reader).readObject(clazz); + return new MappingParserImpl(config, mappings, reader, clazz).readObject(clazz); } http://git-wip-us.apache.org/repos/asf/johnzon/blob/2ca9baf8/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java ---------------------------------------------------------------------- diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java index 7ecf2b3..1ed6a3d 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java @@ -37,15 +37,19 @@ public class MappingGeneratorImpl implements MappingGenerator { private final MapperConfig config; private final JsonGenerator generator; private final Mappings mappings; + + private final Boolean isDeduplicateObjects; private Map<Object, String> jsonPointers; - MappingGeneratorImpl(MapperConfig config, JsonGenerator jsonGenerator, final Mappings mappings) { + MappingGeneratorImpl(MapperConfig config, JsonGenerator jsonGenerator, final Mappings mappings, Boolean isDeduplicateObjects) { this.config = config; this.generator = jsonGenerator; this.mappings = mappings; - this.jsonPointers = config.isDeduplicateObjects() ? new HashMap<>() : Collections.emptyMap(); + this.isDeduplicateObjects = isDeduplicateObjects; + + this.jsonPointers = isDeduplicateObjects ? new HashMap<>() : Collections.emptyMap(); } @Override @@ -60,7 +64,7 @@ public class MappingGeneratorImpl implements MappingGenerator { } else if (object instanceof JsonValue) { generator.write((JsonValue) object); } else { - doWriteObject(object, generator, false, null, config.isDeduplicateObjects() ? new JsonPointerTracker(null, "/") : null); + doWriteObject(object, generator, false, null, isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null); } return this; } @@ -294,7 +298,7 @@ public class MappingGeneratorImpl implements MappingGenerator { val, getter.objectConverter, getter.ignoreNested, - config.isDeduplicateObjects() ? new JsonPointerTracker(jsonPointer, getterEntry.getKey()) : null); + isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, getterEntry.getKey()) : null); } } @@ -344,7 +348,7 @@ public class MappingGeneratorImpl implements MappingGenerator { if (valJsonPointer != null) { writePrimitives(valJsonPointer); } else { - writeItem(itemConverter != null ? itemConverter.from(o) : o, ignoredProperties, config.isDeduplicateObjects() ? new JsonPointerTracker(jsonPointer, i) : null); + writeItem(itemConverter != null ? itemConverter.from(o) : o, ignoredProperties, isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null); } } generator.writeEnd(); @@ -369,7 +373,7 @@ public class MappingGeneratorImpl implements MappingGenerator { generator.writeEnd(); } else { writeItem(itemConverter != null ? itemConverter.from(o) : o, ignoredProperties, - config.isDeduplicateObjects() ? new JsonPointerTracker(jsonPointer, i) : null); + isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null); } } i++; @@ -428,7 +432,7 @@ public class MappingGeneratorImpl implements MappingGenerator { if (t == null) { generator.writeNull(); } else { - writeItem(t, ignoredProperties, config.isDeduplicateObjects() ? new JsonPointerTracker(jsonPointer, i) : null); + writeItem(t, ignoredProperties, isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null); } } generator.writeEnd(); @@ -460,7 +464,7 @@ public class MappingGeneratorImpl implements MappingGenerator { if (t == null) { generator.writeNull(); } else { - writeItem(t, ignoredProperties, config.isDeduplicateObjects() ? new JsonPointerTracker(jsonPointer, i) : null); + writeItem(t, ignoredProperties, isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null); } } i++; http://git-wip-us.apache.org/repos/asf/johnzon/blob/2ca9baf8/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java ---------------------------------------------------------------------- diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java index 41731d4..ce604bd 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java @@ -34,6 +34,7 @@ import javax.json.JsonString; import javax.json.JsonStructure; import javax.json.JsonValue; import javax.xml.bind.DatatypeConverter; + import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -88,6 +89,7 @@ public class MappingParserImpl implements MappingParser { private final MapperConfig config; private final Mappings mappings; + private final boolean isDeduplicateObjects; private final JsonReader jsonReader; @@ -99,7 +101,7 @@ public class MappingParserImpl implements MappingParser { private Map<String, Object> jsonPointers; - public MappingParserImpl(MapperConfig config, Mappings mappings, JsonReader jsonReader) { + public MappingParserImpl(MapperConfig config, Mappings mappings, JsonReader jsonReader, Type rootType) { this.config = config; this.mappings = mappings; @@ -107,7 +109,18 @@ public class MappingParserImpl implements MappingParser { reverseAdaptersRegistry = new ConcurrentHashMap<>(config.getAdapters().size()); - if (config.isDeduplicateObjects()) { + + Boolean dedup = null; + Mappings.ClassMapping classMapping = mappings.findOrCreateClassMapping(rootType); + if (classMapping != null) { + dedup = classMapping.isDeduplicateObjects(); + } + if (dedup == null) { + dedup = config.isDeduplicateObjects(); + } + this.isDeduplicateObjects = dedup; + + if (isDeduplicateObjects) { jsonPointers = new HashMap<>(); } else { jsonPointers = Collections.emptyMap(); @@ -138,7 +151,7 @@ public class MappingParserImpl implements MappingParser { return (T) jsonValue; } if (JsonObject.class.isInstance(jsonValue)) { - return (T) buildObject(targetType, JsonObject.class.cast(jsonValue), applyObjectConverter, config.isDeduplicateObjects() ? new JsonPointerTracker(null, "/") : null); + return (T) buildObject(targetType, JsonObject.class.cast(jsonValue), applyObjectConverter, isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null); } if (JsonString.class.isInstance(jsonValue) && (targetType == String.class || targetType == Object.class)) { return (T) JsonString.class.cast(jsonValue).getString(); @@ -168,7 +181,7 @@ public class MappingParserImpl implements MappingParser { if (Class.class.isInstance(targetType) && ((Class) targetType).isArray()) { final Class componentType = ((Class) targetType).getComponentType(); return (T) buildArrayWithComponentType(jsonArray, componentType, config.findAdapter(componentType), - config.isDeduplicateObjects() ? new JsonPointerTracker(null, "/") : null); + isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null); } if (ParameterizedType.class.isInstance(targetType)) { @@ -180,11 +193,11 @@ public class MappingParserImpl implements MappingParser { final Type arg = pt.getActualTypeArguments()[0]; return (T) mapCollection(mapping, jsonArray, Class.class.isInstance(arg) ? config.findAdapter(Class.class.cast(arg)) : null, - null, config.isDeduplicateObjects() ? new JsonPointerTracker(null, "/") : null); + null, isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null); } if (Object.class == targetType) { return (T) new ArrayList(asList(Object[].class.cast(buildArrayWithComponentType(jsonArray, Object.class, null, - config.isDeduplicateObjects() ? new JsonPointerTracker(null, "/") : null)))); + isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null)))); } } if (JsonValue.NULL.equals(jsonValue)) { @@ -305,7 +318,7 @@ public class MappingParserImpl implements MappingParser { t = classMapping.factory.create(createParameters(classMapping, object, jsonPointer)); } // store the new object under it's jsonPointer in case it gets referenced later - if (config.isDeduplicateObjects()) { + if (isDeduplicateObjects) { jsonPointers.put(jsonPointer.toString(), t); } @@ -348,7 +361,7 @@ public class MappingParserImpl implements MappingParser { if (!classMapping.setters.containsKey(key)) { try { classMapping.anySetter.invoke(t, key, toValue(null, entry.getValue(), null, null, Object.class, null, - config.isDeduplicateObjects() ? new JsonPointerTracker(jsonPointer, entry.getKey()) : null)); + isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, entry.getKey()) : null)); } catch (final IllegalAccessException e) { throw new IllegalStateException(e); } catch (final InvocationTargetException e) { @@ -602,7 +615,7 @@ public class MappingParserImpl implements MappingParser { int i = 0; for (final JsonValue value : jsonArray) { Array.set(array, i, toObject(null, value, componentType, itemConverter, - config.isDeduplicateObjects() ? new JsonPointerTracker(jsonPointer, i) : null)); + isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null)); i++; } return array; @@ -635,7 +648,7 @@ public class MappingParserImpl implements MappingParser { collection.add(JsonValue.NULL.equals(value) ? null : toValue(null, value, null, itemConverter, mapping.arg, objectConverter, - config.isDeduplicateObjects() ? new JsonPointerTracker(jsonPointer, i) : null)); + isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null)); i++; } @@ -667,7 +680,7 @@ public class MappingParserImpl implements MappingParser { mapping.factory.getParameterItemConverter()[i], mapping.factory.getParameterTypes()[i], mapping.factory.getObjectConverter()[i], - config.isDeduplicateObjects() ? new JsonPointerTracker(jsonPointer, paramName) : null); //X TODO ObjectConverter in @JOhnzonConverter with Constructors! + isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, paramName) : null); //X TODO ObjectConverter in @JOhnzonConverter with Constructors! } return objects; http://git-wip-us.apache.org/repos/asf/johnzon/blob/2ca9baf8/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java ---------------------------------------------------------------------- diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java index 51ae62c..c8d63fe 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java @@ -65,6 +65,9 @@ public class Mappings { public final Getter anyGetter; public final Method anySetter; + private Boolean deduplicateObjects; + private boolean deduplicationEvaluated = false; + protected ClassMapping(final Class<?> clazz, final AccessMode.Factory factory, final Map<String, Getter> getters, final Map<String, Setter> setters, final Adapter<?, ?> adapter, @@ -80,6 +83,18 @@ public class Mappings { this.anyGetter = anyGetter; this.anySetter = anySetter; } + + public Boolean isDeduplicateObjects() { + if (!deduplicationEvaluated) { + JohnzonDeduplicateObjects jdo = ((Class<JohnzonDeduplicateObjects>) clazz).getAnnotation(JohnzonDeduplicateObjects.class); + if (jdo != null){ + deduplicateObjects = jdo.value(); + } + deduplicationEvaluated = true; + } + return deduplicateObjects; + } + } public static class CollectionMapping { http://git-wip-us.apache.org/repos/asf/johnzon/blob/2ca9baf8/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularObjectsTest.java ---------------------------------------------------------------------- diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularObjectsTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularObjectsTest.java index c473c6d..b4c8af6 100644 --- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularObjectsTest.java +++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularObjectsTest.java @@ -63,6 +63,34 @@ public class CircularObjectsTest { } @Test + public void testSimpleCyclicPersonAnnotatedDedup() { + DeduplicatedPerson john = new DeduplicatedPerson("John"); + DeduplicatedPerson marry = new DeduplicatedPerson("Marry"); + + john.setMarriedTo(marry); + marry.setMarriedTo(john); + + Mapper mapper = new MapperBuilder().setAccessModeName("field").build(); + String ser = mapper.writeObjectAsString(john); + + assertNotNull(ser); + assertTrue(ser.contains("\"name\":\"John\"")); + assertTrue(ser.contains("\"marriedTo\":\"/\"")); + assertTrue(ser.contains("\"name\":\"Marry\"")); + + // and now de-serialise it back + DeduplicatedPerson john2 = mapper.readObject(ser, DeduplicatedPerson.class); + assertNotNull(john2); + assertEquals("John", john2.getName()); + + DeduplicatedPerson marry2 = john2.getMarriedTo(); + assertNotNull(marry2); + assertEquals("Marry", marry2.getName()); + + assertEquals(john2, marry2.getMarriedTo()); + } + + @Test public void testComplexCyclicPerson() { Person karl = new Person("Karl"); Person andrea = new Person("Andrea"); @@ -211,4 +239,60 @@ public class CircularObjectsTest { } } + @JohnzonDeduplicateObjects + public static class DeduplicatedPerson { + private String name; + private DeduplicatedPerson marriedTo; + private DeduplicatedPerson mother; + private DeduplicatedPerson father; + private List<DeduplicatedPerson> kids = new ArrayList<>(); + + public DeduplicatedPerson() { + } + + public DeduplicatedPerson(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public DeduplicatedPerson getMarriedTo() { + return marriedTo; + } + + public void setMarriedTo(DeduplicatedPerson marriedTo) { + this.marriedTo = marriedTo; + } + + public DeduplicatedPerson getMother() { + return mother; + } + + public void setMother(DeduplicatedPerson mother) { + this.mother = mother; + } + + public DeduplicatedPerson getFather() { + return father; + } + + public void setFather(DeduplicatedPerson father) { + this.father = father; + } + + public List<DeduplicatedPerson> getKids() { + return kids; + } + + public void setKids(List<DeduplicatedPerson> kids) { + this.kids = kids; + } + } + }
