/*
 * 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.servicemix.components.xslt;

import org.apache.commons.lang.math.NumberUtils;
import org.apache.servicemix.components.util.ComponentSupport;
import org.apache.servicemix.components.util.CopyTransformer;
import org.apache.servicemix.components.util.MarshalerSupport;
import org.apache.xalan.extensions.XSLProcessorContext;
import org.apache.xalan.lib.ExsltDynamic;
import org.apache.xalan.templates.ElemTemplateElement;
import org.apache.xalan.transformer.TransformerImpl;
import org.apache.xml.utils.DOMBuilder;
import org.apache.xml.utils.PrefixResolver;
import org.apache.xpath.objects.XObject;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXNotSupportedException;

import javax.jbi.messaging.Fault;
import javax.jbi.messaging.InOnly;
import javax.jbi.messaging.InOut;
import javax.jbi.messaging.MessageExchange;
import javax.jbi.messaging.MessagingException;
import javax.jbi.messaging.NormalizedMessage;
import javax.xml.namespace.QName;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;

/**
 * An extension to allow <a href="http://xml.apache.org/xalan/">Xalan</a> to perform XPath based routing using
 * ServiceMix.
 *
 * @version $Revision: 426415 $
 */
public class XalanExtension extends MarshalerSupport {

    // Extension elements
    //-------------------------------------------------------------------------


    /**
     * Forwards the inbound message to the destination copying the content and properties
     */
    public void forward(XSLProcessorContext context, Element element) throws MessagingException {
        ComponentSupport component = getComponent(context, element);
        if (component == null) {
            throw new MessagingException("Could not find a component on which to perform the service invocation!");
        }

        TransformerImpl transformer = context.getTransformer();
        PrefixResolver namespaceContext = transformer.getXPathContext().getNamespaceContext();

        QName service = getQNameAttribute(namespaceContext, element, "service");
        QName interfaceName = getQNameAttribute(namespaceContext, element, "interface");
        QName operation = getQNameAttribute(namespaceContext, element, "operation");

        MessageExchange exchange = getExchange(context, element);
        NormalizedMessage in = getInMessage(context, element);

        // TODO we should allow nested setOutProperty tags

        component.invoke(exchange, in, service, interfaceName, operation);
    }

    /**
     * Invokes the service with the XML content included in the XML element
     */
    public void invoke(XSLProcessorContext context, ElemTemplateElement element) throws MessagingException, ParserConfigurationException, TransformerException {
        ComponentSupport component = getComponent(context, element);
        if (component == null) {
            throw new MessagingException("Could not find a component on which to perform the service invocation!");
        }

        TransformerImpl transformer = context.getTransformer();
        PrefixResolver namespaceContext = transformer.getXPathContext().getNamespaceContext();

        QName service = getQNameAttribute(namespaceContext, element, "service");
        QName interfaceName = getQNameAttribute(namespaceContext, element, "interface");
        QName operation = getQNameAttribute(namespaceContext, element, "operation");

        InOnly outExchange = component.createInOnlyExchange(service, interfaceName, operation);
        NormalizedMessage out = outExchange.createMessage();
        outExchange.setInMessage(out);

        transformer.setParameter("out", out);

        // lets copy the content into the body
        Document document = getTransformer().createDocument();
        DOMBuilder builder = new DOMBuilder(document);
        transformer.executeChildTemplates(element, context.getContextNode(), context.getMode(), builder);
        
        out.setContent(new DOMSource(document));

        // now lets perform the invocation
        component.send(outExchange);
    }

