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

import org.apache.axiom.soap.RolePlayer;
import org.apache.axiom.soap.SOAP11Constants;
import org.apache.axiom.soap.SOAP12Constants;
import org.apache.axiom.soap.SOAPConstants;
import org.apache.axiom.soap.SOAPEnvelope;
import org.apache.axiom.soap.SOAPHeaderBlock;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.context.OperationContext;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.i18n.Messages;
import org.apache.axis2.jaxws.description.EndpointDescription;
import org.apache.axis2.util.JavaUtils;
import org.apache.axis2.util.LoggingControl;
import org.apache.axis2.wsdl.WSDLConstants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.xml.namespace.QName;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.soap.SOAPHandler;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/*
 * Utility class to perform static utility type of operations on Handlers.
 */
public class HandlerUtils {
    private static Log log = LogFactory.getLog(HandlerUtils.class);

    /**
     * registerHandlerHeaders will invoke getHeaders on SOAPHandlers and return a List of headers
     * that are Understood by the handlers.
     * @param msgContext
     * @param handlers
     */
    public static List<QName> registerSOAPHandlerHeaders(MessageContext msgContext, List<Handler> handlers){
    	List<QName> understood = new ArrayList<QName>();
        if(msgContext == null){
            return understood;
        }

        for(Handler handler:handlers){
            if(handler instanceof SOAPHandler){
                SOAPHandler soapHandler = (SOAPHandler)handler;
                //Invoking getHeaders.
                if(log.isDebugEnabled()){
                    log.debug("Invoking getHeader() on SOAPHandler");
                }
                Set<QName> headers = soapHandler.getHeaders();
                if(headers!=null){
                    for(QName header:headers){
                        if(!understood.contains(header)){
                            if(log.isDebugEnabled()){
                                log.debug("Adding Header QName" + header + " to uderstoodHeaderQName List");
                            }
                            //Adding this to understood header list.
                            understood.add(header);
                        }
                    }
                }
            }
        }
        return understood;
    }

    /**
     * checkMustUnderstand will validate headers that where delegated by Axis Engine
     * to MessageReceiver for mustUnderstand check.
     * 
     * Note that there is other JAX-WS related mustUnderstand checking occuring elsewhere:
     * @see org.apache.axis2.jaxws.dispatchers.MustUnderstandValidationDispatcher 
     * @see org.apache.axis2.jaxws.dispatchers.MustUnderstandChecker 
     * 
     * @param msgContext Contains the SOAPEnvelope and optionally a list of headers not 
     *    understood by the AxisEngine
     * @param understood A list of header QNames understood by JAX-WS, for example those understood
     *   by associated application handlers. Can be null.
     * @param additionalRoles An instance of RolePlayer for any addtional roles played by JAX-WS
     *   for example, roles configured for associated handlers.  Can be null.
     * 
     * @throws AxisFault if any headers marked mustUndersand are not understood.
     */
    public static void checkMustUnderstand(MessageContext msgContext, List<QName> understood, List<String> additionalRoles) throws AxisFault {
        if (msgContext == null || !msgContext.isHeaderPresent()) {
            return;
        }
        SOAPEnvelope envelope = msgContext.getEnvelope();
        if (envelope.getHeader() == null) {
            return;
        }
        
        if(log.isDebugEnabled()){
            log.debug("Reading UnprocessedHeaderNames from Message Context properties");
        }
 
        List<QName> unprocessed = (List)msgContext.getProperty(Constants.UNPROCESSED_HEADER_QNAMES);

        // Add to the unprocessed header list any headers that are unprocssed and mustUnderstand
        // for addtional roles, for example those played by associated JAXWS handlers
        if (additionalRoles != null) {
            if (log.isDebugEnabled()) {
                log.debug("Adding any mustUnderstand headers based on additonal SOAP roles: " + additionalRoles);
            }
            HandlerRolePlayer handlerRolePlayer = new HandlerRolePlayer(additionalRoles);
            Iterator headerBlocks = envelope.getHeader().getHeadersToProcess(handlerRolePlayer);
            while (headerBlocks.hasNext()) {
                SOAPHeaderBlock shb = (SOAPHeaderBlock) headerBlocks.next();
                if (unprocessed == null) {
                    unprocessed = new ArrayList<QName>();
                }
                if (!shb.isProcessed() && shb.getMustUnderstand()) {
                    unprocessed.add(shb.getQName());
                    if (log.isDebugEnabled()) {
                        log.debug("Added header to unprocessed list: " + shb.getQName());
                    }
                }
            }
        }
        
        if(unprocessed == null || unprocessed.size() == 0){
            if(log.isDebugEnabled()){
                log.debug("UNPROCESSED_HEADER_QNAMES not found.");
            }
            return;
        }
        
        //lets go thru each header only if @HandlerChain is present
        if(!canUnderstand(msgContext)){
            QName[] qNames = unprocessed.toArray(new QName[0]);
            String[] headerNames = new String[qNames.length];
            for(int i=0; i<qNames.length; i++){
                headerNames[i] ="{" + qNames[i].getNamespaceURI()+ "}" + qNames[i].getLocalPart();
            }
            QName faultQName = envelope.getVersion().getMustUnderstandFaultCode();
            throw new AxisFault(Messages.getMessage("mustunderstandfailed2", headerNames), faultQName); 
        }

        checkUnprocessed(envelope, unprocessed, understood, msgContext);
        //resetting the FAULTY_HEADER_QNAME to null.
        msgContext.setProperty(Constants.UNPROCESSED_HEADER_QNAMES, null);
    }

