/* 
 * GnomeTransformer.java
 * Copyright (C) 2003, 2004 Free Software Foundation, Inc.
 * 
 * This file is part of GNU Classpathx/jaxp.
 * 
 * GNU Classpath 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, or (at your option)
 * any later version.
 *  
 * GNU Classpath 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
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with GNU Classpath; see the file COPYING.  If not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
 * 02111-1307 USA.
 * 
 * Linking this library statically or dynamically with other modules is
 * making a combined work based on this library.  Thus, the terms and
 * conditions of the GNU General Public License cover the whole
 * combination.
 * 
 * As a special exception, the copyright holders of this library give you
 * permission to link this library with independent modules to produce an
 * executable, regardless of the license terms of these independent
 * modules, and to copy and distribute the resulting executable under
 * terms of your choice, provided that you also meet, for each linked
 * independent module, the terms and conditions of the license of that
 * module.  An independent module is a module which is not derived from
 * or based on this library.  If you modify this library, you may extend
 * this exception to your version of the library, but you are not
 * obligated to do so.  If you do not wish to do so, delete this
 * exception statement from your version.
 */

package gnu.xml.libxmlj.transform;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PushbackInputStream;

import java.net.URL;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import javax.xml.transform.ErrorListener;
import javax.xml.transform.Source;
import javax.xml.transform.SourceLocator;
import javax.xml.transform.Result;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;

import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Node;

import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;

import gnu.xml.libxmlj.dom.GnomeDocument;
import gnu.xml.libxmlj.sax.GnomeXMLReader;
import gnu.xml.libxmlj.util.NamedInputStream;
import gnu.xml.libxmlj.util.StandaloneLocator;
import gnu.xml.libxmlj.util.XMLJ;

/**
 * An implementation of {@link javax.xml.transform.Transformer} which
 * performs XSLT transformation using <code>libxslt</code>.
 *
 * @author Julian Scheid
 * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
 */
