org.conqat.lib.commons.reflect.ReflectionUtils Maven / Gradle / Ivy
Show all versions of teamscale-lib-commons Show documentation
/*
* Copyright (c) CQSE GmbH
*
* Licensed 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.conqat.lib.commons.reflect;
import java.awt.Color;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.conqat.lib.commons.assertion.CCSMAssert;
import org.conqat.lib.commons.color.ColorUtils;
import org.conqat.lib.commons.enums.EnumUtils;
import org.conqat.lib.commons.string.StringUtils;
/**
* This class provides utility methods for reflection purposes. In particular, it provides access to
* {@link FormalParameter}.
*/
public class ReflectionUtils {
/**
* Convert a String to an Object of the provided type. It supports conversion to primitive types and
* simple tests (char: use first character of string, boolean: test for values "true", "on", "1",
* "yes"). Enums are handled by the {@link EnumUtils#valueOfIgnoreCase(Class, String)} method.
* Otherwise, it is checked if the target type has a constructor that takes a single string, and it
* is invoked. For all other cases an exception is thrown, as no conversion is possible.
*
* @see #convertString(String, Class)
*
* @param value
* the string to be converted.
* @param targetType
* the type of the resulting object.
* @return the converted object.
* @throws TypeConversionException
* in the case that no conversion could be performed.
*
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static T convertString(String value, Class targetType) throws TypeConversionException {
// value must be provided
if (value == null) {
if (String.class.equals(targetType)) {
return (T) StringUtils.EMPTY_STRING;
}
throw new TypeConversionException("Null value can't be converted to type '" + targetType.getName() + "'.");
}
if (targetType.equals(Object.class) || targetType.equals(String.class)) {
return (T) value;
}
if (targetType.isPrimitive() || EJavaPrimitive.isWrapperType(targetType)) {
return convertPrimitive(value, targetType);
}
if (targetType.isEnum()) {
// we checked manually before
Object result = EnumUtils.valueOfIgnoreCase((Class) targetType, value);
if (result == null) {
throw new TypeConversionException("'" + value + "' is no valid value for enum " + targetType.getName());
}
return (T) result;
}
if (targetType.equals(Color.class)) {
Color result = ColorUtils.fromString(value);
if (result == null) {
throw new TypeConversionException("'" + value + "' is not a valid color!");
}
return (T) result;
}
// Check if the target type has a constructor taking a single string.
try {
Constructor c = targetType.getConstructor(String.class);
return c.newInstance(value);
} catch (Exception e) {
throw new TypeConversionException("No constructor taking one String argument found for type '" + targetType
+ "' (" + e.getMessage() + ")", e);
}
}
/**
* Obtain array of formal parameters for a method.
*
* @see FormalParameter
*/
public static FormalParameter[] getFormalParameters(Method method) {
int parameterCount = method.getParameterTypes().length;
FormalParameter[] parameters = new FormalParameter[parameterCount];
for (int i = 0; i < parameterCount; i++) {
parameters[i] = new FormalParameter(method, i);
}
return parameters;
}
/**
* Get super class list of a class.
*
* @param clazz
* the class to start traversal from
* @return a list of super class where the direct super class of the provided class is the first
* member of the list.
* For {@link Object}, primitives and interfaces this returns an empty list.
* For arrays this returns a list containing only {@link Object}.
* For enums this returns a list containing {@link Enum} and {@link Object}
*/
public static List> getSuperClasses(Class clazz) {
ArrayList> superClasses = new ArrayList<>();
findSuperClasses(clazz, superClasses);
return superClasses;
}
/**
* Check whether an Object of the source type can be used instead of an Object of the target type.
* This method is required, as the {@link Class#isAssignableFrom(java.lang.Class)} does not handle
* primitive types.
*
* @param source
* type of the source object
* @param target
* type of the target object
* @return whether an assignment would be possible.
*/
public static boolean isAssignable(Class source, Class target) {
return resolvePrimitiveClass(target).isAssignableFrom(resolvePrimitiveClass(source));
}
/**
* Returns the wrapper class type for a primitive type (e.g. Integer
for an
* int
). If the given class is not a primitive, the class itself is returned.
*
* @param clazz
* the class.
* @return the corresponding class type.
*/
public static Class resolvePrimitiveClass(Class clazz) {
if (!clazz.isPrimitive()) {
return clazz;
}
EJavaPrimitive primitive = EJavaPrimitive.getForPrimitiveClass(clazz);
if (primitive == null) {
throw new IllegalStateException("Did Java get a new primitive? " + clazz.getName());
}
return primitive.getWrapperClass();
}
/**
* Convert a String to an Object of the provided type. This only works for primitive types and
* wrapper types.
*
* @param value
* the string to be converted.
* @param targetType
* the type of the resulting object.
* @return the converted object.
* @throws TypeConversionException
* in the case that no conversion could be performed.
*/
@SuppressWarnings("unchecked")
/* package */static T convertPrimitive(String value, Class targetType) throws TypeConversionException {
EJavaPrimitive primitive = EJavaPrimitive.getForPrimitiveOrWrapperClass(targetType);
if (primitive == null) {
throw new IllegalArgumentException("Type '" + targetType.getName() + "' is not a primitive type!");
}
try {
switch (primitive) {
case BOOLEAN:
boolean b = "1".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value)
|| "on".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value);
return (T) Boolean.valueOf(b);
case CHAR:
return (T) Character.valueOf(value.charAt(0));
case BYTE:
return (T) Byte.valueOf(value);
case SHORT:
return (T) Short.valueOf(value);
case INT:
return (T) Integer.valueOf(value);
case LONG:
return (T) Long.valueOf(value);
case FLOAT:
return (T) Float.valueOf(value);
case DOUBLE:
return (T) Double.valueOf(value);
default:
throw new TypeConversionException("No conversion possible for " + primitive);
}
} catch (NumberFormatException e) {
throw new TypeConversionException("Value'" + value + "' can't be converted to type '" + targetType.getName()
+ "' (" + e.getMessage() + ").", e);
}
}
/**
* Resolves the class object for a type name. Type name can be a primitive. For resolution,
* {@link Class#forName(String)} is used, that uses the caller's class loader.
*
* While method Class.forName(...)
resolves fully qualified names, it does not resolve
* primitives, e.g. "java.lang.Boolean" can be resolved but "boolean" cannot.
*
* @param typeName
* name of the type. For primitives case is ignored.
*
* @throws ClassNotFoundException
* if the typeName neither resolves to a primitive, nor to a known class.
*/
public static Class resolveType(String typeName) throws ClassNotFoundException {
return resolveType(typeName, null);
}
/**
* Resolves the class object for a type name. Type name can be a primitive. For resolution, the
* given class loader is used.
*
* While method Class.forName(...)
resolves fully qualified names, it does not resolve
* primitives, e.g. "java.lang.Boolean" can be resolved but "boolean" cannot.
*
* @param typeName
* name of the type. For primitives case is ignored.
*
* @param classLoader
* the class loader used for loading the class. If this is null, the caller class loader
* is used.
*
* @throws ClassNotFoundException
* if the typeName neither resolves to a primitive, nor to a known class.
*/
public static Class resolveType(String typeName, ClassLoader classLoader) throws ClassNotFoundException {
EJavaPrimitive primitive = EJavaPrimitive.getPrimitiveIgnoreCase(typeName);
if (primitive != null) {
return primitive.getClassObject();
}
if (classLoader == null) {
return Class.forName(typeName);
}
return Class.forName(typeName, true, classLoader);
}
/**
* Recursively add super classes to a list.
*
* @param clazz
* class to start from
* @param superClasses
* list to store super classes.
*/
private static void findSuperClasses(Class clazz, List> superClasses) {
Class superClass = clazz.getSuperclass();
if (superClass == null) {
return;
}
superClasses.add(superClass);
findSuperClasses(superClass, superClasses);
}
/**
* Returns the value from the map, whose key is the best match for the given class. The best match
* is defined by the first match occurring in a breath first search of the inheritance tree, where
* the base class is always visited before the implemented interfaces. Interfaces are traversed in
* the order they are defined in the source file. The only exception is {@link Object}, which is
* considered only as the very last option.
*
* As this lookup can be expensive (reflective iteration over the entire inheritance tree) the
* results should be cached if multiple lookups for the same class are expected.
*
*
* @param clazz
* the class being looked up.
* @param classMap
* the map to perform the lookup in.
* @return the best match found or null
if no matching entry was found. Note that
* null
will also be returned if the entry for the best matching class was
* null
.
*/
public static T performNearestClassLookup(Class clazz, Map, T> classMap) {
Queue> q = new LinkedList<>();
q.add(clazz);
while (!q.isEmpty()) {
Class current = q.poll();
if (classMap.containsKey(current)) {
return classMap.get(current);
}
Class superClass = current.getSuperclass();
if (superClass != null && superClass != Object.class) {
q.add(superClass);
}
q.addAll(Arrays.asList(current.getInterfaces()));
}
return classMap.get(Object.class);
}
/**
* Creates a list that contains only the types that are instances of a specified type from the
* objects of an input list. The input list is not modified.
*
* @param objects
* List of objects that gets filtered
*
* @param type
* target type whose instances are returned
*/
@SuppressWarnings("unchecked")
public static List listInstances(List objects, Class type) {
List filtered = new ArrayList<>();
for (Object object : objects) {
if (type.isInstance(object)) {
filtered.add((T) object);
}
}
return filtered;
}
/**
* Returns the set of all interfaces implemented by the given class. This includes also interfaces
* that are indirectly implemented as they are extended by an interfaces that is implemented by the
* given class. If the given class is an interface, it is included itself.
*/
public static Set> getAllInterfaces(Class baseClass) {
Queue> q = new LinkedList<>();
q.add(baseClass);
Set> result = new HashSet<>();
if (baseClass.isInterface()) {
result.add(baseClass);
}
while (!q.isEmpty()) {
for (Class iface : q.poll().getInterfaces()) {
if (result.add(iface)) {
q.add(iface);
}
}
}
return result;
}
/**
* Returns all fields declared for a class (all visibilities and also inherited from super classes).
* Note that multiple fields with the same name may exist due to redeclaration/shadowing in sub
* classes. We ignore fields from Java internal classes e.g. when inspecting enums as that causes
* "An illegal reflective access operation has occurred" warnings and will be disallowed in Java 17
* and above.
*/
public static List getAllFields(Class type) {
List fields = new ArrayList<>();
while (type != null && type.getPackage() != null && !type.getPackage().getName().equals("java.lang")) {
fields.addAll(Arrays.asList(type.getDeclaredFields()));
type = type.getSuperclass();
}
return fields;
}
/**
* Returns the field with the given name from the given type or null if none exists. If the field
* cannot be found in the given type, all super-classes are searched as well. Different from
* {@link Class#getField(String)}, this will also work for non-public fields.
*/
public static Field getField(Class type, String name) throws SecurityException {
while (type != null) {
try {
return type.getDeclaredField(name);
} catch (NoSuchFieldException e) {
type = type.getSuperclass();
}
}
return null;
}
/**
* Returns true when the given object is an array or implements {@link Iterable}, false otherwise.
*/
public static boolean isIterable(T parsedObject) {
return parsedObject.getClass().isArray() || parsedObject instanceof Iterable;
}
/**
* Unified interface to iterable types (i.e. array or implements {@link Iterable}).
*
* @throws IllegalArgumentException
* if given object does not conform to one of the expected types
*/
public static Iterable asIterable(Object arrayOrIterable) {
if (arrayOrIterable instanceof Iterable) {
return (Iterable) arrayOrIterable;
}
if (arrayOrIterable.getClass().isArray()) {
Class componentType = arrayOrIterable.getClass().getComponentType();
if (componentType.isArray() || componentType.isPrimitive()) {
ArrayList