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
commit 4f53103f64c9f7c0ec645c1b0d04c1d34f9a8dcf Author: Romain Manni-Bucau <rmannibu...@gmail.com> AuthorDate: Mon Jan 25 11:48:36 2021 +0100 [JOHNZON-332] exception de/serialization --- .../org/apache/johnzon/jsonb/JohnzonBuilder.java | 37 +- .../org/apache/johnzon/jsonb/JsonbWriteTest.java | 31 ++ .../org/apache/johnzon/mapper/MapperBuilder.java | 4 + .../access/KnownNotOpenedJavaTypesAccessMode.java | 415 +++++++++++++++++++++ .../johnzon/mapper/CircularExceptionTest.java | 4 +- 5 files changed, 475 insertions(+), 16 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 ab82053..52878dc 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 @@ -38,6 +38,7 @@ import org.apache.johnzon.mapper.ObjectConverter; import org.apache.johnzon.mapper.SerializeValueFilter; import org.apache.johnzon.mapper.access.AccessMode; import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode; +import org.apache.johnzon.mapper.access.KnownNotOpenedJavaTypesAccessMode; import org.apache.johnzon.mapper.converter.LocaleConverter; import org.apache.johnzon.mapper.internal.AdapterKey; @@ -213,21 +214,27 @@ public class JohnzonBuilder implements JsonbBuilder { final AccessMode accessMode = config.getProperty("johnzon.accessMode") .map(this::toAccessMode) - .orElseGet(() -> new JsonbAccessMode( - propertyNamingStrategy, orderValue, visibilityStrategy, - !namingStrategyValue.orElse("").equals(PropertyNamingStrategy.CASE_INSENSITIVE), - builder.getAdapters(), - factory, jsonp, builderFactorySupplier, parserFactoryProvider, - config.getProperty("johnzon.accessModeDelegate") - .map(this::toAccessMode) - .orElseGet(() -> new FieldAndMethodAccessMode(true, true, false, true)), - config.getProperty("johnzon.failOnMissingCreatorValues") - .map(this::toBool) - .orElse(true) /*spec 1.0 requirement*/, - isNillable, - config.getProperty("johnzon.supportsPrivateAccess") - .map(this::toBool) - .orElse(false))); + .orElseGet(() -> { + final AccessMode access = new JsonbAccessMode( + propertyNamingStrategy, orderValue, visibilityStrategy, + !namingStrategyValue.orElse("").equals(PropertyNamingStrategy.CASE_INSENSITIVE), + builder.getAdapters(), + factory, jsonp, builderFactorySupplier, parserFactoryProvider, + config.getProperty("johnzon.accessModeDelegate") + .map(this::toAccessMode) + .orElseGet(() -> new FieldAndMethodAccessMode(true, true, false, true)), + config.getProperty("johnzon.failOnMissingCreatorValues") + .map(this::toBool) + .orElse(true) /*spec 1.0 requirement*/, + isNillable, + config.getProperty("johnzon.supportsPrivateAccess") + .map(this::toBool) + .orElse(false)); + return ofNullable(config.getProperty("johnzon.enable-exception-serialization")) + .map(v -> Boolean.parseBoolean(String.valueOf(v))) + .map(it -> it ? new KnownNotOpenedJavaTypesAccessMode(access) : access) + .orElseGet(() -> new KnownNotOpenedJavaTypesAccessMode(access)); + }); builder.setAccessMode(accessMode); // user adapters diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbWriteTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbWriteTest.java index 95541ee..36b41a5 100644 --- a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbWriteTest.java +++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/JsonbWriteTest.java @@ -22,8 +22,11 @@ import org.junit.Test; import javax.json.bind.Jsonb; import javax.json.bind.JsonbBuilder; +import javax.json.bind.JsonbConfig; +import javax.json.bind.annotation.JsonbCreator; import javax.json.bind.annotation.JsonbDateFormat; import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.config.PropertyOrderStrategy; import javax.json.bind.spi.JsonbProvider; import java.io.ByteArrayOutputStream; import java.io.StringWriter; @@ -36,10 +39,30 @@ import java.util.Map; import java.util.TreeMap; import static java.util.Arrays.asList; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; public class JsonbWriteTest { @Test + public void throwable() throws Exception { + try (final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig() + .withPropertyOrderStrategy(PropertyOrderStrategy.LEXICOGRAPHICAL))){ + final IllegalStateException exception = new IllegalStateException("oops"); + exception.setStackTrace(new StackTraceElement[]{ + new StackTraceElement("foo", "bar", "dummy", 1), + new StackTraceElement("foo2", "bar2", "dummy2", 2) + }); + final String json = jsonb.toJson(exception, Throwable.class); + final Throwable throwable = jsonb.fromJson(json, Throwable.class); + assertEquals(exception.getMessage(), throwable.getMessage()); + assertArrayEquals(exception.getStackTrace(), throwable.getStackTrace()); + assertEquals("{\"message\":\"oops\"," + + "\"stackTrace\":[{\"className\":\"foo\",\"fileName\":\"dummy\",\"lineNumber\":1,\"methodName\":\"bar\"}," + + "{\"className\":\"foo2\",\"fileName\":\"dummy2\",\"lineNumber\":2,\"methodName\":\"bar2\"}]}", json); + } + } + + @Test public void mapOfSimple() throws Exception { final Map<String, Simple> list = new TreeMap<>(); list.put("1", new Simple()); @@ -201,5 +224,13 @@ public class JsonbWriteTest { } } + public static class CustomException extends RuntimeException { + private final int code; + @JsonbCreator + public CustomException(final String message, final int code) { + super(message); + this.code = code; + } + } } 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 171f93a..cbb2213 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 @@ -28,6 +28,7 @@ import org.apache.johnzon.mapper.access.BaseAccessMode; import org.apache.johnzon.mapper.access.FieldAccessMode; import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode; import org.apache.johnzon.mapper.access.MethodAccessMode; +import org.apache.johnzon.mapper.access.KnownNotOpenedJavaTypesAccessMode; import org.apache.johnzon.mapper.converter.BooleanConverter; import org.apache.johnzon.mapper.converter.ByteConverter; import org.apache.johnzon.mapper.converter.CachedDelegateConverter; @@ -199,6 +200,9 @@ public class MapperBuilder { throw new IllegalStateException("AccessMode is not an BaseAccessMode"); } } + if (!KnownNotOpenedJavaTypesAccessMode.class.isInstance(accessMode)) { + accessMode = new KnownNotOpenedJavaTypesAccessMode(accessMode); + } if (primitiveConverters) { adapters.put(new AdapterKey(Byte.class, String.class), new ConverterAdapter<>(new CachedDelegateConverter<>(new ByteConverter()), Byte.class)); diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/KnownNotOpenedJavaTypesAccessMode.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/KnownNotOpenedJavaTypesAccessMode.java new file mode 100644 index 0000000..917ef9c --- /dev/null +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/KnownNotOpenedJavaTypesAccessMode.java @@ -0,0 +1,415 @@ +/* + * 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.access; + +import org.apache.johnzon.mapper.Adapter; +import org.apache.johnzon.mapper.ObjectConverter; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import static java.util.Collections.emptyMap; + +// on last java releases, throwable, stacktraceelement etc are no more opened by default +// so hardcode the model we want for serialization/deserialization +// +// Note: it is still a bad habit to serialize exceptions but it happens +public class KnownNotOpenedJavaTypesAccessMode implements AccessMode { + private final AccessMode delegate; + + public KnownNotOpenedJavaTypesAccessMode(final AccessMode delegate) { + this.delegate = delegate; + } + + private boolean isInaccessibleAndThrowable(final Class<?> clazz, final RuntimeException ioe) { + return "java.lang.reflect.InaccessibleObjectException".equals(ioe.getClass().getName()) && + Throwable.class.isAssignableFrom(clazz); + } + + private Map<String, Writer> findThrowableWriters(final Class<?> clazz) { + final Map<String, Writer> map = new HashMap<>(); + // map.put("message", ...); // constructor + map.put("stackTrace", new InlineWriter<>(Throwable::setStackTrace, StackTraceElement[].class)); + // todo: if (SubTypeException.class == clazz) addSpecificFields(map); + return map; + } + + private Map<String, Reader> findThrowableReaders(final Class<?> clazz) { + final Map<String, Reader> map = new HashMap<>(); + map.put("message", new InlineReader<>(Throwable::getMessage, String.class)); + map.put("stackTrace", new InlineReader<>(Throwable::getStackTrace, StackTraceElement[].class)); + // todo: if (SubTypeException.class == clazz) addSpecificFields(map); + return map; + } + + private Map<String, Reader> findStackTraceElementReaders(final Class<?> clazz) { + final Map<String, Reader> map = new HashMap<>(); + map.put("className", new InlineReader<>(StackTraceElement::getClassName, String.class)); + map.put("methodName", new InlineReader<>(StackTraceElement::getMethodName, String.class)); + map.put("fileName", new InlineReader<>(StackTraceElement::getFileName, String.class)); + map.put("lineNumber", new InlineReader<>(StackTraceElement::getLineNumber, int.class)); + return map; + } + + // for now we only support "roots" and delegate to system "openness" the others + // but strictly speaking we can add all java.* exception there + // + // in practise exceptions are generally serialized but not deserialized so "ok-ish" + private Factory findThrowableFactory(final Class<?> clazz) { + if (Throwable.class == clazz) { + return new ExceptionMessageFactory() { + @Override + public Object create(final Object[] params) { + return new Throwable(String.class.cast(params[0])); + } + }; + } + if (Exception.class == clazz) { + return new ExceptionMessageFactory() { + @Override + public Object create(final Object[] params) { + return new Exception(String.class.cast(params[0])); + } + }; + } + if (Error.class == clazz) { + return new ExceptionMessageFactory() { + @Override + public Object create(final Object[] params) { + return new Error(String.class.cast(params[0])); + } + }; + } + // todo: add more + try { + return delegate.findFactory(clazz); + } catch (final RuntimeException | Error e) { + return null; + } + } + + private boolean isJavaThrowable(final Class<?> clazz) { + return clazz.getName().startsWith("java.") && Throwable.class.isAssignableFrom(clazz); + } + + private boolean isStackTraceElement(final Class<?> clazz) { + return clazz == StackTraceElement.class; + } + + @Override + public Factory findFactory(final Class<?> clazz, final Function<AnnotatedElement, String>... parameterNameExtractors) { + return delegate.findFactory(clazz, parameterNameExtractors); + } + + @Override + public Factory findFactory(final Class<?> clazz) { + if (isJavaThrowable(clazz)) { + return findThrowableFactory(clazz); + } + if (isStackTraceElement(clazz)) { + return new BaseFactory() { + @Override + public Object create(final Object[] params) { + return new StackTraceElement( + String.class.cast(params[0]), + String.class.cast(params[1]), + String.class.cast(params[2]), + params.length < 4 || params[3] == null ? -1 : Integer.class.cast(params[3])); + } + + @Override + public Type[] getParameterTypes() { + return new Type[]{String.class, String.class, String.class, int.class}; + } + + @Override + public String[] getParameterNames() { + return new String[]{"className", "methodName", "fileName", "lineNumber"}; + } + + @Override + public Adapter<?, ?>[] getParameterConverter() { + return new Adapter[]{null, null, null, null}; + } + + @Override + public Adapter<?, ?>[] getParameterItemConverter() { + return new Adapter[]{null, null, null, null}; + } + + @Override + public ObjectConverter.Codec<?>[] getObjectConverter() { + return new ObjectConverter.Codec[]{null, null, null, null}; + } + }; + } + return delegate.findFactory(clazz); + } + + @Override + public Map<String, Reader> findReaders(final Class<?> clazz) { + if (isJavaThrowable(clazz)) { + return findThrowableReaders(clazz); + } + if (isStackTraceElement(clazz)) { + return findStackTraceElementReaders(clazz); + } + try { + return delegate.findReaders(clazz); + } catch (final RuntimeException ioe) { + if (isInaccessibleAndThrowable(clazz, ioe)) { + return findThrowableReaders(clazz); + } + throw ioe; + } + } + + @Override + public Map<String, Writer> findWriters(final Class<?> clazz) { + if (isJavaThrowable(clazz)) { + return findThrowableWriters(clazz); + } + if (isStackTraceElement(clazz)) { + return emptyMap(); + } + try { + return delegate.findWriters(clazz); + } catch (final RuntimeException ioe) { + if (isInaccessibleAndThrowable(clazz, ioe)) { + return findThrowableWriters(clazz); + } + throw ioe; + } + } + + @Override + public ObjectConverter.Reader<?> findReader(final Class<?> clazz) { + if (isJavaThrowable(clazz) || isStackTraceElement(clazz)) { + return null; + } + return delegate.findReader(clazz); + } + + @Override + public ObjectConverter.Writer<?> findWriter(final Class<?> clazz) { + if (isJavaThrowable(clazz) || isStackTraceElement(clazz)) { + return null; + } + return delegate.findWriter(clazz); + } + + @Override + public Method findAnyGetter(final Class<?> clazz) { + if (isJavaThrowable(clazz) || isStackTraceElement(clazz)) { + return null; + } + return delegate.findAnyGetter(clazz); + } + + @Override + public Method findAnySetter(final Class<?> clazz) { + if (isJavaThrowable(clazz) || isStackTraceElement(clazz)) { + return null; + } + return delegate.findAnySetter(clazz); + } + + @Override + public Field findAnyField(final Class<?> clazz) { + if (isJavaThrowable(clazz) || isStackTraceElement(clazz)) { + return null; + } + return delegate.findAnyField(clazz); + } + + @Override + public Method findMapAdder(final Class<?> clazz) { + if (isJavaThrowable(clazz) || isStackTraceElement(clazz)) { + return null; + } + return delegate.findMapAdder(clazz); + } + + @Override + public Adapter<?, ?> findAdapter(final Class<?> clazz) { + return delegate.findAdapter(clazz); + } + + @Override + public Comparator<String> fieldComparator(final Class<?> clazz) { + return delegate.fieldComparator(clazz); + } + + @Override + public void afterParsed(final Class<?> clazz) { + delegate.afterParsed(clazz); + } + + private static class InlineWriter<T, F> implements Writer { + private final BiConsumer setter; + private final Type type; + + private InlineWriter(final BiConsumer<T, F> setter, final Type type) { + this.setter = setter; + this.type = type; + } + + @Override + public Type getType() { + return type; + } + + @Override + public <T extends Annotation> T getAnnotation(final Class<T> clazz) { + return null; + } + + @Override + public <T extends Annotation> T getClassOrPackageAnnotation(final Class<T> clazz) { + return null; + } + + @Override + public Adapter<?, ?> findConverter() { + return null; + } + + @Override + public boolean isNillable(final boolean globalConfig) { + return false; + } + + @Override + public void write(final Object instance, final Object value) { + setter.accept(instance, value); + } + + @Override + public ObjectConverter.Reader<?> findObjectConverterReader() { + return null; + } + } + + private static class InlineReader<T, F> implements Reader { + private final Function accessor; + private final Class<F> type; + + private InlineReader(final Function<T, F> accessor, final Class<F> type) { + this.accessor = Function.class.cast(accessor); + this.type = type; + } + + @Override + public Type getType() { + return type; + } + + @Override + public <T extends Annotation> T getAnnotation(final Class<T> clazz) { + return null; + } + + @Override + public <T extends Annotation> T getClassOrPackageAnnotation(final Class<T> clazz) { + return null; + } + + @Override + public Adapter<?, ?> findConverter() { + return null; + } + + @Override + public boolean isNillable(final boolean globalConfig) { + return false; + } + + @Override + public Object read(final Object instance) { + return accessor.apply(instance); + } + + @Override + public ObjectConverter.Writer<?> findObjectConverterWriter() { + return null; + } + } + + private static abstract class BaseFactory implements Factory { + @Override + public Type[] getParameterTypes() { + return new Type[0]; + } + + @Override + public String[] getParameterNames() { + return new String[0]; + } + + @Override + public Adapter<?, ?>[] getParameterConverter() { + return new Adapter[0]; + } + + @Override + public Adapter<?, ?>[] getParameterItemConverter() { + return new Adapter[0]; + } + + @Override + public ObjectConverter.Codec<?>[] getObjectConverter() { + return new ObjectConverter.Codec[0]; + } + } + + private static abstract class ExceptionMessageFactory extends BaseFactory { + @Override + public Type[] getParameterTypes() { + return new Type[]{String.class}; + } + + @Override + public String[] getParameterNames() { + return new String[]{"message"}; + } + + @Override + public Adapter<?, ?>[] getParameterConverter() { + return new Adapter[]{null}; + } + + @Override + public Adapter<?, ?>[] getParameterItemConverter() { + return new Adapter[]{null}; + } + + @Override + public ObjectConverter.Codec<?>[] getObjectConverter() { + return new ObjectConverter.Codec[]{null}; + } + } +} diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularExceptionTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularExceptionTest.java index d6da4cb..1b44da0 100644 --- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularExceptionTest.java +++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/CircularExceptionTest.java @@ -23,13 +23,15 @@ import org.junit.Test; import static org.junit.Assert.assertTrue; public class CircularExceptionTest { + // note that with KnownNotOpenedJavaTypes this test will not test circular case anymore + // but we still care to test exceptions don't loop so kept it @Test public void dontStackOverFlow() { final Throwable oopsImVicous = new Exception("circular"); oopsImVicous.getStackTrace(); // fill it oopsImVicous.initCause(new IllegalArgumentException(oopsImVicous)); final String serialized = new MapperBuilder().setAccessModeName("field").build().writeObjectAsString(oopsImVicous); - assertTrue(serialized.contains("\"detailMessage\":\"circular\"")); + assertTrue(serialized.contains("\"message\":\"circular\"")); assertTrue(serialized.contains("\"stackTrace\":[{")); }