All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.conqat.lib.commons.reflect.ReflectionUtils Maven / Gradle / Ivy

There is a newer version: 2024.7.2
Show newest version
/*
 * 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 arrayContent = new ArrayList<>(); for (int i = 0; i < Array.getLength(arrayOrIterable) - 1; i++) { arrayContent.add(Array.get(arrayOrIterable, i)); } return arrayContent; } return Arrays.asList((Object[]) arrayOrIterable); } throw new IllegalArgumentException( "Array or Iterable expected, but was given " + arrayOrIterable.getClass().getCanonicalName()); } /** * Returns the annotation the given element is annotated with. If the annotation is not present an * {@link AssertionError} is thrown. */ public static T getAnnotation(Class element, Class annotationClass) throws AssertionError { T annotation = element.getAnnotation(annotationClass); CCSMAssert.isNotNull(annotation, element + " is not annotated with @" + annotationClass.getSimpleName()); return annotation; } /** * @return The cast {@code object} if it is of type {@code targetType}, otherwise {@code null}. */ public static @Nullable T castIfPossible(Object object, Class targetType) { if (targetType.isInstance(object)) { return targetType.cast(object); } return null; } }