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

import org.apache.axiom.om.OMElement;
import org.apache.axiom.soap.SOAPFault;
import org.apache.axiom.soap.SOAPFaultCode;
import org.apache.axiom.soap.SOAPFaultDetail;
import org.apache.axiom.soap.SOAPFaultNode;
import org.apache.axiom.soap.SOAPFaultReason;
import org.apache.axiom.soap.SOAPFaultRole;
import org.apache.axiom.soap.SOAPFaultSubCode;
import org.apache.axiom.soap.SOAPHeaderBlock;
import org.apache.axis2.context.MessageContext;

import javax.xml.namespace.QName;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

/**
 * An exception which maps cleanly to a SOAP fault.
 * This is a base class for exceptions which are mapped to faults.
 *
 * @see <a href="http://www.w3.org/TR/2003/REC-soap12-part1-20030624/#soapfault">
 *      SOAP1.2 specification</a>
 * @see <a href="http://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383507">SOAP1.1 Faults</a>
 *      <p/>
 *      SOAP faults contain
 *      <ol>
 *      <li>A fault string
 *      <li>A fault code
 *      <li>A fault actor
 *      <li>Fault details; an xml tree of fault specific elements
 *      </ol>
 *      <p/>
 *      As SOAP1.2 faults are a superset of SOAP1.1 faults, this type holds soap1.2 fault information. When
 *      a SOAP1.1 fault is created, spurious information can be discarded.
 *      Mapping
 *      <pre>
 *                                                             SOAP1.2              SOAP1.1
 *                                                             node                 faultactor
 *                                                             reason(0).text       faultstring
 *                                                             faultcode.value      faultcode
 *                                                             faultcode.subcode    (discarded)
 *                                                             detail               detail
 *                                                             role                 (discarded)
 *                                                             </pre>
 */


public class AxisFault extends RemoteException {
    private static final long serialVersionUID = -374933082062124907L;

    /**
     * assume headers are not used very often
     */
    private List<SOAPHeaderBlock> headers = new ArrayList<SOAPHeaderBlock>(0);

    private String message;

    private List<FaultReason> faultReasonList = new ArrayList<FaultReason>(1);
    private QName faultCode;
    private List<QName> faultSubCodes;
    private String faultNode;
    private String faultRole;
    private List<OMElement> details;

    private SOAPFaultCode soapFaultCode;
    private SOAPFaultReason soapFaultReason;
    private SOAPFaultNode soapFaultNode;
    private SOAPFaultRole soapFaultRole;
    private SOAPFaultDetail soapFaultDetail;

    /**
     * If not null, this MessageContext represents the fault as it
     * should be returned.  This is used by higher-level layers
     * that want to generate the message themselves so that
     * processing may take place before they return control (e.g. JAX-WS.)
     */
    private MessageContext faultMessageContext;

    /**
     * SOAP1.2: URI of faulting node. Null for unknown.
     * <p/>
     * The value of the Node element information item is the URI that
     * identifies the SOAP node that generated the fault.
     * SOAP nodes that do not act as the ultimate SOAP receiver MUST include this element
     * information item.
     * An ultimate SOAP receiver MAY include this element information item to
     * indicate explicitly that it generated the fault.
     */
    private String nodeURI;

	private String faultAction;


    /**
     * Constructor.
     *
     * @param message the human-readable text describing the fault
     */
    public AxisFault(String message) {
        this.message = message;
        addReason(message);
    }

    /**
     * Constructor
     *
     * @param faultCode   - fault code of the message as a QName
     * @param faultReason - the reason for the fault. The language will be defaulted to 'en'
     * @param cause embedded fault which caused this one
     */
    public AxisFault(QName faultCode, String faultReason, Throwable cause) {
        this(faultReason, cause);
        setFaultCode(faultCode);
    }
    
