JOHNZON-382 Diagnose and guide users on invalid bean constructors
JOHNZON-376 Mapper API Bean constructor exceptions tests
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/DeserializationExceptionMessagesTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/DeserializationExceptionMessagesTest.java
index 435cf23..4243552 100644
--- a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/DeserializationExceptionMessagesTest.java
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/DeserializationExceptionMessagesTest.java
@@ -450,8 +450,7 @@
     public void arrayOfObjectFromObject() throws Exception {
         assertMessage("{ \"arrayOfObject\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
                 "Widget property 'arrayOfObject' of type Color[] cannot be mapped to json object value: {\"red\":255,\"g" +
-                        "reen\":1...\nclass [Lorg.apache.johnzon.jsonb.DeserializationExceptionMessagesTest$Color; not instanti" +
-                        "able");
+                        "reen\":1...\nColor[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
     }
 
     @Test
@@ -507,7 +506,7 @@
     public void arrayOfStringFromObject() throws Exception {
         assertMessage("{ \"arrayOfString\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
                 "Widget property 'arrayOfString' of type String[] cannot be mapped to json object value: {\"red\":255,\"" +
-                        "green\":1...\nclass [Ljava.lang.String; not instantiable");
+                        "green\":1...\nString[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
     }
 
     @Test
@@ -558,7 +557,7 @@
     public void arrayOfNumberFromObject() throws Exception {
         assertMessage("{ \"arrayOfNumber\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
                 "Widget property 'arrayOfNumber' of type Number[] cannot be mapped to json object value: {\"red\":255,\"" +
-                        "green\":1...\nclass [Ljava.lang.Number; not instantiable");
+                        "green\":1...\nNumber[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
     }
 
     @Test
@@ -588,7 +587,8 @@
     public void arrayOfNumberFromArrayOfObject() throws Exception {
         assertMessage("{ \"arrayOfNumber\" : [{\"red\": 255, \"green\": 165, \"blue\":0},{\"red\": 0, \"green\": 45, \"blue\":127}] }",
                 "Widget property 'arrayOfNumber' of type Number[] cannot be mapped to json array value: [{\"red\":255,\"" +
-                        "green\":...\njava.lang.InstantiationException");
+                        "green\":...\nNumber cannot be constructed to deserialize json object value: {\"red\":255,\"green\":1...\nja" +
+                        "va.lang.InstantiationException");
     }
 
     @Test
@@ -610,7 +610,7 @@
     public void arrayOfBooleanFromObject() throws Exception {
         assertMessage("{ \"arrayOfBoolean\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
                 "Widget property 'arrayOfBoolean' of type Boolean[] cannot be mapped to json object value: {\"red\":255" +
-                        ",\"green\":1...\nclass [Ljava.lang.Boolean; not instantiable");
+                        ",\"green\":1...\nBoolean[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
     }
 
     @Test
@@ -661,7 +661,7 @@
     public void arrayOfIntFromObject() throws Exception {
         assertMessage("{ \"arrayOfInt\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
                 "Widget property 'arrayOfInt' of type int[] cannot be mapped to json object value: {\"red\":255,\"green\"" +
-                        ":1...\nclass [I not instantiable");
+                        ":1...\nint[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
     }
 
     @Test
@@ -719,7 +719,7 @@
     public void arrayOfByteFromObject() throws Exception {
         assertMessage("{ \"arrayOfByte\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
                 "Widget property 'arrayOfByte' of type byte[] cannot be mapped to json object value: {\"red\":255,\"gree" +
-                        "n\":1...\nclass [B not instantiable");
+                        "n\":1...\nbyte[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
     }
 
     @Test
@@ -770,7 +770,7 @@
     public void arrayOfCharFromObject() throws Exception {
         assertMessage("{ \"arrayOfChar\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
                 "Widget property 'arrayOfChar' of type char[] cannot be mapped to json object value: {\"red\":255,\"gree" +
-                        "n\":1...\nclass [C not instantiable");
+                        "n\":1...\nchar[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
     }
 
     @Test
