/*
 * Copyright (c) 1998-2007 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.soap.skeleton;

import com.caucho.jaxb.JAXBUtil;
import com.caucho.jaxb.JAXBContextImpl;
import static com.caucho.soap.wsdl.WSDLConstants.*;
import com.caucho.soap.jaxws.HandlerChainInvoker;
import com.caucho.soap.jaxws.JAXWSUtil;
import com.caucho.soap.jaxws.PortInfoImpl;
import com.caucho.soap.wsdl.WSDLDefinitions;
import com.caucho.util.Attachment;
import com.caucho.util.AttachmentReader;
import com.caucho.util.L10N;
import com.caucho.xml.XmlPrinter;
import com.caucho.xml.stream.StaxUtil;

import org.w3c.dom.Node;
import javax.activation.DataHandler;
import javax.jws.HandlerChain;
import javax.jws.WebService;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static javax.xml.XMLConstants.*;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import static javax.xml.soap.SOAPConstants.*;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.stream.StreamResult;
import javax.xml.ws.BindingType;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.HandlerResolver;
import javax.xml.ws.handler.PortInfo;
import static javax.xml.ws.handler.MessageContext.*;
import javax.xml.ws.soap.SOAPBinding;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.CharArrayReader;
import java.io.CharArrayWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;

import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Invokes a SOAP request on a Java POJO
 */
public class DirectSkeleton extends Skeleton {
  private static final Logger log =
    Logger.getLogger(DirectSkeleton.class.getName());
  public static final L10N L = new L10N(DirectSkeleton.class);

  private static final String TARGET_NAMESPACE_PREFIX = "m";

  private boolean _separateSchema = false;
  private JAXBContextImpl _context;
  private Marshaller _marshaller;
  private Node _wsdlNode;

  private HashMap<String,AbstractAction> _actionNames
    = new HashMap<String,AbstractAction>();

  private HashMap<Method,AbstractAction> _actionMethods
    = new HashMap<Method,AbstractAction>();

  private Class _api;

  private HandlerChainInvoker _handlerChain;
  
  private String _bindingId;
  private String _namespace;
  private String _portType;
  private String _portName;
  private String _serviceName;
  private String _wsdlLocation = "REPLACE_WITH_ACTUAL_URL";
  private PortInfo _portInfo;
  private WSDLDefinitions _wsdl;

  // The URI in SOAPBinding is wrong, but matches that of JAVAEE
  private String _soapNamespaceURI = "http://schemas.xmlsoap.org/wsdl/soap/";
  private String _soapTransport = "http://schemas.xmlsoap.org/soap/http";
  private String _soapStyle = "document";

  private CharArrayWriter _wsdlBuffer = new CharArrayWriter();
  private boolean _wsdlGenerated = false;

  private CharArrayWriter _schemaBuffer = new CharArrayWriter();
  private boolean _schemaGenerated = false;

  private static XMLInputFactory _inputFactory;
  private static XMLOutputFactory _outputFactory;

  private static XMLInputFactory getXMLInputFactory()
    throws XMLStreamException
  {
    if (_inputFactory == null)
      _inputFactory = XMLInputFactory.newInstance();

    return _inputFactory;
  }

  private static XMLOutputFactory getXMLOutputFactory()
    throws XMLStreamException
  {
    if (_outputFactory == null) { 
      _outputFactory = XMLOutputFactory.newInstance();
      _outputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES,
                                 Boolean.TRUE);
    }