    /**
     * Constructor
     *
     * @param faultCode       - fault code of the message as a QName
     * @param faultSubCodes   - list sub fault codes as a list if QNames
     * @param faultReason - the reason for the fault. The language will be defaulted to 'en'
     * @param cause embedded fault which caused this one
     */
    public AxisFault(QName faultCode, List<QName> faultSubCodes, String faultReason, Throwable cause) {
        this(faultReason, cause);
        setFaultCode(faultCode);
        setFaultSubCodes(faultSubCodes);
    }

    /**
     * Constructor
     *
     * @param faultCode a QName for the fault code
     * @param faultReason the reason for the fault. The language will be defaulted to 'en'
     * @param faultNode a URL identifying the SOAP node generating this fault, or null
     * @param faultRole a URL identifying the SOAP role active when generating this fault, or null
     * @param faultDetail arbitrary XML containing application-specific fault data
     */
    public AxisFault(QName faultCode, String faultReason, String faultNode, String faultRole,
                     OMElement faultDetail) {
        this(faultReason, faultCode);
        this.faultNode = faultNode;
        this.faultRole = faultRole;
        setDetail(faultDetail);
    }

    /**
     * This is just a convenience method for the user. If you set these, do not use other methods
     * in this class to get and set things.
     * Any of the parameters can be null
     *
     * @param soapFaultCode the fault code
     * @param soapFaultReason the fault reason
     * @param soapFaultNode the SOAPFaultNode representing the source node for this fault
     * @param soapFaultRole the SOAPFaultRole representing the source role for this fault
     * @param soapFaultDetail the SOAPFaultDetail containing any application-specific info
     */
    public AxisFault(SOAPFaultCode soapFaultCode, SOAPFaultReason soapFaultReason,
                     SOAPFaultNode soapFaultNode, SOAPFaultRole soapFaultRole,
                     SOAPFaultDetail soapFaultDetail) {
        initializeValues(soapFaultCode, soapFaultReason, soapFaultNode, soapFaultRole,
                         soapFaultDetail);
    }

    public AxisFault(SOAPFault fault) {
        initializeValues(fault);
    }

    public AxisFault(SOAPFault fault, MessageContext faultCtx) {
        initializeValues(fault);
        faultMessageContext = faultCtx;
    }

    private void initializeValues(SOAPFault fault) {
        if (fault != null) {
            initializeValues(fault.getCode(), fault.getReason(), fault.getNode(),
                             fault.getRole(), fault.getDetail());
        }
    }

    private void initializeValues(SOAPFaultCode soapFaultCode,
                                  SOAPFaultReason soapFaultReason,
                                  SOAPFaultNode soapFaultNode,
                                  SOAPFaultRole soapFaultRole,
                                  SOAPFaultDetail soapFaultDetail) {
        this.soapFaultCode = soapFaultCode;
        this.soapFaultReason = soapFaultReason;
        this.soapFaultNode = soapFaultNode;
        this.soapFaultRole = soapFaultRole;
        this.soapFaultDetail = soapFaultDetail;

        if (soapFaultDetail != null) {
//            OMElement exceptionElement = soapFaultDetail.getFirstChildWithName(
//                    new QName(SOAPConstants.SOAP_FAULT_DETAIL_EXCEPTION_ENTRY));
//            if (exceptionElement != null && exceptionElement.getText() != null) {
//                cause = new Exception(exceptionElement.getText());
//            }

            details = new ArrayList<OMElement>();
            for (Iterator<?> it = soapFaultDetail.getChildElements(); it.hasNext(); ) {
                details.add((OMElement)it.next());
            }
        }

        if (soapFaultReason != null) {
            message = soapFaultReason.getText();
        }

        if (soapFaultCode != null) {
            // This works the same regardless of SOAP version
            faultCode = soapFaultCode.getValueAsQName();

            SOAPFaultSubCode subCode = soapFaultCode.getSubCode();
            if (subCode != null) {
                faultSubCodes = new ArrayList<QName>();
                while (subCode != null) {
                    faultSubCodes.add(subCode.getValueAsQName());
                    subCode = subCode.getSubCode();
                }
            }
        }
    }

