/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.communication.rpc; import java.io.Serializable; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import de.rcenvironment.core.communication.common.IdentifierException; import de.rcenvironment.core.communication.common.NodeIdentifierUtils; import de.rcenvironment.core.communication.common.SerializationException; import de.rcenvironment.core.communication.model.NetworkResponse; import de.rcenvironment.core.communication.model.internal.PayloadTestFuzzer; import de.rcenvironment.core.utils.common.LogUtils; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.core.utils.common.rpc.RemoteOperationException; /** * Represents all possible outcomes of a remote service call (on either side) into {@link ServiceCallResult} instances. * * @author Robert Mischke */ public final class ServiceCallResultFactory { private static final Log sharedLog = LogFactory.getLog(ServiceCallResultFactory.class); private ServiceCallResultFactory() {} /** * Represents a low-level network error (e.g. a timeout) on the service call level. For the caller, this is represented as a * {@link RemoteOperationException} with a message describing the error. * * Note that the network error is not logged by this method, as this is expected to have happened already. (TODO 7.0.0 review) * * @param serviceCallRequest the related request * @param networkResponse the {@link NetworkResponse} representing the error * @return the generated ServiceCallResult */ public static ServiceCallResult representNetworkErrorAsRemoteOperationException(ServiceCallRequest serviceCallRequest, NetworkResponse networkResponse) { final String resultCodeMessage = networkResponse.getResultCode().toString(); String additionalErrorInformation = null; try { Serializable deserializedContent = networkResponse.getDeserializedContent(); if (deserializedContent != null) { if (deserializedContent instanceof String) { additionalErrorInformation = (String) deserializedContent; additionalErrorInformation = parseEncodedErrorInformation(additionalErrorInformation); } else { additionalErrorInformation = "Internal error: deserialized extended error information, but it is not a string: " + deserializedContent.toString(); } } } catch (SerializationException e) { additionalErrorInformation = "Internal error: failed to deserialize the extended error message: " + e.getMessage(); } // TODO add local metadata again? decide if useful // LogUtils.logErrorAndAssignUniqueMarker(sharedLog, // StringUtils.format("Network error while handling service request for '%s#%s' on '%s': %s (message trace: %s)", // serviceCallRequest.getServiceName(), serviceCallRequest.getMethodName(), serviceCallRequest.getDestination(), // resultCodeMessage, networkResponse.accessMetaData().getTrace())); final String userMessage; if (additionalErrorInformation != null) { userMessage = StringUtils.format("%s: %s", resultCodeMessage, additionalErrorInformation); } else { userMessage = resultCodeMessage; } // throw a RemoteOperationException from the called service's proxy return new ServiceCallResult(null, null, null, userMessage); } private static String parseEncodedErrorInformation(String additionalErrorInformation) { String[] parts = StringUtils.splitAndUnescape(additionalErrorInformation); if (parts.length != 2) { LogFactory.getLog(ServiceCallResultFactory.class).error( "Received unexpected content in additional error information: " + additionalErrorInformation); return additionalErrorInformation; } String errorId = parts[0]; String nodeIdString = parts[1]; String reportingNodeRepresentation; if (!StringUtils.isNullorEmpty(nodeIdString)) { try { reportingNodeRepresentation = NodeIdentifierUtils.parseInstanceNodeSessionIdString(nodeIdString).toString(); } catch (IdentifierException e) { // emergency fallback - should not happen reportingNodeRepresentation = StringUtils.format("[Failed to parse received node id '%s': %s]", nodeIdString, e.toString()); } } else { reportingNodeRepresentation = null; } if (reportingNodeRepresentation != null) { if (StringUtils.isNullorEmpty(errorId)) { return StringUtils.format("The error was reported by %s", reportingNodeRepresentation); } else { return StringUtils.format( "The error was reported by %s; technical details were logged there as error '%s'", reportingNodeRepresentation, errorId); } } else { // note: both cases are unusual if (StringUtils.isNullorEmpty(errorId)) { return "No further information available"; } else { return StringUtils.format("Technical details were logged on the target instance as error '%s'", errorId); } } } /** * Represents an internal, non-network error on the caller's instance. Once example would be a failed permission check before sending * the actual request across the network. * * For the caller, this is represented as a {@link RemoteOperationException} with a standard error message. The given message is logged * by this method, given a error correlation id, and that id is included in the generated message. * * @param serviceCallRequest the related request * @param errorMessage an internal description that is logged; it is *not* directly presented to the user * @return the generated ServiceCallResult */ public static ServiceCallResult representInternalErrorAtSender(ServiceCallRequest serviceCallRequest, String errorMessage) { return representInternalErrorAtSender(serviceCallRequest, errorMessage, null); } /** * Represents an internal, non-network error on the caller's instance. Once example would be a failed permission check before sending * the actual request across the network. * * For the caller, this is represented as a {@link RemoteOperationException} with a standard error message. The given message and * exception cause are logged by this method, given a error correlation id, and that id is included in the generated message. * * @param serviceCallRequest the related request * @param errorMessage an internal description that is logged; it is *not* directly presented to the user * @param throwable a cause parameter that is included in the internal logging; it is *not* directly presented to the user * @return the generated ServiceCallResult */ public static ServiceCallResult representInternalErrorAtSender(ServiceCallRequest serviceCallRequest, final String errorMessage, final Throwable throwable) { @SuppressWarnings("unused")// suppress Eclipse warning when the ENABLED constant is false - misc_ro final boolean suppressStacktrace = PayloadTestFuzzer.ENABLED && throwable.getClass() == SerializationException.class; final String errorId; if (suppressStacktrace) { errorId = LogUtils.logErrorAndAssignUniqueMarker(sharedLog, StringUtils.format("Local error at sender while performing service request to '%s#%s' on target node '%s': %s: %s", serviceCallRequest.getServiceName(), serviceCallRequest.getMethodName(), serviceCallRequest.getTargetNodeId(), errorMessage, throwable.toString())); } else { errorId = LogUtils.logExceptionWithStacktraceAndAssignUniqueMarker(sharedLog, StringUtils.format("Local error at sender while performing service request to '%s#%s' on target node '%s': %s", serviceCallRequest.getServiceName(), serviceCallRequest.getMethodName(), serviceCallRequest.getTargetNodeId(), errorMessage), throwable); } final String userMessage = StringUtils.format("There was a local error while performing this operation; " + "you can find more information by looking for the marker '%s' in your instance's log files", errorId); return new ServiceCallResult(null, null, null, userMessage); } /** * Wraps the result of a successful method invocation. * * @param returnValue the return value * @return the generated ServiceCallResult */ public static ServiceCallResult wrapReturnValue(Serializable returnValue) { return new ServiceCallResult(returnValue, null, null, null); } /** * Wraps an exception thrown by the invoked method. Note that only checked (ie, declared) exceptions should be wrapped by this method; * unexpected {@link Throwable}s should be wrapped with {@link #representInternalErrorAtHandler(ServiceCallRequest, String, Throwable)} * instead. * * @param methodException the thrown exception * @return the generated ServiceCallResult */ public static ServiceCallResult wrapMethodException(Exception methodException) { // FIXME review for 7.0.0: "downgrade" to declared exception types to ensure reconstruction at client? String methodExceptionType = methodException.getClass().getName(); String methodExceptionMessage = methodException.getMessage(); return new ServiceCallResult(null, methodExceptionType, methodExceptionMessage, null); } /** * Represents a failed permission check on the receiving instance. * * The failure is logged on the receiving instance, and a {@link RemoteOperationException} with a generic message is thrown for the * caller. * * @param serviceCallRequest the related request * @param internalInfo internal log information to help with debugging * @return the generated ServiceCallResult */ public static ServiceCallResult representInvalidRequestAtHandler(ServiceCallRequest serviceCallRequest, String internalInfo) { final String errorId = LogUtils.logErrorAndAssignUniqueMarker(sharedLog, StringUtils.format("Refused request for invalid method '%s#%s()' sent by '%s': %s", serviceCallRequest.getServiceName(), serviceCallRequest.getMethodName(), serviceCallRequest.getCallerNodeId(), internalInfo)); final String userMessage = StringUtils.format("Request refused by destination instance (remote error id: %s)", errorId); return new ServiceCallResult(null, null, null, userMessage); } /** * Represents an internal, non-network error on the receiving instance. A typical example is a {@link RuntimeException} leaking from the * invoked method. * * For the caller, this is represented as a {@link RemoteOperationException} with a standard error message. The given message and * exception cause are logged on the receiving instance by this method, given a error correlation id, and that id is included in the * generated message. * * @param serviceCallRequest the related request * @param errorMessage an internal description that is logged; it is *not* directly presented to the user * @return the generated ServiceCallResult */ public static ServiceCallResult representInternalErrorAtHandler(ServiceCallRequest serviceCallRequest, final String errorMessage) { return representInternalErrorAtHandler(serviceCallRequest, errorMessage, null); } /** * Represents an internal, non-network error on the receiving instance. A typical example is a {@link RuntimeException} leaking from the * invoked method. * * For the caller, this is represented as a {@link RemoteOperationException} with a standard error message. The given message is logged * on the receiving instance by this method, given a error correlation id, and that id is included in the generated message. * * @param serviceCallRequest the related request * @param errorMessage an internal description that is logged; it is *not* directly presented to the user * @param throwable a cause parameter that is included in the internal logging; it is *not* directly presented to the user * @return the generated ServiceCallResult */ public static ServiceCallResult representInternalErrorAtHandler(ServiceCallRequest serviceCallRequest, final String errorMessage, final Throwable throwable) { final String errorId = LogUtils.logExceptionWithStacktraceAndAssignUniqueMarker(sharedLog, StringUtils.format("Error while handling service request to '%s#%s' from '%s': %s", serviceCallRequest.getServiceName(), serviceCallRequest.getMethodName(), serviceCallRequest.getCallerNodeId(), errorMessage), throwable); final String userMessage = StringUtils.format( "There was an error performing this remote operation; if you have access to the log files of %s, " + "you can find more information by looking for the marker '%s' in its log files", serviceCallRequest.getTargetNodeId(), errorId); return new ServiceCallResult(null, null, null, userMessage); } }