public class GnomeTransformer
  extends Transformer
  implements Templates
{

  /**
   * The parameters added by the user via {@link setParameter()}.
   */
  private Map parameters;

  /**
   * The output properties set by the user.
   */
  private Properties outputProperties;

  /**
   * The URI resolver to use during transformation.
   */
  private URIResolver resolver;

  /**
   * The error listener for transformation errors.
   */
  private ErrorListener errorListener;

  /**
   * Handle to the source stylesheet.
   * This is a native pointer of type xsltStylesheetPtr.
   */
  private Object stylesheet;

  /**
   * Constructor.
   * @param source the XSLT stylesheet document source
   * @param resolver the resolver to use during transformation
   * @param errorListener the error listener for transformation errors
   */
  GnomeTransformer (Source source,
                    URIResolver resolver, 
                    ErrorListener errorListener) 
    throws TransformerConfigurationException
  {
    this.resolver = resolver;
    this.errorListener = errorListener;
    parameters = new HashMap ();
    outputProperties = new Properties ();
   
    if (source == null)
      {
        stylesheet = newStylesheet ();
      } 
    else if (source instanceof StreamSource)
      {
        try
          {
            StreamSource ss = (StreamSource) source;
            NamedInputStream in = XMLJ.getInputStream (ss);
            String systemId = ss.getSystemId ();
            String publicId = ss.getPublicId ();
            String base = XMLJ.getBaseURI (systemId);
            byte[] detectBuffer = in.getDetectBuffer ();
            if (detectBuffer == null)
              {
                String msg = "No document element";
                throw new TransformerConfigurationException (msg);
              }
            stylesheet = newStylesheetFromStream (in, detectBuffer, publicId,
                                                  systemId, base,
                                                  (resolver != null),
                                                  (errorListener != null));
          }
        catch (IOException e)
          {
            throw new TransformerConfigurationException (e);
          }
      }
    else if (source instanceof DOMSource)
      {
        DOMSource ds = (DOMSource) source;
        Node node = ds.getNode ();
        if (!(node instanceof GnomeDocument))
          {
            String msg = "Node is not a GnomeDocument";
            throw new TransformerConfigurationException (msg);
          }
        GnomeDocument doc = (GnomeDocument) node;
        stylesheet = newStylesheetFromDoc (doc);
      }
    else
      {
        String msg = "Source type not supported (" + source + ")";
        throw new TransformerConfigurationException (msg);
      }
  }
  
  /**
   * Copy constructor.
   */
  private GnomeTransformer (Object stylesheet,
                            URIResolver resolver, 
                            ErrorListener errorListener,
                            Map parameters,
                            Properties outputProperties)
  {
    this.stylesheet = stylesheet;
    this.resolver = resolver;
    this.errorListener = errorListener;
    this.parameters = parameters;
    this.outputProperties = outputProperties;
  }

  private native Object newStylesheet ()
    throws TransformerConfigurationException;

  private native Object newStylesheetFromStream (InputStream in,
                                                 byte[] detectBuffer,
                                                 String publicId,
                                                 String systemId,
                                                 String base,
                                                 boolean entityResolver,
                                                 boolean errorHandler)
    throws TransformerConfigurationException;

  private native Object newStylesheetFromDoc (GnomeDocument doc)
    throws TransformerConfigurationException;

  //--- Implementation of javax.xml.transform.Transformer follows.

  // Set, get and clear the parameters to use on transformation

  public synchronized void setParameter (String parameter, Object value)
  {
    parameters.put (parameter, value);
  } 

  public synchronized Object getParameter (String name)
  {
    return parameters.get (name);
  }

  public synchronized void clearParameters ()
  {
    parameters.clear ();
  }

  // Set and get the ErrorListener to use on transformation

  public void setErrorListener (ErrorListener listener)
  {
    this.errorListener = listener;
  } 

  public ErrorListener getErrorListener ()
  {
    return errorListener;
  }

  // Set and get the URIResolver to use on transformation

  public void setURIResolver (URIResolver resolver)
  {
    this.resolver = resolver;
  } 

  public URIResolver getURIResolver ()
  {
    return resolver;
  }

  // Set the output properties to use on transformation; get default
  // output properties and output properties specified in the
  // stylesheet or by the user.

  public void setOutputProperties (Properties outputProperties)
  {
    // Note: defensive copying
    this.outputProperties = new Properties (outputProperties);
  } 

  public void setOutputProperty (String name, String value)
  {
    outputProperties.setProperty (name, value);
  } 

  public Properties getOutputProperties ()
  {
    // Note: defensive copying
    return new Properties (this.outputProperties);
  }

  public String getOutputProperty (String name)
  {
    return outputProperties.getProperty (name);
  }

  // -- Templates --

  public Transformer newTransformer ()
  {
    return new GnomeTransformer (stylesheet, resolver, errorListener,
                                 new HashMap (parameters),
                                 new Properties (outputProperties));
  }

  // -- transform --

  /**
   * Transforms the given source and writes the result to the
   * given target.
   */
  public void transform (Source source, Result result)
    throws TransformerException
  {
    if (source instanceof StreamSource)
      {
        try
          {
            StreamSource ss = (StreamSource) source;
            NamedInputStream in = XMLJ.getInputStream (ss);
            String publicId = ss.getPublicId ();
            String systemId = ss.getSystemId ();
            String base = XMLJ.getBaseURI (systemId);
            byte[] detectBuffer = in.getDetectBuffer ();
            if (detectBuffer == null)
              {
                throw new TransformerException ("No document element");
              }
            if (result instanceof StreamResult)
              {
                OutputStream out = XMLJ.getOutputStream ((StreamResult) result);
                transformStreamToStream (in, detectBuffer, publicId, systemId,
                                         base, (resolver != null),
                                         (errorListener != null), out);
              }
            else if (result instanceof DOMResult)
              {
                DOMResult dr = (DOMResult) result;
                GnomeDocument ret =
                  transformStreamToDoc (in, detectBuffer, publicId, systemId,
                                        base, (resolver != null),
                                        (errorListener != null));
                dr.setNode (ret);
                dr.setSystemId (null);
              }
            else if (result instanceof SAXResult)
              {
                SAXResult sr = (SAXResult) result;
                transformStreamToSAX (in, detectBuffer, publicId, systemId,
                                      base, (resolver != null),
                                      (errorListener != null),
                                      getSAXContext (sr));
              }
            else
              {
                String msg = "Result type not supported (" + result + ")";
                throw new TransformerConfigurationException (msg);
              }
          }
        catch (IOException e)
          {
            throw new TransformerException (e);
          }
      }
    else if (source instanceof DOMSource)
      {
        DOMSource ds = (DOMSource) source;
        Node node = ds.getNode ();
        if (!(node instanceof GnomeDocument))
          {
            String msg = "Node is not a GnomeDocument (" + node + ")";
            throw new TransformerException (msg);
          }
        GnomeDocument doc = (GnomeDocument) node;
        if (result instanceof StreamResult)
          {
            try
              {
                OutputStream out = XMLJ.getOutputStream ((StreamResult) result);
                transformDocToStream (doc, out);
              }
            catch (IOException e)
              {
                throw new TransformerException (e);
              }
          }
        else if (result instanceof DOMResult)
          {
            DOMResult dr = (DOMResult) result;
            GnomeDocument ret = transformDocToDoc (doc);
            dr.setNode (ret);
            dr.setSystemId (null);
          }
        else if (result instanceof SAXResult)
          {
            SAXResult sr = (SAXResult) result;
            transformDocToSAX (doc, getSAXContext (sr));
          }
        else
          {
            String msg = "Result type not supported";
            throw new TransformerConfigurationException (msg);
          }
      }
    else
      {
        String msg = "Source type not supported";
        throw new TransformerConfigurationException (msg);
      }
  }

  private GnomeXMLReader getSAXContext (SAXResult result)
  {
    GnomeXMLReader ctx = new GnomeXMLReader ();
    ctx.setContentHandler (result.getHandler ());
    ctx.setLexicalHandler (result.getLexicalHandler ());
    if (errorListener != null)
      {
        ErrorHandler errorHandler =
          new ErrorListenerErrorHandler (errorListener);
        ctx.setErrorHandler (errorHandler);
      }
    if (resolver != null)
      {
        EntityResolver entityResolver =
          new URIResolverEntityResolver (resolver);
        ctx.setEntityResolver (entityResolver);
      }
    return ctx;
  }

  private native void transformStreamToStream (InputStream in,
                                               byte[] detectBuffer,
                                               String publicId,
                                               String systemId,
                                               String base,
                                               boolean entityResolver,
                                               boolean errorHandler,
                                               OutputStream out)
    throws TransformerException;

  private native GnomeDocument transformStreamToDoc (InputStream in,
                                                     byte[] detectBuffer,
                                                     String publicId,
                                                     String systemId,
                                                     String base,
                                                     boolean entityResolver,
                                                     boolean errorHandler)
    throws TransformerException;

  private native void transformStreamToSAX (InputStream in,
                                            byte[] detectBuffer,
                                            String publicId,
                                            String systemId,
                                            String base,
                                            boolean entityResolver,
                                            boolean errorHandler,
                                            GnomeXMLReader out)
    throws TransformerException;

  private native void transformDocToStream (GnomeDocument in,
                                            OutputStream out)
    throws TransformerException;

  private native GnomeDocument transformDocToDoc (GnomeDocument in)
    throws TransformerException;

  private native void transformDocToSAX (GnomeDocument in,
                                         GnomeXMLReader out)
    throws TransformerException;

  /*
   * Retrieve parameters as a string array.
   * This is a convenience method called from native code.
   */
  private String[] getParameterArray ()
  {
    String[] parameterArray = new String[parameters.size () * 2];
    int index = 0;
    for (Iterator it = parameters.keySet ().iterator ();
         it.hasNext ();
         ++index)
      {
        String parameterKey = (String) it.next ();
        String parameterValue = (String) parameters.get (parameterKey);
        parameterArray[index * 2 + 0] = parameterKey;
        parameterArray[index * 2 + 1] =
          "'" + ((parameterValue != null) ? parameterValue : "") + "'";
        // FIXME encode parameter value correctly for XPath
      }
    return parameterArray;
  }

  // -- Free xsltStylesheet handle --

  public void finalize ()
  {
    if (stylesheet != null)
      {
        free ();
        stylesheet = null;
      }
  }

  private native void free ();

  // -- Callbacks --

  private InputStream resolveEntity (String publicId, String systemId)
    throws TransformerException
  {
    if (resolver != null)
      {
        systemId = resolver.resolve (null, systemId).getSystemId ();
      }
    if (systemId == null)
      {
        return null;
      }
    try
      {
        URL url = new URL (systemId);
        return XMLJ.getInputStream (url);
      }
    catch (IOException e)
      {
        throw new TransformerException (e);
      }
  }
  
  private void setDocumentLocator (Object ctx, Object loc)
  {
  }

  private void warning (String message,
                        int lineNumber,
                        int columnNumber,
                        String publicId,
                        String systemId)
    throws TransformerException
  {
    if (errorListener == null)
      {
        return;
      }
    SourceLocator l = new StandaloneLocator (lineNumber,
                                             columnNumber,
                                             publicId,
                                             systemId);
    errorListener.warning (new TransformerException (message, l));
  }

  private void error (String message,
                      int lineNumber,
                      int columnNumber,
                      String publicId,
                      String systemId)
    throws TransformerException
  {
    if (errorListener == null)
      {
        return;
      }
    SourceLocator l = new StandaloneLocator (lineNumber,
                                             columnNumber,
                                             publicId,
                                             systemId);
    errorListener.error (new TransformerException (message, l));
  }
  
  private void fatalError (String message,
                           int lineNumber,
                           int columnNumber,
                           String publicId,
                           String systemId)
    throws TransformerException
  {
    if (errorListener == null)
      {
        return;
      }
    SourceLocator l = new StandaloneLocator (lineNumber,
                                             columnNumber,
                                             publicId,
                                             systemId);
    errorListener.fatalError (new TransformerException (message, l));
  }
  
}
