Skip to content

Commit

Permalink
added specific support for sealed hierarchies
Browse files Browse the repository at this point in the history
  • Loading branch information
lpandzic committed Oct 12, 2023
1 parent 751323d commit 926e5a8
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.infobip.jackson;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class SealedJsonHierarchiesTypeResolver implements JsonTypeResolver {

private final String typePropertyName;
private final Map<String, ? extends SimpleJsonTypeResolver<?>> typeValueToResolver;

public SealedJsonHierarchiesTypeResolver(String typePropertyName,
Map<String, ? extends SimpleJsonTypeResolver<?>> typeValueToResolver) {
this.typePropertyName = typePropertyName;
this.typeValueToResolver = typeValueToResolver;
}

public static SealedJsonHierarchiesTypeResolver of(Class<?> parentType,
Function<Class<?>, Optional<JsonTypeResolver>> jsonTypeResolverFactory) {
var permittedSubclasses = parentType.getPermittedSubclasses();

if (Objects.isNull(permittedSubclasses)) {
throw new IllegalArgumentException(parentType + " is not a sealed hierarchy");
}

var resolvers = Stream.of(permittedSubclasses)
.map(jsonTypeResolverFactory)
.flatMap(Optional::stream)
.toList();

for (JsonTypeResolver resolver : resolvers) {
if (!(resolver instanceof SimpleJsonTypeResolver<?>)) {
throw new IllegalArgumentException(
parentType + " has a sub hierarchy which does not use SimpleJsonTypeResolver, this is currently unsupported");
}
}

var simpleResolvers = resolvers.stream()
.map(resolver -> (SimpleJsonTypeResolver<?>) resolver)
.toList();

if (simpleResolvers.isEmpty()) {
throw new IllegalArgumentException(parentType + " does not have any resolvable sub hierarchies");
}

var firstSimpleResolver = simpleResolvers.get(0);
var typePropertyName = firstSimpleResolver.getTypePropertyName();

for (SimpleJsonTypeResolver<?> simpleResolver : simpleResolvers) {
if (!simpleResolver.getTypePropertyName().equals(typePropertyName)) {
throw new IllegalArgumentException(
simpleResolver.getType() + " does not use the expected type property name. Expected " + typePropertyName +
" Got " + firstSimpleResolver.getTypePropertyName());
}
}

requireNoDuplicates(simpleResolvers);

var typeValueToResolver = simpleResolvers.stream()
.flatMap(resolver -> Stream.of(resolver.getType().getEnumConstants())
.map(String::valueOf)
.collect(Collectors.toMap(
Function.identity(),
b -> resolver))
.entrySet()
.stream())
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue));

return new SealedJsonHierarchiesTypeResolver(typePropertyName, typeValueToResolver);
}

private static void requireNoDuplicates(List<? extends SimpleJsonTypeResolver<?>> simpleResolvers) {
var enumValues = new HashSet<String>();
simpleResolvers.forEach(resolver -> Stream.of(resolver.getType().getEnumConstants())
.map(String::valueOf)
.forEach(type -> {
if (enumValues.contains(type)) {
throw new IllegalArgumentException(
"Duplicate type in multi hierarchy" + type);
}
enumValues.add(String.valueOf(type));
}));
}