@@ -813,7 +813,7 @@
     public void arrayOfShortFromObject() throws Exception {
         assertMessage("{ \"arrayOfShort\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
                 "Widget property 'arrayOfShort' of type short[] cannot be mapped to json object value: {\"red\":255,\"gr" +
-                        "een\":1...\nclass [S not instantiable");
+                        "een\":1...\nshort[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
     }
 
     @Test
@@ -864,7 +864,7 @@
     public void arrayOfLongFromObject() throws Exception {
         assertMessage("{ \"arrayOfLong\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
                 "Widget property 'arrayOfLong' of type long[] cannot be mapped to json object value: {\"red\":255,\"gree" +
-                        "n\":1...\nclass [J not instantiable");
+                        "n\":1...\nlong[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
     }
 
     @Test
@@ -915,7 +915,7 @@
     public void arrayOfFloatFromObject() throws Exception {
         assertMessage("{ \"arrayOfFloat\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
                 "Widget property 'arrayOfFloat' of type float[] cannot be mapped to json object value: {\"red\":255,\"gr" +
-                        "een\":1...\nclass [F not instantiable");
+                        "een\":1...\nfloat[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
     }
 
     @Test
@@ -966,7 +966,7 @@
     public void arrayOfDoubleFromObject() throws Exception {
         assertMessage("{ \"arrayOfDouble\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
                 "Widget property 'arrayOfDouble' of type double[] cannot be mapped to json object value: {\"red\":255,\"" +
-                        "green\":1...\nclass [D not instantiable");
+                        "green\":1...\ndouble[] array not a suitable datatype for json object value: {\"red\":255,\"green\":1...");
     }
 
     @Test
@@ -1017,7 +1017,8 @@
     public void arrayOfBooleanPrimitiveFromObject() throws Exception {
         assertMessage("{ \"arrayOfBooleanPrimitive\" : {\"red\": 255, \"green\": 165, \"blue\":0} }",
                 "Widget property 'arrayOfBooleanPrimitive' of type boolean[] cannot be mapped to json object value: {" +
-                        "\"red\":255,\"green\":1...\nclass [Z not instantiable");
+                        "\"red\":255,\"green\":1...\nboolean[] array not a suitable datatype for json object value: {\"red\":255,\"gr" +
+                        "een\":1...");
     }
 
     @Test
@@ -1213,7 +1214,8 @@
     public void listOfNumberFromArrayOfObject() throws Exception {
         assertMessage("{ \"listOfNumber\" : [{\"red\": 255, \"green\": 165, \"blue\":0},{\"red\": 0, \"green\": 45, \"blue\":127}] }",
                 "Widget property 'listOfNumber' of type List<Number> cannot be mapped to json array value: [{\"red\":25" +
-                        "5,\"green\":...\njava.lang.InstantiationException");
+                        "5,\"green\":...\nNumber cannot be constructed to deserialize json object value: {\"red\":255,\"green\":1..." +
+                        "\njava.lang.InstantiationException");
     }
 
     @Test
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 209375c..36f3a28 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
@@ -365,7 +365,7 @@
         */
 
         if (classMapping.factory == null) {
-            throw new MapperException(classMapping.clazz + " not instantiable");
+            throw new MissingFactoryException(classMapping.clazz, object, config.getSnippet().of(object));
         }
 
         if (config.isFailOnUnknown()) {
@@ -377,11 +377,20 @@
         }
 
         Object t;
