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 aaa2af9 [JOHNZON-337] avoid jsonb deserializers to loop aaa2af9 is described below commit aaa2af985b51517f6d636d3a28b176f3109c5958 Author: Romain Manni-Bucau <rmannibu...@gmail.com> AuthorDate: Fri Mar 5 14:41:14 2021 +0100 [JOHNZON-337] avoid jsonb deserializers to loop --- .../apache/johnzon/RecursivePolymorphismTest.java | 131 +++++++++++++++++++++ .../org/apache/johnzon/mapper/MappingParser.java | 6 +- .../apache/johnzon/mapper/MappingParserImpl.java | 66 +++++++---- 3 files changed, 182 insertions(+), 21 deletions(-) diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/RecursivePolymorphismTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/RecursivePolymorphismTest.java new file mode 100644 index 0000000..75996ca --- /dev/null +++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/RecursivePolymorphismTest.java @@ -0,0 +1,131 @@ +/* + * 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; + +import org.junit.Test; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.json.bind.JsonbConfig; +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 java.lang.reflect.Type; + +import static javax.json.stream.JsonParser.Event.KEY_NAME; +import static javax.json.stream.JsonParser.Event.START_OBJECT; +import static javax.json.stream.JsonParser.Event.VALUE_NUMBER; +import static org.junit.Assert.assertEquals; + +public class RecursivePolymorphismTest { + @Test + public void read() throws Exception { + try (final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig() + .withDeserializers(new PolyDeserializer()))) { + final Parent parent = jsonb.fromJson("{\"type\":1,\"name\":\"first\",\"uno\":true,\"duo\":true}", Parent.class); + assertEquals("Child1{name='first', uno=true}", parent.toString()); + } + } + + @Test + public void write() throws Exception { + final Child1 child1 = new Child1(); + child1.name = "first"; + child1.uno = true; + try (final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig() + .withSerializers(new PolySerializer()))) { + final String json = jsonb.toJson(child1); + assertEquals("{\"type\":1,\"name\":\"first\",\"uno\":true}", json); + } + } + + public static class Parent { + public String name; + + @Override + public String toString() { + return "Parent{name='" + name + "'}"; + } + } + + public static class Child1 extends Parent { + public boolean uno; + + @Override + public String toString() { + return "Child1{name='" + name + "', uno=" + uno + '}'; + } + } + + public static class Child2 extends Parent { + public boolean duo; + + @Override + public String toString() { + return "Child2{name='" + name + "', duo=" + duo + '}'; + } + } + + public static class PolySerializer implements JsonbSerializer<Parent> { + @Override + public void serialize(final Parent obj, final JsonGenerator generator, final SerializationContext ctx) { + generator.writeStartObject(); + generator.write("type", Child1.class.isInstance(obj) ? 1 : 2); + ctx.serialize(obj, generator); + generator.writeEnd(); + } + } + + public static class PolyDeserializer implements JsonbDeserializer<Parent> { + @Override + public Parent deserialize(final JsonParser parser, + final DeserializationContext ctx, + final Type rtType) { + moveToType(parser); + final int type = parser.getInt(); + switch (type) { + case 1: + return ctx.deserialize(Child1.class, parser); + case 2: + return ctx.deserialize(Child2.class, parser); + default: + throw new IllegalArgumentException(String.valueOf(type)); + } + } + + private void moveToType(final JsonParser parser) { + ensureNext(parser, START_OBJECT); + ensureNext(parser, KEY_NAME); + if (!"type".equals(parser.getString())) { + throw new IllegalArgumentException("Expected 'type' but got '" + parser.getString() + "'"); + } + ensureNext(parser, VALUE_NUMBER); + } + + private void ensureNext(final JsonParser parser, final JsonParser.Event expected) { + final JsonParser.Event next = parser.next(); + if (expected != next) { + throw new IllegalArgumentException(next + " != " + expected); + } + } + } +} diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParser.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParser.java index c129264..7a265a6 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParser.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParser.java @@ -20,14 +20,18 @@ package org.apache.johnzon.mapper; import javax.json.JsonValue; import java.lang.reflect.Type; +import java.util.Collection; /** * Handles reading Json for Objects. - * */ public interface MappingParser { <T> T readObject(Type targetType); <T> T readObject(JsonValue jsonValue, Type targetType); + + default Collection<Class<?>> getSkippedConverters() { + return null; + } } 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 71e0848..80ca4ad 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 @@ -136,18 +136,22 @@ public class MappingParserImpl implements MappingParser { } @Override - public <T> T readObject(JsonValue jsonValue, Type targetType) { - return readObject(jsonValue, targetType, targetType instanceof Class || targetType instanceof ParameterizedType); + public <T> T readObject(final JsonValue jsonValue, final Type targetType) { + return readObject(jsonValue, targetType, targetType instanceof Class || targetType instanceof ParameterizedType, null); } - private <T> T readObject(JsonValue jsonValue, Type targetType, boolean applyObjectConverter) { + public <T> T readObject(final JsonValue jsonValue, final Type targetType, final boolean applyObjectConverter, + final Collection<Class<?>> skippedConverters) { final JsonValue.ValueType valueType = jsonValue != null ? jsonValue.getValueType() : null; if (JsonStructure.class == targetType || JsonObject.class == targetType || JsonValue.class == targetType) { return (T) jsonValue; } if (JsonObject.class.isInstance(jsonValue)) { - return (T) buildObject(targetType, JsonObject.class.cast(jsonValue), applyObjectConverter, isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null); + return (T) buildObject( + targetType, JsonObject.class.cast(jsonValue), applyObjectConverter, + isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, + skippedConverters); } if (JsonString.class.isInstance(jsonValue)) { if ((targetType == String.class || targetType == Object.class)) { @@ -221,7 +225,7 @@ public class MappingParserImpl implements MappingParser { isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, Object.class); } if (Collection.class.isAssignableFrom(asClass)) { - return readObject(jsonValue, new JohnzonParameterizedType(asClass, Object.class), applyObjectConverter); + return readObject(jsonValue, new JohnzonParameterizedType(asClass, Object.class), applyObjectConverter, skippedConverters); } } if (ParameterizedType.class.isInstance(targetType)) { @@ -256,7 +260,7 @@ public class MappingParserImpl implements MappingParser { private Object buildObject(final Type inType, final JsonObject object, final boolean applyObjectConverter, - final JsonPointerTracker jsonPointer) { + final JsonPointerTracker jsonPointer, final Collection<Class<?>> skippedConverters) { Type type = inType; if (inType == Object.class) { type = new JohnzonParameterizedType(Map.class, String.class, Object.class); @@ -268,9 +272,16 @@ public class MappingParserImpl implements MappingParser { throw new MapperException("ObjectConverters are only supported for Classes not Types"); } - ObjectConverter.Reader objectConverter = config.findObjectConverterReader((Class) type); - if (objectConverter != null) { - return objectConverter.fromJson(object, type, new SuppressConversionMappingParser(this, object)); + final Class clazz = (Class) type; + if (skippedConverters == null || !skippedConverters.contains(clazz)) { + ObjectConverter.Reader objectConverter = config.findObjectConverterReader(clazz); + if (objectConverter != null) { + final Collection<Class<?>> skipped = skippedConverters == null ? new ArrayList<>() : skippedConverters; + skipped.add(clazz); + return objectConverter.fromJson( + object, type, + new SuppressConversionMappingParser(this, object, skipped)); + } } } @@ -280,7 +291,7 @@ public class MappingParserImpl implements MappingParser { 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); + return buildObject(nestedType, object, applyObjectConverter, jsonPointer, skippedConverters); } } } @@ -346,8 +357,12 @@ public class MappingParserImpl implements MappingParser { throw new MapperException("Can't map " + type); } - if (applyObjectConverter && classMapping.reader != null) { - return classMapping.reader.fromJson(object, type, new SuppressConversionMappingParser(this, object)); + if (applyObjectConverter && classMapping.reader != null && (skippedConverters == null || !skippedConverters.contains(type))) { + final Collection<Class<?>> skipped = skippedConverters == null ? new ArrayList<>() : skippedConverters; + if (Class.class.isInstance(type)) { // more than likely, drop this check? + skipped.add(Class.class.cast(type)); + } + return classMapping.reader.fromJson(object, type, new SuppressConversionMappingParser(this, object, skipped)); } /* doesn't work yet if (classMapping.adapter != null) { @@ -502,7 +517,7 @@ public class MappingParserImpl implements MappingParser { final Object param; try { Type to = key.getTo(); - param = buildObject(to, JsonObject.class.cast(jsonValue), to instanceof Class, jsonPointer); + param = buildObject(to, JsonObject.class.cast(jsonValue), to instanceof Class, jsonPointer, getSkippedConverters()); } catch (final Exception e) { throw new MapperException(e); } @@ -648,7 +663,7 @@ public class MappingParserImpl implements MappingParser { baseInstance != null ? baseInstance.getClass() : ( typedAdapter ? TypeAwareAdapter.class.cast(itemConverter).getTo() : type), JsonObject.class.cast(jsonValue), type instanceof Class, - jsonPointer); + jsonPointer, getSkippedConverters()); return typedAdapter ? itemConverter.to(object) : object; } else if (JsonArray.class.isInstance(jsonValue)) { if (JsonArray.class == type || JsonStructure.class == type || JsonValue.class == type) { @@ -1151,24 +1166,35 @@ public class MappingParserImpl implements MappingParser { private static class SuppressConversionMappingParser implements MappingParser { private final MappingParserImpl delegate; private final JsonObject suppressConversionFor; + private final Collection<Class<?>> skippedConverters; - public SuppressConversionMappingParser(MappingParserImpl delegate, JsonObject suppressConversionFor) { + public SuppressConversionMappingParser(final MappingParserImpl delegate, final JsonObject suppressConversionFor, + final Collection<Class<?>> skippedConverters) { this.delegate = delegate; this.suppressConversionFor = suppressConversionFor; + this.skippedConverters = skippedConverters; + } + + @Override + public Collection<Class<?>> getSkippedConverters() { + return skippedConverters; } @Override - public <T> T readObject(Type targetType) { + public <T> T readObject(final Type targetType) { return delegate.readObject(targetType); } @Override - public <T> T readObject(JsonValue jsonValue, Type targetType) { + public <T> T readObject(final JsonValue jsonValue, final Type targetType) { + final Collection<Class<?>> skippedConverters = getSkippedConverters(); if (suppressConversionFor == jsonValue) { - return delegate.readObject(jsonValue, targetType, false); + return delegate.readObject(jsonValue, targetType, false, skippedConverters); } - return delegate.readObject(jsonValue, targetType); + final boolean useConverters = (Class.class.isInstance(targetType) && + (skippedConverters == null || skippedConverters.stream().noneMatch(it -> it.isAssignableFrom(Class.class.cast(targetType))))) || + ParameterizedType.class.isInstance(targetType); + return delegate.readObject(jsonValue, targetType, useConverters, skippedConverters); } } - }