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

import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedList;

import javax.xml.soap.SOAPConstants;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;

import org.apache.axiom.om.OMContainer;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMException;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.om.OMSourcedElement;
import org.apache.axiom.om.ds.ParserInputStreamDataSource;
import org.apache.axiom.om.impl.builder.CustomBuilder;
import org.apache.axis2.datasource.jaxb.JAXBCustomBuilderMonitor;
import org.apache.axis2.jaxws.handler.HandlerUtils;
import org.apache.axiom.soap.SOAPFactory;
import org.apache.axiom.soap.SOAPHeader;
import org.apache.axis2.jaxws.message.databinding.ParsedEntityReader;
import org.apache.axis2.jaxws.message.factory.ParsedEntityReaderFactory;
import org.apache.axis2.jaxws.registry.FactoryRegistry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
 * A custom builder to that does the following:
 * 1) Uses the parser to fetch the inputStream if parser supports reading of element contents
 * 2) Use the inputStream to create a DataSource backed by the InputStream read from Parser.
 * 3) Use the OMFactory to create OMSourcedElement, OMSourcedElement is backed by ParsedEntityDataSource.
 */

public class ParserInputStreamCustomBuilder implements CustomBuilder {
    private static final Log log = 
        LogFactory.getLog(ParserInputStreamCustomBuilder.class);

    private String encoding = null;

    /**
     * Constructor
     * @param encoding 
     */
    public ParserInputStreamCustomBuilder(String encoding) {
        this.encoding = (encoding == null) ? "utf-8" :encoding;
    }
    /*
     * (non-Javadoc)
     * @see org.apache.axiom.om.impl.builder.CustomBuilder#create(java.lang.String, java.lang.String, org.apache.axiom.om.OMContainer, javax.xml.stream.XMLStreamReader, org.apache.axiom.om.OMFactory)
     */
    public OMElement create(String namespace, 
        String localPart, 
        OMContainer parent,
        XMLStreamReader reader, 
        OMFactory factory) throws OMException {

        if (log.isDebugEnabled()) {
            log.debug("create namespace = " + namespace);
            log.debug("  localPart = " + localPart);
            log.debug("  reader = " + reader.getClass());
        }
        
        if (!shouldUnmarshal(namespace, localPart)) {
            if (log.isDebugEnabled()) {
                log.debug("This element won't be unmarshalled with the custom builder");
            }
            return null;
        }
        
        /*
         * 1) Use the the parser to fetch the inputStream
         * 2) Use the inputStream to create a DataSource, delay reading of content as much as you can.
         * 3) Use the OMFactory to create OMSourcedElement, OMSourcedElement is backed by ParsedEntityDataSource.
         */
        try{
            ParsedEntityReaderFactory perf = (ParsedEntityReaderFactory)FactoryRegistry.getFactory(ParsedEntityReaderFactory.class);
            ParsedEntityReader entityReader = perf.getParsedEntityReader();
            if (log.isDebugEnabled()) {
                log.debug("ParsedEntityReader = " + entityReader);
            }
            //Do not user custom builder if Parser does not have ability to read sub content.
            if(!entityReader.isParsedEntityStreamAvailable()){
                if (log.isDebugEnabled()) {
                    log.debug("ParsedEntityStream is not available, defaulting to normal build");
                }
                return null;
            }
            // Create an OMSourcedElement backed by the ParsedData
            InputStream parsedStream = getPayloadContent(reader, entityReader);
            if(parsedStream == null){
                //cant read content from EntityReader, returning null.
                if (log.isDebugEnabled()) {
                    log.debug("Unable to read content from the entity reader, defaulting to normal build");
                }
                return null;
            }
            HashMap<String, String> nsElementDecls = getElementNamespaceDeclarations(reader);
            HashMap<String, String> attrElementDecls = getElementAttributeDeclarations(reader);
            
            //read the payload. Lets move the parser forward.
            if(reader.hasNext()){
                reader.next();
            }
            if(namespace == null){
                //lets look for ns in reader
                namespace = reader.getNamespaceURI();
                if(namespace == null){
                    //still cant find the namespace, just set it to "";
                    namespace = "";
                }
            }
            OMNamespace ns = factory.createOMNamespace(namespace, reader.getPrefix());
            InputStream payload = ContextListenerUtils.createPayloadElement(parsedStream, ns, localPart, parent, 
                        nsElementDecls, attrElementDecls);

            ParserInputStreamDataSource ds = new ParserInputStreamDataSource(payload, encoding);
            OMSourcedElement om = null;
            if (parent instanceof SOAPHeader && factory instanceof SOAPFactory) {
                om = ((SOAPFactory)factory).createSOAPHeaderBlock(localPart, ns, ds);
            } else {
                om = factory.createOMElement(ds, localPart, ns);
            }           
            //Add the new OMSourcedElement ot the parent
            parent.addChild(om); 
            /*
            //Lets Mark the body as complete so Serialize calls dont fetch data from parser for body content.
            if(parent instanceof SOAPBodyImpl){
                ((SOAPBodyImpl)parent).setComplete(true);
            }
            */
            return om;
        } catch (OMException e) {
            throw e;
        } catch (Throwable t) {
            throw new OMException(t);
        }
    }