@Override
public Class<?> resolve(Map<String, Object> json) {
var value = String.valueOf(json.get(typePropertyName));
SimpleJsonTypeResolver<?> simpleJsonTypeResolver = typeValueToResolver.get(value);
return simpleJsonTypeResolver.resolve(json);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.infobip.jackson;

@JsonTypeResolveWith(SealedJsonHierarchiesTypeResolver.class)
public interface SealedSimpleJsonHierarchies {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@

public class SimpleJsonTypeResolver<E extends Enum<E> & TypeProvider<?>> extends CompositeJsonTypeResolver<E> {

private final Class<E> type;

public SimpleJsonTypeResolver(Class<E> type) {
this(type, DEFAULT_TYPE_PROPERTY_NAME);
}

public SimpleJsonTypeResolver(Class<E> type, String typePropertyName) {
super(type, typePropertyName, e -> e.getType());
this.type = type;
}

public SimpleJsonTypeResolver(Class<E> type, String typePropertyName, boolean valueInUpperCase) {
super(type, typePropertyName, e -> e.getType(), valueInUpperCase);
this.type = type;
}

@Override
public Class<E> getType() {
return type;
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package com.infobip.jackson;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.*;
import java.util.*;

public class JsonTypeResolverFactory {

private final Set<Class<?>> preferredInterfaces = Set.of(SimpleJsonHierarchy.class, PresentPropertyJsonHierarchy.class);
private final Set<Class<?>> ignoredClasses = Set.of(SimpleJsonHierarchy.class,
PresentPropertyJsonHierarchy.class);

Expand All @@ -35,12 +33,23 @@ private JsonTypeResolver createJsonTypeResolver(Class<?> targetType,
return createPresentPropertyJsonTypeResolver(targetType);
}

try {
if (isSubtypeOf(type, PresentPropertyJsonTypeResolver.class)) {
if (isSubtypeOf(type, PresentPropertyJsonTypeResolver.class)) {
try {
return createSubtypeOfPresentPropertyJsonTypeResolver(type, targetType);
} catch (IllegalAccessException | InvocationTargetException | InstantiationException |
NoSuchMethodException e) {
throw new IllegalArgumentException("Failed to create PresentPropertyJsonTypeResolver for " + type, e);
}
}

if (type.equals(SealedJsonHierarchiesTypeResolver.class)) {
return SealedJsonHierarchiesTypeResolver.of(targetType, this::create);
}

try {
return (JsonTypeResolver) getConstructor(type).newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
} catch (InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException e) {
throw new IllegalArgumentException("Failed to resolve default constructor for " + type, e);
}
}
Expand All @@ -57,24 +66,24 @@ private <E extends Enum<E> & TypeProvider<?>> SimpleJsonTypeResolver<E> createSi
}

private <E extends Enum<E> & TypeProvider<?>> PresentPropertyJsonTypeResolver<E> createPresentPropertyJsonTypeResolver(
Class<?> type) {
Class<?> type) {
Class<E> enumType = resolveFirstGenericTypeArgument(type, PresentPropertyJsonHierarchy.class);
return new PresentPropertyJsonTypeResolver<>(enumType);
}

@SuppressWarnings("unchecked")
private <E extends Enum<E> & TypeProvider<?>> PresentPropertyJsonTypeResolver<E> createSubtypeOfPresentPropertyJsonTypeResolver(
Class<?> resolverType,
Class<?> targetType) throws
IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
Class<?> resolverType,
Class<?> targetType) throws
IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {

Class<E> enumType = resolveFirstGenericTypeArgument(targetType, PresentPropertyJsonHierarchy.class);
for (Constructor<?> cctor : resolverType.getConstructors()) {
if (cctor.getParameterCount() == 0) {
return (PresentPropertyJsonTypeResolver<E>) getConstructor(resolverType).newInstance();
} else if (cctor.getParameterCount() == 1 && cctor.getParameterTypes()[0].equals(Class.class)) {
return (PresentPropertyJsonTypeResolver<E>) getConstructor(resolverType, Class.class).newInstance(
enumType);
enumType);
}
}
throw new IllegalArgumentException("Failed to resolve default constructor for " + resolverType);
Expand All @@ -101,7 +110,15 @@ private <A extends Annotation> A extractAnnotation(Class<?> type, Class<A> annot
return currentJsonTypeResolveWith;
}

Class<?>[] interfaces = type.getInterfaces();
var interfaces = type.getInterfaces();

for (Class<?> preferredInterface : preferredInterfaces) {
for (Class<?> anInterface : interfaces) {
if(preferredInterface.equals(anInterface)) {
return anInterface.getAnnotation(annotationType);
}
}
}

for (Class<?> anInterface : interfaces) {
A annotationJsonTypeResolveWith = extractAnnotation(anInterface, annotationType);
Expand All @@ -126,7 +143,7 @@ private <T> T resolveFirstGenericTypeArgument(Class<?> type, Class<?> interfaceT
}

throw new IllegalArgumentException(
"Failed to resolve type argument " + type + " " + interfaceType + " with index" + 0);
"Failed to resolve type argument " + type + " " + interfaceType + " with index" + 0);
}

private List<Type> getAllInterfaces(Class<?> type) {
Expand All @@ -149,7 +166,6 @@ private List<Type> getAllInterfaces(Class<?> type) {

private boolean isSubtypeOf(Class<?> type, Class<?> parentType) {
return type.getSuperclass() != null
&& (type.getSuperclass().equals(parentType) || isSubtypeOf(type.getSuperclass(), parentType));
&& (type.getSuperclass().equals(parentType) || isSubtypeOf(type.getSuperclass(), parentType));
}

}
Loading

0 comments on commit 926e5a8

Please sign in to comment.