-        if (classMapping.factory.getParameterTypes() == null || classMapping.factory.getParameterTypes().length == 0) {
-            t = classMapping.factory.create(null);
-        } else {
-            t = classMapping.factory.create(createParameters(classMapping, object, jsonPointer));
+        try {
+            if (classMapping.factory.getParameterTypes() == null || classMapping.factory.getParameterTypes().length == 0) {
+                t = classMapping.factory.create(null);
+            } else {
+                t = classMapping.factory.create(createParameters(classMapping, object, jsonPointer));
+            }
+        } catch (Exception e) {
+            final String message = String.format("%s cannot be constructed to deserialize %s: %s%n%s",
+                    ExceptionMessages.simpleName(type), ExceptionMessages.description(object),
+                    config.getSnippet().of(object), e.getMessage()
+            );
+            throw new MapperException(message, e);
         }
+
         // store the new object under it's jsonPointer in case it gets referenced later
         if (jsonPointers == null) {
             if (classMapping.deduplicateObjects || config.isDeduplicateObjects()) {
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MissingFactoryException.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MissingFactoryException.java
new file mode 100644
index 0000000..0f1ccc8
--- /dev/null
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MissingFactoryException.java
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+import javax.json.JsonObject;
+import java.lang.reflect.Constructor;
+import java.util.stream.Stream;
+
+
+public class MissingFactoryException extends MapperException {
+    public MissingFactoryException(final Class<?> clazz, final JsonObject object, final String json) {
+        super(message(clazz, object, json));
+    }
+
+    private static String message(final Class<?> clazz, final JsonObject object, final String json) {
+        if (clazz.isArray()) {
+            return String.format("%s array not a suitable datatype for %s: %s",
+                    clazz.getSimpleName(),
+                    ExceptionMessages.description(object),
+                    json);
+        }
+
+        if (clazz.isInterface()) {
+            return String.format("%s is an interface and requires an adapter or factory.  Cannot deserialize %s: %s%n%s not instantiable",
+                    clazz.getSimpleName(),
+                    ExceptionMessages.description(object),
+                    json,
+                    clazz);
+        }
+
+        String message = String.format("%s has no suitable constructor or factory.  Cannot deserialize %s: %s",
+                clazz.getSimpleName(),
+                ExceptionMessages.description(object),
+                json);
+
+        final Constructor<?>[] constructors = clazz.getDeclaredConstructors();
+        final long constructorsWithParameters = Stream.of(constructors)
+                .filter(constructor -> constructor.getParameterTypes().length > 0)
+                .count();
+
+        // Was a constructor with parameters our only option?  If so, help people
+        // learn how to properly use constructors with parameters
+        if (constructorsWithParameters > 0 && constructors.length == constructorsWithParameters) {
+            message += "\nUse Johnzon @ConstructorProperties or @JsonbCreator if constructor arguments are needed";
+        }
+
+        // Add full class name as final detail
+        message += "\n" + clazz + " not instantiable";
+        return message;
+    }
+}
diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ExceptionAsserts.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ExceptionAsserts.java
index abaf530..7ada91f 100644
--- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ExceptionAsserts.java
+++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/ExceptionAsserts.java
@@ -17,6 +17,7 @@
 package org.apache.johnzon.mapper;
 
 import java.io.ByteArrayOutputStream;
+import java.lang.reflect.Type;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -54,6 +55,14 @@
         return this;
     }
 
+    /**
+     * Useful for debugging tests
+     */
+    public ExceptionAsserts printStackTrace() {
+        throwable.printStackTrace();
+        return this;
+    }
+
     public Throwable getThrowable() {
         return throwable;
     }
@@ -72,7 +81,7 @@
         }
     }
 
