/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2009 Bull S.A.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id: QSUpdateServiceWSDLHandler.java 21566 2011-08-08 12:28:12Z cazauxj $
 * --------------------------------------------------------------------------
 */
package org.ow2.jonas.ws.axis;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.wsdl.Definition;
import javax.wsdl.Port;
import javax.wsdl.Service;
import javax.wsdl.WSDLException;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.extensions.soap.SOAPAddress;
import javax.wsdl.factory.WSDLFactory;
import javax.wsdl.xml.WSDLReader;
import javax.wsdl.xml.WSDLWriter;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.axis.AxisFault;
import org.apache.axis.Constants;
import org.apache.axis.MessageContext;
import org.apache.axis.i18n.Messages;
import org.apache.axis.server.AxisServer;
import org.apache.axis.transport.http.AbstractQueryStringHandler;
import org.apache.axis.transport.http.HTTPConstants;
import org.apache.axis.utils.XMLUtils;
import org.ow2.jonas.deployment.ws.PortComponentDesc;
import org.ow2.jonas.deployment.ws.ServiceDesc;
import org.ow2.jonas.ws.jaxrpc.WSException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;



/**
 * Update the wsdlFile specified in wsdd with the good URLs for endpoints.
 * responds to the ?JWSDL Supports wsdl:import and xsd:include
 * @author Guillaume Sauthier
 */
public class QSUpdateServiceWSDLHandler extends AbstractQueryStringHandler {

    /**
     * SOAP NS URI
     */
    private static final String NS_URI_SOAP = "http://schemas.xmlsoap.org/wsdl/soap/";

    /**
     * soap:address Type QName
     */
    private static final QName QNAME_SOAP_ADDRESS = new QName(NS_URI_SOAP, "address");

    /**
     * WSDL NS URI
     */
    private static final String NS_URI_WSDL = "http://schemas.xmlsoap.org/wsdl/";

    /**
     * XSD NS URI
     */
    private static final String NS_URI_XSD = "http://www.w3.org/2001/XMLSchema";

    /**
     * filename parameter name
     */
    private static final String PARAM_FILENAME = "filename";

    /**
     * context parameter name
     */
    private static final String PARAM_CONTEXT = "context";

    /**
     * JWSDL parameter QSHandler name
     */
    private static final String PARAM_JWSDL = "JWSDL";

    /**
     * wsdl:definition QName
     */
    private static final QName WSDL_DEFINITIONS_QNAME = new QName(NS_URI_WSDL, "definitions");

    /**
     * Service meta data
     */
    private ServiceDesc sd = null;

