/*
 * Copyright (C) The Apache Software Foundation. All rights reserved.
 *
 * This software is published under the terms of the Apache Software License
 * version 1.1, a copy of which has been included with this distribution in
 * the docs/licenses/apache-1.1.txt file.
 */

package org.jboss.axis;

import org.jboss.axis.encoding.SerializationContext;
import org.jboss.axis.encoding.Serializer;
import org.jboss.axis.message.SOAPEnvelopeAxisImpl;
import org.jboss.axis.message.SOAPFaultImpl;
import org.jboss.axis.message.SOAPHeaderElementAxisImpl;
import org.jboss.axis.soap.SOAPConstants;
import org.jboss.axis.utils.JavaUtils;
import org.jboss.axis.utils.XMLUtils;
import org.jboss.logging.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;

import javax.xml.namespace.QName;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.rpc.JAXRPCException;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Vector;

/**
 * An exception which maps cleanly to a SOAP fault.
 * This is a base class for exceptions which are mapped to faults.
 * 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 stuff
 * </ol>
 *
 * @author Doug Davis (dug@us.ibm.com)
 * @author James Snell (jasnell@us.ibm.com)
 * @author Steve Loughran
 */

public class AxisFault extends java.rmi.RemoteException
{

   private static Logger log = Logger.getLogger(AxisFault.class.getName());

   protected QName faultCode;
   /**
    * SOAP1.2 addition: subcodes of faults; a Vector of QNames
    */
   protected Vector faultSubCode;
   protected String faultString = "";
   protected String faultActor;
   protected Vector faultDetails;  // vector of Element's
   protected String faultNode;

   /** SOAP headers which should be serialized with the Fault
    *  List<SOAPHeaderElementAxisImpl>
    */
   protected ArrayList faultHeaders;

   /**
    * 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.
    */
   public static AxisFault makeFault(Exception e)
   {
      if (e instanceof InvocationTargetException)
      {
         InvocationTargetException ite = (InvocationTargetException)e;
         Throwable t = ite.getTargetException();
         if (t instanceof Exception)
            e = (Exception)t;
      }

      if (e instanceof JAXRPCException)
      {
         JAXRPCException rte = (JAXRPCException)e;
         Throwable t = rte.getLinkedCause();
         if (t instanceof Exception)
            e = (Exception)t;
      }

      if (e instanceof AxisFault)
      {
         return (AxisFault)e;
      }

      return new AxisFault(e);
   }

   /**
    * make a fault
    *
    * @param code        fault code which will be passed into the Axis namespace
    * @param faultString fault string
    * @param actor       fault actor
    * @param details     details; if null the current stack trace and classname is
    *                    inserted into the details.
    */
   public AxisFault(String code, String faultString, String actor, Element[] details)
   {
      this(new QName(Constants.NS_URI_AXIS, code),
              faultString, actor, details);
   }

   /**
    * make a fault in any namespace
    *
    * @param code        fault code which will be passed into the Axis namespace
    * @param faultString fault string
    * @param actor       fault actor
    * @param details     details; if null the current stack trace and classname is
    *                    inserted into the details.
    */
   public AxisFault(QName code, String faultString, String actor, Element[] details)
   {
      super(faultString);
      setFaultCode(code);
      setFaultString(faultString);
      setFaultActor(actor);
      setFaultDetail(details);
   }

   /**
    * make a fault in any namespace
    *
    * @param code        fault code which will be passed into the Axis namespace
    * @param subcodes    fault subcodes which will be pased into the Axis namespace
    * @param faultString fault string
    * @param actor       fault actor, same as fault role in SOAP 1.2
    * @param node        which node caused the fault on the SOAP path
    * @param details     details; if null the current stack trace and classname is
    *                    inserted into the details.
    * @since axis1.1
    */
   public AxisFault(QName code, QName[] subcodes, String faultString,
                    String actor, String node, Element[] details)
   {
      super(faultString);
      setFaultCode(code);
      if (subcodes != null)
      {
         for (int i = 0; i < subcodes.length; i++)
         {
            addFaultSubCode(subcodes[i]);
         }
      }
      setFaultString(faultString);
      setFaultActor(actor);
      setFaultNode(node);
      setFaultDetail(details);
   }

