org.conqat.engine.commons.util.canonical.OrderingBeanSerializerModifier Maven / Gradle / Ivy
Show all versions of teamscale-commons Show documentation
package org.conqat.engine.commons.util.canonical;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.checkerframework.checker.nullness.qual.NonNull;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.ClassUtil;
import com.fasterxml.jackson.databind.util.Converter;
/**
* Implements the behavior described in {@link CanonicalJson}.
*
* Implements the {@link CanonicalJson#ordering() ordering} by replacing the respective serializer
* with one that orders the elements first (see {@link SortedCollection} and
* {@link SortedCollectionConverter}).
*/
/* package */ class OrderingBeanSerializerModifier extends BeanSerializerModifier {
@Override
public List changeProperties(SerializationConfig config, BeanDescription beanDesc,
List beanProperties) {
for (BeanPropertyWriter beanProperty : beanProperties) {
CanonicalJson canonicalJson = beanProperty.getAnnotation(CanonicalJson.class);
if (canonicalJson != null) {
applyCanonicalJson(config, beanProperty, canonicalJson);
}
}
return beanProperties;
}
private static void applyCanonicalJson(SerializationConfig config, BeanPropertyWriter beanProperty,
CanonicalJson canonicalJson) {
if (!Collection.class.isAssignableFrom(beanProperty.getType().getRawClass())) {
// For now only support Collections
throw new IllegalArgumentException(String.format("Type %s is not supported for ordered serialization",
beanProperty.getType().getRawClass()));
}
Comparator comparator = buildComparator(config, beanProperty, canonicalJson.ordering());
StdDelegatingSerializer serializer = new StdDelegatingSerializer(
new SortedCollectionConverter<>(comparator, beanProperty.getType()));
beanProperty.assignSerializer(serializer);
}
private static Comparator buildComparator(SerializationConfig config, BeanPropertyWriter beanProperty,
CanonicalJson.OrderBy orderBy) {
boolean hasComparator = orderBy.comparator() != CanonicalJson.OrderBy.NoComparator.class;
boolean hasProperties = orderBy.properties().length > 0;
if (hasComparator && hasProperties) {
throw new IllegalArgumentException(String.format(
"%s annotation on %s has properties (%s) and a comparator (%s) defined, but only one can be present",
CanonicalJson.OrderBy.class.getSimpleName(), beanProperty.getMember(),
Arrays.toString(orderBy.properties()), orderBy.comparator()));
} else if (!hasComparator && !hasProperties) {
throw new IllegalArgumentException(
String.format("%s annotation on %s has neither properties nor a comparator defined",
CanonicalJson.OrderBy.class.getSimpleName(), beanProperty.getMember()));
}
if (hasComparator) {
return instantiateComparatorByClass(config, orderBy.comparator());
} else {
return instantiateComparatorByProperties(config, beanProperty.getType().getContentType(),
orderBy.properties());
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private static Comparator instantiateComparatorByProperties(SerializationConfig config, JavaType contentType,
String[] properties) {
Comparator result = EmptyComparator.getInstance();
Map propertiesByName = config.introspect(contentType).findProperties().stream()
.collect(Collectors.toMap(BeanPropertyDefinition::getName, Function.identity()));
for (String propertyName : properties) {
BeanPropertyDefinition propertyDefinition = propertiesByName.get(propertyName);
if (propertyDefinition == null) {
throw new IllegalArgumentException(
String.format("Property \"%s\" was not found on %s", propertyName, contentType.getRawClass()));
}
if (!Comparable.class.isAssignableFrom(propertyDefinition.getRawPrimaryType())
// primitives are also comparable (by their wrapper)
&& !propertyDefinition.getRawPrimaryType().isPrimitive()) {
throw new IllegalArgumentException(
String.format("Property \"%s\" is not of type Comparable (detected type: %s)", propertyName,
propertyDefinition.getRawPrimaryType()));
}
AnnotatedMember accessor = propertyDefinition.getAccessor();
if (config.isEnabled(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)) {
accessor.fixAccess(config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
}
result = result.thenComparing(accessor::getValue, Comparator.nullsFirst(Comparator.naturalOrder()));
}
return result;
}
@NonNull
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Comparator instantiateComparatorByClass(SerializationConfig config,
Class extends Comparator> comparatorClass) {
try {
Constructor extends Comparator> constructor = comparatorClass.getDeclaredConstructor();
if (Modifier.isPrivate(constructor.getModifiers())) {
// Do not allow any private constructor, as the class expects this to be
// not invoked externally
throw new IllegalArgumentException(
String.format("Comparator %s only has a private no-args constructor and cannot be instantiated",
comparatorClass));
}
if (config.isEnabled(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)) {
// Still overwrite accessibility for package/protected constructors or private
// inner class with public constructor
ClassUtil.checkAndFixAccess(constructor,
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
}
return constructor.newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalArgumentException(String.format("Unable to instantiate %s", comparatorClass), e);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(
String.format("Comparator %s has no invocable no-args constructor", comparatorClass), e);
}
}
/**
* Simple wrapper around any collection, that provides a stable sorted JSON representation.
*/
private static class SortedCollection {
private final Collection source;
private final Comparator comparator;
public SortedCollection(Collection source, Comparator comparator) {
this.source = source;
this.comparator = comparator;
}
/**
* @return Sorted list according to the #comparator
*/
@JsonValue
public List toJson() {
return source.stream().sorted(comparator).collect(Collectors.toList());
}
}
/**
* {@link Converter} from any {@link Collection} to a {@link SortedCollection}.
*/
private static class SortedCollectionConverter implements Converter, SortedCollection> {
private final Comparator comparator;
private final JavaType inputType;
private SortedCollectionConverter(Comparator comparator, JavaType inputType) {
this.inputType = inputType;
this.comparator = comparator;
}
@Override
public JavaType getInputType(TypeFactory typeFactory) {
return inputType;
}
@Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructCollectionLikeType(SortedCollection.class, inputType.getContentType());
}
@Override
public SortedCollection convert(Collection value) {
return new SortedCollection<>(value, comparator);
}
}
/**
* "Empty" implementation of {@link Comparator}, that orders all elements the same.
*/
private static class EmptyComparator implements Comparator {
@SuppressWarnings("rawtypes")
private static final EmptyComparator INSTANCE = new EmptyComparator();
@SuppressWarnings("unchecked")
public static EmptyComparator getInstance() {
return INSTANCE;
}
@Override
public Comparator reversed() {
// nothing to do, as everything is the same
return this;
}
@Override
@SuppressWarnings("unchecked")
public Comparator thenComparing(Comparator super T> other) {
// Can directly return other, as this one will always delegate to it anyway.
return (Comparator) other;
}
@Override
public int compare(T o1, T o2) {
return 0;
}
}
}