    /**
     * Performs the action associated with this particular query string handler.
     * @param msgContext a MessageContext object containing message context
     *        information for this query string handler.
     * @throws AxisFault if an error occurs.
     */
    public void invoke(final MessageContext msgContext) throws AxisFault {
        // Obtain objects relevant to the task at hand from the provided
        // MessageContext's bag.
        configureFromContext(msgContext);

        AxisServer engine = (AxisServer) msgContext.getProperty(HTTPConstants.PLUGIN_ENGINE);
        PrintWriter writer = (PrintWriter) msgContext.getProperty(HTTPConstants.PLUGIN_WRITER);
        HttpServletResponse response = (HttpServletResponse) msgContext
                .getProperty(HTTPConstants.MC_HTTP_SERVLETRESPONSE);
        HttpServletRequest request = (HttpServletRequest) msgContext.getProperty(HTTPConstants.MC_HTTP_SERVLETREQUEST);

        InitialContext ctx;
        try {
            ctx = new InitialContext();
            sd = (ServiceDesc) ctx.lookup("java:comp/jonas/" + engine.getName() + "/dd");
        } catch (NamingException e) {
            throw new AxisFault("Servlet name not found : " + engine.getName(), e);
        }

        String wsdlFilename = request.getParameter(PARAM_FILENAME);
        String context = request.getParameter(PARAM_CONTEXT);
        try {
            Document doc = null;
            if (wsdlFilename == null) {
                // as a Document
                doc = getDefinitionAsDocument(sd.getWSDL().getDefinition());
                wsdlFilename = sd.getWSDL().getName();
                String[] pathElements = wsdlFilename.split("/");
                if (pathElements.length <= 2) {
                    throw new WSException("invalid filename");
                }

                StringBuffer buf = new StringBuffer();
                for (int i = 2; i < pathElements.length; i++) {
                    buf.append(pathElements[i]);
                    if (i != (pathElements.length - 1)) {
                        // last part is a filename
                        buf.append("/");
                    }
                }
                // remove WEB-INF/wsdl/
                // remove META-INF/wsdl/
                wsdlFilename = buf.toString();
                context = ".";
                msgContext.setProperty("WSDL", doc);
            } else {
                // try to get the specified WSDL from cache ...
                doc = (Document) msgContext.getProperty("WSDL_" + wsdlFilename);

                if (doc == null) {
                    // create the WSDL/Imported file
                    doc = getDocument(wsdlFilename, context);
                }

                msgContext.setProperty("WSDL_" + wsdlFilename, doc);
            }

            if (doc != null) {
                // update WSDL
                modifyImports(doc, request, new File(context, wsdlFilename).getParent());
                Document up2date = updateWSDLPortLocations(doc);
                response.setContentType("text/xml; charset=" + XMLUtils.getEncoding().toLowerCase());
                reportWSDL(up2date, writer);
            } else {
                // report Error
                if (log.isDebugEnabled()) {
                    log.debug("processWsdlRequest: failed to create WSDL");
                }
                reportNoWSDL(response, writer, "noWSDL02", null);
            }
        } catch (AxisFault axisFault) {
            //the no-service fault is mapped to a no-wsdl error
            if (axisFault.getFaultCode().equals(Constants.QNAME_NO_SERVICE_FAULT_CODE)) {
                //which we log
                processAxisFault(axisFault);

                //then report under a 404 error
                response.setStatus(HttpURLConnection.HTTP_NOT_FOUND);
                reportNoWSDL(response, writer, "noWSDL01", axisFault);
            } else {
                //all other faults get thrown
                throw axisFault;
            }
        }
    }

    /**
     * @param definition Definition to convert
     * @return returns the given Definition instance as a Document instance
     * @throws AxisFault when convertion is not possible
     */
    private Document getDefinitionAsDocument(final Definition definition) throws AxisFault {

        try {
            WSDLWriter writer = getWSDLWriter();
            return writer.getDocument(definition);
        } catch (WSDLException e) {
            throw new AxisFault(e.getMessage(), e);
        }

    }

    /**
     * Update the wsdl:import and xsd:include elements of the given Document.
     * @param doc Definitions or Schema document instance.
     * @param request HTTP request
     * @param context loading Context
     */
    private void modifyImports(final Document doc, final HttpServletRequest request, final String context) {
        // Document may be a wsdl:definition or xsd:schema
        // So we must handle import (definitions) and include (schema)

        // wsdl:definition / xsd:schema
        Element de = doc.getDocumentElement();

        NodeList imports = de.getElementsByTagNameNS(NS_URI_WSDL, "import");

        // modify wsdl:import location
        for (int i = 0; i < imports.getLength(); i++) {
            Element imp = (Element) imports.item(i);
            Attr location = imp.getAttributeNode("location");
            if (!location.getValue().startsWith("http://")) {
                // relative import
                String req = computeUpdatedURL(request, context, location);

                log.debug("Replacing wsdl:location '" + location.getValue() + "' with '" + req.toString() + "'");
                location.setValue(req.toString());
            }
        }

        // modify xsd:include schemaLocation
        updateSchema(de, "include", request, context);
        // modify xsd:import schemaLocation
        updateSchema(de, "import", request, context);

        // wsdl:definitions/wsdl:types/xsd:schema/(xsd:import|xsd:include)
        NodeList types = de.getElementsByTagNameNS(NS_URI_WSDL, "types");
        // is there a types here ?
        if (types.getLength() != 0) {

            // get the only wsdl:types element
            Element typesElement = (Element) types.item(0);

            // is there some xsd:schema out there ?
            NodeList schemasList = typesElement.getElementsByTagNameNS(NS_URI_XSD, "schema");
            for (int i = 0; i < schemasList.getLength(); i++) {
                Element schema = (Element) schemasList.item(i);
                updateSchema(schema, "include", request, context);
                updateSchema(schema, "import", request, context);
            }
        }
    }