    private static void checkUnprocessed(SOAPEnvelope envelope, List<QName> unprocessed, List<QName> understood, MessageContext msgContext) throws AxisFault{
        for (QName headerQName : unprocessed) {           
            if (understood != null && !understood.isEmpty()) {
                if (understood.contains(headerQName)) {
                    if (LoggingControl.debugLoggingAllowed && log.isDebugEnabled()) {
                        log.debug("MustUnderstand header registered as understood on AxisOperation: " + headerQName);
                    }    
                    continue;
                }
            }
            if (LoggingControl.debugLoggingAllowed && log.isDebugEnabled()) {
                log.debug("MustUnderstand header not processed or registered as understood " + headerQName);
            }
            // Throw a MustUnderstand fault for the current SOAP version
            String prefix = envelope.getNamespace().getPrefix();
            if (!msgContext.isSOAP11()) {
                if (prefix == null || "".equals(prefix)) {
                    prefix = SOAPConstants.SOAP_DEFAULT_NAMESPACE_PREFIX;
                }
                // TODO: should we be using a prefix on the faultcode?  What about
                // the QName object Constants.FAULT_SOAP12_MUSTUNDERSTAND?
                throw new AxisFault(Messages.getMessage("mustunderstandfailed",
                    prefix,
                    headerQName.toString()),
                    SOAP12Constants.FAULT_CODE_MUST_UNDERSTAND);
            } else {
                // TODO: should we be using a prefix on the faultcode?  What about
                // the QName object Constants.FAULT_MUSTUNDERSTAND?
                throw new AxisFault(Messages.getMessage("mustunderstandfailed",
                    prefix,
                    headerQName.toString()),
                    SOAP11Constants.FAULT_CODE_MUST_UNDERSTAND);
            }
        }
    }

    private static boolean canUnderstand(MessageContext msgContext){
        //JAXWSMessageReceiver will only commit to handling must understand if @HandlerChain annotation is present on the
        //Endpoint. This will indicate to AxisEngine that Faulty Header names are understood however the mustUnderstand 
        //Check will be performed in HandlerUtils class after Handlers are injected in application.
        AxisService axisSvc = msgContext.getAxisService();
        if (axisSvc.getParameter(EndpointDescription.AXIS_SERVICE_PARAMETER) != null) {
            Parameter param = axisSvc.getParameter(EndpointDescription.AXIS_SERVICE_PARAMETER);
            EndpointDescription ed = (EndpointDescription)param.getValue();
            //Lets check if there is a Handler implementation present using Metadata layer.
            //HandlerChain annotation can be present in Service Endpoint or ServiceEndpointInterface.
            // ed.getHandlerChain() looks for HandlerAnnotation at both Endpoint and at SEI.
            if(log.isDebugEnabled()){
                log.debug("Check to see if a jaxws handler is configured.");
            }
            if(ed.getHandlerChain()!=null){
                return true;
            }
            return false;
        }
        else{
            //If we cannot get to ServiceDescription to check for HandlerChain annotation we will return true;
            return true;
        }

    }

