io.permazen.Util Maven / Gradle / Ivy
Show all versions of permazen-main Show documentation
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package io.permazen;
import com.google.common.base.Converter;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.reflect.TypeToken;
import io.permazen.annotation.OnValidate;
import io.permazen.util.ApplicationClassLoader;
import jakarta.validation.Constraint;
import jakarta.validation.groups.Default;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Various utility routines.
*/
public final class Util {
private static final String ANNOTATION_ELEMENT_UTILS_CLASS_NAME = "org.springframework.core.annotation.AnnotatedElementUtils";
private static final String ANNOTATION_ELEMENT_UTILS_GET_MERGED_ANNOTATION_METHOD_NAME = "getMergedAnnotation";
private static BiFunction, Annotation> annotationRetriever;
private Util() {
}
/**
* Find the annotation on the given element.
*
*
* If {@code spring-core} is available on the classpath, the implementation in {@link AnnotationScanner} utilizes Spring's
* {@link org.springframework.core.annotation.AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class)} method
* to find annotations that are either present or meta-present on the element, and includes support for
* annotation attribute overrides; otherwise, it just invokes {@link AnnotatedElement#getAnnotation(Class)}.
*
* @param element element with annotation or meta-present annotation
* @param annotationType type of the annotation to find
* @param annotation type
* @return the annotation found, or null if not found
*/
public static A getAnnotation(AnnotatedElement element, Class annotationType) {
Preconditions.checkArgument(element != null, "null element");
Preconditions.checkArgument(annotationType != null, "null annotationType");
synchronized (Util.class) {
if (Util.annotationRetriever == null) {
final Logger log = LoggerFactory.getLogger(Util.class);
try {
final Class> cl = Class.forName(
ANNOTATION_ELEMENT_UTILS_CLASS_NAME, true, ApplicationClassLoader.getInstance());
final Method method = cl.getMethod(ANNOTATION_ELEMENT_UTILS_GET_MERGED_ANNOTATION_METHOD_NAME,
AnnotatedElement.class, Class.class);
Util.annotationRetriever = (elem, atype) -> {
try {
return atype.cast(method.invoke(null, elem, atype));
} catch (Exception e) {
throw new RuntimeException("internal error", e);
}
};
if (log.isDebugEnabled())
log.debug("using Spring's " + cl.getSimpleName() + "." + method.getName() + "() for annotation retrieval");
} catch (ClassNotFoundException e) {
if (log.isDebugEnabled())
log.debug("using JDK AnnotatedElement.getAnnotation() for annotation retrieval");
Util.annotationRetriever = (elem, atype) -> elem.getAnnotation(atype);
} catch (Exception e) {
log.warn("using JDK AnnotatedElement.getAnnotation() for annotation retrieval", e);
Util.annotationRetriever = (elem, atype) -> elem.getAnnotation(atype);
}
}
assert Util.annotationRetriever != null;
}
return annotationType.cast(Util.annotationRetriever.apply(element, annotationType));
}
/**
* Determine if any JSR 303 validation annotations are present on the given type itself
* or any of its methods (public methods only).
*
* @param type object type
* @return a non-null object with JSR 303 validation requirements, or null if none found
* @throws IllegalArgumentException if {@code type} is null
*/
public static AnnotatedElement hasValidation(Class> type) {
// Sanity check
Preconditions.checkArgument(type != null, "null type");
// Check for annotations on the class itself
if (Util.hasValidationAnnotation(type))
return type;
// Check methods
for (Method method : type.getDeclaredMethods()) {
// Check for JSR 303 annotation
if ((method.getModifiers() & Modifier.PUBLIC) != 0 && Util.hasValidationAnnotation(method))
return method;
}
// Recurse on supertypes
final HashSet> visited = new HashSet<>();
visited.add(type);
for (TypeToken> typeToken : TypeToken.of(type).getTypes()) {
final Class> superType = typeToken.getRawType();
if (!visited.add(superType))
continue;
final AnnotatedElement annotatedElement = Util.hasValidation(superType);
if (annotatedElement != null)
return annotatedElement;
}
// None found
return null;
}
/**
* Determine if instances of the given type require any validation under the default validation group.
*
*
* This will be true if {@code type} or any of its declared methods has a JSR 303 (public methods only)
* or {@link OnValidate @OnValidate} annotation, or if any of its super-types requires validation.
*
* @param type object type
* @return true if {@code type} has any validation requirements
* @throws IllegalArgumentException if {@code type} is null
* @see ValidationMode
*/
public static boolean requiresDefaultValidation(Class> type) {
// Sanity check
Preconditions.checkArgument(type != null, "null type");
// Check for annotations on the class itself
if (Util.hasDefaultValidationAnnotation(type))
return true;
// Check methods
for (Method method : type.getDeclaredMethods()) {
// Check for @OnValidate annotation
if (method.isAnnotationPresent(OnValidate.class))
return true;
// Check for JSR 303 annotation
if ((method.getModifiers() & Modifier.PUBLIC) != 0 && Util.requiresDefaultValidation(method))
return true;
}
// Recurse on superclasses
for (TypeToken> typeToken : TypeToken.of(type).getTypes()) {
final Class> superType = typeToken.getRawType();
if (superType != type && Util.requiresDefaultValidation(superType))
return true;
}
// Done
return false;
}
/**
* Determine if the given getter method, or any method it overrides, has a JSR 303 validation constraint
* applicable under the default validation group.
*
* @param method annotated method
* @return true if {@code obj} has one or more JSR 303 annotations
* @throws IllegalArgumentException if {@code method} is null
*/
public static boolean requiresDefaultValidation(Method method) {
Preconditions.checkArgument(method != null, "null method");
final String methodName = method.getName();
final Class>[] paramTypes = method.getParameterTypes();
for (TypeToken> typeToken : TypeToken.of(method.getDeclaringClass()).getTypes()) {
final Class> superType = typeToken.getRawType();
try {
method = superType.getMethod(methodName, paramTypes);
} catch (NoSuchMethodException e) {
continue;
}
if (Util.hasDefaultValidationAnnotation(method))
return true;
}
return false;
}
/**
* Determine whether the given object has any JSR 303 annotation(s) defining validation constraints in the default group.
*
* @param obj annotated element
* @return true if {@code obj} has one or more JSR 303 default validation constraint annotations
* @throws IllegalArgumentException if {@code obj} is null
*/
public static boolean hasDefaultValidationAnnotation(AnnotatedElement obj) {
return Util.hasValidationAnnotation(obj, new Class>[] { Default.class });
}
/**
* Determine whether the given object has any JSR 303 annotation(s).
*
* @param obj annotated element
* @return true if {@code obj} has one or more JSR 303 validation constraint annotations
* @throws IllegalArgumentException if {@code obj} is null
*/
public static boolean hasValidationAnnotation(AnnotatedElement obj) {
return Util.hasValidationAnnotation(obj, null);
}
private static boolean hasValidationAnnotation(AnnotatedElement obj, Class>[] validationGroups) {
Preconditions.checkArgument(obj != null, "null obj");
for (Annotation annotation : obj.getAnnotations()) {
final Class> annotationType = annotation.annotationType();
if (!annotationType.isAnnotationPresent(Constraint.class))
continue;
final Class>[] groups;
try {
groups = (Class>[])annotation.annotationType().getMethod("groups").invoke(annotation);
} catch (NoSuchMethodException e) {
return true;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
if (groups == null || groups.length == 0)
return true;
return validationGroups == null || Util.isAnyGroupBeingValidated(groups, validationGroups);
}
return false;
}
/**
* Determine if a constraint whose {@code groups()} contain the given constraint group should be applied
* when validating with the given validation groups.
*
* @param constraintGroup validation group associated with a validation constraint
* @param validationGroups groups for which validation is being performed
* @return whether to apply the validation constraint
* @throws IllegalArgumentException if any null values are encountered
*/
public static boolean isGroupBeingValidated(Class> constraintGroup, Class>[] validationGroups) {
Preconditions.checkArgument(constraintGroup != null, "null constraintGroup");
Preconditions.checkArgument(validationGroups != null, "null validationGroups");
for (Class> validationGroup : validationGroups) {
Preconditions.checkArgument(validationGroup != null, "null validationGroup");
if (constraintGroup.isAssignableFrom(validationGroup))
return true;
}
return false;
}
/**
* Determine if a constraint whose {@code groups()} contain the given constraint groups should be applied
* when validating with the given validation groups.
*
* @param constraintGroups validation groups associated with a validation constraint
* @param validationGroups groups for which validation is being performed
* @return whether to apply the validation constraint
* @throws IllegalArgumentException if any null values are encountered
*/
public static boolean isAnyGroupBeingValidated(Class>[] constraintGroups, Class>[] validationGroups) {
Preconditions.checkArgument(constraintGroups != null, "null constraintGroups");
for (Class> constraintGroup : constraintGroups) {
if (Util.isGroupBeingValidated(constraintGroup, validationGroups))
return true;
}
return false;
}
/**
* Identify the named field in the given {@link PermazenClass}.
*
*
* The field may be specified by name like {@code "myfield"} or by name and storage ID like {@code "myfield#1234"}.
*
*
* To specify a sub-field of a complex field, qualify it with the parent field like {@code "mymap.key"}.
*
*
* This method is equivalent to {@code findField(pclass, fieldName, null)}.
*
* @param pclass containing object type
* @param fieldName field name
* @return resulting {@link PermazenField}, or null if no such field is found in {@code pclass}
* @throws IllegalArgumentException if {@code fieldName} is ambiguous or invalid
* @throws IllegalArgumentException if {@code pclass} or {@code fieldName} is null
*/
public static PermazenField findField(PermazenClass> pclass, String fieldName) {
return Util.findField(pclass, fieldName, null);
}
/**
* Identify the named simple field in the given {@link PermazenClass}.
*
*
* The field may be specified by name like {@code "myfield"} or by name and storage ID like {@code "myfield#1234"}.
*
*
* To specify a sub-field of a complex field, qualify it with the parent field like {@code "mymap.key"}.
*
*
* This method is equivalent to {@code findField(pclass, fieldName, true)} plus filtering out non-{@link PermazenSimpleField}s.
*
* @param pclass containing object type
* @param fieldName field name
* @return resulting {@link PermazenField}, or null if no such field is found in {@code pclass}
* @throws IllegalArgumentException if {@code fieldName} is ambiguous or invalid
* @throws IllegalArgumentException if {@code pclass} or {@code fieldName} is null
*/
public static PermazenSimpleField findSimpleField(PermazenClass> pclass, String fieldName) {
final PermazenField pfield = Util.findField(pclass, fieldName, true);
try {
return (PermazenSimpleField)pfield;
} catch (ClassCastException e) {
return null;
}
}
/**
* Identify the named field in the given {@link PermazenClass}.
*
*
* The field may be specified by name like {@code "myfield"} or by name and storage ID like {@code "myfield#1234"}.
*
*
* To specify a sub-field of a complex field, qualify it with the parent field like {@code "mymap.key"}.
*
*
* The {@code expectSubField} parameter controls what happens when a complex field is matched. If true, then
* either a sub-field must be specified, or else the complex field must have only one sub-field and then that
* sub-field is assumed. If false, it is an error to specify a sub-field of a complex field. If null, either is OK.
*
* @param pclass containing object type
* @param fieldName field name
* @param expectSubField true if the field should be a complex sub-field instead of a complex field,
* false if field should not be complex field instead of a complex sub-field, or null for don't care
* @return resulting {@link PermazenField}, or null if no such field is found in {@code pclass}
* @throws IllegalArgumentException if {@code fieldName} is ambiguous or invalid
* @throws IllegalArgumentException if {@code pclass} or {@code fieldName} is null
*/
public static PermazenField findField(PermazenClass> pclass, final String fieldName, Boolean expectSubField) {
// Sanity check
Preconditions.checkArgument(pclass != null, "null pclass");
Preconditions.checkArgument(fieldName != null, "null fieldName");
// Logging
final Logger log = LoggerFactory.getLogger(Util.class);
if (log.isTraceEnabled())
log.trace("Util.findField(): pclass={} fieldName={} expectSubField={}", pclass, fieldName, expectSubField);
// Split field name into components
final ArrayDeque components = new ArrayDeque<>(Arrays.asList(fieldName.split("\\.", -1)));
if (components.isEmpty() || components.size() > 2)
throw new IllegalArgumentException(String.format("invalid field name \"%s\"", fieldName));
// Represents a parsed field name with optional storage ID
class NameParse {
final String name;
final int storageId;
NameParse(String name) {
final int hash = name.indexOf('#');
if (hash == -1) {
this.name = name;
this.storageId = 0;
} else {
try {
if ((this.storageId = Integer.parseInt(name.substring(hash + 1))) <= 0)
throw new NumberFormatException();
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format("invalid field name \"%s\"", name));
}
this.name = name.substring(0, hash);
}
}
boolean matches(PermazenField pfield) {
return pfield.name.equals(this.name) && (this.storageId == 0 || pfield.storageId == this.storageId);
}
@Override
public String toString() {
return this.name;
}
}
// Get first field name component
final NameParse firstName = new NameParse(components.removeFirst());
// Find the PermazenField matching 'component' in pclass
PermazenField matchingField = pclass.fieldsByName.get(firstName.name);
if (matchingField == null || !firstName.matches(matchingField))
return null;
// Logging
if (log.isTraceEnabled())
log.trace("Util.findField(): found field {} in {}", matchingField, pclass.getType());
// Get sub-field requirements
final boolean requireSimpleField = Boolean.TRUE.equals(expectSubField);
final boolean disallowSubField = Boolean.FALSE.equals(expectSubField);
// Handle complex fields
if (matchingField instanceof PermazenComplexField) {
// Get complex field
final PermazenComplexField complexField = (PermazenComplexField)matchingField;
String description = "field \"" + firstName + "\" in " + pclass;
// Logging
if (log.isTraceEnabled())
log.trace("Util.findField(): field is a complex field");
// If no sub-field is given, field has only one sub-field, and a simple field is required, then default to that
if (requireSimpleField && components.isEmpty()) {
final List subFields = complexField.getSubFields();
if (subFields.size() == 1)
components.add(subFields.get(0).name);
}
// Is there a sub-field component?
if (!components.isEmpty()) {
// Find the specified sub-field
final NameParse subFieldName = new NameParse(components.removeFirst());
description = String.format("sub-field \"%s\" of %s", subFieldName.name, description);
try {
matchingField = complexField.getSubField(subFieldName.name);
if (!subFieldName.matches(matchingField)) {
throw new IllegalArgumentException(String.format("explicit storage ID %d != field storage ID %d",
subFieldName.storageId, matchingField.storageId));
}
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(String.format("invalid %s: %s", description, e.getMessage()), e);
}
// Verify it's OK to specify a complex sub-field
if (disallowSubField) {
throw new IllegalArgumentException(String.format(
"invalid %s: %s", description, "instead, specify the complex field itself"));
}
// Logging
if (log.isTraceEnabled()) {
log.trace("Util.findField(): also stepping through sub-field [{}.{}] to reach {}",
firstName, subFieldName, matchingField);
}
} else {
// Verify it's OK to end on a complex field
if (requireSimpleField) {
final String hints = complexField.getSubFields().stream()
.map(subField -> String.format("\"%s\"", subField.getFullName()))
.collect(Collectors.joining(" or "));
throw new IllegalArgumentException(String.format(
"for complex %s a sub-field must be specified (i.e., %s)", description, hints));
}
// Done
if (log.isTraceEnabled())
log.trace("Util.findField(): ended on complex field; result={}", matchingField);
}
} else if (log.isTraceEnabled()) {
if (matchingField instanceof PermazenSimpleField) {
final PermazenSimpleField simpleField = (PermazenSimpleField)matchingField;
log.trace("Util.findField(): field is a simple field of type {}", simpleField.getTypeToken());
} else
log.trace("Util.findField(): field is {}", matchingField);
}
// Check for extra garbage
if (!components.isEmpty())
throw new IllegalArgumentException(String.format("invalid field name \"%s\"", fieldName));
// Done
if (log.isTraceEnabled())
log.trace("Util.findField(): result={}", matchingField);
return matchingField;
}
/**
* Find the getter method we should override corresponding to the nominal getter method.
*
*
* This deals with generic sub-type bridge methods.
*
* @param type Java type (possibly a sub-type of the type in which {@code getter} is declared)
* @param getter supertype Java bean property getter method
* @return corresponding Java bean property getter method in {@code type}, possibly {@code getter}
*/
static Method findPermazenFieldGetterMethod(Class type, Method getter) {
Preconditions.checkArgument(type != null);
Preconditions.checkArgument(getter != null);
Preconditions.checkArgument(getter.getParameterTypes().length == 0);
Preconditions.checkArgument(getter.getReturnType() != void.class);
final TypeToken typeType = TypeToken.of(type);
final TypeToken> propertyType = typeType.resolveType(getter.getGenericReturnType());
for (TypeToken> superType : TypeToken.of(type).getTypes()) {
for (Method method : superType.getRawType().getDeclaredMethods()) {
if (!method.getName().equals(getter.getName()))
continue;
if (method.getParameterTypes().length != 0)
continue;
if (!typeType.resolveType(method.getGenericReturnType()).equals(propertyType))
continue;
if ((method.getModifiers() & (Modifier.PROTECTED | Modifier.PUBLIC)) == 0
|| (method.getModifiers() & Modifier.PRIVATE) != 0) {
throw new IllegalArgumentException(String.format(
"invalid getter method %s(): %s", getter.getName(), "method must be public or protected"));
}
return method;
}
}
return getter;
}
/**
* Find the setter method corresponding to a getter method. It must be either public or protected.
*
* @param type Java type (possibly a sub-type of the type in which {@code getter} is declared)
* @param getter Java bean property getter method
* @return Java bean property setter method
* @throws IllegalArgumentException if no corresponding setter method exists
*/
static Method findPermazenFieldSetterMethod(Class type, Method getter) {
final Matcher matcher = Pattern.compile("(is|get)(.+)").matcher(getter.getName());
if (!matcher.matches()) {
throw new IllegalArgumentException(String.format(
"can't infer setter method name from getter method %s() because name does not follow Java bean naming conventions",
getter.getName()));
}
final String setterName = "set" + matcher.group(2);
final TypeToken typeType = TypeToken.of(type);
final TypeToken> propertyType = typeType.resolveType(getter.getGenericReturnType());
for (TypeToken> superType : TypeToken.of(type).getTypes()) {
for (Method setter : superType.getRawType().getDeclaredMethods()) {
if (!setter.getName().equals(setterName) || setter.getReturnType() != void.class)
continue;
final Type[] ptypes = setter.getGenericParameterTypes();
if (ptypes.length != 1)
continue;
if (!typeType.resolveType(ptypes[0]).equals(propertyType))
continue;
if ((setter.getModifiers() & (Modifier.PROTECTED | Modifier.PUBLIC)) == 0
|| (setter.getModifiers() & Modifier.PRIVATE) != 0) {
throw new IllegalArgumentException(String.format(
"invalid setter method %s() corresponding to getter method %s(): method must be public or protected",
setterName, getter.getName()));
}
return setter;
}
}
throw new IllegalArgumentException(String.format(
"can't find any setter method %s() corresponding to getter method %s() taking %s and returning void",
setterName, getter.getName(), getter.getReturnType()));
}
/**
* Find unimplemented abstract methods in the given class.
*/
static Map findAbstractMethods(Class> type) {
final HashMap map = new HashMap<>();
// First find all methods, but don't include overridden supertype methods
for (TypeToken> superType : TypeToken.of(type).getTypes()) {
for (Method method : superType.getRawType().getDeclaredMethods()) {
final MethodKey key = new MethodKey(method);
if (!map.containsKey(key))
map.put(key, method);
}
}
// Now discard all the non-abstract methods
for (Iterator> i = map.entrySet().iterator(); i.hasNext(); ) {
final Map.Entry entry = i.next();
if ((entry.getValue().getModifiers() & Modifier.ABSTRACT) == 0)
i.remove();
}
// Done
return map;
}
/**
* Check whether a generic type and its erased equivalent match the same set of candidate types.
*
* @param genType parameter generic type
* @param candidates possible candidates for parameter
* @return mismatching candidate, or null if generic and erased match the same candidates
*/
public static TypeToken> findErasureDifference(TypeToken> genType, Collection extends TypeToken>> candidates) {
Preconditions.checkArgument(genType != null, "null genType");
Preconditions.checkArgument(candidates != null, "null candidates");
final Class> rawType = genType.getRawType();
for (TypeToken> candidate : candidates) {
final boolean matchesGen = genType.isSupertypeOf(candidate);
final boolean matchesRaw = rawType.isAssignableFrom(candidate.getRawType());
if (matchesGen != matchesRaw)
return candidate;
}
return null;
}
/**
* Substitute for {@link Stream#of(Object)}.
*
*
* Permazen needs this method because the v1.6 class files we generate don't support invoking
* static methods on interfaces.
*/
public static Stream streamOf(T obj) {
return Stream.of(obj);
}
/**
* Get the n'th generic type parameter.
*
* @param type parameterized generic type
* @param index type parameter index (zero based)
* @return type parameter at {@code index}
* @throws IllegalArgumentException if {@code type} is not a parameterized type with more than {@code index} type variables
*/
public static Type getTypeParameter(Type type, int index) {
Preconditions.checkArgument(type instanceof ParameterizedType, "type is missing generic type parameter(s)");
final ParameterizedType parameterizedType = (ParameterizedType)type;
final Type[] parameters = parameterizedType.getActualTypeArguments();
if (index >= parameters.length)
throw new IllegalArgumentException("type is missing generic type parameter(s)");
return parameters[index];
}
/**
* Invoke method via reflection and re-throw any checked exception wrapped in an {@link PermazenException}.
*
* @param method method to invoke
* @param target instance, or null if method is static
* @param params method parameters
* @return method return value
* @throws PermazenException if an error occurs
*/
public static Object invoke(Method method, Object target, Object... params) {
try {
return method.invoke(target, params);
} catch (InvocationTargetException e) {
Throwables.throwIfUnchecked(e.getCause());
throw new PermazenException(String.format("unexpected error invoking method %s on %s", method, target), e);
} catch (Exception e) {
Throwables.throwIfUnchecked(e);
throw new PermazenException(String.format("unexpected error invoking method %s on %s", method, target), e);
}
}
/**
* Return the reverse of the given {@link Converter}, treating a null converter as if it were the identity.
*
* @param converter original converter, or null for the identity conversion
* @return reversed converter
*/
@SuppressWarnings("unchecked")
public static Converter reverse(Converter converter) {
if (converter != null)
return converter.reverse();
return (Converter)Converter.