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

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.OMXMLBuilderFactory;
import org.apache.axiom.util.UIDGenerator;
import org.apache.axis2.AxisFault;
import org.apache.axis2.context.externalize.ExternalizeConstants;
import org.apache.axis2.context.externalize.SafeObjectInputStream;
import org.apache.axis2.context.externalize.SafeObjectOutputStream;
import org.apache.axis2.context.externalize.SafeSerializable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Class EndpointReference
 * This class models the WS-A EndpointReferenceType. But this can be used without any WS-A handlers as well
 * Since the models for this in Submission and Final versions are different, lets make this to comply with
 * WS-A Final version. So any information found with WS-A submission will be "pumped" in to this model.
 */
public class EndpointReference implements Externalizable, SafeSerializable {

    private static final long serialVersionUID = 5278892171162372439L;

    private static final Log log = LogFactory.getLog(EndpointReference.class);
    
    //  supported revision levels, add a new level to manage compatible changes
    private static final int REVISION_2 = 2;
    // current revision level of this object
    private static final int revisionID = REVISION_2;

    private static final String myClassName = "EndpointReference";

    /**
     * An ID which can be used to correlate operations on an instance of
     * this object in the log files
     */
    private String logCorrelationIDString;
    
    /**
     * The list of URIs that should be considered equivalent to 
     * the WS-Addressing anonymous URI
     */
    private static volatile List<String> anonymousEquivalentURIs = new ArrayList<String>();


    /**
     * <EndpointReference>
     * <Address>xs:anyURI</Address>
     * <ReferenceParameters>xs:any*</ReferenceParameters>
     * <MetaData>xs:any*</MetaData>
     * <!-- In addition to this, EPR can contain any number of OMElements -->
     * </EndpointReference>
     */

    private String name;
    private String address;
    private ArrayList<OMAttribute> addressAttributes;
    private ArrayList<OMNode> metaData;
    private ArrayList<OMAttribute> metaDataAttributes;
    private Map<QName, OMElement> referenceParameters;
    private ArrayList<OMElement> extensibleElements;
    private ArrayList<OMAttribute> attributes;

    /**
     * No-Arg Constructor
     * Required for Externalizable objects
     */
    public EndpointReference() {}
    
    /**
     * Adds a parameter to the list of anonymous equivalent URIs. 
     * @param anonymousEquivalentURI any URI that has an address that
     * begins with this value will be considered to be anonymous
     */
    public static void addAnonymousEquivalentURI(String anonymousEquivalentURI){
        if (log.isTraceEnabled())
        	log.trace("addAnonymousEquivalentURI: " + anonymousEquivalentURI);
        
        // Avoid synchronization in hasAnonymousAddress by using the
        // technique outlined at http://is.gd/gv3
        synchronized (anonymousEquivalentURIs) {
        	ArrayList<String> newList = new ArrayList<String>(anonymousEquivalentURIs);
        	newList.add(anonymousEquivalentURI);
            anonymousEquivalentURIs = newList;
        }
    }
 

    /**
     * @param address
     */
    public EndpointReference(String address) {
        this.address = address;
    }

    /**
     * @param omElement
     */
    public void addReferenceParameter(OMElement omElement) {
        if (omElement == null) {
            return;
        }
        if (referenceParameters == null) {
            referenceParameters = new HashMap<QName, OMElement>();
        }
        referenceParameters.put(omElement.getQName(), omElement);
    }

    /**
     * @param qname
     * @param value - the text of the OMElement. Remember that this is a convenient method for the user,
     *              which has limited capability. If you want more power use @See EndpointReference#addReferenceParameter(OMElement)
     */
    public void addReferenceParameter(QName qname, String value) {
        if (qname == null) {
            return;
        }
        OMElement omElement = OMAbstractFactory.getOMFactory().createOMElement(qname, null);
        omElement.setText(value);
        addReferenceParameter(omElement);
    }

    /**
     * This will return a Map of reference parameters with QName as the key and an OMElement
     * as the value
     *
     * @return - map of the reference parameters, where the key is the QName of the reference parameter
     *         and the value is an OMElement
     */
    public Map<QName, OMElement> getAllReferenceParameters() {
        return referenceParameters;
    }

    public String getAddress() {
        return address;
    }

    /**
     * @param address - xs:anyURI
     */
    public void setAddress(String address) {
        this.address = address;
    }

    public ArrayList<OMAttribute> getAddressAttributes() {
        return addressAttributes;
    }

    public void setAddressAttributes(ArrayList<OMAttribute> al) {
        addressAttributes = al;
    }

    public ArrayList<OMAttribute> getMetadataAttributes() {
        return metaDataAttributes;
    }

    public void setMetadataAttributes(ArrayList<OMAttribute> al) {
        metaDataAttributes = al;
    }
    
