Merge pull request #113 from jungm/site-updates

Site updates
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbMappings.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbMappings.java
index b399eb8..21a2384 100644
--- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbMappings.java
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JsonbMappings.java
@@ -49,7 +49,6 @@
         }
 
         polymorphismHandler.validateJsonbPolymorphismAnnotations(original.clazz);
-        polymorphismHandler.populateTypeInfoCache(original.clazz);
         return new ClassMapping(
                 original.clazz, original.factory, original.getters, original.setters,
                 original.adapter, original.reader, original.writer, original.anyGetter,
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismHandler.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismHandler.java
index 36f1aed..48a5de6 100644
--- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismHandler.java
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismHandler.java
@@ -38,47 +38,46 @@
     private final Map<Class<?>, JsonbPolymorphismTypeInfo> typeInfoCache = new HashMap<>();
 
     public boolean hasPolymorphism(Class<?> clazz) {
-        return Meta.getAnnotation((AnnotatedElement) clazz, JsonbTypeInfo.class) != null || getParentWithTypeInfo(clazz) != null;
+        return getOrCreatePolymorphismTypeInfo(clazz) != null;
     }
 
     public Map.Entry<String, String>[] getPolymorphismPropertiesToSerialize(Class<?> clazz, Collection<String> otherProperties) {
         List<Map.Entry<String, String>> result = new ArrayList<>();
 
-        Class<?> current = clazz;
-        while (current != null) {
-            // Only try to resolve types when there's a JsonbTypeInfo Annotation present on the current type, Meta.getAnnotation tries to
-            // walk up parents by itself until it finds the given Annotation and could incorrectly cause JsonbExceptions to be thrown
-            // (multiple JsonbTypeInfos with same key found even if thats not actually the case)
-            JsonbTypeInfo typeInfo = Meta.getAnnotation((AnnotatedElement) current, JsonbTypeInfo.class);
-            if (typeInfo != null) {
-                if (otherProperties.contains(typeInfo.key())) {
-                    throw new JsonbException("JsonbTypeInfo key '" + typeInfo.key() + "' collides with other properties in json");
+        JsonbPolymorphismTypeInfo polymorphismTypeInfo = getOrCreatePolymorphismTypeInfo(clazz);
+        while (polymorphismTypeInfo != null) {
+            if (polymorphismTypeInfo.hasSubtypeInformation()) {
+                if (otherProperties.contains(polymorphismTypeInfo.getTypeKey())) {
+                    throw new JsonbException("JsonbTypeInfo key '" + polymorphismTypeInfo.getTypeKey() + "' collides with other properties in json");
                 }
 
                 String bestMatchingAlias = null;
-                for (JsonbSubtype subtype : typeInfo.value()) {
-                    if (subtype.type().isAssignableFrom(clazz)) {
-                        bestMatchingAlias = subtype.alias();
+                for (Map.Entry<String, Class<?>> aliasToType : polymorphismTypeInfo.getAliases().entrySet()) {
+                    String alias = aliasToType.getKey();
+                    Class<?> type = aliasToType.getValue();
 
-                        if (clazz == subtype.type()) { // Exact match found, no need to continue further
+                    if (type.isAssignableFrom(clazz)) {
+                        bestMatchingAlias = alias;
+
+                        if (clazz == type) { // Exact match found, no need to continue further
                             break;
                         }
                     }
                 }
 
                 if (bestMatchingAlias != null) {
-                    result.add(0, Map.entry(typeInfo.key(), bestMatchingAlias));
+                    result.add(0, Map.entry(polymorphismTypeInfo.getTypeKey(), bestMatchingAlias));
                 }
             }
 
-            current = getParentWithTypeInfo(current);
+            polymorphismTypeInfo = polymorphismTypeInfo.getFirstParent();
         }
 
         return result.toArray(Map.Entry[]::new);
     }
 
     public Class<?> getTypeToDeserialize(JsonObject jsonObject, Class<?> clazz) {
-        JsonbPolymorphismTypeInfo typeInfo = typeInfoCache.get(clazz);
+        JsonbPolymorphismTypeInfo typeInfo = getOrCreatePolymorphismTypeInfo(clazz);
         if (typeInfo == null) {
             return clazz;
         }
@@ -101,21 +100,74 @@
         return result;
     }
 
-    public void populateTypeInfoCache(Class<?> clazz) {
+    /**
+     * Looks up a {@link JsonbPolymorphismTypeInfo} from the cache or creates it for the given <code>clazz</code> if it supports polymorphism.
+     * This is the case if either one of these conditions is truthy:
+     * <ul>
+     *     <li><code>clazz</code> has an {@link JsonbTypeInfo} annotation</li>
+     *     <li>any class in the type hierarchy of <code>clazz</code> has an {@link JsonbTypeInfo} annotation</li>
+     * </ul>
+     * @param clazz Class to inspect
+     * @return {@link JsonbPolymorphismTypeInfo} if the class supports polymorphism, <code>null</code> otherwise
+     */
+    public JsonbPolymorphismTypeInfo getOrCreatePolymorphismTypeInfo(Class<?> clazz) {
         if (typeInfoCache.containsKey(clazz)) {
-            return;
+            return typeInfoCache.get(clazz);
         }
 
-        final JsonbTypeInfo annotation = Meta.getAnnotation((AnnotatedElement) clazz, JsonbTypeInfo.class);
-        if (annotation != null) {
-            typeInfoCache.put(clazz, new JsonbPolymorphismTypeInfo(annotation));
+        JsonbPolymorphismTypeInfo result = null;
+        JsonbTypeInfo directAnnotation = Meta.getAnnotation((AnnotatedElement) clazz, JsonbTypeInfo.class);
+        if (directAnnotation != null) {
+            result = new JsonbPolymorphismTypeInfo(clazz, directAnnotation);
         }
+
+        List<JsonbPolymorphismTypeInfo> parents = new ArrayList<>();
+        List<Class<?>> candidates = List.of(clazz);
+        while (!candidates.isEmpty()) {
+            // Parents have been found on previous level -> don't walk inheritance tree further to avoid processing the same classes twice
+            if (!parents.isEmpty()) {
+                break;
+            }
+
+            List<Class<?>> candidatesNextLevel = new ArrayList<>();
+            for (Class<?> current : candidates) {
+                if (current.getSuperclass() != null) {
+                    candidatesNextLevel.add(current.getSuperclass());
+
+                    if (Meta.getAnnotation((AnnotatedElement) current.getSuperclass(), JsonbTypeInfo.class) != null) {
+                        parents.add(getOrCreatePolymorphismTypeInfo(current.getSuperclass()));
+                    }
+                }
+
+                for (Class<?> iface : current.getInterfaces()) {
+                    candidatesNextLevel.add(iface);
+
+                    if (Meta.getAnnotation((AnnotatedElement) iface, JsonbTypeInfo.class) != null) {
+                        parents.add(getOrCreatePolymorphismTypeInfo(iface));
+                    }
+                }
+            }
+
+            candidates = candidatesNextLevel;
+        }
+
+        if (!parents.isEmpty()) {
+            if (result == null) {
+                result = new JsonbPolymorphismTypeInfo(clazz, null);
+            }
+
+            result.getParents().addAll(parents);
+        }
+
+        typeInfoCache.put(clazz, result);
+        return result;
     }
 
     /**
      * Validates {@link JsonbTypeInfo} annotation on clazz and its parents (superclass/interfaces),
      * see {@link JsonbPolymorphismHandler#validateSubtypeCompatibility(Class)}, {@link JsonbPolymorphismHandler#validateOnlyOneParentWithTypeInfo(Class)}
      * and {@link JsonbPolymorphismHandler#validateNoTypeInfoKeyCollision(Class)}
+     *
      * @param classToValidate Class to validate
      * @throws JsonbException validation failed
      */
@@ -132,15 +184,18 @@
      * @throws JsonbException validation failed
      */
     protected void validateSubtypeCompatibility(Class<?> classToValidate) {
-        JsonbTypeInfo typeInfo = Meta.getAnnotation((AnnotatedElement) classToValidate, JsonbTypeInfo.class);
-        if (typeInfo == null) {
+        JsonbPolymorphismTypeInfo polymorphismTypeInfo = getOrCreatePolymorphismTypeInfo(classToValidate);
+        if (polymorphismTypeInfo == null || !polymorphismTypeInfo.hasSubtypeInformation()) {
             return;
         }
 
-        for (JsonbSubtype subtype : typeInfo.value()) {
-            if (!classToValidate.isAssignableFrom(subtype.type())) {
-                throw new JsonbException("JsonbSubtype '" + subtype.alias() + "'" +
-                        " (" + subtype.type().getName() + ") is not a subclass of " + classToValidate);
+        for (Map.Entry<String, Class<?>> aliasToType : polymorphismTypeInfo.getAliases().entrySet()) {
+            String alias = aliasToType.getKey();
+            Class<?> type = aliasToType.getValue();
+
+            if (!classToValidate.isAssignableFrom(type)) {
+                throw new JsonbException("JsonbSubtype '" + alias + "'" +
+                        " (" + type.getName() + ") is not a subclass of " + classToValidate);
             }
         }
     }
@@ -152,17 +207,10 @@
      * @throws JsonbException validation failed
      */
     protected void validateOnlyOneParentWithTypeInfo(Class<?> classToValidate) {
-        boolean found = classToValidate.getSuperclass() != null && Meta.getAnnotation((AnnotatedElement) classToValidate.getSuperclass(), JsonbTypeInfo.class) != null;
-
-        for (Class<?> iface : classToValidate.getInterfaces()) {
-            if (iface != null && Meta.getAnnotation((AnnotatedElement) iface, JsonbTypeInfo.class) != null) {
-                if (found) {
-                    throw new JsonbException("More than one interface/superclass of " + classToValidate.getName() +
-                            " has JsonbTypeInfo Annotation");
-                }
-
-                found = true;
-            }
+        JsonbPolymorphismTypeInfo polymorphismTypeInfo = getOrCreatePolymorphismTypeInfo(classToValidate);
+        if (polymorphismTypeInfo != null && polymorphismTypeInfo.getParents().size() > 1) {
+            throw new JsonbException("More than one interface/superclass of " + classToValidate.getName() +
+                    " has JsonbTypeInfo Annotation");
         }
     }
 
@@ -175,34 +223,15 @@
      */
     protected void validateNoTypeInfoKeyCollision(Class<?> classToValidate) {
         Map<String, Class<?>> keyToDefiningClass = new HashMap<>();
-
-        Class<?> current = classToValidate;
+        JsonbPolymorphismTypeInfo current = getOrCreatePolymorphismTypeInfo(classToValidate);
         while (current != null) {
-            final JsonbTypeInfo annotation = Meta.getAnnotation((AnnotatedElement) current, JsonbTypeInfo.class);
-            if (annotation != null) {
-                String key = annotation.key();
-                final Class<?> existing = keyToDefiningClass.put(key, current);
-                if (existing != null) {
-                    throw new JsonbException("JsonbTypeInfo key '" + key + "' found more than once in type hierarchy of " + classToValidate
-                            + " (first defined in " + existing.getName() + ", then defined again in " + current.getName() + ")");
-                }
+            final Class<?> existing = keyToDefiningClass.put(current.getTypeKey(), current.getClazz());
+            if (existing != null) {
+                throw new JsonbException("JsonbTypeInfo key '" + current.getTypeKey() + "' found more than once in type hierarchy of " + classToValidate
+                        + " (first defined in " + existing.getName() + ", then defined again in " + current.getClazz().getName() + ")");
             }
 
-            current = getParentWithTypeInfo(current);
+            current = current.getFirstParent();
         }
     }
-
-    protected Class<?> getParentWithTypeInfo(Class<?> clazz) {
-        if (clazz.getSuperclass() != null && Meta.getAnnotation((AnnotatedElement) clazz.getSuperclass(), JsonbTypeInfo.class) != null) {
-            return clazz.getSuperclass();
-        }
-
-        for (Class<?> iface : clazz.getInterfaces()) {
-            if (Meta.getAnnotation((AnnotatedElement) iface, JsonbTypeInfo.class) != null) {
-                return iface;
-            }
-        }
-
-        return null;
-    }
 }
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTypeInfo.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTypeInfo.java
index 15f5c71..d5eafec 100644
--- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTypeInfo.java
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTypeInfo.java
@@ -20,14 +20,30 @@
 
 import jakarta.json.bind.annotation.JsonbSubtype;
 import jakarta.json.bind.annotation.JsonbTypeInfo;
+
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 public class JsonbPolymorphismTypeInfo {
+    private final List<JsonbPolymorphismTypeInfo> parents;
+    private final Class<?> clazz;
+
     private final String typeKey;
     private final Map<String, Class<?>> aliases;
 
-    protected JsonbPolymorphismTypeInfo(JsonbTypeInfo annotation) {
+    protected JsonbPolymorphismTypeInfo(Class<?> clazz, JsonbTypeInfo annotation) {
+        this.parents = new ArrayList<>();
+        this.clazz = clazz;
+
+        if (annotation == null) {
+            this.typeKey = null;
+            this.aliases = null;
+
+            return;
+        }
+
         this.typeKey = annotation.key();
 
         aliases = new HashMap<>();
@@ -36,6 +52,10 @@
         }
     }
 
+    public boolean hasSubtypeInformation() {
+        return typeKey != null;
+    }
+
     public String getTypeKey() {
         return typeKey;
     }
@@ -43,4 +63,20 @@
     public Map<String, Class<?>> getAliases() {
         return aliases;
     }
+
+    public Class<?> getClazz() {
+        return clazz;
+    }
+
+    public List<JsonbPolymorphismTypeInfo> getParents() {
+        return parents;
+    }
+
+    public JsonbPolymorphismTypeInfo getFirstParent() {
+        if (parents.isEmpty()) {
+            return null;
+        }
+
+        return parents.get(0);
+    }
 }
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTest.java
index 87d8389..7fcc019 100644
--- a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTest.java
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/polymorphism/JsonbPolymorphismTest.java
@@ -42,6 +42,17 @@
     @Rule public JsonbRule jsonb = new JsonbRule();
 
     @Test
+    public void testNestedSerialization() {
+        Labrador labrador = new Labrador();
+        labrador.dogAge = 3;
+        labrador.labradorName = "john";
+        AnimalWrapper wrapper = new AnimalWrapper();
+        wrapper.animal = labrador;
+
+        assertEquals("{\"animal\":{\"@animal\":\"dog\",\"@dog\":\"labrador\",\"dogAge\":3,\"labradorName\":\"john\"}}", jsonb.toJson(wrapper));
+    }
+
+    @Test
     public void testSerialization() {
         Labrador labrador = new Labrador();
         labrador.dogAge = 3;
@@ -100,6 +111,10 @@
 
     }
 
+    public static class AnimalWrapper {
+        public Animal animal;
+    }
+
     @JsonbTypeInfo(key = "@animal", value = @JsonbSubtype(alias = "dog", type = Dog.class))
     public interface Animal {
     }
@@ -136,4 +151,17 @@
             return localDate;
         }
     }
+
+    @Test
+    public void typeInfoNotOnDirectParent() {
+        ConcreteSomething something = new ConcreteSomething();
+        assertEquals("{\"@type\":\"concrete\"}", jsonb.toJson(something));
+    }
+
+    @JsonbTypeInfo(
+            @JsonbSubtype(alias = "concrete", type = ConcreteSomething.class)
+    )
+    public static abstract class AbstractTopLevelSomething { }
+    public static abstract class AbstractMiddleLevelSomething extends AbstractTopLevelSomething { }
+    public static class ConcreteSomething extends AbstractMiddleLevelSomething { }
 }
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java
index cf44c5c..c06f38d 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingGeneratorImpl.java
@@ -192,12 +192,6 @@
                     generator.writeStartObject();
                 }
 
-                if (classMapping.serializedPolymorphicProperties != null) {
-                    for (Map.Entry<String, String> polymorphicProperty : classMapping.serializedPolymorphicProperties) {
-                        generator.write(polymorphicProperty.getKey(), polymorphicProperty.getValue());
-                    }
-                }
-
                 final boolean writeEnd = doWriteObjectBody(object, ignoredProperties, jsonPointer, generator);
                 if (writeEnd && writeBody) {
                     generator.writeEnd();
@@ -376,6 +370,12 @@
             return true;
         }
 
+        if (classMapping.serializedPolymorphicProperties != null) {
+            for (Map.Entry<String, String> polymorphicProperty : classMapping.serializedPolymorphicProperties) {
+                generator.write(polymorphicProperty.getKey(), polymorphicProperty.getValue());
+            }
+        }
+
         for (final Map.Entry<String, Mappings.Getter> getterEntry : classMapping.getters.entrySet()) {
             final Mappings.Getter getter = getterEntry.getValue();
             if (ignored != null && ignored.contains(getterEntry.getKey())) {
diff --git a/tck/jsonb/pom.xml b/tck/jsonb/pom.xml
index 880acaf..578adb5 100644
--- a/tck/jsonb/pom.xml
+++ b/tck/jsonb/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <artifactId>tck</artifactId>
     <groupId>org.apache.johnzon</groupId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.1-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
diff --git a/tck/jsonp/pom.xml b/tck/jsonp/pom.xml
index 8b5c048..012b3ce 100644
--- a/tck/jsonp/pom.xml
+++ b/tck/jsonp/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <artifactId>tck</artifactId>
     <groupId>org.apache.johnzon</groupId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.1-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
diff --git a/tck/pom.xml b/tck/pom.xml
index 01aff41..0b253f1 100644
--- a/tck/pom.xml
+++ b/tck/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <artifactId>johnzon</artifactId>
     <groupId>org.apache.johnzon</groupId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.1-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>