blob: 48a5de651f578f9674025f4c5574cd18a583674e [file] [log] [blame]
/*
* 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.jsonb.polymorphism;
import org.apache.johnzon.mapper.access.Meta;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import jakarta.json.bind.JsonbException;
import jakarta.json.bind.annotation.JsonbSubtype;
import jakarta.json.bind.annotation.JsonbTypeInfo;
import java.lang.reflect.AnnotatedElement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonbPolymorphismHandler {
private final Map<Class<?>, JsonbPolymorphismTypeInfo> typeInfoCache = new HashMap<>();
public boolean hasPolymorphism(Class<?> clazz) {
return getOrCreatePolymorphismTypeInfo(clazz) != null;
}
public Map.Entry<String, String>[] getPolymorphismPropertiesToSerialize(Class<?> clazz, Collection<String> otherProperties) {
List<Map.Entry<String, String>> result = new ArrayList<>();
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 (Map.Entry<String, Class<?>> aliasToType : polymorphismTypeInfo.getAliases().entrySet()) {
String alias = aliasToType.getKey();
Class<?> type = aliasToType.getValue();
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(polymorphismTypeInfo.getTypeKey(), bestMatchingAlias));
}
}
polymorphismTypeInfo = polymorphismTypeInfo.getFirstParent();
}
return result.toArray(Map.Entry[]::new);
}
public Class<?> getTypeToDeserialize(JsonObject jsonObject, Class<?> clazz) {
JsonbPolymorphismTypeInfo typeInfo = getOrCreatePolymorphismTypeInfo(clazz);
if (typeInfo == null) {
return clazz;
}
JsonValue typeValue = jsonObject.get(typeInfo.getTypeKey());
if (typeValue == null) {
return clazz;
}
if (typeValue.getValueType() != JsonValue.ValueType.STRING) {
throw new JsonbException("Property '" + typeInfo.getTypeKey() + "' isn't a String, resolving JsonbSubtype is impossible");
}
String typeValueString = ((JsonString) typeValue).getString();
final Class<?> result = typeInfo.getAliases().get(typeValueString);
if (result == null) {
throw new JsonbException("No JsonbSubtype found for alias '" + typeValueString + "' on " + clazz.getName());
}
return result;
}
/**
* 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 typeInfoCache.get(clazz);
}
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
*/
public void validateJsonbPolymorphismAnnotations(Class<?> classToValidate) {
validateSubtypeCompatibility(classToValidate);
validateOnlyOneParentWithTypeInfo(classToValidate);
validateNoTypeInfoKeyCollision(classToValidate);
}
/**
* Validation fails if any clazz and {@link JsonbSubtype#type()} aren't compatible.
*
* @param classToValidate Class to validate
* @throws JsonbException validation failed
*/
protected void validateSubtypeCompatibility(Class<?> classToValidate) {
JsonbPolymorphismTypeInfo polymorphismTypeInfo = getOrCreatePolymorphismTypeInfo(classToValidate);
if (polymorphismTypeInfo == null || !polymorphismTypeInfo.hasSubtypeInformation()) {
return;
}
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);
}
}
}
/**
* Validates that only one parent class (superclass + interfaces) has {@link JsonbTypeInfo} annotation
*
* @param classToValidate class to validate
* @throws JsonbException validation failed
*/
protected void validateOnlyOneParentWithTypeInfo(Class<?> classToValidate) {
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");
}
}
/**
* Validates that {@link JsonbTypeInfo#key()} is only defined once in type hierarchy.
* Assumes {@link JsonbPolymorphismHandler#validateOnlyOneParentWithTypeInfo(Class)} already passed.
*
* @param classToValidate class to validate
* @throws JsonbException validation failed
*/
protected void validateNoTypeInfoKeyCollision(Class<?> classToValidate) {
Map<String, Class<?>> keyToDefiningClass = new HashMap<>();
JsonbPolymorphismTypeInfo current = getOrCreatePolymorphismTypeInfo(classToValidate);
while (current != null) {
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 = current.getFirstParent();
}
}
}