    public OMElement create(String namespace, 
        String localPart, 
        OMContainer parent,
        XMLStreamReader reader, 
        OMFactory factory,
        InputStream payload) throws OMException {

        if (log.isDebugEnabled()) {
            log.debug("create namespace = " + namespace);
            log.debug("  localPart = " + localPart);
            log.debug("  reader = " + reader.getClass());
        }
        /*
         * 1) Use the the parser to fetch the inputStream
         * 2) Use the inputStream to create a DataSource, delay reading of content as much as you can.
         * 3) Use the OMFactory to create OMSourcedElement, OMSourcedElement is backed by ParsedEntityDataSource.
         */
        try{
            if(namespace == null){
                //lets look for ns in reader
                namespace = reader.getNamespaceURI();
                if(namespace == null){
                    //still cant find the namespace, just set it to "";
                    namespace = "";
                }
            }
            if (!shouldUnmarshal(namespace, localPart)) {
                if (log.isDebugEnabled()) {
                    log.debug("This element won't be unmarshalled with the custom builder");
                }
                return null;
            }
            OMNamespace ns = factory.createOMNamespace(namespace, reader.getPrefix());
            ParserInputStreamDataSource ds = new ParserInputStreamDataSource(payload, encoding);
            OMSourcedElement om = null;
            if (parent instanceof SOAPHeader && factory instanceof SOAPFactory) {
                om = ((SOAPFactory)factory).createSOAPHeaderBlock(localPart, ns, ds);
            } else {
                om = factory.createOMElement(ds, localPart, ns);
            }           
            //Add the new OMSourcedElement ot the parent
            parent.addChild(om); 
            return om;
        } catch (OMException e) {
            throw e;
        } catch (Throwable t) {
            throw new OMException(t);
        }
    }

    private HashMap<String, String> getElementNamespaceDeclarations(XMLStreamReader reader)
    {
      HashMap<String, String> nsElementDecls = new HashMap<String, String>();
      int count = reader.getNamespaceCount();
      for (int i = 0; i < count; i++){
        String prefix = reader.getNamespacePrefix(i);
        String namespace = reader.getNamespaceURI(i);
        if (namespace != null && namespace.length() > 0){
          nsElementDecls.put(prefix == null ? "":prefix, namespace);
        }
      }
      return nsElementDecls;
    }
    
    private HashMap<String, String> getElementAttributeDeclarations(XMLStreamReader reader)
    {
      HashMap<String, String> attrElementDecls = new HashMap<String, String>();
      int count = reader.getAttributeCount();

      for (int i = 0; i < count; i++) {
        String prefix = reader.getAttributePrefix(i);
        String name = reader.getAttributeLocalName(i);
        String value = convertEntityReferences(reader.getAttributeValue(i));
        String compoundName;
        if (prefix != null && prefix.length() > 0){
          compoundName = prefix+":"+name;
        }
        else {
          compoundName = name;
        }
        attrElementDecls.put(compoundName, value);
      }
      return attrElementDecls;
    }
    
