Repository: johnzon Updated Branches: refs/heads/master f9a916200 -> 5f5b40906
JOHNZON-170 adding polymorphic extension Project: http://git-wip-us.apache.org/repos/asf/johnzon/repo Commit: http://git-wip-us.apache.org/repos/asf/johnzon/commit/5f5b4090 Tree: http://git-wip-us.apache.org/repos/asf/johnzon/tree/5f5b4090 Diff: http://git-wip-us.apache.org/repos/asf/johnzon/diff/5f5b4090 Branch: refs/heads/master Commit: 5f5b40906d97d67d9ee7d9acf970fb5d6a933758 Parents: f9a9162 Author: Romain Manni-Bucau <[email protected]> Authored: Mon Apr 23 20:02:46 2018 +0200 Committer: Romain Manni-Bucau <[email protected]> Committed: Mon Apr 23 20:02:46 2018 +0200 ---------------------------------------------------------------------- johnzon-json-extras/pom.xml | 51 +++++ .../jsonb/extras/polymorphism/Polymorphic.java | 206 +++++++++++++++++++ .../extras/polymorphism/PolymorphicTest.java | 111 ++++++++++ johnzon-jsonb/pom.xml | 1 - .../JohnzonDeserializationContext.java | 2 +- pom.xml | 1 + src/site/markdown/index.md | 48 +++++ 7 files changed, 418 insertions(+), 2 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/johnzon/blob/5f5b4090/johnzon-json-extras/pom.xml ---------------------------------------------------------------------- diff --git a/johnzon-json-extras/pom.xml b/johnzon-json-extras/pom.xml new file mode 100644 index 0000000..c0a4a94 --- /dev/null +++ b/johnzon-json-extras/pom.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>johnzon</artifactId> + <groupId>org.apache.johnzon</groupId> + <version>1.1.8-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>johnzon-jsonb-extras</artifactId> + <name>Johnzon :: JSON-B Extensions</name> + <packaging>bundle</packaging> + + <properties> + <staging.directory>${project.parent.reporting.outputDirectory}</staging.directory> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.geronimo.specs</groupId> + <artifactId>geronimo-jsonb_1.0_spec</artifactId> + <version>1.0</version> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>org.apache.johnzon</groupId> + <artifactId>johnzon-jsonb</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> http://git-wip-us.apache.org/repos/asf/johnzon/blob/5f5b4090/johnzon-json-extras/src/main/java/org/apache/johnzon/jsonb/extras/polymorphism/Polymorphic.java ---------------------------------------------------------------------- diff --git a/johnzon-json-extras/src/main/java/org/apache/johnzon/jsonb/extras/polymorphism/Polymorphic.java b/johnzon-json-extras/src/main/java/org/apache/johnzon/jsonb/extras/polymorphism/Polymorphic.java new file mode 100644 index 0000000..6affc96 --- /dev/null +++ b/johnzon-json-extras/src/main/java/org/apache/johnzon/jsonb/extras/polymorphism/Polymorphic.java @@ -0,0 +1,206 @@ +/* + * 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.extras.polymorphism; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.Objects.requireNonNull; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Stream; + +import javax.json.bind.annotation.JsonbProperty; +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; + +public final class Polymorphic { + private Polymorphic() { + // no-op + } + + private static String getId(final Class<?> type) { + final JsonId mapping = type.getAnnotation(JsonId.class); + if (mapping == null) { + throw new IllegalArgumentException("No @Id on " + type); + } + final String id = mapping.value(); + return id.isEmpty() ? type.getSimpleName() : id; + } + + public static class Serializer<T> implements JsonbSerializer<T> { + private transient volatile ConcurrentMap<Class<?>, String> idMapping = new ConcurrentHashMap<>(); + + @Override + public void serialize(final T obj, final JsonGenerator generator, final SerializationContext ctx) { + ensureInit(); + ctx.serialize(new Wrapper<>(getOrLoadId(obj), obj), generator); + } + + private String getOrLoadId(final T obj) { + final Class<?> type = obj.getClass(); + String id = idMapping.get(type); + if (id == null) { + id = getId(type); + idMapping.putIfAbsent(type, id); + } + return id; + } + + private void ensureInit() { + if (idMapping == null) { + synchronized (this) { + if (idMapping == null) { + idMapping = new ConcurrentHashMap<>(); + } + } + } + } + } + + public static class DeSerializer<T> implements JsonbDeserializer<T> { + private transient volatile ConcurrentMap<String, Type> classMapping = new ConcurrentHashMap<>(); + + @Override + public T deserialize(final JsonParser parser, final DeserializationContext ctx, final Type rtType) { + ensureInit(); + if (classMapping == null || classMapping.isEmpty()) { + synchronized (this) { + if (classMapping == null || classMapping.isEmpty()) { + loadMapping(rtType); + } + } + } + if (!parser.hasNext()) { + return null; + } + eatStartObject(parser); + eatTypeKey(parser); + final String typeId = getTypeValue(parser); + eatValueStart(parser); + final Type type = requireNonNull(classMapping.get(typeId), "No mapping for " + typeId); + parser.next(); + return (T) ctx.deserialize(type, parser); + } + + private void loadMapping(final Type rtType) { + final Class<?> from; + if (ParameterizedType.class.isInstance(rtType)) { + final Type rawType = ParameterizedType.class.cast(rtType).getRawType(); + if (!Class.class.isInstance(rawType)) { + throw new IllegalStateException("Unsupported type: " + rawType); + } + from = Class.class.cast(rawType); + } else if (Class.class.isInstance(rtType)) { + from = Class.class.cast(rtType); + } else { + throw new IllegalStateException("Unsupported type: " + rtType); + } + + final JsonChildren classes = from.getAnnotation(JsonChildren.class); + if (classes == null) { + throw new IllegalArgumentException("No @Classes on " + from); + } + + classMapping.putAll(Stream.of(classes.value()) + .collect(toMap(Polymorphic::getId, identity()))); + } + + private void eatStartObject(final JsonParser parser) { + if (parser.next() != JsonParser.Event.START_OBJECT) { + throw new IllegalArgumentException("Invalid JSON, expected START_OBJECT"); + } + } + + private void eatTypeKey(final JsonParser parser) { + if (!parser.hasNext() || parser.next() != JsonParser.Event.KEY_NAME) { + throw new IllegalArgumentException("Invalid JSON, expected KEY_NAME"); + } + if (!"_type".equals(parser.getString())) { + throw new IllegalArgumentException("Expected key _type"); + } + } + + private void eatValueStart(final JsonParser parser) { + if (!parser.hasNext() || parser.next() != JsonParser.Event.KEY_NAME) { + throw new IllegalArgumentException("Invalid JSON, expected KEY_NAME"); + } + if (!parser.hasNext() || !"_value".equals(parser.getString())) { + throw new IllegalArgumentException("Expected key _value"); + } + } + + private String getTypeValue(final JsonParser parser) { + final JsonParser.Event next = parser.next(); + if (!parser.hasNext() || next != JsonParser.Event.VALUE_STRING) { + throw new IllegalArgumentException("Unexpected event " + next); + } + return parser.getString(); + } + + private void ensureInit() { + if (classMapping == null) { + synchronized (this) { + if (classMapping == null) { + classMapping = new ConcurrentHashMap<>(); + } + } + } + } + } + + public static class Wrapper<T> { + @JsonbProperty("_type") + public String id; + + @JsonbProperty("_value") + public T value; + + private Wrapper(final String id, final T obj) { + this.id = id; + this.value = obj; + } + } + + @Inherited + @Retention(RUNTIME) + public @interface JsonChildren { + /** + * @return the list of leaf classes which can be instantiated by the children. + */ + Class<?>[] value(); + } + + @Target(TYPE) + @Retention(RUNTIME) + public @interface JsonId { + String value() default ""; + } +} http://git-wip-us.apache.org/repos/asf/johnzon/blob/5f5b4090/johnzon-json-extras/src/test/java/org/apache/johnzon/jsonb/extras/polymorphism/PolymorphicTest.java ---------------------------------------------------------------------- diff --git a/johnzon-json-extras/src/test/java/org/apache/johnzon/jsonb/extras/polymorphism/PolymorphicTest.java b/johnzon-json-extras/src/test/java/org/apache/johnzon/jsonb/extras/polymorphism/PolymorphicTest.java new file mode 100644 index 0000000..1b34443 --- /dev/null +++ b/johnzon-json-extras/src/test/java/org/apache/johnzon/jsonb/extras/polymorphism/PolymorphicTest.java @@ -0,0 +1,111 @@ +/* + * 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.extras.polymorphism; + +import static java.util.Arrays.asList; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import java.util.List; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.json.bind.JsonbConfig; +import javax.json.bind.annotation.JsonbTypeDeserializer; +import javax.json.bind.annotation.JsonbTypeSerializer; +import javax.json.bind.config.PropertyOrderStrategy; + +import org.junit.Test; + +public class PolymorphicTest { + private static final String JSON = "{\"root\":{\"_type\":\"first\",\"_value\":{\"name\":\"simple\",\"type\":\"c1\"}}," + + "\"roots\":[{\"_type\":\"first\",\"_value\":{\"name\":\"c-simple\",\"type\":\"c1\"}}," + + "{\"_type\":\"second\",\"_value\":{\"name\":\"c-other\",\"type\":2}}]}"; + + @Test + public void serialize() throws Exception { + final Child1 mainRoot = new Child1(); + mainRoot.type = "c1"; + mainRoot.name = "simple"; + final Child1 roots1 = new Child1(); + roots1.type = "c1"; + roots1.name = "c-simple"; + final Child2 roots2 = new Child2(); + roots2.type = 2; + roots2.name = "c-other"; + final Wrapper wrapper = new Wrapper(); + wrapper.root = mainRoot; + wrapper.roots = asList(roots1, roots2); + + try (final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig().withPropertyOrderStrategy(PropertyOrderStrategy.LEXICOGRAPHICAL))) { + final String json = jsonb.toJson(wrapper); + assertEquals(JSON, json); + } + } + + @Test + public void deserialize() throws Exception { + try (final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig().withPropertyOrderStrategy(PropertyOrderStrategy.LEXICOGRAPHICAL))) { + final Wrapper wrapper = jsonb.fromJson(JSON, Wrapper.class); + assertNotNull(wrapper.root); + assertThat(wrapper.root, instanceOf(Child1.class)); + assertEquals("simple", wrapper.root.name); + assertEquals("c1", Child1.class.cast(wrapper.root).type); + + assertNotNull(wrapper.roots); + assertEquals(2, wrapper.roots.size()); + assertThat(wrapper.roots.get(0), instanceOf(Child1.class)); + assertThat(wrapper.roots.get(1), instanceOf(Child2.class)); + assertEquals("c-simple", wrapper.roots.get(0).name); + assertEquals("c1", Child1.class.cast(wrapper.roots.get(0)).type); + assertEquals("c-other", wrapper.roots.get(1).name); + assertEquals(2, Child2.class.cast(wrapper.roots.get(1)).type); + } + } + + @Polymorphic.JsonChildren({ + Child1.class, + Child2.class + }) + public static abstract class Root { + public String name; + } + + @Polymorphic.JsonId("first") + public static class Child1 extends Root { + public String type; + } + + @Polymorphic.JsonId("second") + public static class Child2 extends Root { + public int type; + } + + public static class Wrapper { + @JsonbTypeSerializer(Polymorphic.Serializer.class) + @JsonbTypeDeserializer(Polymorphic.DeSerializer.class) + public Root root; + + @JsonbTypeSerializer(Polymorphic.Serializer.class) + @JsonbTypeDeserializer(Polymorphic.DeSerializer.class) + public List<Root> roots; + } +} http://git-wip-us.apache.org/repos/asf/johnzon/blob/5f5b4090/johnzon-jsonb/pom.xml ---------------------------------------------------------------------- diff --git a/johnzon-jsonb/pom.xml b/johnzon-jsonb/pom.xml index 87b88be..5bf0a65 100644 --- a/johnzon-jsonb/pom.xml +++ b/johnzon-jsonb/pom.xml @@ -30,7 +30,6 @@ <packaging>bundle</packaging> <properties> - <java-compile.version>1.8</java-compile.version> <staging.directory>${project.parent.reporting.outputDirectory}</staging.directory> </properties> http://git-wip-us.apache.org/repos/asf/johnzon/blob/5f5b4090/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/serializer/JohnzonDeserializationContext.java ---------------------------------------------------------------------- diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/serializer/JohnzonDeserializationContext.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/serializer/JohnzonDeserializationContext.java index 929589c..004b440 100644 --- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/serializer/JohnzonDeserializationContext.java +++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/serializer/JohnzonDeserializationContext.java @@ -45,6 +45,6 @@ public class JohnzonDeserializationContext implements DeserializationContext { } private JsonValue read(final JsonParser parser) { // TODO: use jsonp 1.1 and not johnzon internals - return new JsonReaderImpl(parser).readValue(); + return new JsonReaderImpl(parser, true).readValue(); } } http://git-wip-us.apache.org/repos/asf/johnzon/blob/5f5b4090/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml index f39d348..6f31957 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,7 @@ <module>johnzon-maven-plugin</module> <module>johnzon-websocket</module> <module>johnzon-jsonb</module> + <module>johnzon-json-extras</module> </modules> <dependencies> http://git-wip-us.apache.org/repos/asf/johnzon/blob/5f5b4090/src/site/markdown/index.md ---------------------------------------------------------------------- diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index deb746d..61da87c 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -390,3 +390,51 @@ in `MessageDecoder`. } } + + +### JSON-B Extra + +<pre class="prettyprint linenums"><![CDATA[ +<dependency> + <groupId>org.apache.johnzon</groupId> + <artifactId>johnzon-jsonb-extras</artifactId> + <version>${johnzon.version}</version> +</dependency> +]]></pre> + +This module provides some extension to JSON-B. + +#### Polymorphism + +This extension provides a way to handle polymorphism: + +For the deserialization side you have to list the potential children +on the root class: + + @Polymorphic.JsonChildren({ + Child1.class, + Child2.class + }) + public abstract class Root { + public String name; + } + +Then on children you bind an "id" for each of them (note that if you don't give one, the simple name is used): + + @Polymorphic.JsonId("first") + public class Child1 extends Root { + public String type; + } + +Finally on the field using the root type (polymorphic type) you can +bind the corresponding serializer and/or deserializer: + + public class Wrapper { + @JsonbTypeSerializer(Polymorphic.Serializer.class) + @JsonbTypeDeserializer(Polymorphic.DeSerializer.class) + public Root root; + + @JsonbTypeSerializer(Polymorphic.Serializer.class) + @JsonbTypeDeserializer(Polymorphic.DeSerializer.class) + public List<Root> roots; + } \ No newline at end of file
