/*
* 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.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import de.rcenvironment.core.communication.api.PlatformService;
import de.rcenvironment.core.communication.api.ServiceCallContextUtils;
import de.rcenvironment.core.communication.common.CommunicationException;
import de.rcenvironment.core.communication.messaging.internal.InternalMessagingException;
import de.rcenvironment.core.communication.rpc.ServiceCallRequest;
import de.rcenvironment.core.communication.rpc.ServiceCallResult;
import de.rcenvironment.core.communication.rpc.ServiceCallResultFactory;
import de.rcenvironment.core.communication.rpc.api.CallbackProxyService;
import de.rcenvironment.core.communication.rpc.api.CallbackService;
import de.rcenvironment.core.communication.rpc.spi.LocalServiceLookupResult;
import de.rcenvironment.core.communication.rpc.spi.LocalServiceResolver;
import de.rcenvironment.core.communication.rpc.spi.RemoteServiceCallHandlerService;
import de.rcenvironment.core.toolkitbridge.api.StaticToolkitHolder;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.common.rpc.RemotableService;
import de.rcenvironment.core.utils.common.rpc.RemoteOperationException;
import de.rcenvironment.core.utils.common.security.AllowRemoteAccess;
import de.rcenvironment.core.utils.common.security.MethodPermissionCheck;
import de.rcenvironment.core.utils.common.security.MethodPermissionCheckHasAnnotation;
import de.rcenvironment.core.utils.incubator.Assertions;
import de.rcenvironment.toolkit.modules.concurrency.api.threadcontext.ThreadContextMemento;
import de.rcenvironment.toolkit.modules.statistics.api.CounterCategory;
import de.rcenvironment.toolkit.modules.statistics.api.StatisticsFilterLevel;
import de.rcenvironment.toolkit.modules.statistics.api.StatisticsTrackerService;
/**
* Implementation of the {@link RemoteServiceCallHandlerService}.
*
* @author Heinrich Wendel
* @author Doreen Seider
* @author Robert Mischke
*/
public class ServiceCallHandlerServiceImpl implements RemoteServiceCallHandlerService {
// the callback that verifies the presence of @AllowRemoteCall annotations
private static final MethodPermissionCheck METHOD_PERMISSION_CHECK = new MethodPermissionCheckHasAnnotation(AllowRemoteAccess.class);
private PlatformService platformService;
private CallbackService callbackService;
private CallbackProxyService callbackProxyService;
private LocalServiceResolver serviceResolver;
private final Map<String, LocalServiceLookupResult> serviceCache;
private final Log log = LogFactory.getLog(getClass());
private final CounterCategory parameterTypesCounter;
public ServiceCallHandlerServiceImpl() {
serviceCache = new HashMap<>();
// not injecting this via OSGi-DS as this service is planned to move to the toolkit layer anyway - misc_ro
final StatisticsTrackerService statisticsService =
StaticToolkitHolder.getServiceWithUnitTestFallback(StatisticsTrackerService.class);
parameterTypesCounter =
statisticsService.getCounterCategory("Remote service calls (received): parameter types", StatisticsFilterLevel.DEVELOPMENT);
}
/**
* OSGi-DS bind method; made public for integration testing.
*
* @param newInstance the new service instance
*/
public void bindLocalServiceResolver(LocalServiceResolver newInstance) {
this.serviceResolver = newInstance;
}
/**
* OSGi-DS bind method; made public for integration testing.
*
* @param newInstance the new service instance
*/
public void bindPlatformService(PlatformService newInstance) {
platformService = newInstance;
}
/**
* OSGi-DS bind method; made public for integration testing.
*
* @param newInstance the new service instance
*/
public void bindCallbackService(CallbackService newInstance) {
callbackService = newInstance;
}
/**
* OSGi-DS bind method; made public for integration testing.
*
* @param newInstance the new service instance
*/
public void bindCallbackProxyService(CallbackProxyService newInstance) {
callbackProxyService = newInstance;
}
@Override
public ServiceCallResult handle(ServiceCallRequest serviceCallRequest) throws InternalMessagingException {
if (!platformService.matchesLocalInstance(serviceCallRequest.getTargetNodeId())) {
throw new IllegalStateException("Internal consistency error: called to handle a ServiceCallResult for another node");
}
Assertions.isDefined(serviceCallRequest, "The parameter \"serviceCallRequest\" must not be null.");
final ThreadContextMemento previousThreadContext =
ServiceCallContextUtils.attachServiceCallDataToThreadContext(serviceCallRequest.getCallerNodeId(),
serviceCallRequest.getTargetNodeId(), serviceCallRequest.getServiceName(), serviceCallRequest.getMethodName());
try {
return invokeLocalService(serviceCallRequest);
} finally {
previousThreadContext.restore();
}
}
/**
* Handles a service call request locally.
*
* @param serviceCallRequest {@link ServiceCallRequest} with all information about the method to call.
* @return The {@link ServiceCallResult} with the result of the service call.
* @throws CommunicationException Thrown if the call failed.
*/
private ServiceCallResult invokeLocalService(ServiceCallRequest serviceCallRequest) throws InternalMessagingException {
Object[] parameters = serviceCallRequest.getParameterList().toArray();
List<Serializable> parameterList = new ArrayList<>();
for (Object parameter : parameters) {
parameterList.add((Serializable) CallbackUtils.handleCallbackProxy(parameter, callbackService, callbackProxyService));
}
// count parameter types if enabled
if (parameterTypesCounter.isEnabled()) {
for (Object parameter : parameterList) {
parameterTypesCounter.countClass(parameter);
}
}
final String serviceName = serviceCallRequest.getServiceName();
LocalServiceLookupResult serviceLookupResult;
synchronized (serviceCache) {
serviceLookupResult = serviceCache.get(serviceName);
if (serviceLookupResult == null) {
serviceLookupResult = lookupAndValidateService(serviceCallRequest, serviceName);
serviceCache.put(serviceName, serviceLookupResult);
}
}
if (!serviceLookupResult.isValidRemotableService()) {
// no matching valid service
return ServiceCallResultFactory.representInvalidRequestAtHandler(serviceCallRequest, "No matching service found");
}
final String methodName = serviceCallRequest.getMethodName();
if (!serviceLookupResult.isValidMethodRequest(methodName)) {
// invalid method of existing service requested
return ServiceCallResultFactory.representInvalidRequestAtHandler(serviceCallRequest,
"Matching service found, but the method is not allowed to be called");
}
try {
Object returnValue;
try {
returnValue =
MethodCaller.callMethod(serviceLookupResult.getImplementation(), methodName, parameterList, METHOD_PERMISSION_CHECK);
} catch (InvocationTargetException e) {
final Throwable methodException = e.getCause();
// TODO 7.0.0: review: more detailed checks necessary?
if (methodException instanceof Exception && !(methodException instanceof RuntimeException)) {
return ServiceCallResultFactory.wrapMethodException((Exception) methodException);
} else {
return ServiceCallResultFactory.representInternalErrorAtHandler(serviceCallRequest,
"Unexpected Throwable during service method invocation", methodException);
}
} catch (RemoteOperationException e) {
return ServiceCallResultFactory.representInternalErrorAtHandler(serviceCallRequest,
"Error during service method invocation", e);
}
if (returnValue != null) {
if (!(returnValue instanceof Serializable)) {
final String message = StringUtils.format("Return value is not serializable: " + returnValue.getClass().getName());
return ServiceCallResultFactory.representInternalErrorAtHandler(serviceCallRequest, message);
}
returnValue =
CallbackUtils.handleCallbackObject(returnValue, serviceCallRequest.getCallerNodeId().convertToInstanceNodeSessionId(),
callbackService);
return ServiceCallResultFactory.wrapReturnValue((Serializable) returnValue);
} else {
return ServiceCallResultFactory.wrapReturnValue(null);
}
} catch (RuntimeException e) {
return ServiceCallResultFactory.representInternalErrorAtHandler(serviceCallRequest, "Uncaught RuntimeException", e);
}
}
private LocalServiceLookupResult lookupAndValidateService(ServiceCallRequest serviceCallRequest, final String serviceName) {
final Class<?> serviceInterface;
try {
serviceInterface = Class.forName(serviceName);
} catch (ClassNotFoundException e) {
log.warn(StringUtils.format("Found no interface for service '%s' requested from '%s'", serviceName,
serviceCallRequest.getCallerNodeId()));
return LocalServiceLookupResult.createInvalidServicePlaceholder();
}
if (!serviceInterface.isAnnotationPresent(RemotableService.class)) {
log.warn(StringUtils
.format("Found the requested service interface '%s', but it is not a %s; refusing access",
serviceName, RemotableService.class.getSimpleName()));
return LocalServiceLookupResult.createInvalidServicePlaceholder();
}
final Object serviceImplementation = serviceResolver.getLocalService(serviceName);
if (serviceImplementation == null) {
log.warn(StringUtils
.format("Found the service interface '%s' requested by %s, but no registered implementation",
serviceName, serviceCallRequest.getCallerNodeId()));
return LocalServiceLookupResult.createInvalidServicePlaceholder();
}
if (!serviceInterface.isAssignableFrom(serviceImplementation.getClass())) {
log.error(StringUtils
.format("Consistency error: Found the service interface '%s', but the resolved implementation %s is not assignable to it!",
serviceName, serviceImplementation.getClass()));
return LocalServiceLookupResult.createInvalidServicePlaceholder();
}
Set<String> encounteredMethodNamesWithParameterCount = new HashSet<>();
Set<String> validatedMethodNames = new HashSet<>();
// in case of collisions, make sure following methods of the same name don't get allowed
Set<String> blockedMethodNames = new HashSet<>();
final Method[] methods = serviceInterface.getMethods();
for (Method method : methods) {
final String methodName = method.getName();
final int parameterCount = method.getParameterTypes().length;
final Class<?>[] exceptionTypes = method.getExceptionTypes();
boolean roeDeclared = false;
boolean allowMethodAccess = true;
// check for method overloading with same number of parameters
if (!encounteredMethodNamesWithParameterCount.add(methodName + "/" + parameterCount)) {
log.error(StringUtils.format("Found overloaded method variants with same parameter count for %s#%s(), "
+ "which is not allowed in remote service interfaces", serviceName, methodName));
blockedMethodNames.add(methodName);
allowMethodAccess = false;
}
// check exception declarations
for (Class<?> exceptionClass : exceptionTypes) {
if (RuntimeException.class.isAssignableFrom(exceptionClass)) {
log.error(StringUtils.format("Method %s#%s() declares 'throws %s', which is a RuntimeException", serviceName,
methodName, exceptionClass.getName()));
allowMethodAccess = false;
}
if (exceptionClass == RemoteOperationException.class) {
roeDeclared = true;
} else {
// for all other exceptions, check presence of a string-only constructor
try {
final Constructor<?> stringOnlyConstructor = exceptionClass.getConstructor(String.class);
Assertions.isDefined(stringOnlyConstructor, "Unexpected: getConstructor() should never return null");
} catch (NoSuchMethodException e) {
log.error(StringUtils.format(
"Method %s#%s() declares 'throws %s', but the exception class does not have a string-only constructor",
serviceName, methodName, exceptionClass.getName()));
allowMethodAccess = false;
}
}
}
// check for presence of RemoteOperationException
if (!roeDeclared) {
log.error(StringUtils.format(
"Method %s#%s() is used as part of a remote service interface, but does not declare 'throws %s'",
serviceName, methodName, RemoteOperationException.class.getSimpleName()));
allowMethodAccess = false;
}
// TODO add additional method signature checks? e.g. valid parameter types
// TODO also check implementing class?
if (allowMethodAccess) {
validatedMethodNames.add(methodName);
} else {
log.warn(StringUtils.format(
"Preventing remote access to %s#%s() as it violates one or more remote service method constraints",
serviceName, methodName, RemoteOperationException.class.getSimpleName()));
}
}
validatedMethodNames.removeAll(blockedMethodNames); // "blocked" overrides permission
log.debug(StringUtils.format("Verified remote service methods for interface %s: %s", serviceName,
Arrays.toString(validatedMethodNames.toArray())));
return new LocalServiceLookupResult(serviceImplementation, validatedMethodNames);
}
}