    return _outputFactory;
  }

  public DirectSkeleton(Class impl, Class api,
                        JAXBContextImpl context, 
                        String wsdlLocation,
                        String targetNamespace,
                        WSDLDefinitions wsdl)
    throws WebServiceException
  {
    WebService webService = (WebService) impl.getAnnotation(WebService.class);

    _api = api;
    _namespace = targetNamespace;
    _portType = getPortType(impl, api);

    if (webService != null && ! "".equals(webService.portName()))
      _portName = webService.portName();
    else if (webService != null && ! "".equals(webService.name()))
      _portName = webService.name() + "Port";
    else
      _portName = impl.getSimpleName() + "Port";

    if (webService != null && ! "".equals(webService.serviceName()))
      _serviceName = webService.serviceName();
    else
      _serviceName = impl.getSimpleName() + "Service";

    if (webService != null && ! "".equals(webService.wsdlLocation()))
      _wsdlLocation = webService.wsdlLocation();
    else
      _wsdlLocation = wsdlLocation;

    _wsdl = wsdl;

    _bindingId = SOAPBinding.SOAP11HTTP_BINDING;
    
    BindingType bindingType = 
      (BindingType) _api.getAnnotation(BindingType.class);

    if (bindingType != null)
      _bindingId = bindingType.value();

    javax.jws.soap.SOAPBinding soapBinding = 
      (javax.jws.soap.SOAPBinding) 
      _api.getAnnotation(javax.jws.soap.SOAPBinding.class);

    if (soapBinding != null && 
        soapBinding.style() == javax.jws.soap.SOAPBinding.Style.RPC)
      _soapStyle = "rpc";

    _context = context;

    QName portName = new QName(_namespace, _portName);
    QName serviceName = new QName(_namespace, _serviceName);

    _portInfo = new PortInfoImpl(_bindingId, portName, serviceName);

    HandlerChain handlerChain = 
      (HandlerChain) _api.getAnnotation(HandlerChain.class);

    if (handlerChain != null) { 
      HandlerResolver handlerResolver = 
        JAXWSUtil.createHandlerResolver(_api, handlerChain);

      List<Handler> chain = handlerResolver.getHandlerChain(_portInfo);

      if (chain != null)
        _handlerChain = new HandlerChainInvoker(chain);
    }
  }

  public String getWsdlLocation()
  {
    return _wsdlLocation;
  }

  public String getPortName()
  {
    return _portName;
  }

  public String getNamespace()
  {
    return _namespace;
  }

  static String getPortType(Class impl, Class api) 
    throws WebServiceException
  {
    WebService webService = (WebService) impl.getAnnotation(WebService.class);

    if (webService != null) {
      if ("".equals(webService.name()) && 
          "".equals(webService.endpointInterface()))
        return impl.getSimpleName();

      if (! "".equals(webService.name()) && 
          "".equals(webService.endpointInterface()))
        return webService.name();

      if ("".equals(webService.name()) && 
          ! "".equals(webService.endpointInterface())) {
        webService = (WebService) api.getAnnotation(WebService.class);
        
        if (webService != null && ! "".equals(webService.name()))
          return webService.name();

        else
          return api.getSimpleName();
      }

      if (! "".equals(webService.name()) && 
          ! "".equals(webService.endpointInterface()))
        throw new WebServiceException(L.l("Cannot specify both name and endpointInterface properties in a WebService annotation: {0}", impl));
    }

    return impl.getSimpleName();
  }

  public void addAction(Method method, AbstractAction action)
  {
    if (log.isLoggable(Level.FINER))
      log.finer("Adding " + action + " to " + this);

    _actionNames.put(action.getInputName(), action);
    _actionMethods.put(method, action);
  }

  public Object invoke(Method method, String url, Object[] args)
    throws IOException, XMLStreamException, MalformedURLException, 
           JAXBException, Throwable
  {
    return invoke(method, url, args, null);
  }

  /**
   * Invokes the request on a remote object using an outbound XML stream.
   */
  public Object invoke(Method method, String url, Object[] args, 
                       HandlerChainInvoker handlerChain)
    throws IOException, XMLStreamException, MalformedURLException, 
           JAXBException, Throwable
  {
    AbstractAction action = _actionMethods.get(method);

    if (action != null)
      return action.invoke(url, args, handlerChain);
    else if ("toString".equals(method.getName()))
      return "SoapStub[" + (_api != null ? _api.getName() : "") + "]";
    else
      throw new RuntimeException(L.l("not a web method: {0}", 
                                     method.getName()));
  }
  
  /**
   * Invokes the request on a local object using an inbound XML stream.
   */
  public void invoke(Object service, 
                     HttpServletRequest request,
                     HttpServletResponse response)
    throws IOException, XMLStreamException, Throwable
  {
    InputStream is = request.getInputStream();
    OutputStream os = response.getOutputStream();

    XMLStreamReader in = null;
    XMLStreamWriter out = null;
    DOMResult domResult = null;

    String contentType = request.getHeader("Content-Type");

    List<Attachment> attachments = null;

    if (contentType != null && contentType.startsWith("multipart/related"))
      attachments = AttachmentReader.read(is, contentType);

    if (_handlerChain != null) {
      is = _handlerChain.invokeServerInbound(request, os);

      if (is == null)
        return;

      domResult = new DOMResult();

      in = getXMLInputFactory().createXMLStreamReader(is);
      out = getXMLOutputFactory().createXMLStreamWriter(domResult);
    }
    else if (attachments != null && attachments.size() > 0) {
      Attachment body = attachments.get(0);
      ByteArrayInputStream bais = new ByteArrayInputStream(body.getContents());

      in = getXMLInputFactory().createXMLStreamReader(bais);
      out = getXMLOutputFactory().createXMLStreamWriter(os);
    }
    else {
      in = getXMLInputFactory().createXMLStreamReader(is);
      out = getXMLOutputFactory().createXMLStreamWriter(os);
    }

    response.setStatus(invoke(service, in, out, attachments));

    if (_handlerChain != null) {
      Source source = new DOMSource(domResult.getNode());
      _handlerChain.invokeServerOutbound(source, os);
    }
  }

  private int invoke(Object service, XMLStreamReader in, XMLStreamWriter out,
                     List<Attachment> attachments)
    throws IOException, XMLStreamException, Throwable
  {
    in.nextTag();

    // XXX Namespace
    in.require(XMLStreamReader.START_ELEMENT, null, "Envelope");

    in.nextTag();

    XMLStreamReader header = null;

    if ("Header".equals(in.getName().getLocalPart())) {
      in.nextTag();

      XMLOutputFactory outputFactory = getXMLOutputFactory();
      CharArrayWriter writer = new CharArrayWriter();
      StreamResult result = new StreamResult(writer);
      XMLStreamWriter xmlWriter = outputFactory.createXMLStreamWriter(result);

      StaxUtil.copyReaderToWriter(in, xmlWriter);

      CharArrayReader reader = new CharArrayReader(writer.toCharArray());

      XMLInputFactory inputFactory = getXMLInputFactory();
      header = inputFactory.createXMLStreamReader(reader);

      in.nextTag();
    }

    // XXX Namespace?
    in.require(XMLStreamReader.START_ELEMENT, null, "Body");

    in.nextTag();

    String actionName = in.getName().getLocalPart();

    // services/1318: special corner case where no method name is given
    // May happen with Document BARE methods w/no arguments
    if ("Body".equals(actionName) && in.getEventType() == in.END_ELEMENT)
      actionName = "";

    out.writeStartDocument("UTF-8", "1.0");
    out.writeStartElement(SOAP_ENVELOPE_PREFIX, "Envelope", SOAP_ENVELOPE);
    out.writeNamespace(SOAP_ENVELOPE_PREFIX, SOAP_ENVELOPE);
    //out.writeNamespace("xsi", XMLNS_XSI);
    out.writeNamespace("xsd", XMLNS_XSD);

    AbstractAction action = _actionNames.get(actionName);

    // XXX: exceptions<->faults
    int responseCode = 500;

    if (action != null)
      responseCode = action.invoke(service, header, in, out, attachments);

    else {
      // skip the unknown action
      while (in.getEventType() != in.END_ELEMENT ||
             ! "Body".equals(in.getName().getLocalPart()))
        in.nextTag();

      writeClientFault(out);
    }

    // XXX Namespace?
    in.require(XMLStreamReader.END_ELEMENT, null, "Body");
    in.nextTag();
    in.require(XMLStreamReader.END_ELEMENT, null, "Envelope");

    out.writeEndElement(); // Envelope

    out.flush();

    return responseCode;
  }

  public void setSeparateSchema(boolean separateSchema) 
  {
    if (_separateSchema != separateSchema) {
      _separateSchema = separateSchema;
      _wsdlGenerated = false;
    }
  }

  public void dumpWSDL(OutputStream os)
    throws IOException, XMLStreamException, JAXBException
  {
    OutputStreamWriter out = null;

    try {
      out = new OutputStreamWriter(os);
      dumpWSDL(out);
    }
    finally {
      if (out != null)
        out.close();
    }
  }

  public void dumpWSDL(Writer w)
    throws IOException, XMLStreamException, JAXBException
  {
    generateWSDL();
    _wsdlBuffer.writeTo(w);
  }

  /**
   * To be accurate, all of the actions must have been added before this
   * method is run for the first time.
   **/
  public void generateWSDL()
    throws IOException, XMLStreamException, JAXBException
  {
    if (_wsdlGenerated)
      return;

    // We write to DOM so that we can pretty print it.  Since this only
    // happens once, it's not too much of a burden.
    DOMResult result = new DOMResult();
    XMLOutputFactory factory = getXMLOutputFactory();
    XMLStreamWriter out = factory.createXMLStreamWriter(result);

    out.writeStartDocument("UTF-8", "1.0");

    // <definitions>

    out.setDefaultNamespace(WSDL_NAMESPACE);
    out.writeStartElement(WSDL_NAMESPACE, "definitions");
    out.writeAttribute("targetNamespace", _namespace);
    out.writeAttribute("name", _serviceName);
    out.writeNamespace(TARGET_NAMESPACE_PREFIX, _namespace);
    out.writeNamespace("soap", _soapNamespaceURI);

    // <types>
    
    out.writeStartElement(WSDL_NAMESPACE, "types");

    if (_separateSchema) {
      out.writeStartElement(W3C_XML_SCHEMA_NS_URI, "schema");

      out.writeEmptyElement(W3C_XML_SCHEMA_NS_URI, "import");
      out.writeAttribute("namespace", _namespace);
      out.writeAttribute("schemaLocation",  _serviceName + "_schema1.xsd");

      out.writeEndElement(); // schema 
    }
    else
      writeSchema(out);

    out.writeEndElement(); // types

    // <messages>

    for (AbstractAction action : _actionNames.values())
      action.writeWSDLMessages(out, _soapNamespaceURI);

    // <portType>

    out.writeStartElement(WSDL_NAMESPACE, "portType");
    out.writeAttribute("name", _portType);

    for (AbstractAction action : _actionNames.values())
      action.writeWSDLOperation(out, _soapNamespaceURI);

    out.writeEndElement(); // portType

    // <binding>

    out.writeStartElement(WSDL_NAMESPACE, "binding");
    out.writeAttribute("name", _portName + "Binding");
    out.writeAttribute("type", TARGET_NAMESPACE_PREFIX + ':' + _portType);

    out.writeEmptyElement(_soapNamespaceURI, "binding");
    out.writeAttribute("transport", _soapTransport);
    out.writeAttribute("style", _soapStyle);

    for (AbstractAction action : _actionNames.values())
      action.writeWSDLBindingOperation(out, _soapNamespaceURI);

    out.writeEndElement(); // binding

    // <service>

    out.writeStartElement(WSDL_NAMESPACE, "service");
    out.writeAttribute("name", _serviceName);

    out.writeStartElement(WSDL_NAMESPACE, "port");
    out.writeAttribute("name", _portName);
    out.writeAttribute("binding",
                       TARGET_NAMESPACE_PREFIX + ':' + _portName + "Binding");

    out.writeEmptyElement(_soapNamespaceURI, "address");
    out.writeAttribute("location", _wsdlLocation);

    out.writeEndElement(); // port

    out.writeEndElement(); // service 

    out.writeEndElement(); // definitions

    _wsdlBuffer = new CharArrayWriter();

    XmlPrinter printer = new XmlPrinter(_wsdlBuffer);
    printer.setPrintDeclaration(true);
    printer.setStandalone("true");
    printer.printPrettyXml(result.getNode());
    
    _wsdlGenerated = true;
  }

  public void dumpSchema(OutputStream os)
    throws IOException, XMLStreamException, JAXBException
  {
    OutputStreamWriter out = null;

    try {
      out = new OutputStreamWriter(os);
      dumpSchema(out);
    }
    finally {
      if (out != null)
        out.close();
    }
  }

  public void dumpSchema(Writer w)
    throws IOException, XMLStreamException, JAXBException
  {
    generateSchema();
    _schemaBuffer.writeTo(w);
  }

  public void generateSchema()
    throws IOException, XMLStreamException, JAXBException
  {
    if (_schemaGenerated)
      return;

    // We write to DOM so that we can pretty print it.  Since this only
    // happens once, it's not too much of a burden.
    DOMResult result = new DOMResult();
    XMLOutputFactory factory = getXMLOutputFactory();
    XMLStreamWriter out = factory.createXMLStreamWriter(result);

    out.writeStartDocument("UTF-8", "1.0");

    writeSchema(out);

    _schemaBuffer = new CharArrayWriter();

    XmlPrinter printer = new XmlPrinter(_schemaBuffer);
    printer.setPrintDeclaration(true);
    printer.setStandalone("true");
    printer.printPrettyXml(result.getNode());
    
    _schemaGenerated = true;
  }

  public void writeSchema(XMLStreamWriter out)
    throws XMLStreamException, JAXBException
  {
    out.writeStartElement("xsd", "schema", W3C_XML_SCHEMA_NS_URI);
    out.writeAttribute("version", "1.0");
    out.writeAttribute("targetNamespace", _namespace);
    out.writeNamespace(TARGET_NAMESPACE_PREFIX, _namespace);

    _context.generateSchemaWithoutHeader(out);

    for (AbstractAction action : _actionNames.values())
      action.writeSchema(out, _namespace, _context);

    out.writeEndElement(); // schema

    out.flush();
  }

  /**
   * Dumps a WSDL into the specified directory using the service name
   * annotation if present.  (Mainly for TCK, wsgen)
   */
  public void dumpWSDL(String dir)
    throws IOException, XMLStreamException, JAXBException
  {
    FileWriter wsdlOut = null;
    FileWriter xsdOut = null;
    
    try {
      wsdlOut = new FileWriter(new File(dir, _serviceName + ".wsdl"));
      dumpWSDL(wsdlOut);

      if (_separateSchema) {
        xsdOut = new FileWriter(new File(dir, _serviceName + "_schema1.xsd"));
        dumpSchema(xsdOut);
      }
    }
    finally {
      if (wsdlOut != null)
        wsdlOut.close();

      if (xsdOut != null)
        xsdOut.close();
    }
  }

  public String toString()
  {
    return "DirectSkeleton[" + _api + "]";
  }

  private void writeClientFault(XMLStreamWriter out)
    throws IOException, XMLStreamException, JAXBException
  {
    out.writeStartElement(SOAP_ENVELOPE_PREFIX, "Body", SOAP_ENVELOPE);
    out.writeStartElement(Skeleton.SOAP_ENVELOPE_PREFIX, 
                          "Fault", 
                          Skeleton.SOAP_ENVELOPE);

    out.writeStartElement("faultcode");
    out.writeCharacters(Skeleton.SOAP_ENVELOPE_PREFIX + ":Client");
    out.writeEndElement(); // faultcode

    out.writeEndElement(); // Fault
    out.writeEndElement(); // Body
  }
}