    /**
     * @param request the Http Request
     * @param context loading context
     * @param location attribute to update
     * @return return the new location value
     */
    private String computeUpdatedURL(final HttpServletRequest request, final String context, final Attr location) {
        StringBuffer req = request.getRequestURL();
        req.append("?" + PARAM_JWSDL);
        req.append("&" + PARAM_FILENAME + "=" + location.getValue());
        req.append("&" + PARAM_CONTEXT + "=" + context);
        return req.toString();
    }

    /**
     * @param schema The Element representing a Schema to be updated
     * @param elementName the element name with "schemaLocation" attribute (can be "import" or "include")
     * @param request the HTTP Request
     * @param context loading context
     */
    private void updateSchema(final Element schema, final String elementName, final HttpServletRequest request, final String context) {

        NodeList elements = schema.getElementsByTagNameNS(NS_URI_XSD, elementName);

        // modify xsd:include|xsd:import schemaLocation
        for (int i = 0; i < elements.getLength(); i++) {
            Element e = (Element) elements.item(i);
            Attr location = e.getAttributeNode("schemaLocation");
            if ((location != null) && (!location.getValue().startsWith("http://"))) {
                // relative import
                String req = computeUpdatedURL(request, context, location);

                log.debug("Replacing xsd:schemaLocation '" + location.getValue() + "' with '" + req.toString() + "'");
                location.setValue(req.toString());
            }
        }
    }

    /**
     * @param wsdlFilename resource to load.
     * @param context loading context
     * @return Returns a Document created from the filename loaded in the
     *         context ClassLoader.
     * @throws AxisFault if resource if not found or if resource is not XML.
     */
    private Document getDocument(final String wsdlFilename, final String context) throws AxisFault {

        // Check that the filename is only relative to META-INF/wsdl or
        // WEB-INF/wsdl no deeper !
        // TODO Security Check !

        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        URL res = cl.getResource(context + "/" + wsdlFilename);
        if (res == null) {
            // Just a patch: may be in 'META-INF/wsdl' ?
            // It's needed to normalize the path by simplifying '.' and '..', and prefix it by 'META-INF/wsdl'
            // Ex: if context=./a/b/c and wsdlFilename=../../thefile.wsdl, than resourcename=META-INF/wsdl/a/thefile.wsdl
            // Use URI.normalize() instead of File.getCanonicalPath() to not be OS dependent
            String resourceName = null;
            try {
                URI uriwsdl = (new URI("META-INF/wsdl" + '/' + context + "/" + wsdlFilename)).normalize();
                resourceName = uriwsdl.getPath();
            } catch (URISyntaxException ioe) {
                throw new AxisFault("Cannot normalize the path of the document : " + wsdlFilename + "(" + context + ")");
            }
            res = cl.getResource(resourceName);
        }

        Document doc = null;
        if (res != null) {
            try {
                doc = createDocument(res.openStream());
            } catch (IOException ioe) {
                throw new AxisFault("Cannot open requested URL : " + res);
            }
        } else {
            throw new AxisFault("Cannot find requested document : " + wsdlFilename);
        }

        return doc;
    }

