/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.axis2.jaxws;

import org.apache.axiom.om.OMException;
import org.apache.axis2.AxisFault;
import org.apache.axis2.jaxws.i18n.Messages;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.xml.ws.ProtocolException;
import javax.xml.ws.WebServiceException;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;

/**
 * ExceptionFactory is used to create exceptions within the JAX-WS implementation. There are several
 * reasons for using a factory to create exceptions. 1. We can intercept all exception creation and
 * add the appropriate logging/serviceability. 2. Exceptions are chained.  ExceptionFactory can
 * lengthen or reduce the cause chains as necessary to support the JAX-WS programming model. 3.
 * Prevents construction of the same exception.  Uses similar principles as
 * AxisFault.makeException.
 * <p/>
 * Example Usage: // Example usage
 * <p/>
 * public fooMethod() throws WebServiceException { try{ ... } catch(Exception e){ throw
 * ExceptionFactory.makeWebServiceException(e); } }
 */
public class ExceptionFactory {

    protected static Log log =
            LogFactory.getLog(ExceptionFactory.class.getName());

    /** Private Constructor All methods are static.  The private constructor prevents instantiation. */
    private ExceptionFactory() {
    }

    /**
     * Create a WebServiceException using the information from a given Throwable instance and message
     *
     * @param message
     * @param throwable
     * @return WebServiceException
     */
    public static WebServiceException makeWebServiceException(String message, Throwable throwable) {
        try {
            // See if there is already a WebServiceException (Note that the returned exception could be a ProtocolException or
            // other kind of exception)
            WebServiceException e =
                    (WebServiceException)findException(throwable, WebServiceException.class);
            if (e == null) {
                e = createWebServiceException(message, throwable);
            }
            return e;
        } catch (RuntimeException re) {
            // TODO
            // This is not a good situation, an exception occurred while building the exception.
            // This should never occur!  For now log the problem and rethrow...we may revisit this later
            if (log.isDebugEnabled()) {
                log.debug(Messages.getMessage("exceptionDuringExceptionFlow"), re);
            }
            throw re;
        }
    }

    /**
     * Create a ProtocolException using the information from a Throwable and message
     *
     * @param message
     * @param throwable
     * @return ProtocolException
     */
    public static ProtocolException makeProtocolException(String message, Throwable throwable) {
        try {
            // See if there is already a ProtocolException
            ProtocolException e =
                    (ProtocolException)findException(throwable, ProtocolException.class);
            if (e == null) {
                e = createProtocolException(message, throwable);
            }
            return e;
        } catch (RuntimeException re) {
            // TODO
            // This is not a good situation, an exception occurred while building the exception.
            // This should never occur!  For now log the problem and rethrow...we may revisit this later
            if (log.isDebugEnabled()) {
                log.debug(Messages.getMessage("exceptionDuringExceptionFlow"), re);
            }
            throw re;
        }
    }

    /**
     * Make a WebServiceException with a given message
     *
     * @param message
     * @return WebServiceException
     */
    public static WebServiceException makeWebServiceException(String message) {
        return makeWebServiceException(message, null);
    }

    /**
     * Create a WebServiceException using the information from a given Throwable instance
     *
     * @param throwable
     * @return WebServiceException
     */
    public static WebServiceException makeWebServiceException(Throwable throwable) {
        return makeWebServiceException(null, throwable);
    }


    /**
     * Create a WebServiceException
     *
     * @param message
     * @param t       Throwable
     * @return WebServiceException
     */
    private static WebServiceException createWebServiceException(String message, Throwable t) {

        // We might have an embedded WebServiceException that has a good message on it
        WebServiceException me = (WebServiceException)findException(t, WebServiceException.class);
        if (me != null) {
            String meMessage = me.getMessage();
            if (meMessage != null) {
                if (message == null) {
                    message = meMessage;
                } else {
                    message = message + ": " + meMessage;
                }
            }
        }

        // Get the root cause.  We want to strip out the intermediate exceptions (like AxisFault) because
        // these won't make sense to the user.
        Throwable rootCause = null;
        if (t != null) {
            rootCause = getRootCause(t);
        }
        rootCause = rootCause == null ? t : rootCause;
        WebServiceException e = null;

        // The root cause may not have a good message.  We might want to enhance it
        String enhancedMessage = enhanceMessage(rootCause);
        if (enhancedMessage != null) {
            if (message != null)
                message = message + ": " + enhancedMessage;
            else
                message = enhancedMessage;
        }

        if (message != null) {
            e = new WebServiceException(message, rootCause);
        } else {
            e = new WebServiceException(rootCause);
        }

        if (log.isDebugEnabled()) {
            log.debug("Create Exception:", e);
        }
        return e;
    }