   /**
    * Wrap an AxisFault around an existing Exception - this is private
    * to force everyone to use makeFault() above, which sanity-checks us.
    */
   protected AxisFault(Exception target)
   {
      super(target.getMessage(), assertTarget(target));
      // ? SOAP 1.2 or 1.1 ?
      setFaultCodeAsString(Constants.FAULT_SERVER_USER);
      initFromException(target);
   }

   /**
    * Check for nested AxisFault
    * [todo] TDI 07-June-2004
    */
   private static Throwable assertTarget(Throwable target)
   {
//        if (target instanceof AxisFault)
//            log.warn("Nested AxisFault detected", new IllegalArgumentException());
      return target;
   }

   /**
    * create a simple axis fault from the message. Classname and stack trace
    * go into the fault details.
    *
    * @param message
    */
   public AxisFault(String message)
   {
      super(message);
      setFaultCodeAsString(Constants.FAULT_SERVER_GENERAL);
      setFaultString(message);
   }

   /**
    * No-arg constructor for building one from an XML stream.
    */
   public AxisFault()
   {
      super();
      setFaultCodeAsString(Constants.FAULT_SERVER_GENERAL);
   }

   /**
    * create a fault from any throwable;
    * When faulting a throwable (as opposed to an exception),
    * stack trace information does not go into the fault.
    *
    * @param message any extra text to with the fault
    * @param target  whatever is to be turned into a fault
    */
   public AxisFault(String message, Throwable target)
   {
      super(message, assertTarget(target));
      setFaultCodeAsString(Constants.FAULT_SERVER_GENERAL);
      setFaultString(getMessage());
   }

   /**
    * fill in soap fault details from the exception, unless
    * this object already has a stack trace in its details. Which, given
    * the way this private method is invoked, is a pretty hard situation to ever achieve.
    * This method adds classname of the exception and the stack trace.
    *
    * @param target what went wrong
    */
   private void initFromException(Exception target)
   {
      assertTarget(target);

      // Set the exception message (if any) as the fault string
      String faultString = target.getMessage();
      setFaultString(faultString != null ? faultString : target.toString());

      Throwable cause = target;
      if (target instanceof AxisFault)
         cause = ((AxisFault)target).detail;

      // Add the stack trace as detail element
      if (cause != null)
      {
         //add stack trace
         addFaultDetail(Constants.QNAME_FAULTDETAIL_STACKTRACE, JavaUtils.stackToString(cause));
      }
   }

   /**
    * init the fault details data structure; does nothing
    * if this exists already
    */
   private void initFaultDetails()
   {
      if (faultDetails == null)
      {
         faultDetails = new Vector();
      }
   }

   /**
    * clear the fault details list
    */
   public void clearFaultDetails()
   {
      faultDetails = null;
   }

   /**
    * dump the fault info to the log at debug level
    */
   public void dump()
   {
      log.debug(dumpToString());
   }


