blob: c4707789abd6757d37c17d66905ca49d1d6e0daf [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.wayang.core.util;
import org.apache.commons.lang3.Validate;
import org.apache.wayang.core.api.exception.WayangException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Utilities for reflection code.
*/
public class ReflectionUtils {
private static final Logger logger = LogManager.getLogger(ReflectionUtils.class);
private static final Pattern defaultConstructorPattern = Pattern.compile(
"new ([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\(\\)"
);
private static final Pattern arglessMethodPattern = Pattern.compile(
"([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\.([a-zA-Z_][a-zA-Z0-9_]*)\\(\\)"
);
private static final Pattern constantPattern = Pattern.compile(
"([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\.([a-zA-Z_][a-zA-Z0-9_]*)"
);
private static final List<Tuple<Class<?>, Supplier<?>>> defaultParameterSuppliers = Arrays.asList(
new Tuple<>(byte.class, () -> (byte) 0),
new Tuple<>(Integer.class, () -> (byte) 0),
new Tuple<>(short.class, () -> (short) 0),
new Tuple<>(Short.class, () -> (short) 0),
new Tuple<>(int.class, () -> 0),
new Tuple<>(Integer.class, () -> 0),
new Tuple<>(long.class, () -> 0L),
new Tuple<>(Long.class, () -> 0L),
new Tuple<>(boolean.class, () -> false),
new Tuple<>(Boolean.class, () -> false),
new Tuple<>(float.class, () -> 0f),
new Tuple<>(Float.class, () -> 0f),
new Tuple<>(double.class, () -> 0d),
new Tuple<>(Double.class, () -> 0d),
new Tuple<>(char.class, () -> '\0'),
new Tuple<>(Character.class, () -> '\0'),
new Tuple<>(String.class, () -> "")
);
/**
* Identifies and returns the JAR file declaring the {@link Class} of the given {@code object} or {@code null} if
* no such file could be determined.
*/
public static String getDeclaringJar(Object object) {
Validate.notNull(object);
return getDeclaringJar(object.getClass());
}
/**
* Identifies and returns the JAR file declaring the given {@link Class} if no such file could be determined.
*/
public static String getDeclaringJar(Class<?> cls) {
try {
final URL location = cls.getProtectionDomain().getCodeSource().getLocation();
final URI uri = location.toURI();
final String path = uri.getPath();
if (path.endsWith(".jar")) {
return path;
} else {
logger.warn("Class {} is not loaded from a JAR file, but from {}. Thus, cannot provide the JAR file.", cls, path);
}
} catch (Exception e) {
logger.error(String.format("Could not determine JAR file declaring %s.", cls), e);
}
return null;
}
/**
* Provides a resource as an {@link InputStream}.
*
* @param resourceName the name or path of the resource
* @return an {@link InputStream} with the contents of the resource or {@code null} if the resource could not be found
*/
public static InputStream loadResource(String resourceName) {
return Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName);
}
/**
* Provides a resource as {@link URL}.
*
* @param resourceName the name or path of the resource
* @return a {@link URL} describing the path to the resource or {@code null} if the resource could not be found
*/
public static URL getResourceURL(String resourceName) {
return Thread.currentThread().getContextClassLoader().getResource(resourceName);
}
/**
* Casts the given {@link Class} to a more specific one.
*
* @param baseClass that should be casted
* @param <T> the specific type parameter for the {@code baseClass}
* @return the {@code baseClass}, casted
*/
@SuppressWarnings("unchecked")
public static <T> Class<T> specify(Class<? super T> baseClass) {
return (Class<T>) baseClass;
}
/**
* Casts the given {@link Class} to a more general one.
*
* @param baseClass that should be casted
* @param <T> the specific type parameter for the {@code baseClass}
* @return the {@code baseClass}, casted
*/
@SuppressWarnings("unchecked")
public static <T> Class<T> generalize(Class<? extends T> baseClass) {
return (Class<T>) baseClass;
}
/**
* Tries to evaluate the given statement, thereby supporting the following statement types:
* <ul>
* <li>{@code new <class name>()}</li>
* <li>{@code <class name>.<constant>}</li>
* <li>{@code <class name>.<static method>}()</li>
* </ul>
*
* @param statement the statement
* @param <T> the return type
* @return the result of the evaluated statement
*/
public static <T> T evaluate(String statement) throws IllegalArgumentException {
statement = statement.trim();
Matcher matcher = defaultConstructorPattern.matcher(statement);
if (matcher.matches()) {
try {
return instantiateDefault(matcher.group(1));
} catch (Exception e) {
throw new IllegalArgumentException(String.format("Could not instantiate '%s'.", statement), e);
}
}
matcher = arglessMethodPattern.matcher(statement);
if (matcher.matches()) {
try {
return executeStaticArglessMethod(matcher.group(1), matcher.group(2));
} catch (Exception e) {
throw new IllegalArgumentException(String.format("Could not execute '%s'.", statement), e);
}
}
matcher = constantPattern.matcher(statement);
if (matcher.matches()) {
try {
return retrieveStaticVariable(matcher.group(1), matcher.group(2));
} catch (Exception e) {
throw new IllegalArgumentException(String.format("Could not execute '%s'.", statement), e);
}
}
throw new IllegalArgumentException(String.format("Unknown expression type: '%s'.", statement));
}
/**
* Executes a parameterless static method.
*
* @param className the name of the {@link Class} that comprises the method
* @param methodName the name of the method
* @param <T>
* @return the return value of the executed method
*/
@SuppressWarnings("unchecked")
public static <T> T executeStaticArglessMethod(String className, String methodName) {
try {
final Class<?> cls = Class.forName(className);
final Method method = cls.getMethod(methodName);
Validate.isTrue(method.getParameters().length == 0, "Method has parameters.");
return (T) method.invoke(null);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalArgumentException(String.format("Could not execute %s.%s().", className, methodName), e);
}
}
/**
* Retrieves a static variable.
*
* @param className the name of the {@link Class} that comprises the method
* @param variableName the name of the method
* @param <T>
* @return the return value of the executed method
*/
@SuppressWarnings("unchecked")
public static <T> T retrieveStaticVariable(String className, String variableName) {
try {
final Class<?> cls = Class.forName(className);
final Field field = cls.getField(variableName);
return (T) field.get(null);
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
throw new IllegalArgumentException(String.format("Could not retrieve %s.%s.", className, variableName), e);
}
}
/**
* Creates a new instance of a {@link Class} via the default constructor.
*
* @param className name of the {@link Class} to be instantiated
* @return the instance
*/
public static <T> T instantiateDefault(String className) {
try {
@SuppressWarnings("unchecked") // Will fail anyway, if incorrect.
Class<T> cls = (Class<T>) Class.forName(className);
return cls.newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new WayangException("Could not instantiate class.", e);
}
}
/**
* Creates a new instance of the given {@link Class} via the default constructor.
*
* @param cls the {@link Class} to be instantiated
* @return the instance
*/
public static <T> T instantiateDefault(Class<? extends T> cls) {
try {
return cls.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new WayangException("Could not instantiate class.", e);
}
}
/**
* Tries to instantiate an arbitrary instance of the given {@link Class}.
*
* @param cls that should be instantiated
* @param defaultParameters designate specific default parameter values for parameter {@link Class}es
* @return the instance
*/
@SuppressWarnings("unchecked")
public static <T> T instantiateSomehow(Class<T> cls, Tuple<Class<?>, Supplier<?>>... defaultParameters) {
return instantiateSomehow(cls, Arrays.asList(defaultParameters));
}
/**
* Tries to instantiate an arbitrary instance of the given {@link Class}.
*
* @param cls that should be instantiated
* @param defaultParameters designate specific default parameter values for parameter {@link Class}es
* @return the instance
*/
@SuppressWarnings("unchecked")
public static <T> T instantiateSomehow(Class<T> cls, List<Tuple<Class<?>, Supplier<?>>> defaultParameters) {
try {
for (Constructor<?> constructor : cls.getConstructors()) {
try {
final Class<?>[] parameterTypes = constructor.getParameterTypes();
Object[] parameters = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
Object parameter = getDefaultParameter(parameterType, defaultParameters);
if (parameter == null) {
parameter = getDefaultParameter(parameterType, defaultParameterSuppliers);
}
parameters[i] = parameter;
}
return (T) constructor.newInstance(parameters);
} catch (Throwable t) {
logger.debug("Could not instantiate {}.", cls.getSimpleName(), t);
}
}
} catch (Throwable t) {
throw new WayangException(String.format("Could not get constructors for %s.", cls.getSimpleName()));
}
throw new WayangException(String.format("Could not instantiate %s.", cls.getSimpleName()));
}
/**
* Searches for a compatible {@link Class} in the given {@link List} (subclass or equal) for the given parameter
* {@link Class}. If a match is found, the corresponding {@link Supplier} is used to create a default parameter.
*
* @param parameterClass {@link Class} of a parameter
* @param defaultParameterSuppliers supply default values for various parameter {@link Class}es
* @return the first match's {@link Supplier} value or {@code null} if no match was found
*/
private static Object getDefaultParameter(Class<?> parameterClass, List<Tuple<Class<?>, Supplier<?>>> defaultParameterSuppliers) {
for (Tuple<Class<?>, Supplier<?>> defaultParameterSupplier : defaultParameterSuppliers) {
if (parameterClass.isAssignableFrom(defaultParameterSupplier.getField0())) {
return defaultParameterSupplier.getField1().get();
}
}
return null;
}
/**
* Retrieve the {@link Type}s of type parameters from an extended/implemented superclass/interface.
*
* @param subclass the {@link Class} implementing the superclass/interface with type parameters
* @param superclass the superclass/interface defining the type parameters
* @return a {@link Map} mapping the type parameter names to their implemented {@link Type}
*/
public static Map<String, Type> getTypeParameters(Class<?> subclass, Class<?> superclass) {
// Gather implemented interfaces and superclass.
List<Type> genericSupertypes = Stream.concat(
Stream.of(subclass.getGenericSuperclass()), Stream.of(subclass.getGenericInterfaces())
).collect(Collectors.toList());
for (Type supertype : genericSupertypes) {
if (supertype instanceof Class<?>) {
// If the supertype is a Class, there are no type parameters to worry about.
Class<?> cls = (Class<?>) supertype;
if (!superclass.isAssignableFrom(cls)) continue;
return getTypeParameters(cls, superclass);
} else if (supertype instanceof ParameterizedType) {
// Handle type parameters.
ParameterizedType parameterizedType = (ParameterizedType) supertype;
final Type rawType = parameterizedType.getRawType();
if (!(rawType instanceof Class<?>)) continue;
Class<?> cls = (Class<?>) rawType;
if (!superclass.isAssignableFrom(cls)) continue;
final Map<String, Type> localTypeArguments = getTypeArguments(parameterizedType);
if (cls.equals(superclass)) {
// If we reached the superclass, we are good.
return localTypeArguments;
} else {
// If we are not there yet, we need to consider to "redirect" type parameters.
final Map<String, Type> preliminaryResult = getTypeParameters(cls, superclass);
final Map<String, Type> result = new HashMap<>(preliminaryResult.size());
for (Map.Entry<String, Type> entry : preliminaryResult.entrySet()) {
if (entry.getValue() instanceof TypeVariable<?>) {
final Type translatedTypeArgument = localTypeArguments.getOrDefault(
((TypeVariable) entry.getValue()).getName(),
entry.getValue()
);
result.put(entry.getKey(), translatedTypeArgument);
} else {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}
}
}
return Collections.emptyMap();
}
/**
* Put the {@link TypeVariable}s of a {@link ParameterizedType} into a {@link Map}.
*
* @param type the {@link ParameterizedType}
* @return the {@link Map} with the type parameter names as keys
*/
private static Map<String, Type> getTypeArguments(ParameterizedType type) {
final Type[] typeArguments = type.getActualTypeArguments();
final Class<?> rawType = (Class<?>) type.getRawType();
final TypeVariable<? extends Class<?>>[] typeParameters = rawType.getTypeParameters();
Map<String, Type> result = new HashMap<>(typeArguments.length);
for (int i = 0; i < typeArguments.length; i++) {
final TypeVariable<?> typeParameter = typeParameters[i];
final Type typeArgument = typeArguments[i];
result.put(typeParameter.getName(), typeArgument);
}
return result;
}
/**
* Retrieve a property from an object.
*
* @param obj from which the property should be retrieved
* @param property the property
* @param <T> the return type
* @return the property
*/
@SuppressWarnings("unchecked")
public static <T> T getProperty(Object obj, String property) {
Class<?> artifactClass = obj.getClass();
String accessorName = "get" + Character.toUpperCase(property.charAt(0)) + property.substring(1);
try {
do {
final Optional<Method> optMethod = Arrays.stream(artifactClass.getMethods())
.filter(method -> method.getName().equals(accessorName))
.findAny();
if (optMethod.isPresent()) {
return (T) artifactClass.getMethod(accessorName).invoke(obj);
}
artifactClass = artifactClass.getSuperclass();
} while (artifactClass != null);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(String.format("Could not execute %s() on %s.", accessorName, obj), e);
}
throw new IllegalArgumentException(String.format("Did not find method %s() for %s.", accessorName, obj));
}
/**
* Convert the given {@link Object} to a {@code double}.
*
* @param o the {@link Object}
* @return the {@code double}
*/
public static double toDouble(Object o) {
if (o instanceof Double) return (Double) o;
else if (o instanceof Integer) return (Integer) o;
else if (o instanceof Float) return (Float) o;
else if (o instanceof Long) return (Long) o;
else if (o instanceof Short) return (Short) o;
else if (o instanceof Byte) return (Byte) o;
else if (o instanceof BigDecimal) return ((BigDecimal) o).doubleValue();
else if (o instanceof BigInteger) return ((BigInteger) o).doubleValue();
throw new IllegalStateException(String.format("%s (%s) cannot be retrieved as double.", o,
o == null ? "unknown class" : o.getClass().getCanonicalName()));
}
public static Type getWrapperClass(Type type, int index) {
if (type != null && (type instanceof ParameterizedType)) {
ParameterizedType parameterizedType = (ParameterizedType) type;
return parameterizedType.getActualTypeArguments()[index];
}
return null;
}
}