TypeManufacturerUtil.java
package uk.co.jemos.podam.typeManufacturers;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.co.jemos.podam.api.DataProviderStrategy;
import uk.co.jemos.podam.api.ObjectStrategy;
import uk.co.jemos.podam.api.PodamUtils;
import uk.co.jemos.podam.common.*;
import jakarta.validation.Constraint;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
/**
* Type Manufacturer utility class.
*
* Created by tedonema on 01/07/2015.
*
* @since 6.0.0.RELEASE
*/
public abstract class TypeManufacturerUtil {
/** The application logger */
private static final Logger LOG = LoggerFactory.getLogger(TypeManufacturerUtil.class);
/**
* It returns a {@link AttributeStrategy} if one was specified in
* annotations, or {@code null} otherwise.
*
* @param strategy
* The data provider strategy
* @param annotations
* The list of annotations, irrelevant annotations will be removed
* @param attributeType
* Type of attribute expected to be returned
* @return {@link AttributeStrategy}, if {@link PodamStrategyValue} or bean
* validation constraint annotation was found among annotations
* @throws IllegalAccessException
* if attribute strategy cannot be instantiated
* @throws InstantiationException
* if attribute strategy cannot be instantiated
* @throws SecurityException
* if access security is violated
* @throws InvocationTargetException
* if invocation failed
* @throws IllegalArgumentException
* if illegal argument provided to a constructor
*/
public static AttributeStrategy<?> findAttributeStrategy(DataProviderStrategy strategy,
List<Annotation> annotations, Class<?> attributeType)
throws InstantiationException, IllegalAccessException, SecurityException, IllegalArgumentException, InvocationTargetException {
List<Annotation> localAnnotations = new ArrayList<Annotation>(annotations);
List<Class<? extends Annotation>> annotationsToCheck = new ArrayList<Class<? extends Annotation>>();
List<Class<? extends Annotation>> constraintAnnotationsWithoutRegisteredStrategy = new ArrayList<Class<? extends Annotation>>();
Iterator<Annotation> localAnnotationsIter = localAnnotations.iterator();
while (localAnnotationsIter.hasNext()) {
Annotation annotation = localAnnotationsIter.next();
if (annotation instanceof PodamStrategyValue) {
PodamStrategyValue strategyAnnotation = (PodamStrategyValue) annotation;
return strategyAnnotation.value().newInstance();
}
/* Podam annotation is present, this will be handled later by type manufacturers */
if (annotation.annotationType().getAnnotation(PodamAnnotation.class) != null) {
return null;
}
/* Find real class out of proxy */
Class<? extends Annotation> annotationClass = annotation.getClass();
if (Proxy.isProxyClass(annotationClass)) {
Class<?>[] interfaces = annotationClass.getInterfaces();
if (interfaces.length == 1) {
@SuppressWarnings("unchecked")
Class<? extends Annotation> tmp = (Class<? extends Annotation>) interfaces[0];
annotationClass = tmp;
}
}
AttributeStrategy<?> attrStrategy = strategy.getStrategyForAnnotation(annotationClass);
if (null != attrStrategy) {
return attrStrategy;
} else {
for (Class<?> iface : annotationClass.getInterfaces()) {
if (Annotation.class.isAssignableFrom(iface)) {
@SuppressWarnings("unchecked")
Class<? extends Annotation> tmp = (Class<? extends Annotation>) iface;
annotationsToCheck.add(tmp);
}
}
}
if (annotation.annotationType().getAnnotation(Constraint.class) != null) {
if (annotation instanceof NotNull ||
annotation instanceof NotBlank ||
annotation instanceof NotEmpty ||
annotation.annotationType().getName().equals("org.hibernate.validator.constraints.NotEmpty") ||
annotation.annotationType().getName().equals("org.hibernate.validator.constraints.NotBlank")) {
/* We don't need to do anything for NotNull constraint */
localAnnotationsIter.remove();
} else if (!NotNull.class.getPackage().equals(annotationClass.getPackage())) {
constraintAnnotationsWithoutRegisteredStrategy.add(annotationClass);
}
} else {
localAnnotationsIter.remove();
}
}
Iterator<Class<? extends Annotation>> annotationsToCheckIter = annotationsToCheck.iterator();
while (annotationsToCheckIter.hasNext()) {
Class<? extends Annotation> annotationClass = annotationsToCheckIter.next();
AttributeStrategy<?> attrStrategy = strategy.getStrategyForAnnotation(annotationClass);
if (null != attrStrategy) {
return attrStrategy;
} else {
for (Class<?> iface : annotationClass.getInterfaces()) {
if (Annotation.class.isAssignableFrom(iface)) {
@SuppressWarnings("unchecked")
Class<? extends Annotation> tmp = (Class<? extends Annotation>) iface;
annotationsToCheck.add(tmp);
}
}
}
}
for (Class<? extends Annotation> constraintAnnotationWithoutRegisteredStrategy : constraintAnnotationsWithoutRegisteredStrategy) {
/* This message is logged only when no applicable strategy is found for given annotation - neither for
* the annotation itself nor for any interface it implements. */
LOG.warn("Please, register AttributeStrategy for custom "
+ "constraint {}, in DataProviderStrategy! Value "
+ "will be left to null", constraintAnnotationWithoutRegisteredStrategy);
}
AttributeStrategy<?> retValue = null;
if (!localAnnotations.isEmpty()
&& !Collection.class.isAssignableFrom(attributeType)
&& !Map.class.isAssignableFrom(attributeType)
&& !attributeType.isArray()) {
retValue = new BeanValidationStrategy(attributeType);
}
return retValue;
}
/**
* Finds suitable static constructors for POJO instantiation
* <p>
* This method places required and provided types for object creation into a
* map, which will be used for type mapping.
* </p>
*
* @param factoryClass
* Factory class to produce the POJO
* @param pojoClass
* Typed class
* @return an array of suitable static constructors found
*/
public static Method[] findSuitableConstructors(final Class<?> factoryClass,
final Class<?> pojoClass) {
// If no publicly accessible constructors are available,
// the best we can do is to find a constructor (e.g.
// getInstance())
Method[] declaredMethods = factoryClass.getDeclaredMethods();
List<Method> constructors = new ArrayList<Method>();
// A candidate factory method is a method which returns the
// Class type
for (Method candidateConstructor : declaredMethods) {
if (candidateConstructor.getReturnType().equals(pojoClass)) {
if (Modifier.isStatic(candidateConstructor.getModifiers())
|| !factoryClass.equals(pojoClass)) {
constructors.add(candidateConstructor);
}
}
}
return constructors.toArray(new Method[constructors.size()]);
}
/**
* Searches for annotation with information about collection/map size
* and filling strategies
*
* @param strategy
* a data provider strategy
* @param annotations
* a list of annotations to inspect
* @param collectionElementType
* a collection element type
* @param elementStrategyHolder
* a holder to pass found element strategy back to the caller,
* can be null
* @param keyStrategyHolder
* a holder to pass found key strategy back to the caller,
* can be null
* @return
* A number of element in collection or null, if no annotation was
* found
* @throws InstantiationException
* A strategy cannot be instantiated
* @throws IllegalAccessException
* A strategy cannot be instantiated
*/
public static Integer findCollectionSize( DataProviderStrategy strategy,
List<Annotation> annotations,
Class<?> collectionElementType,
Holder<AttributeStrategy<?>> elementStrategyHolder,
Holder<AttributeStrategy<?>> keyStrategyHolder)
throws InstantiationException, IllegalAccessException {
// If the user defined a strategy to fill the collection elements,
// we use it
Size size = null;
for (Annotation annotation : annotations) {
if (annotation instanceof PodamCollection) {
PodamCollection collectionAnnotation = (PodamCollection) annotation;
if (null != elementStrategyHolder) {
Class<? extends AttributeStrategy<?>> attributeStrategy
= collectionAnnotation.collectionElementStrategy();
if (null == attributeStrategy || ObjectStrategy.class.isAssignableFrom(attributeStrategy)) {
attributeStrategy = collectionAnnotation.mapElementStrategy();
}
if (null != attributeStrategy) {
elementStrategyHolder.setValue(attributeStrategy.newInstance());
}
}
if (null != keyStrategyHolder) {
Class<? extends AttributeStrategy<?>> attributeStrategy
= collectionAnnotation.mapKeyStrategy();
if (null != attributeStrategy) {
keyStrategyHolder.setValue(attributeStrategy.newInstance());
}
}
return collectionAnnotation.nbrElements();
} else if (annotation instanceof Size) {
size = (Size) annotation;
}
}
Integer nbrElements = strategy
.getNumberOfCollectionElements(collectionElementType);
if (null != size) {
if (nbrElements > size.max()) {
nbrElements = size.max();
}
if (nbrElements < size.min()) {
nbrElements = size.min();
}
}
return nbrElements;
}
/**
* Utility to merge actual types with supplied array of generic type
* substitutions
*
* @param attributeType
* actual type of object
* @param genericAttributeType
* generic type of object
* @param suppliedTypes
* an array of supplied types for generic type substitution
* @param manufacturingCtx
* a context with a map relating the generic class arguments ("<T, V>" for
* example) with their actual types
* @return An array of merged actual and supplied types with generic types
* resolved
*/
public static Type[] mergeActualAndSuppliedGenericTypes(
Class<?> attributeType, Type genericAttributeType, Type[] suppliedTypes,
ManufacturingContext manufacturingCtx) {
TypeVariable<?>[] actualTypes = attributeType.getTypeParameters();
if (actualTypes.length <= suppliedTypes.length) {
return suppliedTypes;
}
Type[] genericTypes = null;
if (genericAttributeType instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) genericAttributeType;
genericTypes = paramType.getActualTypeArguments();
} else if (genericAttributeType instanceof WildcardType) {
WildcardType wildcardType = (WildcardType) genericAttributeType;
genericTypes = wildcardType.getLowerBounds();
if (ArrayUtils.isEmpty(genericTypes)) {
genericTypes = wildcardType.getUpperBounds();
}
}
List<Type> resolvedTypes = new ArrayList<Type>();
List<Type> substitutionTypes = new ArrayList<Type>(Arrays.asList(suppliedTypes));
for (int i = 0; i < actualTypes.length; i++) {
Type type = null;
if (actualTypes[i] instanceof TypeVariable) {
type = manufacturingCtx.resolveType(((TypeVariable<?>)actualTypes[i]).getName());
} else if (actualTypes[i] instanceof WildcardType) {
AtomicReference<Type[]> methodGenericTypeArgs
= new AtomicReference<Type[]>(PodamConstants.NO_TYPES);
type = TypeManufacturerUtil.resolveGenericParameter(actualTypes[i], manufacturingCtx,
methodGenericTypeArgs);
}
if ((type == null) && (genericTypes != null)) {
if (genericTypes[i] instanceof Class) {
type = genericTypes[i];
} else if (genericTypes[i] instanceof WildcardType) {
AtomicReference<Type[]> methodGenericTypeArgs
= new AtomicReference<Type[]>(PodamConstants.NO_TYPES);
type = resolveGenericParameter(genericTypes[i], manufacturingCtx,
methodGenericTypeArgs);
} else if (genericTypes[i] instanceof ParameterizedType) {
type = genericTypes[i];
} else {
LOG.debug("Skipping type {} {}", actualTypes[i], genericTypes[i]);
}
}
if (type != null) {
resolvedTypes.add(type);
if (!substitutionTypes.isEmpty() && substitutionTypes.get(0).equals(type)) {
substitutionTypes.remove(0);
}
}
}
Type[] resolved = resolvedTypes.toArray(new Type[resolvedTypes.size()]);
Type[] supplied = substitutionTypes.toArray(new Type[substitutionTypes.size()]);
return ArrayUtils.addAll(resolved, supplied);
}
/**
* It resolves generic parameter type
*
*
* @param paramType
* The generic parameter type
* @param manufacturingCtx
* A manufacturing context with a map of resolved types
* @param methodGenericTypeArgs
* Return value posible generic types of the generic parameter
* type
* @return value for class representing the generic parameter type
*/
public static Class<?> resolveGenericParameter(Type paramType,
ManufacturingContext manufacturingCtx,
AtomicReference<Type[]> methodGenericTypeArgs) {
Class<?> parameterType = null;
//Safe copy
manufacturingCtx.cloneTypeArgsMap();
methodGenericTypeArgs.set(PodamConstants.NO_TYPES);
if (paramType instanceof Class) {
parameterType = (Class<?>) paramType;
} else if (paramType instanceof TypeVariable<?>) {
final TypeVariable<?> typeVariable = (TypeVariable<?>) paramType;
final Type type = manufacturingCtx.resolveType(typeVariable.getName());
if (type != null) {
parameterType = resolveGenericParameter(type, manufacturingCtx,
methodGenericTypeArgs);
}
} else if (paramType instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) paramType;
parameterType = (Class<?>) pType.getRawType();
Type[] actualTypeArgs = pType.getActualTypeArguments();
if (!manufacturingCtx.isTypeArgsEmpty()) {
for (int i = 0; i < actualTypeArgs.length; i++) {
Class<?> tmp = resolveGenericParameter(actualTypeArgs[i],
manufacturingCtx, methodGenericTypeArgs);
if (tmp != actualTypeArgs[i]) {
/* If actual type argument has its own arguments,
* we will loose them now, so we will leave type unresolved
* until lower levels of type resolution */
if (ArrayUtils.isEmpty(methodGenericTypeArgs.get())) {
actualTypeArgs[i] = tmp;
}
}
}
}
methodGenericTypeArgs.set(actualTypeArgs);
} else if (paramType instanceof WildcardType) {
WildcardType wType = (WildcardType) paramType;
Type[] bounds = wType.getLowerBounds();
String msg;
if (ArrayUtils.isNotEmpty(bounds)) {
msg = "Lower bounds:";
} else {
bounds = wType.getUpperBounds();
msg = "Upper bounds:";
}
if (ArrayUtils.isNotEmpty(bounds)) {
LOG.debug(msg + Arrays.toString(bounds));
parameterType = resolveGenericParameter(bounds[0], manufacturingCtx,
methodGenericTypeArgs);
}
}
if (parameterType == null) {
LOG.warn("Unrecognized type {}. Will use Object instead",
paramType);
parameterType = Object.class;
}
manufacturingCtx.restoreTypeArgsMap();
return parameterType;
}
/**
* It retrieves the value for the {@link PodamStrategyValue} annotation with
* which the attribute was annotated
*
* @param attributeType
* The attribute type, used for type checking
* @param annotations
* Annotations attached to the attribute
* @param attributeStrategy
* The {@link AttributeStrategy} to use
* @return The value for the {@link PodamStrategyValue} annotation with
* which the attribute was annotated
* @throws IllegalArgumentException
* If the type of the data strategy defined for the
* {@link PodamStrategyValue} annotation is not assignable to
* the annotated attribute. This de facto guarantees type
* safety.
*/
public static Object returnAttributeDataStrategyValue(Class<?> attributeType,
List<Annotation> annotations,
AttributeStrategy<?> attributeStrategy)
throws IllegalArgumentException {
if (null == attributeStrategy) {
return null;
}
Object retValue = attributeStrategy.getValue(attributeType, annotations);
if (retValue != null) {
Class<?> desiredType = attributeType.isPrimitive() ?
PodamUtils.primitiveToBoxedType(attributeType) : attributeType;
if (!desiredType.isAssignableFrom(retValue.getClass())) {
String errMsg = "The AttributeStrategy "
+ attributeStrategy.getClass().getName()
+ " produced value of type "
+ retValue.getClass().getName()
+ " incompatible with attribute type "
+ attributeType.getName();
throw new IllegalArgumentException(errMsg);
} else {
LOG.debug("The parameter {} will be filled using the following strategy {}",
attributeType, attributeStrategy);
}
}
return retValue;
}
}