/******************************************************************************* * Copyright (c) 2006-2010 eBay Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 *******************************************************************************/ package org.ebayopensource.turmeric.runtime.sif.impl.internal.service; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.ebayopensource.turmeric.common.v1.types.CommonErrorData; import org.ebayopensource.turmeric.common.v1.types.ErrorCategory; import org.ebayopensource.turmeric.common.v1.types.ErrorMessage; import org.ebayopensource.turmeric.runtime.common.errors.ErrorDataProvider; import org.ebayopensource.turmeric.runtime.common.exceptions.ErrorDataFactory; import org.ebayopensource.turmeric.runtime.common.exceptions.ServiceException; import org.ebayopensource.turmeric.runtime.common.exceptions.ServiceExceptionInterface; import org.ebayopensource.turmeric.runtime.common.exceptions.ServiceInvocationException; import org.ebayopensource.turmeric.runtime.common.exceptions.ServiceRuntimeException; import org.ebayopensource.turmeric.runtime.common.impl.internal.service.ServiceOperationDescImpl; import org.ebayopensource.turmeric.runtime.common.impl.protocolprocessor.soap.SOAP11Fault; import org.ebayopensource.turmeric.runtime.common.impl.protocolprocessor.soap.SOAP12Fault; import org.ebayopensource.turmeric.runtime.common.impl.utils.LogManager; import org.ebayopensource.turmeric.runtime.common.pipeline.MessageContext; import org.ebayopensource.turmeric.runtime.common.types.Cookie; import org.ebayopensource.turmeric.runtime.errorlibrary.ErrorConstants; import org.ebayopensource.turmeric.runtime.sif.impl.internal.markdown.SOAClientMarkdownState; import org.ebayopensource.turmeric.runtime.sif.impl.internal.markdown.SOAClientMarkdownStateId; import org.ebayopensource.turmeric.runtime.sif.impl.internal.markdown.SOAClientMarkdownStateManager; import org.ebayopensource.turmeric.runtime.sif.impl.internal.pipeline.ClientMessageContextImpl; import org.ebayopensource.turmeric.runtime.sif.pipeline.AutoMarkdownStateFactory; import org.ebayopensource.turmeric.runtime.sif.pipeline.ErrorResponseAdapter; import org.ebayopensource.turmeric.runtime.sif.service.Service; /** * @author ichernyshev */ public final class ServiceCallHelper { public static final Cookie[] EMPTY_COOKIES = new Cookie[0]; public static ServiceInvocationException createInvocationException(ClientMessageContextImpl ctx, String adminName, String opName, List<Throwable> clientErrors, Object errorResponse, boolean isAppOnlyException, boolean hasServerSystemErrors, String requestGuid) { Throwable rootCause = null; List<CommonErrorData> errorData = new ArrayList<CommonErrorData>(); // iterate client errors for (Throwable error: clientErrors) { if (rootCause == null) { // Remember the first throwable in case we have to use its text to construct // a makeshift ErrorData in block (B) below. This is also placed as the // cause in the creation we finally create below. Note: This may be inaccurate, because // the exception might just be a side exception that was thrown *after* the main // exception we're processing, e.g. a client response handler exception that simply // happened after getting a response with an application error. rootCause = error; } if (error instanceof ServiceExceptionInterface) { ServiceExceptionInterface error2 = (ServiceExceptionInterface)error; ErrorMessage errorMessage = error2.getErrorMessage(); if (errorMessage != null) { List<CommonErrorData> errorMessageDataList = errorMessage.getError(); errorData.addAll(errorMessageDataList); } } // we are not handling non-ServiceExceptionInterface exceptions other than keeping // the first one as the "root cause" } if (errorResponse instanceof ErrorMessage) { // Custom error message types presumably do // not have ErrorData, otherwise most likely the service writer would // have subclassed ErrorMessage and we'd still be using this logic here. ErrorMessage errorMessage = (ErrorMessage)errorResponse; List<CommonErrorData> errorMessageDataList = errorMessage.getError(); errorData.addAll(errorMessageDataList); } else if (errorResponse instanceof SOAP11Fault) { Object detail = ((SOAP11Fault)errorResponse).getDetail(); if (detail instanceof ErrorMessage) { ErrorMessage errorMessage = (ErrorMessage) detail; List<CommonErrorData> errorMessageDataList = errorMessage.getError(); errorData.addAll(errorMessageDataList); errorResponse = errorMessage; } } else if (errorResponse instanceof SOAP12Fault) { Object detail = ((SOAP12Fault)errorResponse).getDetail(); if (detail instanceof ErrorMessage) { ErrorMessage errorMessage = (ErrorMessage) detail; List<CommonErrorData> errorMessageDataList = errorMessage.getError(); errorData.addAll(errorMessageDataList); errorResponse = errorMessage; } } String exceptionMessageText; if (errorData.isEmpty()) { // (B) // The error list is all Throwables which do not implement ServiceExceptionInterface. // Also, any ErrorResponse appearing in the list "errors" is not an instance of ErrorMessage - // it's some alien custom error response. // So, we have nothing to go on so far; the error information isn't SOA recognizable. // Goal of this block is to create an ErrorData that has at least something in it. We do // that at (C). String causeText; String errorName; Long remoteErrorId = null; if (rootCause != null) { // since we do have some throwable to blame on the client side, use its exception. // this should not happen when any SEI-compliant exceptions are present, // so it's OK to provide default error ID errorName = ErrorConstants.SVC_CLIENT_INVOCATION_FAILED_SYS_CLIENT; causeText = rootCause.toString(); } else if (errorResponse != null) { // ErrorResponse is non-null. Note: Whenever this is true, in the current code, the // caller has already put the same ErrorResponse on the "errors" list. So, we're not // likely to find anything new here. We already know the error response isn't an instance of // ErrorMessage from the check above. We'll just use the error response toString() always. if (errorResponse instanceof ErrorMessage) { // the error data list was absolutely empty... use generic info errorName = ErrorConstants.SVC_CLIENT_INVOCATION_FAILED_APP; causeText = "Empty ErrorMessage " + errorResponse.toString(); } else { if (hasServerSystemErrors) { errorName = ErrorConstants.SVC_CLIENT_INVOCATION_FAILED_SYS_SERVER; } else { errorName = ErrorConstants.SVC_CLIENT_INVOCATION_FAILED_APP; } causeText = getCustomResponseText(ctx, errorResponse); remoteErrorId = getCustomResponseId(ctx, errorResponse); if (causeText == null) { // using toString on JAXB-generated objects makes no sense, // but we have no other choice... causeText = errorResponse.toString(); } } } else { // this is really impossible errorName = ErrorConstants.SVC_CLIENT_INVOCATION_FAILED_SYS_CLIENT; causeText = "Unknown"; getLogger().log(Level.SEVERE, "Unexpected internal call to createInvocationException " + "with no client-side errors and no error response"); } // (C) // Create a makeshift ErrorData with as good a message string as we came up with above. CommonErrorData errorData1 = ErrorDataFactory.createErrorData(errorName, ErrorConstants.ERRORDOMAIN, new Object[] {adminName + "." + opName, causeText}); if (remoteErrorId != null) { errorData1.setErrorId(remoteErrorId.longValue()); // errorData2.setErrorId(remoteErrorId.longValue()); } errorData.add(errorData1); // errorData.add(errorData2); // exceptionMessageText = errorData2.getMessage(); exceptionMessageText = errorData1.getMessage(); } else { CommonErrorData seriousErrorData = findErrorData(errorData, ErrorCategory.SYSTEM); if (seriousErrorData == null) { seriousErrorData = errorData.get(0); } exceptionMessageText = seriousErrorData.getMessage(); } Throwable applicationException = decodeApplicationException(ctx, errorResponse); // We could pass an index here - the first system error in the ErrorData list. return new ServiceInvocationException(exceptionMessageText, errorData, clientErrors, errorResponse, isAppOnlyException, applicationException, requestGuid, rootCause); } public static <T> Map<String, T> combineMaps(Map<String, T> sessionMap, Map<String, T> reqMap) { Map<String, T> result = null; if (sessionMap != null && !sessionMap.isEmpty()) { result = new HashMap<String, T>(); result.putAll(sessionMap); } if (reqMap != null && !reqMap.isEmpty()) { if (result == null) { result = new HashMap<String, T>(); } result.putAll(reqMap); } return result; } public static <T> Collection<T> combineCollections( Collection<T> sessionCollection, Collection<T> reqCollection) { Collection<T> result = null; if (sessionCollection != null && !sessionCollection.isEmpty()) { result = new ArrayList<T>(); result.addAll(sessionCollection); } if (reqCollection != null && !reqCollection.isEmpty()) { if (result == null) { result = new ArrayList<T>(); } result.addAll(reqCollection); } return result; } public static void checkMarkdownError(ClientMessageContextImpl context, Throwable e) { checkMarkdownError(context, e, null); } public static void checkState(ClientMessageContextImpl ctx, ClientServiceDesc clientSvcDesc, URL location){ AutoMarkdownStateFactory factory = clientSvcDesc.getAutoMarkdownStateFactory(); if(factory!=null){ SOAClientMarkdownStateManager mgr = SOAClientMarkdownStateManager.getInstance(); SOAClientMarkdownStateId id = mgr.createSoaStateId(ctx.getAdminName(), null, null, location.toString()); SOAClientMarkdownState state = mgr.getState(id); // this will create the state if it wasnt there already // now set the automarkdown factory for this if(!state.isAutoStateSet()) mgr.setAutoMarkdownState(factory, id, state); } } public static void checkMarkdownError(ClientMessageContextImpl context, Throwable e, URL location) { if (context == null) { return; } try { SOAClientMarkdownStateManager.getInstance().countError(context, e, location); } catch (Throwable e2) { getLogger().log( Level.SEVERE, "Unable to call countError for " + context.getServiceDesc().getAdminName() + ": " + e2.toString(), e2); } } public static boolean hasSystemErrorInResponse(ClientMessageContextImpl context, Object errorResponse) { if (!(errorResponse instanceof ErrorMessage)) { ErrorResponseAdapter responseAdapter = context .getServiceContext().getCustomErrorResponseAdapter(); if (responseAdapter == null) { // we cannot process this response type, assume it's all // application errors return false; } try { Boolean result = responseAdapter.hasSystemErrors(errorResponse); return (result != null ? result.booleanValue() : false); } catch (Throwable e) { getLogger() .log( Level.SEVERE, "ErrorResponseAdapter '" + responseAdapter.getClass().getName() + "' threw unexpected error in hasSystemErrors " + e.toString(), e); return false; } } ErrorMessage msg = (ErrorMessage) errorResponse; List<CommonErrorData> errors = msg.getError(); // try to find first application error for (CommonErrorData errorData : errors) { if (errorData != null) { if (errorData.getCategory() != ErrorCategory.APPLICATION) { return true; } } } return false; } private static CommonErrorData findErrorData(List<CommonErrorData> errors, ErrorCategory category) { // try to find first error with the given category for (CommonErrorData errorData: errors) { if (errorData != null) { if (errorData.getCategory() == category) { return errorData; } } } return null; } private static String getCustomResponseText(ClientMessageContextImpl ctx, Object errorResponse) { ErrorResponseAdapter responseAdapter = ctx.getServiceContext().getCustomErrorResponseAdapter(); if (responseAdapter == null) { // we cannot process this response type, assume it's all application errors return null; } try { return responseAdapter.getErrorText(errorResponse); } catch (Throwable e) { getLogger().log(Level.SEVERE, "ErrorResponseAdapter '" + responseAdapter.getClass().getName() + "' threw unexpected error in getErrorText " + e.toString(), e); } return null; } private static Long getCustomResponseId(ClientMessageContextImpl ctx, Object errorResponse) { ErrorResponseAdapter responseAdapter = ctx.getServiceContext().getCustomErrorResponseAdapter(); if (responseAdapter == null) { // we cannot process this response type, use generic error id return null; } try { return responseAdapter.getErrorId(errorResponse); } catch (Throwable e) { getLogger().log(Level.SEVERE, "ErrorResponseAdapter '" + responseAdapter.getClass().getName() + "' threw unexpected error in getErrorId " + e.toString(), e); } return null; } /** * Default internal function to reconstruct an application-specific exception from the error response * * This method matches operation exception names against the name specified in the first ErrorData * of the error response. If there is a match, the corresponding exception is constructed using * a constructor taking the error response class (e.g. ErrorMessage), or taking String * * If successful, the reconstructed exception is returned; otherwise it returns null */ private static Throwable decodeApplicationException(ClientMessageContextImpl ctx, Object errorResponse) { if (errorResponse == null) { return null; } ServiceOperationDescImpl operation = ctx.getOperation(); // try to get existing helper data ServiceErrorHelperData errorHelperData = (ServiceErrorHelperData)operation.getErrorHelperData(); if (errorHelperData != null) { if (!errorHelperData.hasData()) { // nothing was not found before... return null; } } // try to create helper data if it was not found if (errorHelperData == null) { try { errorHelperData = buildErrorHelperData(ctx.getServiceDesc(), operation); } catch (Throwable e) { getLogger().log(Level.SEVERE, "Unexpected exception building error helper data for " + ctx.getAdminName() + "." + operation.getName() + ": " + e.toString(), e); errorHelperData = new ServiceErrorHelperData(null); } if (errorHelperData == null) { // could not build return null; } operation.setErrorHelperData(errorHelperData); if (!errorHelperData.hasData()) { // nothing was not found... return null; } } // get exception name and text from error response String exceptionName; String errorText; if (errorResponse instanceof ErrorMessage) { ErrorMessage errorResponse2 = (ErrorMessage)errorResponse; CommonErrorData errorData = findErrorData(errorResponse2.getError(), ErrorCategory.APPLICATION); if (errorData == null) { return null; } exceptionName = errorData.getExceptionId(); errorText = errorData.getMessage(); errorResponse = errorData; } else { ErrorResponseAdapter responseAdapter = ctx.getServiceContext().getCustomErrorResponseAdapter(); if (responseAdapter == null) { // cannot reconstruct app error return null; } try { exceptionName = responseAdapter.getExceptionClassName(errorResponse); } catch (Throwable e) { exceptionName = null; getLogger().log(Level.SEVERE, "ErrorResponseAdapter '" + responseAdapter.getClass().getName() + "' threw unexpected error in getExceptionClassName " + e.toString(), e); } if (exceptionName == null) { // unknown error response, no appropriate exception, or error... return null; } errorText = getCustomResponseText(ctx, errorResponse); } // look up that exception name in error helper data ServiceExceptionInfo exceptionInfo = errorHelperData.m_exceptions.get(exceptionName); if (exceptionInfo == null) { // unknown exception name getLogger().log(Level.WARNING, "Operation " + ctx.getAdminName() + "." + operation.getName() + " returned an undeclared exception " + exceptionName); // TODO: allow exception's subclasses here return null; } // try to find some constructor that takes errorResponse or its base class Class responseType = errorResponse.getClass(); while (responseType != Object.class) { Constructor constructor = exceptionInfo.m_simpleConstructors.get(responseType); if (constructor != null) { Object[] paramValues = new Object[] { errorResponse }; return constructException(constructor, paramValues); } // try finding constructor that takes Super class responseType = responseType.getSuperclass(); } // try to find some constructor that takes String Constructor constructor = exceptionInfo.m_simpleConstructors.get(String.class); if (constructor != null) { Object[] paramValues = new Object[] {errorText}; return constructException(constructor, paramValues); } // no good constructor // TODO: do we want to use the default one??? getLogger().log(Level.WARNING, "No valid constructor found to instantiate exception " + exceptionName); return null; } private static Throwable constructException(Constructor constructor, Object[] params) { Throwable result; try { result = (Throwable)constructor.newInstance(params); } catch (Exception ex) { result = null; getLogger().log(Level.WARNING, "Failed to instantiate exception class: " + constructor.getDeclaringClass().getName(), ex); } return result; } private static ServiceErrorHelperData buildErrorHelperData(ClientServiceDesc serviceDesc, ServiceOperationDescImpl operation) { Class intfClass = serviceDesc.getServiceInterfaceClass(); if (intfClass == null) { return new ServiceErrorHelperData(null); } Method method = getMethod(intfClass, operation.getMethodName()); if (method == null) { getLogger().log(Level.SEVERE, "Unable to find operation " + operation.getMethodName() + " on service interface " + intfClass.getName()); return new ServiceErrorHelperData(null); } ServiceErrorHelperData result = new ServiceErrorHelperData(method); Class[] exceptions = method.getExceptionTypes(); for (int i=0; i<exceptions.length; i++) { Class exceptionClass = exceptions[i]; ServiceExceptionInfo info = buildExceptionInfo(exceptionClass); result.m_exceptions.put(exceptionClass.getName(), info); } return result; } private static ServiceExceptionInfo buildExceptionInfo(Class exceptionClass) { ServiceExceptionInfo result = new ServiceExceptionInfo(exceptionClass); Constructor[] constructors = exceptionClass.getConstructors(); for (int i=0; i<constructors.length; i++) { Constructor constructor = constructors[i]; Class[] params = constructor.getParameterTypes(); if (params.length == 0) { //result.m_defConstructor = constructor; continue; } if (params.length == 1) { result.m_simpleConstructors.put(params[0], constructor); continue; } // we do not support complex constructors } return result; } private static Method getMethod(Class clazz, String name) { Method[] methods = clazz.getMethods(); if (methods == null) { return null; } for (int i=0; i<methods.length; i++) { Method method = methods[i]; if (name.equals(method.getName())) { return method; } } return null; } private ServiceCallHelper() { // no instances } private static Logger getLogger() { return LogManager.getInstance(Service.class); } private static class ServiceErrorHelperData { final Map<String,ServiceExceptionInfo> m_exceptions = new HashMap<String,ServiceExceptionInfo>(); ServiceErrorHelperData(Method method) { // no impl } boolean hasData() { return !m_exceptions.isEmpty(); } } private static class ServiceExceptionInfo { final Map<Class,Constructor> m_simpleConstructors = new HashMap<Class,Constructor>(); //Constructor m_defConstructor; ServiceExceptionInfo(Class clazz) { // no impl } } /** * Retrieve a SOA error as a CommonErrorData. CommonErrorData is the preferred ErrorData format. * The locale of the CommonErrorData is implied within MessageContext. * * @param key specifies the bundle and errorname to retrieve * @param args placeholder arguments to pass onto the localizable message and resolution * @param ctx MessageContext * @return a CommonErrorData that corresponds to the bundle and errorname specified. * @throws NullPointerException if a validation error occurred -- if key is null, key.getBundle() is null, or key.getErrorName is null * @throws ServiceException if was a problem finding the locale * @throws ServiceRuntimeException if no error could be found or no error data provider was configured */ public static CommonErrorData getCommonErrorData( ErrorDataProvider.ErrorDataKey key, Object[] args, MessageContext ctx ) throws ServiceException { return ErrorHelper.getCommonErrorData( key, args, ctx ); } /** * Retrieve a SOA error as a CommonErrorData. CommonErrorData is the preferred ErrorData format. * This API differs from * {@link #getCommonErrorData(org.ebayopensource.turmeric.runtime.common.errors.ErrorDataProvider.ErrorDataKey, Object[], MessageContext)} * in that it allows specification of a locale other than the one associated with the MessageContext. * * @param key specifies the bundle and errorname to retrieve * @param args placeholder arguments to pass onto the localizable message and resolution * @param ctx MessageContext * @param locale desired Locale * @return a CommonErrorData that corresponds to the bundle and errorname specified. * @throws NullPointerException if a validation error occurred -- if key is null, key.getBundle() is null, or key.getErrorName is null * @throws ServiceRuntimeException if no error could be found or no error data provider was configured */ public static CommonErrorData getCommonErrorData( ErrorDataProvider.ErrorDataKey key, Object[] args, MessageContext ctx, Locale locale ) { return ErrorHelper.getCommonErrorData( key, args, locale, ctx ); } /** * Retrieve a SOA error as an ErrorData. This API was written for backwards compatibility for those legacy clients * who only understand the original ErrorData format. The locale of the ErrorData is implied within the MessageContext. * * @param key specifies the bundle and errorname to retrieve * @param args placeholder arguments to pass onto the localizable message and resolution * @param ctx MessageContext * @return an ErrorData that corresponds to the bundle and errorname specified. * @throws NullPointerException if a validation error occurred -- if key is null, key.getBundle() is null, or key.getErrorName is null * @throws ServiceException if was a problem finding the locale * @throws ServiceRuntimeException if no error could be found or no error data provider was configured */ public static CommonErrorData getErrorData( ErrorDataProvider.ErrorDataKey key, Object[] args, MessageContext ctx ) throws ServiceException { return ErrorHelper.getErrorData( key, args, ctx ); } /** * Retrieve a SOA error as an ErrorData. This API was written for backwards compatibility for those legacy clients * who only understand the original ErrorData format. This API differs from * {@link #getErrorData(org.ebayopensource.turmeric.runtime.common.errors.ErrorDataProvider.ErrorDataKey, Object[], MessageContext)} * by allowing specification of a locale other than the one associated with the MessageContext. * * @param key specifies the bundle and errorname to retrieve * @param args placeholder arguments to pass onto the localizable message and resolution * @param ctx MessageContext * @param locale desired Locale * @return an ErrorData that corresponds to the bundle and errorname specified. * @throws NullPointerException if a validation error occurred -- if key is null, key.getBundle() is null, or key.getErrorName is null * @throws ServiceRuntimeException if no error could be found or no error data provider was configured */ public static CommonErrorData getErrorData( ErrorDataProvider.ErrorDataKey key, Object[] args, MessageContext ctx, Locale locale ) { return ErrorHelper.getErrorData( key, args, locale, ctx ); } /** * Retrieve a SOA error as "Custom" ErrorData. "Custom" ErrorData are subclasses of CommonErrorData. Use of a "Custom" ErrorData may * be necessary if an application requires error state beyond what is provided by CommonErrorData. * The locale of the "Custom" ErrorData is implied within MessageContext. * * @param key specifies the bundle and errorname to retrieve * @param args placeholder arguments to pass onto the localizable message and resolution * @param clazz Class reference to the "Custom" ErrorData * @param ctx MessageContext * @return a "Custom" ErrorData that corresponds to the bundle and errorname specified. * @throws NullPointerException if a validation error occurred -- if key is null, key.getBundle() is null, or key.getErrorName is null * @throws ServiceException if was a problem finding the locale * @throws ServiceRuntimeException if no error could be found or no error data provider was configured */ public static <T extends CommonErrorData> T getCustomErrorData( ErrorDataProvider.ErrorDataKey key, Object[] args, Class<T> clazz, MessageContext ctx ) throws ServiceException { return ErrorHelper.getCustomErrorData( key, args, clazz, ctx ); } /** * Retrieve a SOA error as "Custom" ErrorData. "Custom" ErrorData are subclasses of CommonErrorData. Use of a "Custom" ErrorData may * be necessary if an application requires error state beyond what is provided by CommonErrorData. * This API differs from * {@link #getCustomErrorData(org.ebayopensource.turmeric.runtime.common.errors.ErrorDataProvider.ErrorDataKey, Object[], Class, MessageContext)} * by allowing specification of a locale other than the one associated with the MessageContext. * * @param key specifies the bundle and errorname to retrieve * @param args placeholder arguments to pass onto the localizable message and resolution * @param clazz Class reference to the "Custom" ErrorData * @param ctx MessageContext * @param locale desired Locale * @return a "Custom" ErrorData that corresponds to the bundle and errorname specified. * @throws NullPointerException if a validation error occurred -- if key is null, key.getBundle() is null, or key.getErrorName is null * @throws ServiceRuntimeException if no error could be found or no error data provider was configured */ public static <T extends CommonErrorData> T getCustomErrorData( ErrorDataProvider.ErrorDataKey key, Object[] args, Class<T> clazz, MessageContext ctx, Locale locale ) { return ErrorHelper.getCustomErrorData( key, args, clazz, locale, ctx ); } }