blob: 2ddec4e4b9fcad6833f26ae1c0ca2ab305473e4b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008 The University of York.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* Contributors:
* Dimitrios Kolovos - initial API and implementation
******************************************************************************/
package org.eclipse.epsilon.eol.util;
import java.lang.reflect.*;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import org.eclipse.epsilon.common.module.ModuleElement;
import org.eclipse.epsilon.eol.exceptions.*;
import org.eclipse.epsilon.eol.execute.prettyprinting.PrettyPrinterManager;
import org.eclipse.epsilon.eol.types.EolNativeType;
public class ReflectionUtil {
private ReflectionUtil() {}
public static boolean hasMethods(Object obj, String methodName) {
if (obj == null) return false;
for (Method method : obj.getClass().getMethods()) {
if (getMethodName(method).equals(methodName)) {
return true;
}
}
return false;
}
public static Set<String> getMethodNames(Object obj, boolean includeInheritedMethods) {
if (obj == null) return new HashSet<>(0);
Method[] methods = getMethods(obj, includeInheritedMethods);
Set<String> methodNames = new HashSet<>(methods.length);
for (Method method : methods) {
methodNames.add(getMethodName(method));
}
return methodNames;
}
protected static String getMethodName(Method method) {
String methodName = method.getName();
if (methodName.startsWith("_")) methodName = methodName.substring(1);
return methodName;
}
/**
* Searches for a method matching the name and criteria for the given object,
* including all super methods and super-interfaces recursively.
*
* @param obj The target object to look for methods on.
* @param methodName The name of the method to find.
* @param criteria Function which limits the search scope of methods.
* @return A method (chosen non-deterministically) which matches the criteria.
* @throws EolIllegalOperationException If no method matching the criteria can be found.
* @throws EolIllegalOperationParametersException If the method parameters are invalid.
* @since 1.6
*/
public static Method findApplicableMethodOrThrow(Object obj, String methodName, Predicate<Method> criteria, Collection<?> parameters, ModuleElement ast, PrettyPrinterManager ppm) throws EolIllegalOperationException, EolIllegalOperationParametersException {
final Method[] candidates = getMethodsFromPublicClassesForName(obj, methodName);
Method method = Stream.of(candidates).filter(criteria).findAny().orElse(null);
if (method == null) {
method = searchMethodsFor(candidates, methodName, parameters.toArray(), true);
}
if (method == null) {
Collector<CharSequence, ?, String> paramJoiner = Collectors.joining(", ");
if (candidates.length > 0) {
String expectedParams = Stream.of(candidates[0].getParameterTypes())
.map(Class::getTypeName)
.collect(paramJoiner);
String actualParams = parameters.stream()
.map(expr -> expr.getClass().getTypeName())
.collect(paramJoiner);
throw new EolIllegalOperationParametersException(methodName, expectedParams, actualParams, ast);
}
else throw new EolIllegalOperationException(obj, methodName, ast, ppm);
}
return method;
}
/**
*
* @param clazz
* @return
* @since 1.6
*/
public static Class<?>[] discoverPublicClasses(Class<?> clazz) {
List<Class<?>> interfaces = new ArrayList<>();
discoverPublicClasses(clazz, interfaces);
Collections.reverse(interfaces);
return interfaces.toArray(new Class[interfaces.size()]);
}
/**
*
* @param clazz
* @param interfaces
* @since 1.6
*/
private static void discoverPublicClasses(Class<?> clazz, List<Class<?>> interfaces) {
if (clazz == null) return;
if (Modifier.isPublic(clazz.getModifiers())) {
interfaces.add(clazz);
}
for (Class<?> superInterface : clazz.getInterfaces()) {
discoverPublicClasses(superInterface, interfaces);
}
discoverPublicClasses(clazz.getSuperclass(), interfaces);
}
/**
*
* @param obj
* @param methodName
* @return
* @since 1.6
*/
public static Method[] getMethodsFromPublicClassesForName(Object obj, String methodName) {
Class<?> clazz = obj instanceof EolNativeType ? ((EolNativeType) obj).getJavaClass() : obj.getClass();
return Stream.of(discoverPublicClasses(clazz))
//.parallel()
.flatMap(c -> Arrays.stream(c.getMethods()))
.filter(m -> getMethodName(m).equals(methodName))
.toArray(Method[]::new);
}
private static Method[] getMethods(Object obj, boolean includeInheritedMethods) {
Class<?> clazz = obj.getClass();
if (includeInheritedMethods) {
return clazz.getMethods();
}
else {
return clazz.getDeclaredMethods();
}
}
/**
* @param allowContravariantConversionForParameters
* when false, parameters will have exactly the same class as the arguments to the returned method
* when true, parameters may have a type that is more specific than the arguments to the returned method
*/
public static Method getMethodFor(Object obj, String methodName, Object[] parameters, boolean includeInheritedMethods, boolean allowContravariantConversionForParameters) {
if (obj == null)
return null;
Method instanceMethod = getInstanceMethodFor(obj, methodName, parameters, includeInheritedMethods, allowContravariantConversionForParameters);
if (instanceMethod != null)
return instanceMethod;
Method staticMethod = getStaticMethodFor(obj, methodName, parameters, allowContravariantConversionForParameters);
if (staticMethod != null)
return staticMethod;
return null;
}
private static Method getInstanceMethodFor(Object obj, String methodName, Object[] parameters, boolean includeInheritedMethods, boolean allowContravariantConversionForParameters) {
return searchMethodsFor(getMethods(obj, includeInheritedMethods), methodName, parameters, allowContravariantConversionForParameters);
}
private static Method getStaticMethodFor(Object obj, String methodName, Object[] parameters, boolean allowContravariantConversionForParameters) {
Method staticMethod = null;
Class<?> javaClass = null;
if (obj instanceof EolNativeType) {
javaClass = ((EolNativeType) obj).getJavaClass();
}
if (obj instanceof Class) {
javaClass = (Class<?>) obj;
}
if (javaClass != null) {
staticMethod = searchMethodsFor(javaClass.getMethods(), methodName, parameters, allowContravariantConversionForParameters);
}
return staticMethod;
}
private static Method searchMethodsFor(Method[] methods, String methodName, Object[] parameters, boolean allowContravariantConversionForParameters) {
// Antonio: according to the Java Language Specification, Sections 15.12.2.2 to 15.12.2.4,
// method resolution is done in three stages: in the first one, no autoboxing is used. In
// the second one, autoboxing (like that in our isInstance static method) is used. In the
// third one, varargs are used. We should do the same if we want to tell apart remove(Object)
// from remove(int) like Java normally would.
for (int stage = 0; stage < 2; ++stage) {
for (Method method : methods) {
if (getMethodName(method).equalsIgnoreCase(methodName)) {
Class<?>[] parameterTypes = method.getParameterTypes();
boolean parametersMatch = parameterTypes.length == parameters.length;
if (parametersMatch) {
//TODO: See why parameter type checking does not work with EolSequence
for (int j = 0; j < parameterTypes.length && parametersMatch; j++) {
Class<?> parameterType = parameterTypes[j];
Object parameter = parameters[j];
if (allowContravariantConversionForParameters) {
parametersMatch = parametersMatch && (stage == 0 ? parameterType.isInstance(parameter) : isInstance(parameterType, parameter));
}
else {
parametersMatch = parametersMatch && parameterType.equals(parameter.getClass());
}
}
if (parametersMatch) {
return method;
}
}
}
}
}
return null;
}
public static Object executeMethod(Object obj, String methodName, Object... parameters) throws Throwable {
Method method = getMethodFor(obj, methodName, parameters, true, true);
try {
//TODO: replace with trySetAccessible
if (!method.isAccessible()) {
method.setAccessible(true);
}
return method.invoke(obj, parameters);
}
catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
public static Object executeMethod(Object obj, Method method, ModuleElement ast, Object... parameters) throws EolRuntimeException {
try {
if (!method.isAccessible()) {
method.setAccessible(true);
}
return method.invoke(obj, parameters);
}
catch (InvocationTargetException | IllegalAccessException | IllegalArgumentException ix) {
Throwable cause = ix.getCause();
if (cause == null) cause = ix;
throw new EolInternalException(cause, ast);
}
}
/*
* This method should replace the one above when we move on from Java 8.
* @param obj
* @param method
* @param ast
* @param parameters
* @return
* @throws EolRuntimeException
* @since 1.6
*
public static Object executeMethod(Object obj, Method method, ModuleElement ast, Object... parameters) throws EolRuntimeException {
try {
if (method.trySetAccessible()) {
return method.invoke(obj, parameters);
}
else {
Method legal = getLegalMethod(obj, method);
if (legal != null) {
return legal.invoke(obj, parameters);
}
}
}
catch (InvocationTargetException | IllegalAccessException | IllegalArgumentException ix) {
Throwable cause = ix.getCause();
if (cause == null) cause = ix;
throw new EolInternalException(cause, ast);
}
throw new EolIllegalOperationException(obj, method.getName(), ast, null);
}*/
/**
* This tries to find a method such that invoking via reflection won't be illegal in Java 9+
*
* @param obj
* @param method
* @return
* @since 1.6
*/
public static Method getLegalMethod(Object obj, Method method) {
for (Method m : ReflectionUtil.getMethodsFromPublicClassesForName(obj, method.getName())) {
if (Objects.deepEquals(m.getParameterTypes(), method.getParameterTypes()) && m.getClass().isAssignableFrom(method.getClass())) {
return m;
}
}
return null;
}
/**
* Returns a string representation
* of the method
* @param method
* @return
*/
public static String methodToString(Method method) {
String str = getMethodName(method);
str += "(";
for (int i = 0; i < method.getParameterTypes().length; i++) {
Class<?> parameterType = method.getParameterTypes()[i];
str += parameterType.getName();
if (i < method.getParameterTypes().length - 1) {
str += " ,";
}
}
str += ")";
return str;
}
/**
* Returns the value of a field of an object
* @param object
* @param fieldName
* @return
*/
public static Object getFieldValue(Object object, String fieldName) {
if (object == null) return null;
Field field = getField(object.getClass(), fieldName);
if (field == null) return null;
field.setAccessible(true);
try {
return field.get(object);
}
catch (Exception ex) {
return null;
}
}
/**
* Gets a field of a class using reflection
* by introspecting the class and its supertype(s)
* @param clazz
* @param fieldName
* @return
*/
public static Field getField(Class<?> clazz, String fieldName) {
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
if (fields[i].getName().equals(fieldName))
return fields[i];
}
if (clazz.getSuperclass() != Object.class)
return getField(clazz.getSuperclass(), fieldName);
return null;
}
/**
* Checks if the instance is an instance of clazz
* Necessary because in Java, int.class != Integer.class etc
* @param clazz
* @param instance
* @return
*/
public static boolean isInstance(Class<?> clazz, Object instance) {
if (instance == null) return true;
else if (clazz == int.class) return Integer.class.isInstance(instance);
else if (clazz == float.class) return Float.class.isInstance(instance);
else if (clazz == double.class) return Double.class.isInstance(instance);
else if (clazz == boolean.class) return Boolean.class.isInstance(instance);
else if (clazz == long.class) return Long.class.isInstance(instance);
else if (clazz == char.class) return Character.class.isInstance(instance);
else return clazz.isInstance(instance);
}
public static List<Field> getAllInheritedInstanceFields(Class<?> klazz) {
final List<Field> fields = new ArrayList<>();
for (Field f : klazz.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) {
continue;
}
fields.add(f);
}
if (klazz.getSuperclass() != null) {
fields.addAll(getAllInheritedInstanceFields(klazz.getSuperclass()));
}
return fields;
}
}