    /**
     * Construct a fault from a Throwable.  This is a protected constructor - in general
     * to make an AxisFault from an Exception, you should be calling AxisFault.makeFault(e),
     * which prevents AxisFaults within AxisFaults.
     *
     * @param cause the Throwable that caused the problem
     */
    protected AxisFault(Throwable cause) {
        this((cause != null)
                ? cause.getMessage()
                : null, cause);
    }

    /**
     * Constructor.
     *
     * @param messageText - this will appear as the Text in the Reason information item of SOAP Fault
     * @param faultCode   - this will appear as the Value in the Code information item of SOAP Fault
     */
    public AxisFault(String messageText, String faultCode) {
        this(messageText);
        setFaultCode(faultCode);
    }

    /**
     * Constructor
     *
     * @param messageText this will appear as the Text in the Reason information item of SOAP Fault
     * @param faultCode this will appear as the Value in the Code information item of SOAP Fault
     */
    public AxisFault(String messageText, QName faultCode) {
        this(messageText);
        setFaultCode(faultCode);
    }

    /**
     * Constructor
     *
     * @param message this will appear as the Text in the Reason information item of SOAP Fault
     * @param cause the embedded Throwable that caused this fault
     */
    public AxisFault(String message, Throwable cause) {
        super(message, cause);

        if (message != null) {
            addReason(message);
            this.message = message;
        }
    }

    /**
     * @param messageText - this will appear as the Text in the Reason information item of SOAP Fault
     * @param faultCode   - this will appear as the Value in the Code information item of SOAP Fault
     * @param cause       - this will appear under the Detail information item of SOAP Fault
     */
    public AxisFault(String messageText, QName faultCode, Throwable cause) {
        this(messageText, cause);
        setFaultCode(faultCode);
    }

    
    /**
     * @param message
     * @param faultMessageContext
     * @param cause
     */
    public AxisFault(String message, MessageContext faultMessageContext, Throwable cause) {
        super(message, cause);

        this.faultMessageContext = faultMessageContext;
    }


    /**
     * @param messageText - this will appear as the Text in the Reason information item of SOAP Fault
     * @param faultCode   - this will appear as the Value in the Code information item of SOAP Fault
     * @param cause       - this will appear under the Detail information item of SOAP Fault
     */
    public AxisFault(String messageText, String faultCode, Throwable cause) {
        this(messageText, cause);
        setFaultCode(faultCode);
    }

    /**
     * Create an AxisFault by providing a textual message and a MessageContext
     * that contains the actual fault representation.
     *
     * @param message             A string that's really only useful for logging.
     * @param faultMessageContext A MessageContext which must contain SOAP fault info
     */
    public AxisFault(String message, MessageContext faultMessageContext) {
        this(message);
        this.faultMessageContext = faultMessageContext;
    }

    /**
     * Add a header to the list of fault headers
     *
     * @param header to add.
     */
    public void addHeader(SOAPHeaderBlock header) {
        headers.add(header);
    }

    /**
     * Add a reason for the fault in the empty "" language
     *
     * @param text text message
     */
    public void addReason(String text) {
        faultReasonList.add(new FaultReason(text, ""));
    }

    /**
     * Add a reason for the fault
     *
     * @param text     text message
     * @param language language
     */
    public void addReason(String text, String language) {
        faultReasonList.add(new FaultReason(text, language));
    }

    /**
     * Returns the first fault reason, if available. If not found, returns null.
     *
     * @return faultReason
     */
    public String getReason() {
        if (faultReasonList.size() >= 1) {
            return faultReasonList.get(0).getText();
        } else if (soapFaultReason != null) {
            return soapFaultReason.getText();
        }

        return null;
    }