    protected String convertEntityReferences(String value)
    {
      if ((value == null) || (value.length() == 0))
        return value;
      
      int valueLen = value.length();
      
      int[] positionsToChange = null;
      int numChanged = 0;
      
      for (int i = 0; i < valueLen; i++) {
        switch (value.charAt(i)) {
          case '<':
          case '>':
          case '&':
          case '\"':
          case '\'':
            if (positionsToChange == null)
            {
              positionsToChange = new int[valueLen];
            }
            positionsToChange[numChanged++]=i;
            break;
        }
      }

      if (numChanged == 0) {
        if(log.isDebugEnabled())
        {
          log.debug("No entity references were found in "+value);
        }
        return value;
      }
      else {
        if(log.isDebugEnabled())
        {
          log.debug("Found "+numChanged+" entity references in "+value);
        }
        
        //We'll create the new builder assuming the size of the worst case
        StringBuilder changedValue = new StringBuilder(valueLen+numChanged*5);
        int changedPos = 0; 
        for (int i = 0; i < valueLen; i++) {
          if (i == positionsToChange[changedPos]) {
            switch (value.charAt(i)) {
              case '<':
                changedValue.append("&lt;");
                changedPos++;
                break;
              case '>':
                changedValue.append("&gt;");
                changedPos++;
                break;
              case '&':
                changedValue.append("&amp;");
                changedPos++;
                break;
              case '\'':
                changedValue.append("&apos;");
                changedPos++;
                break;
              case '\"':
                changedValue.append("&quot;");
                changedPos++;
                break;
            }
          }
          else {
            changedValue.append(value.charAt(i));
          }
        }

        if(log.isDebugEnabled())
        {
          log.debug("Converted to "+changedValue.toString());
        }

        return changedValue.toString();
      }
    }
    /*
     * Read content from entityReader.
     */
    private InputStream getPayloadContent(XMLStreamReader parser, ParsedEntityReader entityReader){
        int event = parser.getEventType();
        //Make sure its start element event.
        if(log.isDebugEnabled()){
            log.debug("checking if event is START_ELEMENT");
        }
        InputStream parsedStream = null;
        if(event == XMLStreamConstants.START_ELEMENT){
            if(log.isDebugEnabled()){
                log.debug("event is START_ELEMENT");
            }
            parsedStream = entityReader.readParsedEntityStream(parser);
            if(parsedStream!=null){
                if(log.isDebugEnabled()){
                    log.debug("Read Parsed EntityStream");
                }
            }
        }
        return parsedStream;
    }
    
    /**
     * @param namespace
     * @param localPart
     * @return true if this ns and local part is acceptable for unmarshalling
     */
    private boolean shouldUnmarshal(String namespace, String localPart) {
        
        /**
         * The stream preserves the original message, so I think
         * we want to do unmarshal even if high fidelity is specified.
         
        boolean isHighFidelity = HandlerUtils.isHighFidelity(msgContext);

        if (isHighFidelity) {
            return false;
        }
        */
        
        // Don't unmarshal SOAPFaults.
        // If there is no localPart, this also indicates a potential problem...so don't 
        // use the custom builder
        if (localPart == null || 
            (localPart.equals("Fault") &&
             (SOAPConstants.URI_NS_SOAP_1_1_ENVELOPE.equals(namespace) ||
              SOAPConstants.URI_NS_SOAP_1_2_ENVELOPE.equals(namespace)))) {
            return false;
        }
       
        
        /**
         * For JAXB custom building, we ignore security elements.
         * I don't think it matters for parsed entities since they preserve all the content
        if (localPart.equals("EncryptedData")) {
            return false;
        }
        */
        
        return true;
                
    }
}
