/* * 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 org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import de.rcenvironment.core.communication.common.SerializationException; import de.rcenvironment.core.communication.configuration.NodeConfigurationService; import de.rcenvironment.core.communication.model.NetworkResponse; import de.rcenvironment.core.communication.protocol.ProtocolConstants; import de.rcenvironment.core.communication.routing.MessageRoutingService; 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.RemoteServiceCallSenderService; import de.rcenvironment.core.communication.utils.MessageUtils; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.core.utils.common.rpc.RemoteOperationException; import de.rcenvironment.core.utils.incubator.Assertions; /** * Default {@link RemoteServiceCallSenderService} implementation. * * @author Robert Mischke */ public final class RemoteServiceCallSenderServiceImpl implements RemoteServiceCallSenderService { private MessageRoutingService routingService; // NOTE: used in several locations private final boolean forceLocalRPCSerialization = System .getProperty(NodeConfigurationService.SYSTEM_PROPERTY_FORCE_LOCAL_RPC_SERIALIZATION) != null; private final Log log = LogFactory.getLog(getClass()); public RemoteServiceCallSenderServiceImpl() {} @Override public ServiceCallResult performRemoteServiceCall(ServiceCallRequest serviceCallRequest) { try { byte[] serializedRequest = MessageUtils.serializeObject(serviceCallRequest); if (forceLocalRPCSerialization) { log.debug(StringUtils.format("Handling local RPC with forced serialization: %s#%s()", serviceCallRequest.getServiceName(), serviceCallRequest.getMethodName())); } NetworkResponse networkResponse = routingService.performRoutedRequest(serializedRequest, ProtocolConstants.VALUE_MESSAGE_TYPE_RPC, serviceCallRequest.getTargetNodeId().convertToInstanceNodeSessionId()); // if a low-level network error occurred, the message's payload is either null, or a serialized String with additional error // information; in both cases, convert it into a synthetic ServiceCallResult that will cause a ROE to be thrown // create a synthetic CommunicationException for errors that were not thrown by the // remote method or the remote service invoker (e.g. routing errors) if (!networkResponse.isSuccess()) { return ServiceCallResultFactory.representNetworkErrorAsRemoteOperationException(serviceCallRequest, networkResponse); } Serializable deserializedContent = networkResponse.getDeserializedContent(); // TODO find out how this can be reached without the response code being != SUCCESS, which is caught above - misc_ro if (deserializedContent == null) { String errorMessage = StringUtils.format("Received null service call result for RPC to %s; response code is %s", formatServiceRequest(serviceCallRequest), networkResponse.getResultCode()); return ServiceCallResultFactory.representInternalErrorAtSender(serviceCallRequest, errorMessage); } if (!(deserializedContent instanceof ServiceCallResult)) { return ServiceCallResultFactory.representInternalErrorAtSender(serviceCallRequest, "Received a serialized response of unexpected type " + deserializedContent.getClass().getName()); } return (ServiceCallResult) deserializedContent; } catch (SerializationException | RuntimeException e) { return ServiceCallResultFactory.representInternalErrorAtSender(serviceCallRequest, "Uncaught exception while performing a service call to " + formatServiceRequest(serviceCallRequest), e); } } @Override public Object performRemoteServiceCallAsProxy(ServiceCallRequest serviceCallRequest) throws Throwable { ServiceCallResult serviceCallResult = performRemoteServiceCall(serviceCallRequest); if (serviceCallResult == null) { // this should not happen anymore; left in for safety throw new RemoteOperationException(StringUtils.format( "Received a null object as result for service call to %s", formatServiceRequest(serviceCallRequest))); } if (serviceCallResult.isSuccess()) { return serviceCallResult.getReturnValue(); } else if (serviceCallResult.isRemoteOperationException()) { final String errorMessage = serviceCallResult.getRemoteOperationExceptionMessage(); // the exception will usually cause an upstream warning already, so log the details at DEBUG level log.debug(StringUtils.format("A remote call to %s#%s() on %s failed: %s", serviceCallRequest.getServiceName(), serviceCallRequest.getMethodName(), serviceCallRequest.getTargetNodeId(), errorMessage)); throw new RemoteOperationException(StringUtils.format("%s; the destination instance was %s", errorMessage, serviceCallRequest.getTargetNodeId())); } else { final Throwable methodException = reconstructMethodException(serviceCallRequest, serviceCallResult); log.debug(StringUtils.format("Re-throwing method exception returned from a from call to %s: %s", formatServiceRequest(serviceCallRequest), methodException.toString())); // note: equality check is safe here, as ROEs are always constructed locally if (methodException.getClass() == RemoteOperationException.class) { // re-wrap ROEs to add destination node information to message throw new RemoteOperationException(StringUtils.format("%s; the destination instance was %s", methodException.getMessage(), serviceCallRequest.getTargetNodeId())); } else { throw methodException; } } } private String formatServiceRequest(ServiceCallRequest serviceCallRequest) { return StringUtils.format("%s#%s() on %s", serviceCallRequest.getServiceName(), serviceCallRequest.getMethodName(), serviceCallRequest.getTargetNodeId()); } private Throwable reconstructMethodException(ServiceCallRequest serviceCallRequest, ServiceCallResult serviceCallResult) throws Throwable { final String methodExceptionType = serviceCallResult.getMethodExceptionType(); final String methodExceptionMessage = serviceCallResult.getMethodExceptionMessage(); final Class<?> exceptionClass; try { exceptionClass = Class.forName(methodExceptionType); } catch (ClassNotFoundException e) { log.error("Received an unknown Exception class, generating a RemoteOperationException instead: " + methodExceptionType); throw new RemoteOperationException(StringUtils.format( "The destination instance sent an error type that is not known on the local instance: %s: %s", methodExceptionType, methodExceptionMessage)); } // sanity check to make sure the remote node cannot use this to instantiate arbitrary classes - misc_ro if (!Throwable.class.isAssignableFrom(exceptionClass)) { // TODO add more information log.error("The destination node sent a non-Throwable type as the alleged method exception: " + methodExceptionType); return new RemoteOperationException(StringUtils.format( "The destination instance sent an invalid exception type (unusual behavior): %s: %s", methodExceptionType, methodExceptionMessage)); } final Constructor<?> stringOnlyConstructor; try { stringOnlyConstructor = exceptionClass.getConstructor(String.class); Assertions.isDefined(stringOnlyConstructor, "Unexpected: getConstructor() should never return null"); } catch (NoSuchMethodException e) { log.error(StringUtils.format( "The destination instance sent a known exception type, but it has no accessible String-only constructor: %s: %s", methodExceptionType, methodExceptionMessage)); throw new RemoteOperationException(StringUtils.format("%s: %s", methodExceptionType, methodExceptionMessage)); } final Throwable reconstructedException; try { reconstructedException = (Throwable) stringOnlyConstructor.newInstance(methodExceptionMessage); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { log.error( StringUtils.format("Error reconstructing a method exception (%s: %s)", methodExceptionType, methodExceptionMessage), e); throw new RemoteOperationException(StringUtils.format("%s: %s", methodExceptionType, methodExceptionMessage)); } throw reconstructedException; } /** * Sets the {@link MessageRoutingService} implementation to use; called by OSGi-DS and unit tests. * * @param newInstance the routing service implementation */ public void bindMessageRoutingService(MessageRoutingService newInstance) { this.routingService = newInstance; } }