    /**
     * Create a ProtocolException
     *
     * @param message
     * @param t       Throwable
     * @return ProtocolException
     */
    private static ProtocolException createProtocolException(String message, Throwable t) {
        Throwable rootCause = null;
        if (t != null) {
            rootCause = getRootCause(t);
        }
        rootCause = rootCause == null ? t : rootCause;
        ProtocolException e = null;
        if (message != null) {
            e = new ProtocolException(message, rootCause);
        } else {
            e = new ProtocolException(rootCause);
        }
        if (log.isDebugEnabled()) {
            log.debug("create Exception:", e);
        }
        return e;
    }

    /**
     * Return the exception or nested cause that is assignable from the specified class
     *
     * @param t   Throwable
     * @param cls
     * @return Exception or null
     */
    private static Exception findException(Throwable t, Class cls) {
        while (t != null) {
            if (cls.isAssignableFrom(t.getClass())) {
                return (Exception)t;
            }
            t = getCause(t);
        }
        return null;
    }

    /**
     * Gets the Throwable cause of the Exception.  Some exceptions store the cause in a different
     * field, which is why this method should be used when walking the causes.
     *
     * @param t Throwable
     * @return Throwable or null
     */
    private static Throwable getCause(Throwable t) {
        Throwable cause = null;

        // Look for a specific cause for this kind of exception
        if (t instanceof InvocationTargetException) {
            cause = ((InvocationTargetException)t).getTargetException();
        }

        // If no specific cause, fall back to the general cause.
        if (cause == null) {
            cause = t.getCause();
        }
        return cause;
    }

    /**
     * This method searches the causes of the specified throwable until it finds one that is
     * acceptable as a "root" cause.
     * <p/>
     * Example: If t is an AxisFault, the code traverses to the next cause.
     *
     * @param t Throwable
     * @return Throwable root cause
     */
    private static Throwable getRootCause(Throwable t) {
        while (t != null) {
            Throwable nextCause = null;
            if (t instanceof InvocationTargetException ||
                    t instanceof OMException ||
                    t instanceof AxisFault) {
                // Skip over this cause
                nextCause = getCause(t);
                if (nextCause == null) {
                    logRootCause(t);
                    return t;
                }
                t = nextCause;
            } else {
                // This is the root cause
                logRootCause(t);
                return t;
            }
        }
        logRootCause(t);
        return t;
    }

    /**
     * Other developers may add additional criteria to give better error messages back to the user.
     *
     * @param t Throwable
     * @return String a message that helps the user understand what caused the exception
     */
    private static String enhanceMessage(Throwable t) {
        if (t == null)
            return null;

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream ps = new PrintStream(baos, true);
        t.printStackTrace(ps);
        String stackTrace = baos.toString();

        // TODO better criteria
        if ((t instanceof StackOverflowError) && (stackTrace.contains("JAXB")))
            return Messages.getMessage("JABGraphProblem");

        return null;
    }
    /**
     * Log the root cause
     * @param t
     */
    private static void logRootCause(Throwable t) {
        // TODO Should certain throwables be logged elsewhere
        if (log.isDebugEnabled()) {
            log.debug("Root Cause:" +  t.toString());
            log.debug("stack:" + stackToString(t));
        }
    }
    
    /**
     * Get a string containing the stack of the specified exception
     * @param e
     * @return
     */
    public static String stackToString(Throwable e) {
        java.io.StringWriter sw= new java.io.StringWriter(); 
        java.io.BufferedWriter bw = new java.io.BufferedWriter(sw);
        java.io.PrintWriter pw= new java.io.PrintWriter(bw); 
        e.printStackTrace(pw);
        pw.close();
        return sw.getBuffer().toString();
    }
    
    /**
     * Give a target Throwable, set the initialCause Throwable as the initial cause on the target. 
     * @param target The throwable on which to set the initial cause
     * @param initialCause The initial cause to set on the target Throwable.
     */
    public static void setInitialCause(Throwable target, Throwable initialCause) {
        if (target != null && initialCause != null) {
            // Create a WebServiceException from the initialCause throwable.  This skips over things like
            // AxisFault as a cause.  Set the result on the target throwable.
            try {
                WebServiceException localException = ExceptionFactory.makeWebServiceException(initialCause);
                target.initCause(localException.getCause());
            } catch (Throwable t) {
                // If the cause had already been set, then it can't be set again; it throws an exception.
                if (log.isDebugEnabled()) {
                    log.debug("Caught exception trying to set initial cause on: " + target +".  Initial cause: " +
                            initialCause + ".  Caught: " + t);
                }
            }
        }
    }
}