   /**
    * turn the fault and details into a string, with XML escaping.
    * subclassers: for security (cross-site-scripting) reasons,
    * escape everything that could contain caller-supplied data.
    *
    * @return stringified fault details
    */
   public String dumpToString()
   {
      String details = new String();

      if (faultDetails != null)
      {
         for (int i = 0; i < faultDetails.size(); i++)
         {
            Element e = (Element)faultDetails.get(i);
            String namespace = e.getNamespaceURI();
            if (namespace == null)
            {
               namespace = "";
            }
            String partname = e.getLocalName();
            if (partname == null)
            {
               partname = e.getNodeName();
            }
            details += JavaUtils.LS
                    + "\t{" + namespace + "}"
                    + partname + ": "
                    + XMLUtils.getInnerXMLString(e);
         }
      }

      String subCodes = new String();
      if (faultSubCode != null)
      {
         for (int i = 0; i < faultSubCode.size(); i++)
         {
            subCodes += JavaUtils.LS
                    + (QName)faultSubCode.elementAt(i);
         }
      }
      //encode everything except details and subcodes, which are already
      //dealt with one way or another.
      String code = XMLUtils.xmlEncodeString(faultCode.toString());
      String errorString = XMLUtils.xmlEncodeString(faultString);
      String actor = XMLUtils.xmlEncodeString(faultActor);
      String node = XMLUtils.xmlEncodeString(faultNode);


      return "AxisFault" + JavaUtils.LS
              + " faultCode: " + code + JavaUtils.LS
              + " faultSubcode: " + subCodes + JavaUtils.LS
              + " faultString: " + errorString + JavaUtils.LS
              + " faultActor: " + actor + JavaUtils.LS
              + " faultNode: " + node + JavaUtils.LS
              + " faultDetail: " + details + JavaUtils.LS
              ;
   }

   /**
    * set the fault code
    *
    * @param code a new fault code
    */
   public void setFaultCode(QName code)
   {
      faultCode = code;
   }

   /**
    * Set the fault code (as a String).
    *
    * @deprecated expect to see this go away after 1.1, use
    *             setFaultCodeAsString instead!
    */

   public void setFaultCode(String code)
   {
      setFaultCodeAsString(code);
   }

   /**
    * set a fault code string that is turned into a qname
    * in the SOAP 1.1 or 1.2 namespace, depending on the current context
    *
    * @param code fault code
    */
   public void setFaultCodeAsString(String code)
   {
      SOAPConstants soapConstants = MessageContext.getCurrentContext() == null ?
              SOAPConstants.SOAP11_CONSTANTS :
              MessageContext.getCurrentContext().getSOAPConstants();

      faultCode = new QName(soapConstants.getEnvelopeURI(), code);
   }

   /**
    * get the fault code
    *
    * @return fault code QName or null if there is none yet.
    */
   public QName getFaultCode()
   {
      return (faultCode);
   }

   /**
    * This is new in SOAP 1.2, ignored in SOAP 1.1
    *
    * @since axis1.1
    */
   public void addFaultSubCodeAsString(String code)
   {
      initFaultSubCodes();
      faultSubCode.add(new QName(Constants.NS_URI_AXIS, code));
   }

   /**
    * do whatever is needed to create the fault subcodes
    * data structure, if it is needed
    */
   protected void initFaultSubCodes()
   {
      if (faultSubCode == null)
      {
         faultSubCode = new Vector();
      }
   }

   /**
    * This is new in SOAP 1.2, ignored in SOAP 1.1
    *
    * @since axis1.1
    */
   public void addFaultSubCode(QName code)
   {
      initFaultSubCodes();
      faultSubCode.add(code);
   }

   /**
    * This is new in SOAP 1.2, ignored in SOAP 1.1
    *
    * @since axis1.1
    */
   public void clearFaultSubCodes()
   {
      faultSubCode = null;
   }

   /**
    * get the fault subcode list; only used in SOAP 1.2
    *
    * @return null for no subcodes, or a QName array
    * @since axis1.1
    */
   public QName[] getFaultSubCodes()
   {
      if (faultSubCode == null)
      {
         return null;
      }
      QName[] q = new QName[faultSubCode.size()];
      return (QName[])faultSubCode.toArray(q);
   }


   /**
    * set a fault string;
    *
    * @param str new fault string; null is turned into ""
    */
   public void setFaultString(String str)
   {
      if (str != null)
      {
         faultString = str;
      }
      else
      {
         faultString = "";
      }
   }

   /**
    * get the fault string; this will never be null but may be the
    * empty string
    *
    * @return a fault string
    */
   public String getFaultString()
   {
      return (faultString);
   }

