/*
* 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);
}
}