    /** 
     * This method identifies whether the address is a WS-Addressing spec defined
     * anonymous URI.
     * 
     * @return true if the address is a WS-Addressing spec defined anonymous URI.
     * 
     * @see #hasAnonymousAddress()
     */ 
    public boolean isWSAddressingAnonymous() {
        return (AddressingConstants.Final.WSA_ANONYMOUS_URL.equals(address) ||
                AddressingConstants.Submission.WSA_ANONYMOUS_URL.equals(address));
    }
    
    /**
     * This method is used to identify when response messages should be sent using
     * the back channel of a two-way transport.
     *
     * @return true if the address is a WS-Addressing spec defined anonymous URI,
     * or starts with a string that is specified to be equivalent to an anonymous
     * URI.
     * 
     * @see #addAnonymousEquivalentURI(String)
     */
    public boolean hasAnonymousAddress() {
        boolean result = isWSAddressingAnonymous();
        
        if(!result && address != null) {
        	// If the address is not WS-A anonymous it might still be considered anonymous
        	// Avoid synchronization by using the
            // technique outlined at http://is.gd/gv3
        	List<String> localList = anonymousEquivalentURIs;
        	if(!localList.isEmpty()){
        		Iterator<String> it = localList.iterator();
        		while(it.hasNext()){
        			result = address.startsWith((String)it.next());
        			if(result){
        				break;
        			}
        		}	
        	}    	
        }

        if (log.isTraceEnabled()) {
            log.trace("hasAnonymousAddress: " + address + " is Anonymous: " + result);
        }
        
        return result;
    }

    /**
     * hasNoneAddress
     *
     * @return true if the address is the 'None URI' from the final addressing spec.
     */
    public boolean hasNoneAddress() {
        boolean result = AddressingConstants.Final.WSA_NONE_URI.equals(address);
        if (log.isTraceEnabled()) {
            log.trace("hasNoneAddress: " + address + " is None: " + result);
        }
        return result;
    }

    /**
     * @param localName
     * @param ns
     * @param value
     */
    public void addAttribute(String localName, OMNamespace ns, String value) {
        if (attributes == null) {
            attributes = new ArrayList<OMAttribute>();
        }
        attributes.add(OMAbstractFactory.getOMFactory().createOMAttribute(localName, ns, value));
    }

    public ArrayList<OMAttribute> getAttributes() {
        return attributes;
    }


    /**
     * @param omAttribute
     */
    public void addAttribute(OMAttribute omAttribute) {
        if (attributes == null) {
            attributes = new ArrayList<OMAttribute>();
        }
        attributes.add(omAttribute);
    }

    public ArrayList<OMElement> getExtensibleElements() {
        return extensibleElements;
    }

    /**
     * {any}
     *
     * @param extensibleElements
     */
    public void setExtensibleElements(ArrayList<OMElement> extensibleElements) {
        this.extensibleElements = extensibleElements;
    }

    public void addExtensibleElement(OMElement extensibleElement) {
        if (extensibleElement != null) {
            if (this.extensibleElements == null) {
                this.extensibleElements = new ArrayList<OMElement>();
            }
            this.extensibleElements.add(extensibleElement);
        }
    }

    public ArrayList<OMNode> getMetaData() {
        return metaData;
    }