    /**
     * Iterate over all of the headers
     *
     * @return iterator
     */
    public ListIterator<SOAPHeaderBlock> headerIterator() {
        return headers.listIterator();
    }

    /**
     * Get at the headers. Useful for java1.5 iteration.
     *
     * @return the headers for this fault
     */
    public List<SOAPHeaderBlock> headers() {
        return headers;
    }

    /**
     * Make an AxisFault based on a passed Exception.  If the Exception is
     * already an AxisFault, simply use that.  Otherwise, wrap it in an
     * AxisFault.  If the Exception is an InvocationTargetException (which
     * already wraps another Exception), get the wrapped Exception out from
     * there and use that instead of the passed one.
     *
     * @param e the <code>Exception</code> to build a fault for
     * @return an <code>AxisFault</code> representing <code>e</code>
     */
    public static AxisFault makeFault(Throwable e) {
        if (e instanceof InvocationTargetException) {
            return makeFault(((InvocationTargetException) e).getTargetException());
        } else if (e instanceof UndeclaredThrowableException) {
            Throwable t = ((UndeclaredThrowableException) e).getCause();
            if (t instanceof Exception) {
                e = t;
            }
        }
        if (e instanceof AxisFault) {
            return (AxisFault) e;
        }

        return new AxisFault(e);
    }

    /**
     * Get the current fault detail. If the fault has several detail elements, only the first one is
     * returned.
     * 
     * @return the (first) detail element, or <code>null</code> if there is no detail element
     */
    public OMElement getDetail() {
        return details == null || details.isEmpty() ? null : details.get(0);
    }

    /**
     * Get the SOAP fault detail elements.
     * 
     * @return the list of SOAP fault detail elements, or <code>null</code> if no detail elements
     *         have been set for this fault
     */
    public List<OMElement> getDetails() {
        return details;
    }

    public QName getFaultCode() {
        return faultCode;
    }

    public List<QName> getFaultSubCodes() {
        return faultSubCodes;
    }

    /**
     * @return SOAPFaultCode if, user has set a {@link SOAPFaultCode} element when constructing the
     *         {@link #AxisFault(org.apache.axiom.soap.SOAPFaultCode, org.apache.axiom.soap.SOAPFaultReason, org.apache.axiom.soap.SOAPFaultNode, org.apache.axiom.soap.SOAPFaultRole, org.apache.axiom.soap.SOAPFaultDetail) AxisFault}
     */
    public SOAPFaultCode getFaultCodeElement() {
        return soapFaultCode;
    }

    /**
     * @return SOAPFaultCode if, user has set a {@link SOAPFaultReason} element when constructing the
     *         {@link #AxisFault(org.apache.axiom.soap.SOAPFaultCode, org.apache.axiom.soap.SOAPFaultReason, org.apache.axiom.soap.SOAPFaultNode, org.apache.axiom.soap.SOAPFaultRole, org.apache.axiom.soap.SOAPFaultDetail) AxisFault}
     */
    public SOAPFaultReason getFaultReasonElement() {
        return soapFaultReason;
    }

    /**
     * @return SOAPFaultCode if, user has set a {@link SOAPFaultNode} element when constructing the
     *         {@link #AxisFault(org.apache.axiom.soap.SOAPFaultCode, org.apache.axiom.soap.SOAPFaultReason, org.apache.axiom.soap.SOAPFaultNode, org.apache.axiom.soap.SOAPFaultRole, org.apache.axiom.soap.SOAPFaultDetail) AxisFault}
     */
    public SOAPFaultNode getFaultNodeElement() {
        return soapFaultNode;
    }

    /**
     * @return SOAPFaultCode if, user has set a {@link SOAPFaultRole} element when constructing the
     *         {@link #AxisFault(org.apache.axiom.soap.SOAPFaultCode, org.apache.axiom.soap.SOAPFaultReason, org.apache.axiom.soap.SOAPFaultNode, org.apache.axiom.soap.SOAPFaultRole, org.apache.axiom.soap.SOAPFaultDetail) AxisFault}
     */
    public SOAPFaultRole getFaultRoleElement() {
        return soapFaultRole;
    }

