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\":[{"));
     }
 

Reply via email to