    public void addMetaData(OMNode metaData) {
        if (metaData != null) {
            if (this.metaData == null) {
                this.metaData = new ArrayList<OMNode>();
            }
            this.metaData.add(metaData);
        }

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * Set a Map with QName as the key and an OMElement
     * as the value
     *
     * @param referenceParameters
     */
    public void setReferenceParameters(Map<QName, OMElement> referenceParameters) {
        this.referenceParameters = referenceParameters;
    }

    /*
    *  (non-Javadoc)
    * @see java.lang.Object#toString()
    */
    public String toString() {
        StringBuffer buffer = new StringBuffer("Address: " + address);

        if (addressAttributes != null) {
            buffer.append(", Address Attributes: ").append(addressAttributes);
        }

        if (metaData != null) {
            buffer.append(", Metadata: ").append(metaData);
        }
        
        if (metaDataAttributes != null) {
            buffer.append(", Metadata Attributes: ").append(metaDataAttributes);
        }

        if (referenceParameters != null) {
            buffer.append(", Reference Parameters: ").append(referenceParameters);
        }

        if (extensibleElements != null) {
            buffer.append(", Extensibility elements: ").append(extensibleElements);
        }

        if (attributes != null) {
            buffer.append(", Attributes: ").append(attributes);
        }

        return buffer.toString();
    }   

    /**
     * Compares key parts of the state from the current instance of
     * this class with the specified instance to see if they are
     * equivalent.
     * <p/>
     * This differs from the java.lang.Object.equals() method in
     * that the equals() method generally looks at both the
     * object identity (location in memory) and the object state
     * (data).
     * <p/>
     *
     * @param epr The object to compare with
     * @return TRUE if this object is equivalent with the specified object
     *         that is, key fields match
     *         FALSE, otherwise
     */
    public boolean isEquivalent(EndpointReference epr) {
        // NOTE: the input object is expected to exist (ie, be non-null)

        if ((this.name != null) && (epr.getName() != null)) {
            if (!this.name.equals(epr.getName())) {
                return false;
            }
        } else if ((this.name == null) && (epr.getName() == null)) {
            // continue
        } else {
            // mismatch
            return false;
        }


        if ((this.address != null) && (epr.getAddress() != null)) {
            if (!this.address.equals(epr.getAddress())) {
                return false;
            }
        } else if ((this.address == null) && (epr.getAddress() == null)) {
            // continue
        } else {
            // mismatch
            return false;
        }

        // TODO: is a strict test ok to use?

        ArrayList<OMNode> eprMetaData = epr.getMetaData();

        if ((this.metaData != null) && (eprMetaData != null)) {
            if (!this.metaData.equals(eprMetaData)) {
                // This is a strict test
                // Returns true if and only if the specified object
                // is also a list, both lists have the same size, and
                // all corresponding pairs of elements in the two lists
                // are equal, ie, two lists are defined to be equal if
                // they contain the same elements in the same order.

                return false;
            }
        } else if ((this.metaData == null) && (eprMetaData == null)) {
            // keep going
        } else {
            // one of the lists is null
            return false;
        }


        ArrayList<OMElement> eprExtensibleElements = epr.getExtensibleElements();

        if ((this.extensibleElements != null) && (eprExtensibleElements != null)) {
            if (!this.extensibleElements.equals(eprExtensibleElements)) {
                // This is a strict test
                // Returns true if and only if the specified object
                // is also a list, both lists have the same size, and
                // all corresponding pairs of elements in the two lists
                // are equal, ie, two lists are defined to be equal if
                // they contain the same elements in the same order.

                return false;
            }
        } else if ((this.extensibleElements == null) && (eprExtensibleElements == null)) {
            // keep going
        } else {
            // one of the lists is null
            return false;
        }


        ArrayList<OMAttribute> eprAttributes = epr.getAttributes();

        if ((this.attributes != null) && (eprAttributes != null)) {
            if (!this.attributes.equals(eprAttributes)) {
                // This is a strict test
                // Returns true if and only if the specified object
                // is also a list, both lists have the same size, and
                // all corresponding pairs of elements in the two lists
                // are equal, ie, two lists are defined to be equal if
                // they contain the same elements in the same order.

                return false;
            }
        } else if ((this.attributes == null) && (eprAttributes == null)) {
            // keep going
        } else {
            // one of the lists is null
            return false;
        }

        // TODO: check the Map referenceParameters for equivalency

        return true;
    }

    //REVIEW: The following code is rather heavyweight, because we have to build 
    //   the OM tree -- it would probably be better to have two serialization/deserialization 
    //   paths and therefore, for trivial EPRs, store a smaller amount of info  

    /**
     * Write the EPR to the specified OutputStream.  Because of potential
     * OMElements/Attributes, we need to actually serialize the OM structures
     * (at least in some cases.)
     */
    public synchronized void writeExternal(java.io.ObjectOutput o)
            throws IOException {
        SafeObjectOutputStream out = SafeObjectOutputStream.install(o);
        
        // revision ID
        out.writeInt(revisionID);
        
        // Correlation ID
        String logCorrelationIDString = getLogCorrelationIDString();

        // String object id
        out.writeObject(logCorrelationIDString);

        // Write out the content as xml
        out.writeUTF("start xml"); // write marker
        OMElement om =
                EndpointReferenceHelper.toOM(OMAbstractFactory.getOMFactory(),
                                             this,
                                             new QName("urn:axis2", "omepr", "ser"),
                                             AddressingConstants.Final.WSA_NAMESPACE);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        try {
            om.serialize(baos);
        } catch (Exception e) {
            IOException ioe = new IOException("Unable to serialize the EndpointReference with logCorrelationID ["
                                              +logCorrelationIDString+"]"); 
            ioe.initCause(e);

            if (log.isDebugEnabled()) {
                log.debug("writeObject(): Unable to serialize the EPR with logCorrelationID ["
                          +logCorrelationIDString+"]   original error ["+e.getClass().getName()
                          +"]  message ["+e.getMessage()+"]",e);  
            }

            throw ioe;
        }

        out.writeInt(baos.size());
        out.write(baos.toByteArray());  
        out.writeUTF("end xml"); // write marker

        if (log.isDebugEnabled()) {
            byte[] buffer = baos.toByteArray();
            String content = new String(buffer);

            log.debug("writeObject(): EPR logCorrelationID ["+logCorrelationIDString+"] "    
                      +"    EPR content size ["+baos.size()+"]"
                      +"    EPR content ["+content+"]"); 
        }

    }

    /**
     * Read the EPR to the specified InputStream.
     */
    public void readExternal(java.io.ObjectInput inObject)
            throws IOException, ClassNotFoundException {
        SafeObjectInputStream in = SafeObjectInputStream.install(inObject);
        
        // revision ID
        int revID = in.readInt();
        
        // make sure the object data is in a revision level we can handle
        if (revID != REVISION_2) {
            throw new ClassNotFoundException(ExternalizeConstants.UNSUPPORTED_REVID);
        }
        
        // String object id
        logCorrelationIDString = (String) in.readObject();

        // Read xml content
        in.readUTF(); // read marker
        int numBytes = in.readInt();

        byte[] serBytes = new byte[numBytes];

        // read the data from the input stream

        int bytesRead = 0;
        int numberOfBytesLastRead;

        while (bytesRead < numBytes) {
            numberOfBytesLastRead = in.read(serBytes, bytesRead, numBytes - bytesRead);

            if (numberOfBytesLastRead == -1) {
                // TODO: What should we do if the reconstitution fails?
                // For now, log the event and throw an exception
                if (log.isDebugEnabled()) {
                    log.debug("readObject(): EPR logCorrelationID ["+logCorrelationIDString+"] "    
                            + " ***WARNING*** unexpected end to data:    data read from input stream ["
                            + bytesRead + "]    expected data size [" + numBytes + "]");
                }

                IOException ioe = new IOException("Unable to deserialize the EndpointReference with logCorrelationID ["
                                                  +logCorrelationIDString+"]"
                                                  +"  Cause: Unexpected end to data from input stream"); 

                throw ioe;
            }

            bytesRead += numberOfBytesLastRead;
        }


        if (bytesRead == 0) {
            IOException ioe = new IOException("Unable to deserialize the EndpointReference with logCorrelationID ["
                                              +logCorrelationIDString+"]"
                                              +"  Cause: No data from input stream"); 

            throw ioe;
        }
        in.readUTF(); // read marker

        ByteArrayInputStream bais = new ByteArrayInputStream(serBytes);

        if (log.isDebugEnabled()) {
            String content = new String(serBytes);

            log.debug("readObject(): EPR logCorrelationID ["+logCorrelationIDString+"] "    
                      +"    expected content size ["+numBytes+"]"
                      +"    content size ["+content.length()+"]"
                      +"    EPR buffered content ["+content+"]"); 
        }

        XMLStreamReader xmlReader = null;

        try {
            OMElement om = OMXMLBuilderFactory.createOMBuilder(bais).getDocumentElement();

            // expand the OM so we can close the stream reader
            om.build();

            // trace point
            if (log.isDebugEnabled()) {
                log.debug(myClassName + ":readObject():  "  
                          + " EPR ["+logCorrelationIDString + "]"
                          + " EPR OM content ["+om.toString()+ "]");
            }

            EndpointReferenceHelper.fromOM(this, om, AddressingConstants.Final.WSA_NAMESPACE);


        } catch (Exception e) {
            IOException ioe = new IOException("Unable to deserialize the EndpointReference with logCorrelationID ["
                                              +logCorrelationIDString+"]"); 
            ioe.initCause(e);

            if (log.isDebugEnabled()) {
                log.debug("readObject(): Unable to deserialize the EPR with logCorrelationID ["
                          +logCorrelationIDString+"]   original error ["+e.getClass().getName()
                          +"]  message ["+e.getMessage()+"]",e);  
            }

            throw ioe;

        } finally {
        	// Make sure that the reader is properly closed
            // Note that closing a ByteArrayInputStream has no effect

            if (xmlReader != null) {
                try {
                    xmlReader.close();
                } catch (Exception e2) {
                    IOException ioe2 = new IOException("Unable to close the XMLStreamReader for the EndpointReference with logCorrelationID ["
                                                      +logCorrelationIDString+"]"); 
                    ioe2.initCause(e2);

                    if (log.isDebugEnabled()) {
                        log.debug("readObject(): Unable to close the XMLStreamReader for the EPR with logCorrelationID ["
                                  +logCorrelationIDString+"]   original error ["+e2.getClass().getName()
                                  +"]  message ["+e2.getMessage()+"]",e2);  
                    }

                    throw ioe2;
                }
            }
        }
    }

    /**
     * Get the ID associated with this object instance.
     *
     * @return A string that can be output to a log file as an identifier
     *         for this object instance.  It is suitable for matching related log
     *         entries.
     */
    public String getLogCorrelationIDString() {
        if (logCorrelationIDString == null) {
            logCorrelationIDString = myClassName + "@" + UIDGenerator.generateUID();
        }
        return logCorrelationIDString;
    }

}