    /**
     * @return SOAPFaultCode if, user has set a {@link SOAPFaultDetail} element when constructing the
     *         {@link #AxisFault(org.apache.axiom.soap.SOAPFaultCode, org.apache.axiom.soap.SOAPFaultReason, org.apache.axiom.soap.SOAPFaultNode, org.apache.axiom.soap.SOAPFaultRole, org.apache.axiom.soap.SOAPFaultDetail) AxisFault}
     */
    public SOAPFaultDetail getFaultDetailElement() {
        return soapFaultDetail;
    }

    /**
     * Get the faulting node uri.
     * SOAP1.2
     *
     * @return URI as a string or null
     */
    public String getNodeURI() {
        return nodeURI;
    }

    /**
     * Set the entire detail element of the fault
     *
     * @param detail an OMElement which MUST be 
     */
    public void setDetail(OMElement detail) {
        details = new ArrayList<OMElement>();
        details.add(detail);
    }

    /**
     * Set the SOAP fault details.
     * 
     * @param details
     *            a list of elements for the SOAP fault details
     */
    public void setDetails(List<OMElement> details) {
        this.details = details;
    }

    public void setFaultCode(QName soapFaultCode) {
        this.faultCode = soapFaultCode;
    }
    
    public void setFaultSubCodes(List<QName> faultSubCodes) {
        this.faultSubCodes = faultSubCodes;
    }
    
    public void setFaultCode(String soapFaultCode) {
        // TODO: is it really safe to assume that the passed string is always the localpart?
        // What if someone passes soapenv:Sender?
        faultCode = new QName(soapFaultCode);
    }

    /**
     * Set the faulting node uri. (SOAP1.2)
     *
     * @param nodeURI a String containing a URI indicating which SOAP Node faulted
     */
    public void setNodeURI(String nodeURI) {
        this.nodeURI = nodeURI;
    }

    public String getFaultNode() {
        return faultNode;
    }

    public String getFaultRole() {
        return faultRole;
    }

    /**
     * Returns the MessageContext representation of the fault if the fault
     * was created by providing that.
     *
     * @return The MessageContext representing the fault message or null if the
     *         fault was not created with MessageContext representation.
     */
    public MessageContext getFaultMessageContext() {
        return faultMessageContext;
    }

    class FaultReason implements Serializable{
        
        private static final long serialVersionUID = -8125991422614607169L;

        /**
         * Language of the reason.
         * xml:lang="en" "en-GB" or just ""
         */
        private String language = "";

        /**
         * env:reasontext
         */
        private String text;

        public FaultReason() {
        }

        public FaultReason(String text, String language) {
            this.text = text;
            this.language = language;
        }

        /**
         * Returns a string representation of the object.
         *
         * @return the text value
         */
        public String toString() {
            return text;
        }

        public String getLanguage() {
            return language;
        }

        public String getText() {
            return text;
        }

        public void setLanguage(String language) {
            this.language = language;
        }

        public void setText(String text) {
            this.text = text;
        }
    }
    
    /**
     * @return the action value set for the fault message  
     */
	public String getFaultAction() {
		return faultAction;
	}

	/**
	 * Set the (OPTIONAL) action value for the fault message
     *
	 * @param faultAction a String containing an action URI for the fault
	 */
	public void setFaultAction(String faultAction) {
		this.faultAction = faultAction;
	}

    /**
     * Returns the detail message, including the message from the cause, if any, of this exception.
     *
     * @return the detail message
     */
    public String getMessage() {
        return message;
    }

    /**
     * this field is used to identify the axis2 fault type
     */
    private int faultType;

    public int getFaultType() {
        return faultType;
    }

    public void setFaultType(int faultType) {
        this.faultType = faultType;
    }
}