    /**
     * isHighFidelity
     * 
     * The JAX-WS engine attempts to stream data as fast as possible.
     * For example, the message payload may be transformed into a JAXB object early in the processing.
     * Unfortunately such transformations are lossy, some information is lost.
     * An installed SOAP handler will see different namespaces (etc) then the original message.
     * 
     * If the a customer enables the "jaxws.payload.highFidelity" flag, then lossy transformations are
     * avoided until necessary.  
     * 
     * @see Constants.JAXWS_HIGH_FIDELITY
     * 
     * @param mc
     * @return true if high fidelity is requested
     */
    public static boolean isHighFidelity(MessageContext mc) {
        boolean rc = _isHighFidelity(mc);
        
        // If not true, check the OUT MessageContext.
        // On the client, we need this setting when we receive (inbound)
        // the message; however the customer set it on the outbound context.
        if (!rc) {
            rc = _isHighFidelity(getRelatedMessageContext(mc));
        }
        return rc;
    }
    
    /**
     * @param mc
     * @return
     */
    private static MessageContext getRelatedMessageContext(MessageContext mc) {
        if (log.isDebugEnabled()) {
            log.debug("Enter getRelatedMessageContext for:" + mc);
        }
        MessageContext relatedMC = null;
        if (mc != null) {
            OperationContext oc = mc.getOperationContext();
            if (oc != null) {
                try {
                    relatedMC = oc.getMessageContext(WSDLConstants.MESSAGE_LABEL_IN_VALUE);
                    if (relatedMC == mc) {
                        relatedMC = oc.getMessageContext(WSDLConstants.MESSAGE_LABEL_OUT_VALUE);
                    }
                } catch (AxisFault e) {
                    // TODO This should never occur in this scenario, swallow and continue
                }
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("Exit getRelatedMessageContext related messageContext is" + relatedMC);
        }
        return relatedMC;
    }

    /**
     * isHighFidelity
     * 
     * The JAX-WS engine attempts to stream data as fast as possible.
     * For example, the message payload may be transformed into a JAXB object early in the processing.
     * Unfortunately such transformations are lossy, some information is lost.
     * An installed SOAP handler will see different namespaces (etc) then the original message.
     * 
     * If the a customer enables the "jaxws.payload.highFidelity" flag, then lossy transformations are
     * avoided until necessary.  
     * 
     * @see Constants.JAXWS_HIGH_FIDELITY
     * 
     * @param mc
     * @return true if high fidelity is requested
     */
    private static boolean _isHighFidelity(MessageContext mc) {
        
        boolean value = false;
        if (mc == null) {
            if (log.isDebugEnabled()) {
                log.debug("_isHighFidelity returns false due to missing MessageContext");
            }
            return false;
        }
        
        // First examine the high fidelity flag on the context hierarchy
        Boolean highFidelity = (Boolean) mc.getProperty(
                org.apache.axis2.jaxws.Constants.JAXWS_PAYLOAD_HIGH_FIDELITY);
        if (highFidelity != null) {
            value = highFidelity.booleanValue();
            if (log.isDebugEnabled()) {
                log.debug("_isHighFidelity returns " + value + " per Context property " + 
                        org.apache.axis2.jaxws.Constants.JAXWS_PAYLOAD_HIGH_FIDELITY);
            }
            return value;
        }
        
        // Second examine the deprecated jaxb streaming flag
        Boolean jaxbStreaming = (Boolean) mc.getProperty(
                org.apache.axis2.jaxws.Constants.JAXWS_ENABLE_JAXB_PAYLOAD_STREAMING);
        if (jaxbStreaming != null) {
            value = !jaxbStreaming.booleanValue();
            if (log.isDebugEnabled()) {
                log.debug("_isHighFidelity returns " + value + " per inspection of Context property " + 
                        org.apache.axis2.jaxws.Constants.JAXWS_ENABLE_JAXB_PAYLOAD_STREAMING);
            }
            return value;
        }
        
        // Now look at the high fidelity parameter
        Parameter p = mc.getParameter(org.apache.axis2.jaxws.Constants.JAXWS_PAYLOAD_HIGH_FIDELITY);
        if (p != null) {
            value = JavaUtils.isTrue(p.getValue());
            if (log.isDebugEnabled()) {
                log.debug("_isHighFidelity returns " + value + " per inspection of Configuration property " + 
                        org.apache.axis2.jaxws.Constants.JAXWS_PAYLOAD_HIGH_FIDELITY);
            }
            return value;
        }
        
        if (log.isDebugEnabled()) {
            log.debug("_isHighFidelity returns the default: false");
        }
        return false;
    }
}

class HandlerRolePlayer implements RolePlayer {
    List<String> roles = new ArrayList<String>();

    HandlerRolePlayer(List<String> additionalRoles) {
        roles.addAll(additionalRoles);
    }
    public List getRoles() {
        return roles;
    }
    public boolean isUltimateDestination() {
        return false;
    }
}