-    public static ExceptionAsserts fromMapperReadObject(final String json, final Class<?> clazz) {
+    public static ExceptionAsserts fromMapperReadObject(final String json, final Type clazz) {
         return from(() -> {
             try (final Mapper mapper = new MapperBuilder().setSnippetMaxLength(20).build()) {
                 mapper.readObject(json, clazz);
diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperBeanConstructorExceptionsTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperBeanConstructorExceptionsTest.java
new file mode 100644
index 0000000..2615fe0
--- /dev/null
+++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperBeanConstructorExceptionsTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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;
+
+import org.junit.Test;
+
+import java.beans.ConstructorProperties;
+import java.lang.reflect.Type;
+
+public class MapperBeanConstructorExceptionsTest {
+
+    private static final RuntimeException USER_EXCEPTION = new RuntimeException("I am user, hear me roar");
+
+    @Test
+    public void constructor() {
+        ExceptionAsserts.fromMapperReadObject("{ \"string\" : \"Supercalifragilisticexpialidocious\" }", Circle.class)
+                .assertInstanceOf(MapperException.class)
+                .assertCauseChain(USER_EXCEPTION)
+                .assertMessage("Circle cannot be constructed to deserialize json object value: {\"string\":\"Supercali...\n" +
+                        "java.lang.RuntimeException: I am user, hear me roar");
+    }
+
+    @Test
+    public void constructorParametersWithNoAnnotations() {
+        ExceptionAsserts.fromMapperReadObject("{ \"string\" : \"Supercalifragilisticexpialidocious\" }", Square.class)
+                .assertInstanceOf(MapperException.class)
+                .assertMessage("Square has no suitable constructor or factory.  Cannot deserialize json object value: {\"string\":\"Supercali...\n" +
+                        "Use Johnzon @ConstructorProperties or @JsonbCreator if constructor arguments are needed\n" +
+                        "class org.apache.johnzon.mapper.MapperBeanConstructorExceptionsTest$Square not instantiable");
+    }
+
+    @Test
+    public void constructorWithGenerics() {
+
+        final Type type = new Oval<String>(true) {
+        }.getClass().getGenericSuperclass();
+
+        ExceptionAsserts.fromMapperReadObject("{ \"string\" : \"Supercalifragilisticexpialidocious\" }", type)
+                .assertInstanceOf(MapperException.class)
+                .assertCauseChain(USER_EXCEPTION)
+                .assertMessage("Oval<String> cannot be constructed to deserialize json object value: {\"string\":\"Supercali...\n" +
+                        "java.lang.RuntimeException: I am user, hear me roar");
+    }
+
+    @Test
+    public void constructorProperties() {
+        ExceptionAsserts.fromMapperReadObject("{ \"string\" : \"Supercalifragilisticexpialidocious\" }", Triangle.class)
+                .assertInstanceOf(MapperException.class)
+                .assertCauseChain(USER_EXCEPTION)
+                .assertMessage("Triangle cannot be constructed to deserialize json object value: {\"string\":\"Supercali...\n" +
+                        "java.lang.RuntimeException: I am user, hear me roar");
+    }
+
+    @Test
+    public void noConstructors() {
+        ExceptionAsserts.fromMapperReadObject("{ \"string\" : \"Supercalifragilisticexpialidocious\" }", Sphere.class)
+                .assertInstanceOf(MapperException.class)
+                .assertMessage("Sphere is an interface and requires an adapter or factory.  Cannot deserialize json object value: {\"string\":\"Supercali...\n" +
+                        "interface org.apache.johnzon.mapper.MapperBeanConstructorExceptionsTest$Sphere not instantiable");
+    }
+
+    public static class Circle {
+        private String string;
+
+        public Circle() {
+            throw USER_EXCEPTION;
+        }
+
+        public String getString() {
+            return string;
+        }
+
+        public void setString(final String string) {
+            this.string = string;
+        }
+    }
+
+    public static class Square {
+        private String string;
+
+        public Square(final String string) {
+            throw USER_EXCEPTION;
+        }
+
+        public String getString() {
+            return string;
+        }
+
+        public void setString(final String string) {
+            this.string = string;
+        }
+    }
+
+    public static class Oval<T> {
+        private String s;
+
+        public Oval() {
+            throw USER_EXCEPTION;
+        }
+
+        public Oval(final boolean ignored) {
+        }
+    }
+
+    public static class Triangle {
+        private String string;
+
+        @ConstructorProperties("string")
+        public Triangle(final String string) {
+            throw USER_EXCEPTION;
+        }
+    }
+
+    public interface Sphere {
+    }
+
+
+}