    /**
     * Calls the service with the XML content included in the XML element and outputs the XML content
     */
    public void call(XSLProcessorContext context, ElemTemplateElement element) throws MessagingException, ParserConfigurationException, TransformerException {
        ComponentSupport component = getComponent(context, element);
        if (component == null) {
            throw new MessagingException("Could not find a component on which to perform the service invocation!");
        }

        TransformerImpl transformer = context.getTransformer();
        PrefixResolver namespaceContext = transformer.getXPathContext().getNamespaceContext();

        QName service = getQNameAttribute(namespaceContext, element, "service");
        QName interfaceName = getQNameAttribute(namespaceContext, element, "interface");
        QName operation = getQNameAttribute(namespaceContext, element, "operation");

        InOut outExchange = component.createInOutExchange(service, interfaceName, operation);
        NormalizedMessage out = outExchange.createMessage();
        outExchange.setInMessage(out);


        // lets copy the content into the body
        Document document = getTransformer().createDocument();
        DOMBuilder builder = new DOMBuilder(document);
        transformer.executeChildTemplates(element, context.getContextNode(), context.getMode(), builder);

        out.setContent(new DOMSource(document));

        // now lets perform the invocation
        if (component.getDeliveryChannel().sendSync(outExchange)) {
            NormalizedMessage result = outExchange.getOutMessage();
            String outVarName = getAttribute(element, "outVar", "out");
            transformer.setParameter(outVarName, result);
        }
        else {
            Exception error = outExchange.getError();
            if (error != null) {
                transformer.setParameter("error", error);
            }
            Fault fault = outExchange.getFault();
            if (fault != null) {
                transformer.setParameter("fault", fault);
            }
        }

    }

    /**
     * Copies the properties from the input to the output message
     */
    public void copyProperties(XSLProcessorContext context, Element element) throws MessagingException {
        NormalizedMessage in = getInMessage(context, element);
        NormalizedMessage out = getOutMessage(context, element);
        CopyTransformer.copyProperties(in, out);
    }

    /**
     * Sets a named property on the output message
     */
    public void setOutProperty(XSLProcessorContext context, ElemTemplateElement element) throws MessagingException, SAXNotSupportedException {
        NormalizedMessage out = getOutMessage(context, element);
        String name = element.getAttribute("name");
        if (name == null) {
            throw new IllegalArgumentException("Must specify a 'name' attribute to set a property on the output message");
        }
        String xpath = element.getAttribute("select");
        if (xpath == null) {
            throw new IllegalArgumentException("Must specify a 'select' attribute to set a property on the output message");
        }
        XObject answer = ExsltDynamic.evaluate(context.getTransformer().getXPathContext().getExpressionContext(), xpath);
        Object value;
        try {
            if (answer.getType() == XObject.CLASS_NUMBER) {
                value = NumberUtils.createNumber(answer.str());
            } else if (answer.getType() == XObject.CLASS_BOOLEAN) {
                value = new Boolean(answer.bool());
            } else {
                // XObject guarantees we are never null.
                value = answer.str();
            }
        } catch (TransformerException e) {
            value = answer.str();
        } catch (NumberFormatException e) {
            value = answer.str();
        }
        out.setProperty(name, value);
    }

    // Extension XPath functions
    //-------------------------------------------------------------------------

    // Implementation methods
    //-------------------------------------------------------------------------
    protected ComponentSupport getComponent(XSLProcessorContext context, Element element) {
        return (ComponentSupport) getParameter(context, "component");
    }

    protected MessageExchange getExchange(XSLProcessorContext context, Element element) {
        return (MessageExchange) getParameter(context, "exchange");
    }

    protected NormalizedMessage getInMessage(XSLProcessorContext context, Element element) {
        return (NormalizedMessage) getParameter(context, "in");
    }

    protected NormalizedMessage getOutMessage(XSLProcessorContext context, Element element) {
        return (NormalizedMessage) getParameter(context, "out");
    }

    protected Object getParameter(XSLProcessorContext context, String name) {
        return context.getTransformer().getParameter(name);
    }

    protected QName getQNameAttribute(PrefixResolver namespaceContext, Element element, String name) {
        String qualifiedName = element.getAttribute(name);
        if (qualifiedName != null) {
            int index = qualifiedName.indexOf(':');
            if (index >= 0) {
                String prefix = qualifiedName.substring(0, index);
                String localName = qualifiedName.substring(index + 1);
                String uri = namespaceContext.getNamespaceForPrefix(prefix);
                return new QName(uri, localName, prefix);
            }
            else {
                String uri = namespaceContext.getNamespaceForPrefix("");
                if (uri != null) {
                    return new QName(uri, qualifiedName);
                }
                return new QName(qualifiedName);
            }
        }
        return null;
    }

    protected String getAttribute(Element element, String attribute, String defaultValue) {
        String answer = element.getAttribute(attribute);
        if (answer == null || answer.length() == 0) {
            answer = defaultValue;
        }
        return answer;
    }
}
