/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.communication.rpc.internal; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.logging.LogFactory; import de.rcenvironment.core.utils.common.rpc.RemoteOperationException; import de.rcenvironment.core.utils.common.security.MethodPermissionCheck; /** * Class that calls a specified method. * * This class does: - Detect interfaces and super classes of parameters. - Caching of method detection for faster access. * * This class has some restrictions: - Primitives are not supported. - It only detects public functions. - It can not handle null arguments. * * For a more detailed overview see: - http://www.ddj.com/dept/java/184403978 * * @author Heinrich Wendel * @author Doreen Seider * @author Robert Mischke */ public final class MethodCaller { private static final String ERROR_METHOD_CALL_FAILED = "Method call failed or refused: "; private static Map<String, Method> cache = new HashMap<String, Method>(); private MethodCaller() {} /** * Calls the method of the service. * * @param service The service with the method to call. * @param methodName The name of method to call. * @param parameters The parameters of the method. * @return the return object of the call. * @throws RemoteOperationException on errors invoking the method * @throws InvocationTargetException if the invoked method throws an exception */ public static Object callMethod(Object service, String methodName, List<? extends Serializable> parameters) throws RemoteOperationException, InvocationTargetException { // delegate without using a MethodPermissionCheck; maintains old behavior return callMethod(service, methodName, parameters, null); } /** * Calls the method of the service. An optional {@link MethodPermissionCheck} is performed before execution to see if access to this * method is allowed. * * @param service The service with the method to call. * @param methodName The name of method to call. * @param parameters The parameters of the method. * @param permissionCheck an optional permission check, or null to disable * @return the return object of the call. * @throws RemoteOperationException on errors invoking the method * @throws InvocationTargetException if the invoked method throws an exception */ @SuppressWarnings("unchecked") public static Object callMethod(Object service, String methodName, List<? extends Serializable> parameters, MethodPermissionCheck permissionCheck) throws RemoteOperationException, InvocationTargetException { // Extract parameters Class<? extends Serializable>[] parameterTypes = null; Object[] parameterList = null; if (parameters != null) { parameterTypes = new Class[parameters.size()]; parameterList = new Object[parameters.size()]; for (int i = 0; i < parameters.size(); i++) { if (parameters.get(i) == null) { parameterTypes[i] = Serializable.class; } else { parameterTypes[i] = parameters.get(i).getClass(); } parameterList[i] = parameters.get(i); } } else { parameterTypes = new Class[0]; parameterList = new Class[0]; } String uid = createUniqueIdentifier(service.getClass(), methodName, parameterTypes); Method method = null; // Check in cache if (cache.containsKey(uid)) { method = cache.get(uid); // Otherwise do a new lookup } else { method = lookupMethod(service.getClass(), methodName, parameterTypes); if (method == null) { throw new RemoteOperationException(ERROR_METHOD_CALL_FAILED + uid + " - the method could not be not found"); } // Add to cache cache.put(uid, method); } if (permissionCheck != null) { if (!permissionCheck.checkPermission(method)) { LogFactory.getLog(MethodCaller.class).error("RPC permission check failed for method " + method + " of service " + service.getClass() + " - aborting request"); throw new RemoteOperationException(ERROR_METHOD_CALL_FAILED + uid + " - permission denied"); } } try { return method.invoke(service, parameterList); } catch (IllegalArgumentException e) { throw new RemoteOperationException(ERROR_METHOD_CALL_FAILED + uid + " - invalid arguments."); } catch (IllegalAccessException e) { throw new RemoteOperationException(ERROR_METHOD_CALL_FAILED + uid + " - it could not be not accessed."); } } /** * Searches for a matching method. * * See JLS 15.11.2 for the definition. * * @param clazz The class to call. * @param javaMethodName The name of the method. * @param parameterTypes The array of parameter types. * * @return A matching method or null. */ private static Method lookupMethod(Class<?> clazz, String javaMethodName, Class<? extends Serializable>[] parameterTypes) { Method method = null; try { method = clazz.getMethod(javaMethodName, parameterTypes); } catch (NoSuchMethodException e) { if (parameterTypes.length != 0) { List<Method> methods = new ArrayList<Method>(); // otherwise lookup for (Method candidate : clazz.getMethods()) { if (javaMethodName.equals(candidate.getName()) && parameterTypesMatch(candidate.getParameterTypes(), parameterTypes)) { methods.add(candidate); } } if (methods.size() > 0) { method = mostSpecificMethod(methods); } } } return method; } /** * Checks whether the parameterTypes are matching. * * @param parameterTypesOne The first list of types. * @param parameterTypesTwo The second list of types. * * @return true or false. */ private static boolean parameterTypesMatch(Class<?>[] parameterTypesOne, Class<? extends Serializable>[] parameterTypesTwo) { if (parameterTypesOne.length == parameterTypesTwo.length) { for (int i = 0; i < parameterTypesOne.length; i++) { if (!parameterTypesOne[i].isAssignableFrom(parameterTypesTwo[i])) { return false; } } return true; } return false; } /** * Checks for the best candidate in the list. * * See JLS 15.11.2.2 for the definition. * * @param methods The list of methods. * * @return The best candidate or null if there is no best candidate. */ private static Method mostSpecificMethod(List<Method> methods) { List<Method> methodsToRemove = new ArrayList<Method>(); for (int i = 0; i < methods.size(); i++) { for (int j = 0; j < methods.size(); j++) { if (i != j && moreSpecific(methods.get(i), methods.get(j))) { methodsToRemove.add(methods.get(j)); } } } methods.removeAll(methodsToRemove); if (methods.size() == 1) { return methods.get(0); } else { return null; } } /** * Checks if methodOne is a better candidate than methodTwo. * * @param methodOne The first method. * @param methodTwo The second method. * * @return true or false. */ private static boolean moreSpecific(Method methodOne, Method methodTwo) { Class<?>[] parameterTypesOne = methodOne.getParameterTypes(); Class<?>[] parameterTypesTwo = methodTwo.getParameterTypes(); for (int i = 0; i < parameterTypesOne.length; i++) { if (!parameterTypesTwo[i].isAssignableFrom(parameterTypesOne[i])) { return false; } } return true; } /** * Creates an unique identifier for cashing. * * @param clazz The class to call. * @param javaMethodName The name of the method. * @param parameterTypes The array of parameter types. * * @return A hash String. */ private static String createUniqueIdentifier(Class<?> clazz, String javaMethodName, Class<? extends Serializable>[] parameterTypes) { String hash = ""; hash += clazz.getCanonicalName(); hash += "." + javaMethodName; hash += "("; for (int i = 0; i < parameterTypes.length; i++) { hash += parameterTypes[i].getCanonicalName(); if (i < parameterTypes.length - 1) { hash += ", "; } } hash += ")"; return hash; } }