   /**
    * This is SOAP 1.2 equivalent of {@link #setFaultString(java.lang.String)}
    *
    * @since axis1.1
    */
   public void setFaultReason(String str)
   {
      setFaultString(str);
   }

   /**
    * This is SOAP 1.2 equivalent of {@link #getFaultString()}
    *
    * @return
    * @since axis1.1
    */
   public String getFaultReason()
   {
      return getFaultString();
   }

   /**
    * set the fault actor
    *
    * @param actor fault actor
    */
   public void setFaultActor(String actor)
   {
      faultActor = actor;
   }

   /**
    * get the fault actor
    *
    * @return actor or null
    */
   public String getFaultActor()
   {
      return (faultActor);
   }

   /**
    * This is SOAP 1.2 equivalent of {@link #getFaultActor()}
    *
    * @return
    * @since axis1.1
    */
   public String getFaultRole()
   {
      return getFaultActor();
   }

   /**
    * This is SOAP 1.2 equivalent of {@link #setFaultActor(java.lang.String)}
    *
    * @since axis1.1
    */
   public void setFaultRole(String role)
   {
      setFaultActor(role);
   }

   /**
    * This is new in SOAP 1.2
    *
    * @return
    * @since axis1.1
    */
   public String getFaultNode()
   {
      return (faultNode);
   }

   /**
    * This is new in SOAP 1.2
    *
    * @since axis1.1
    */
   public void setFaultNode(String node)
   {
      faultNode = node;
   }

   /**
    * set the fault detail element to the arrary of details
    *
    * @param details list of detail elements, can be null
    */
   public void setFaultDetail(Element[] details)
   {
      if (details == null)
      {
         faultDetails = null;
         return;
      }

      faultDetails = new Vector(details.length);
      for (int loop = 0; loop < details.length; loop++)
      {
         faultDetails.add(details[loop]);
      }
   }

   /**
    * set the fault details to a string element.
    *
    * @param details XML fragment
    */
   public void setFaultDetailString(String details)
   {
      clearFaultDetails();
      addFaultDetailString(details);
   }

   /**
    * add a string tag to the fault details.
    *
    * @param detail XML fragment
    */
   public void addFaultDetailString(String detail)
   {
      initFaultDetails();
      try
      {
         Document doc = XMLUtils.newDocument();
         Element element = doc.createElement("string");
         Text text = doc.createTextNode(detail);
         element.appendChild(text);
         faultDetails.add(element);
      }
      catch (ParserConfigurationException e)
      {
         // This should not occur
         throw new InternalException(e);
      }
   }

   /**
    * append an element to the fault detail list
    *
    * @param detail the new element to add
    * @since Axis1.1
    */
   public void addFaultDetail(Element detail)
   {
      initFaultDetails();
      faultDetails.add(detail);
   }

   /**
    * create an element of the given qname and add it to the details
    *
    * @param qname qname of the element
    * @param body  string to use as body
    */
   public void addFaultDetail(QName qname, String body)
   {
      Element detail = XMLUtils.StringToElement(qname.getNamespaceURI(),
              qname.getLocalPart(),
              body);

      addFaultDetail(detail);
   }

   /**
    * get all the fault details
    *
    * @return an array of fault details, or null for none
    */
   public Element[] getFaultDetails()
   {
      if (faultDetails == null)
      {
         return null;
      }
      Element result[] = new Element[faultDetails.size()];
      for (int i = 0; i < result.length; i++)
         result[i] = (Element)faultDetails.elementAt(i);
      return result;
   }