    /**
     * @param stream supposed XML InputStream
     * @return Returns the Document parsed from the given InputStream
     * @throws AxisFault if parsing goes wrong.
     */
    private Document createDocument(final InputStream stream) throws AxisFault {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            factory.setValidating(false);
            DocumentBuilder builder = factory.newDocumentBuilder();
            return builder.parse(stream);
        } catch (ParserConfigurationException pce) {
            throw new AxisFault(pce.getMessage(), pce);
        } catch (SAXException se) {
            throw new AxisFault(se.getMessage(), se);
        } catch (IOException ioe) {
            throw new AxisFault(ioe.getMessage(), ioe);
        }
    }

    /**
     * @param doc Document to update
     * @return updated Document
     * @throws AxisFault When Document cannot be parsed as a WSDL Definition
     *         instance
     */
    private Document updateWSDLPortLocations(final Document doc) throws AxisFault {
        log.debug("Entering updateWSDL");

        QName docQname = new QName(doc.getDocumentElement().getNamespaceURI(), doc.getDocumentElement().getLocalName());

        // If this is a wsdl:definition
        if (WSDL_DEFINITIONS_QNAME.equals(docQname)) {
            try {
                WSDLReader reader = getWSDLReader();
                // get Definition from Document
                Definition def = reader.readWSDL(null, doc);

                /**
                 * 1. iterer sur le port-component pour prendre leur URL 2. les
                 * assigner en fonction du wsdl-port
                 */
                QName sQName = sd.getWSDL().getServiceQname();
                Service s = def.getService(sQName);
                if (s != null) {

                    List portsComp = sd.getPortComponents();
                    for (Iterator i = portsComp.iterator(); i.hasNext();) {
                        PortComponentDesc pcd = (PortComponentDesc) i.next();
                        URL endpoint = pcd.getEndpointURL();
                        QName portQName = pcd.getQName();

                        Port port = s.getPort(portQName.getLocalPart());
                        // maybe we have not found the requested Port
                        if (port != null) {
                            // Set the updated soap:address address
                            List ext = port.getExtensibilityElements();
                            for (Iterator it = ext.iterator(); it.hasNext();) {
                                ExtensibilityElement element = (ExtensibilityElement) it.next();
                                if (element.getElementType().equals(QNAME_SOAP_ADDRESS)) {
                                    SOAPAddress sa = (SOAPAddress) element;
                                    sa.setLocationURI(endpoint.toExternalForm());
                                    log.debug("Update port soap:location with " + endpoint);
                                }
                            }
                        } else {
                            log.warn("Cannot find wsdl:port '" + portQName.getLocalPart() + "' in wsdl:service "
                                    + s.getQName());
                        }
                    }
                }

                return WSDLFactory.newInstance().newWSDLWriter().getDocument(def);
            } catch (WSDLException wsdle) {
                throw new AxisFault("Cannot read WSDL Document", wsdle);
            }
        } else {
            // if we have something else (not a wsdl:definition)
            // return the document unmodified
            return doc;
        }

    }

    /**
     * @return Returns a configured WSDLReader
     * @throws WSDLException if factory or reader cannot be instanciated.
     */
    private WSDLReader getWSDLReader() throws WSDLException {
        WSDLFactory factory = WSDLFactory.newInstance();
        WSDLReader reader = factory.newWSDLReader();
        reader.setFeature("javax.wsdl.importDocuments", false);
        return reader;
    }

    /**
     * @return Returns a configured WSDLWriter
     * @throws WSDLException if factory or writer cannot be instanciated.
     */
    private WSDLWriter getWSDLWriter() throws WSDLException {
        WSDLFactory factory = WSDLFactory.newInstance();
        return factory.newWSDLWriter();
    }

    /**
     * Reports WSDL
     * @param doc Document to write
     * @param writer Servlet Writer to use
     */
    public void reportWSDL(final Document doc, final PrintWriter writer) {
        XMLUtils.PrettyDocumentToWriter(doc, writer);
    }

    /**
     * Reports that we have no WSDL
     * @param res HttpServletResponse
     * @param writer PrintWriter
     * @param moreDetailCode optional name of a message to provide more detail
     * @param axisFault optional fault string, for extra info at debug time only
     */
    public void reportNoWSDL(final HttpServletResponse res, final PrintWriter writer, final String moreDetailCode, final AxisFault axisFault) {
        res.setStatus(HttpURLConnection.HTTP_NOT_FOUND);
        res.setContentType("text/html");

        writer.println("<h2>" + Messages.getMessage("error00") + "</h2>");
        writer.println("<p>" + Messages.getMessage("noWSDL00") + "</p>");

        if (moreDetailCode != null) {
            writer.println("<p>" + Messages.getMessage(moreDetailCode) + "</p>");
        }

        if (axisFault != null && isDevelopment()) {
            //dev systems only give fault dumps
            writeFault(writer, axisFault);
        }
    }

}