This is an automated email from the ASF dual-hosted git repository. rmannibucau pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/johnzon.git
The following commit(s) were added to refs/heads/master by this push: new 56cd895 JOHNZON-290 PolumorphicConfig support 56cd895 is described below commit 56cd8957a2a9d36483eacc97911ff990403a5220 Author: Romain Manni-Bucau <rmannibu...@gmail.com> AuthorDate: Fri Oct 18 10:01:08 2019 +0200 JOHNZON-290 PolumorphicConfig support --- .../org/apache/johnzon/jsonb/JohnzonBuilder.java | 10 ++ .../jsonb/api/experimental/PolymorphicConfig.java | 80 ++++++++++++ .../johnzon/jsonb/PolymorphicConfigTest.java | 140 +++++++++++++++++++++ .../org/apache/johnzon/mapper/MapperBuilder.java | 38 +++++- .../org/apache/johnzon/mapper/MapperConfig.java | 46 ++++++- .../johnzon/mapper/MappingGeneratorImpl.java | 7 +- .../apache/johnzon/mapper/MappingParserImpl.java | 13 +- .../apache/johnzon/mapper/MapperConfigTest.java | 10 +- .../test/java/org/superbiz/ExtendMappingTest.java | 14 +-- 9 files changed, 337 insertions(+), 21 deletions(-) diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java index d33f229..427e493 100644 --- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java +++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonBuilder.java @@ -88,6 +88,7 @@ import org.apache.johnzon.core.AbstractJsonFactory; import org.apache.johnzon.core.JsonGeneratorFactoryImpl; import org.apache.johnzon.core.JsonParserFactoryImpl; import org.apache.johnzon.core.Types; +import org.apache.johnzon.jsonb.api.experimental.PolymorphicConfig; import org.apache.johnzon.jsonb.cdi.CDIs; import org.apache.johnzon.jsonb.converter.JohnzonJsonbAdapter; import org.apache.johnzon.jsonb.factory.SimpleJohnzonAdapterFactory; @@ -167,6 +168,15 @@ public class JohnzonBuilder implements JsonbBuilder { builder.setPretty(true); } + config.getProperty(PolymorphicConfig.class.getName()) + .map(PolymorphicConfig.class::cast) + .ifPresent(pc -> { + builder.setPolymorphicDiscriminator(pc.getDiscriminator()); + builder.setPolymorphicDeserializationPredicate(pc.getDeserializationPredicate()); + builder.setPolymorphicSerializationPredicate(pc.getSerializationPredicate()); + builder.setPolymorphicDiscriminatorMapper(pc.getDiscriminatorMapper()); + builder.setPolymorphicTypeLoader(pc.getTypeLoader()); + }); config.getProperty(JsonbConfig.ENCODING).ifPresent(encoding -> builder.setEncoding(String.valueOf(encoding))); final boolean isNillable = config.getProperty(JsonbConfig.NULL_VALUES) .map(it -> String.class.isInstance(it) ? Boolean.parseBoolean(it.toString()) : Boolean.class.cast(it)) diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/api/experimental/PolymorphicConfig.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/api/experimental/PolymorphicConfig.java new file mode 100644 index 0000000..e127a43 --- /dev/null +++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/api/experimental/PolymorphicConfig.java @@ -0,0 +1,80 @@ +/* + * 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.jsonb.api.experimental; + +import java.util.function.Function; +import java.util.function.Predicate; + +public class PolymorphicConfig { + private Function<String, Class<?>> typeLoader = a -> { + throw new IllegalArgumentException("Unknown alias: '" + a + "'"); + }; + private Function<Class<?>, String> discriminatorMapper = type -> { + throw new IllegalArgumentException("Unknown class '" + type.getName() + "'"); + }; + private Predicate<Class<?>> serializationPredicate = c -> false; + private Predicate<Class<?>> deserializationPredicate = c -> false; + private String discriminator = "@type"; + + public PolymorphicConfig withDeserializationPredicate(final Predicate<Class<?>> deserializationPredicate) { + this.deserializationPredicate = deserializationPredicate; + return this; + } + + public PolymorphicConfig withSerializationPredicate(final Predicate<Class<?>> serializationPredicate) { + this.serializationPredicate = serializationPredicate; + return this; + } + + public PolymorphicConfig withDiscriminatorMapper(final Function<Class<?>, String> discriminatorMapper) { + this.discriminatorMapper = discriminatorMapper; + return this; + } + + // note this prevents @JsonbCreator usage but otherwise the user will do its own mapping with a deserializer + public PolymorphicConfig withTypeLoader(final Function<String, Class<?>> typeLoader) { + this.typeLoader = typeLoader; + return this; + } + + public PolymorphicConfig withDiscriminator(final String value) { + this.discriminator = value; + return this; + } + + public Predicate<Class<?>> getDeserializationPredicate() { + return deserializationPredicate; + } + + public Function<String, Class<?>> getTypeLoader() { + return typeLoader; + } + + public Function<Class<?>, String> getDiscriminatorMapper() { + return discriminatorMapper; + } + + public Predicate<Class<?>> getSerializationPredicate() { + return serializationPredicate; + } + + public String getDiscriminator() { + return discriminator; + } +} diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/PolymorphicConfigTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/PolymorphicConfigTest.java new file mode 100644 index 0000000..de2e5d7 --- /dev/null +++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/PolymorphicConfigTest.java @@ -0,0 +1,140 @@ +/* + * 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.jsonb; + +import static java.util.Arrays.asList; +import static java.util.Locale.ROOT; +import static org.junit.Assert.assertEquals; + +import java.util.List; +import java.util.Objects; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.json.bind.JsonbConfig; +import javax.json.bind.config.PropertyOrderStrategy; + +import org.apache.johnzon.jsonb.api.experimental.PolymorphicConfig; +import org.junit.Test; + +public class PolymorphicConfigTest { + @Test + public void roundTrip() throws Exception { + final Dog dog = new Dog(); + dog.name = "wof"; + + final Cat cat = new Cat(); + cat.otherName = "miaou"; + + final Aggregate aggregate = new Aggregate(); + aggregate.animals = asList(dog, cat); + + final String aggJson = + "{\"animals\":[{\"@type\":\"dog\",\"name\":\"wof\"},{\"@type\":\"cat\",\"otherName\":\"miaou\"}]}"; + final String dogJson = "{\"@type\":\"dog\",\"name\":\"wof\"}"; + try (final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig() + .withPropertyOrderStrategy(PropertyOrderStrategy.LEXICOGRAPHICAL) + .setProperty(PolymorphicConfig.class.getName(), new PolymorphicConfig() + .withDiscriminator("@type") + .withSerializationPredicate(c -> asList(Dog.class, Cat.class).contains(c)) // inline/bad/slow impl ok test + .withDeserializationPredicate(c -> Animal.class == c) + .withDiscriminatorMapper(c -> c.getSimpleName().toLowerCase(ROOT)) + .withTypeLoader(c -> { + switch (c) { + case "dog": + return Dog.class; + case "cat": + return Cat.class; + default: + throw new IllegalArgumentException(c); + } + })))) { + assertEquals(aggJson, jsonb.toJson(aggregate)); + assertEquals(aggregate, jsonb.fromJson(aggJson, Aggregate.class)); + assertEquals(dogJson, jsonb.toJson(dog)); + assertEquals(dog, jsonb.fromJson(dogJson, Dog.class)); + } + } + + public interface Animal { + } + + public static class Dog implements Animal { + public String name; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Dog dog = (Dog) o; + return Objects.equals(name, dog.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + + public static class Cat implements Animal { + public String otherName; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Cat cat = (Cat) o; + return Objects.equals(otherName, cat.otherName); + } + + @Override + public int hashCode() { + return Objects.hash(otherName); + } + } + + public static class Aggregate { + public List<Animal> animals; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Aggregate aggregate = (Aggregate) o; + return animals.equals(aggregate.animals); + } + + @Override + public int hashCode() { + return Objects.hash(animals); + } + } +} diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java index 6eadc0e..3482e5a 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java @@ -70,6 +70,8 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import java.util.function.Predicate; // this class is responsible to hold any needed config // to build the runtime @@ -147,6 +149,13 @@ public class MapperBuilder { private boolean useBigDecimalForObjectNumbers; private boolean supportEnumContainerDeserialization = true; + // @experimental polymorphic api + private Function<String, Class<?>> typeLoader; + private Function<Class<?>, String> discriminatorMapper; + private Predicate<Class<?>> deserializationPredicate; + private Predicate<Class<?>> serializationPredicate; + private String discriminator; + public Mapper build() { if (readerFactory == null || generatorFactory == null) { final JsonProvider provider; @@ -261,7 +270,9 @@ public class MapperBuilder { accessMode, encoding, attributeOrder, enforceQuoteString, failOnUnknownProperties, serializeValueFilter, useBigDecimalForFloats, deduplicateObjects, interfaceImplementationMapping, useJsRange, useBigDecimalForObjectNumbers, - supportEnumContainerDeserialization), + supportEnumContainerDeserialization, + typeLoader, discriminatorMapper, discriminator, + deserializationPredicate, serializationPredicate), closeables); } @@ -538,4 +549,29 @@ public class MapperBuilder { this.supportEnumContainerDeserialization = supportEnumContainerDeserialization; return this; } + + public MapperBuilder setPolymorphicSerializationPredicate(final Predicate<Class<?>> serializationPredicate) { + this.serializationPredicate = serializationPredicate; + return this; + } + + public MapperBuilder setPolymorphicDeserializationPredicate(final Predicate<Class<?>> deserializationPredicate) { + this.deserializationPredicate = deserializationPredicate; + return this; + } + + public MapperBuilder setPolymorphicDiscriminatorMapper(final Function<Class<?>, String> discriminatorMapper) { + this.discriminatorMapper = discriminatorMapper; + return this; + } + + public MapperBuilder setPolymorphicTypeLoader(final Function<String, Class<?>> typeLoader) { + this.typeLoader = typeLoader; + return this; + } + + public MapperBuilder setPolymorphicDiscriminator(final String value) { + this.discriminator = value; + return this; + } } diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java index 1f3eafb..fb9f609 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperConfig.java @@ -31,6 +31,8 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import java.util.function.Predicate; /** * Contains internal configuration for all the mapper stuff. @@ -75,6 +77,12 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable { private final Map<Class<?>, Class<?>> interfaceImplementationMapping; private final boolean useBigDecimalForObjectNumbers; + private final Function<String, Class<?>> typeLoader; + private final Function<Class<?>, String> discriminatorMapper; + private final Predicate<Class<?>> serializationPredicate; + private final Predicate<Class<?>> deserializationPredicate; + private final String discriminator; + private final Map<Class<?>, ObjectConverter.Writer<?>> objectConverterWriterCache; private final Map<Class<?>, ObjectConverter.Reader<?>> objectConverterReaderCache; @@ -96,7 +104,12 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable { final Map<Class<?>, Class<?>> interfaceImplementationMapping, final boolean useJsRange, final boolean useBigDecimalForObjectNumbers, - final boolean supportEnumMapDeserialization) { + final boolean supportEnumMapDeserialization, + final Function<String, Class<?>> typeLoader, + final Function<Class<?>, String> discriminatorMapper, + final String discriminator, + final Predicate<Class<?>> deserializationPredicate, + final Predicate<Class<?>> serializationPredicate) { //CHECKSTYLE:ON this.objectConverterWriters = objectConverterWriters; this.objectConverterReaders = objectConverterReaders; @@ -112,11 +125,16 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable { this.useJsRange = useJsRange; this.useBigDecimalForObjectNumbers = useBigDecimalForObjectNumbers; this.supportEnumMapDeserialization = supportEnumMapDeserialization; + this.typeLoader = typeLoader; + this.discriminatorMapper = discriminatorMapper; + this.serializationPredicate = serializationPredicate; + this.deserializationPredicate = deserializationPredicate; + this.discriminator = discriminator; // handle Adapters this.adapters = adapters; this.reverseAdapters = new ConcurrentHashMap<>(adapters.size()); - adapters.entrySet().forEach(e -> this.reverseAdapters.put(e.getValue(), e.getKey())); + adapters.forEach((k, v) -> this.reverseAdapters.put(v, k)); this.attributeOrder = attributeOrder; @@ -125,12 +143,32 @@ public /* DON'T MAKE IT HIDDEN */ class MapperConfig implements Cloneable { this.serializeValueFilter = serializeValueFilter == null ? (name, value) -> false : serializeValueFilter; this.interfaceImplementationMapping = interfaceImplementationMapping; - this.objectConverterWriterCache = new HashMap<Class<?>, ObjectConverter.Writer<?>>(objectConverterWriters.size()); - this.objectConverterReaderCache = new HashMap<Class<?>, ObjectConverter.Reader<?>>(objectConverterReaders.size()); + this.objectConverterWriterCache = new HashMap<>(objectConverterWriters.size()); + this.objectConverterReaderCache = new HashMap<>(objectConverterReaders.size()); this.useBigDecimalForFloats = useBigDecimalForFloats; this.deduplicateObjects = deduplicateObjects; } + public Function<String, Class<?>> getTypeLoader() { + return typeLoader; + } + + public Function<Class<?>, String> getDiscriminatorMapper() { + return discriminatorMapper; + } + + public Predicate<Class<?>> getDeserializationPredicate() { + return deserializationPredicate; + } + + public Predicate<Class<?>> getSerializationPredicate() { + return serializationPredicate; + } + + public String getDiscriminator() { + return discriminator; + } + public boolean isUseBigDecimalForObjectNumbers() { return useBigDecimalForObjectNumbers; } 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 b60bea5..d2fe205 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 @@ -178,7 +178,12 @@ public class MappingGeneratorImpl implements MappingGenerator { if (writeBody) { generator.writeStartObject(); } - doWriteObjectBody(object, ignoredProperties, jsonPointer, generator); + if (config.getSerializationPredicate() != null && config.getSerializationPredicate().test(objectClass)) { + generator.write(config.getDiscriminator(), config.getDiscriminatorMapper().apply(objectClass)); + doWriteObjectBody(object, ignoredProperties, jsonPointer, generator); + } else { + doWriteObjectBody(object, ignoredProperties, jsonPointer, generator); + } if (writeBody) { generator.writeEnd(); } 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 6c16b72..a5ed7b3 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 @@ -238,7 +238,8 @@ public class MappingParserImpl implements MappingParser { } - private Object buildObject(final Type inType, final JsonObject object, final boolean applyObjectConverter, JsonPointerTracker jsonPointer) { + private Object buildObject(final Type inType, final JsonObject object, final boolean applyObjectConverter, + final JsonPointerTracker jsonPointer) { Type type = inType; if (inType == Object.class) { type = new JohnzonParameterizedType(Map.class, String.class, Object.class); @@ -256,6 +257,16 @@ public class MappingParserImpl implements MappingParser { } } + if (config.getDeserializationPredicate() != null && Class.class.isInstance(inType)) { + final Class<?> clazz = Class.class.cast(inType); + if (config.getDeserializationPredicate().test(clazz) && object.containsKey(config.getDiscriminator())) { + final String discriminator = object.getString(config.getDiscriminator()); + final Class<?> nestedType = config.getTypeLoader().apply(discriminator); + if (nestedType != null && nestedType != inType) { + return buildObject(nestedType, object, applyObjectConverter, jsonPointer); + } + } + } final Mappings.ClassMapping classMapping = mappings.findOrCreateClassMapping(type); if (classMapping == null) { diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperConfigTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperConfigTest.java index 3becb92..f0f4020 100644 --- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperConfigTest.java +++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperConfigTest.java @@ -21,14 +21,13 @@ package org.apache.johnzon.mapper; import static java.util.Collections.emptyMap; import org.apache.johnzon.mapper.access.FieldAccessMode; -import org.apache.johnzon.mapper.internal.AdapterKey; import org.junit.Assert; import org.junit.Test; import javax.json.JsonValue; import java.lang.reflect.Type; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -157,7 +156,7 @@ public class MapperConfigTest { private MapperConfig createConfig(Map<Class<?>, ObjectConverter.Codec<?>> converter) { - return new MapperConfig(new ConcurrentHashMap<AdapterKey, Adapter<?, ?>>(0), + return new MapperConfig(new ConcurrentHashMap<>(0), Map.class.cast(converter), Map.class.cast(converter), -1, true, @@ -167,9 +166,10 @@ public class MapperConfigTest { false, false, new FieldAccessMode(true, true), - Charset.forName("UTF-8"), + StandardCharsets.UTF_8, null, - false, false, null, false, false, emptyMap(), true, false, true); + false, false, null, false, false, emptyMap(), true, false, true, + null, null, null, null, null); } diff --git a/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java b/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java index 559302a..8e5264c 100644 --- a/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java +++ b/johnzon-mapper/src/test/java/org/superbiz/ExtendMappingTest.java @@ -18,16 +18,13 @@ */ package org.superbiz; -import org.apache.johnzon.mapper.Adapter; import org.apache.johnzon.mapper.MapperConfig; import org.apache.johnzon.mapper.Mappings; -import org.apache.johnzon.mapper.ObjectConverter; import org.apache.johnzon.mapper.access.FieldAccessMode; -import org.apache.johnzon.mapper.internal.AdapterKey; import org.junit.Test; import java.lang.reflect.Type; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -60,13 +57,12 @@ public class ExtendMappingTest { public static class MyMappings extends Mappings { public MyMappings() { super(new MapperConfig( - new ConcurrentHashMap<AdapterKey, Adapter<?, ?>>(), - new HashMap<Class<?>, ObjectConverter.Writer<?>>(), - new HashMap<Class<?>, ObjectConverter.Reader<?>>(), + new ConcurrentHashMap<>(), new HashMap<>(), new HashMap<>(), -1, true, true, true, false, false, false, new FieldAccessMode(false, false), - Charset.forName("UTF-8"), String::compareTo, false, false, null, false, false, - emptyMap(), true, false, true)); + StandardCharsets.UTF_8, String::compareTo, false, false, null, false, false, + emptyMap(), true, false, true, + null, null, null, null, null)); } @Override