   /**
    * Find a fault detail element by its qname
    *
    * @param qname name of the node to look for
    * @return the matching element or null
    * @since axis1.1
    */
   public Element lookupFaultDetail(QName qname)
   {
      if (faultDetails != null)
      {
         //extract details from the qname. the empty namespace is represented
         //by the empty string
         String searchNamespace = qname.getNamespaceURI();
         String searchLocalpart = qname.getLocalPart();
         //now spin through the elements, seeking a match
         Iterator it = faultDetails.iterator();
         while (it.hasNext())
         {
            Element e = (Element)it.next();
            String localpart = e.getLocalName();
            if (localpart == null)
            {
               localpart = e.getNodeName();
            }
            String namespace = e.getNamespaceURI();
            if (namespace == null)
            {
               namespace = "";
            }
            //we match on matching namespace and local part; empty namespace
            //in an element may be null, which matches QName's ""
            if (searchNamespace.equals(namespace)
                    && searchLocalpart.equals(localpart))
            {
               return e;
            }
         }
      }
      return null;
   }

   /**
    * find and remove a specified fault detail element
    *
    * @param qname qualified name of detail
    * @return true if it was found and removed
    * @since axis1.1
    */
   public boolean removeFaultDetail(QName qname)
   {
      Element elt = lookupFaultDetail(qname);
      if (elt == null)
      {
         return false;
      }
      else
      {
         return faultDetails.remove(elt);
      }
   }

   /**
    * add this fault and any needed headers to the output context
    *
    * @param context
    * @throws Exception
    */
   public void output(SerializationContext context) throws Exception
   {

      SOAPConstants soapConstants = Constants.DEFAULT_SOAP_VERSION;
      if (context.getMessageContext() != null)
         soapConstants = context.getMessageContext().getSOAPConstants();

      SOAPEnvelopeAxisImpl envelope = new SOAPEnvelopeAxisImpl(soapConstants);

      SOAPFaultImpl fault = new SOAPFaultImpl(this);
      envelope.addBodyElement(fault);

      // add any headers we need
      if (faultHeaders != null)
      {
         for (Iterator i = faultHeaders.iterator(); i.hasNext();)
         {
            SOAPHeaderElementAxisImpl header = (SOAPHeaderElementAxisImpl)i.next();
            envelope.addHeader(header);
         }
      }

      envelope.output(context);
   }

   /**
    * string operator
    *
    * @return the current fault string; may be empty but never null
    */
   public String toString()
   {
      return getClass().getName() + ": " + faultString;
   }

   /**
    * The override of the base class method prints out the
    * fault info before the stack trace
    *
    * @param ps where to print
    */
   public void printStackTrace(PrintStream ps)
   {
      ps.println(dumpToString());
      super.printStackTrace(ps);
   }

   /**
    * The override of the base class method prints out the
    * fault info before the stack trace
    *
    * @param pw where to print
    */
   public void printStackTrace(java.io.PrintWriter pw)
   {
      pw.println(dumpToString());
      super.printStackTrace(pw);
   }

   /**
    * Add a SOAP header which should be serialized along with the fault.
    *
    * @param header a SOAPHeaderElement containing some fault-relevant stuff
    */
   public void addHeader(SOAPHeaderElementAxisImpl header)
   {
      if (faultHeaders == null)
      {
         faultHeaders = new ArrayList();
      }
      faultHeaders.add(header);
   }

   /**
    * Get the SOAP headers associated with this fault.
    *
    * @return an ArrayList containing any headers associated with this fault
    */
   public ArrayList getHeaders()
   {
      return faultHeaders;
   }

   /**
    * clear all fault headers
    */
   public void clearHeaders()
   {
      faultHeaders = null;
   }


   /**
    * Writes any exception data to the faultDetails
    * This can be overrided (and is) by emitted exception clases.
    * The base implementation will attempt to serialize exception data
    * the fault was created from an Exception and a type mapping is found for it.
    */
   public void writeDetails(QName qname, SerializationContext context) throws java.io.IOException
   {

      Object detailObject = this.detail;
      if (detailObject == null)
         return;

      Serializer ser = context.getSerializerForJavaType(detailObject.getClass());
      if (ser != null)
      {
         boolean oldMR = context.getDoMultiRefs();
         context.setDoMultiRefs(false);
         context.serialize(qname, null, detailObject);
         context.setDoMultiRefs(oldMR);
      }
   }
}