/*
 * Please read the License Agreement and other supplemental license terms, if
 * any, accompanying this software package before using any of the software
 * provided.  If you do not agree to the terms of the License Agreement or any
 * other supplemental terms, please promptly destroy or return the software to
 * Sun Microsystems, Inc.
 *
 * "CONFIDENTIAL AND PROPRIETARY" Copyright \251 2003 Sun Microsystems, Inc.
 * All rights reserved.
 */

package com.sun.portal.providers.xml;

import com.iplanet.am.util.FileLookup;
import com.iplanet.am.util.FileLookupException;

import com.sun.portal.providers.urlscraper.URLScraperProvider;
import com.sun.portal.providers.ProviderException;
import com.sun.portal.providers.context.ProviderContext;
import com.sun.portal.providers.context.ProviderContextException;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerConfigurationException;

import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.stream.StreamResult;

import java.io.File;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.io.BufferedWriter;
import java.io.BufferedReader;
import java.io.StringReader;
import java.io.FileReader;
import java.io.StringWriter;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;

import java.util.Map;
import java.util.HashMap;
import java.util.ResourceBundle;
import java.util.StringTokenizer;

import java.net.MalformedURLException;
import java.net.URL;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * <P> A XML Provider is used to convert and display the XML file according to
 * the specified style sheet.
 *
 * <P> XMLProvider is an extension of the existing URLScraperProvider. It uses
 * the URLScraperProvider to fetch the XML contents when the url is of type
 * <b> http://url or https://url </b> and if the url is of type <b> file:////
 * </b>, the XMLProvider manually reads the contents of the specified file .
 *
 * <P> The XMLProvider can transform generic XML content to specific markups
 * using an XSLT engine. It allows creating multiple markup channels from a
 * single XML source (local or over http/https)
 *
 * <P> XML Provider requires two arguments to do the transformation.
 * <UL>
 * <LI> The XML file that needs to the transformed
 * <LI> The stylesheet to be used to do the transformation.
 * </UL>
 * <P> The XMLProvider-based channel has the following configurable attributes: 
 * <UL>
 * <LI> url - the XML attribute used to store the path to the XML source
 * <LI> xslFileName - the XML attribute used to store the path to the XSL 
 * style sheet. 
 * </UL> 
 *
 * <P> The path to the XML content can be specified as HTTP,HTTPs or file URL. 
 * If the path is HTTP or HTTPs the provider uses URLScraper provider to fetch
 * the contents. If the path is a file URL, the provider reads the file into a
 * StringBuffer.
 * 
 * <P> The value for the XSL filename to use for transformation is stored as
 * a value of the string attribute as indicated above. The user can either 
 * specify the complete path (including the file name) or can specify just the 
 * file name in which case it will be picked from the default directory. While 
 * specifying a value for this property there should not be any use of the 
 * protocols like "file://" or "http://" or "https://"
 *
 *
 * <P>If both the XML file and the XSL file are present, the XMLProvider does
 * the transformation using the XSLT engine. The generated contents are 
 * displayed in the channel. In order to do the conversion the XMLProvider uses 
 * the JAXP1.1 jar files.
 * <P> NOTE: <CODE>getEdit()</CODE> and&nbsp; <CODE>processEdit()</CODE> methods
 * are  not implemented in the XMLProvider.
 ***/

public class XMLProvider extends URLScraperProvider {

    private ResourceBundle bundle = null;
    private static Map xslTransformerMap = new HashMap(10);
    private static final String EMPTY_XSL_FILENAME = "emptyXslFileName";
    private static final String XML_FETCH_ERROR = "xmlFetchError";
    private static final String XML_TRANSFOMING_ERROR = "xmlTransformingError";

    private static final String DEFAULT_TRANSFORMER_FACTORY = "org.apache.xalan.xsltc.trax.TransformerFactoryImpl";
    /**
    * <P> Gets the XSL file object.
    * @returns the XSL file object
    * @exception ProviderException if the xsl url specified in the storage
    * is not a valid url.
    */
     protected File getXSL(String xslFileName) throws ProviderException {

            File xslFile = null;

	    if (getProviderContext().isDebugMessageEnabled()) {
            	getProviderContext().debugMessage("XMLProvider.getXSL(): XSL File Name  = "+ xslFileName);
	    }
            if ((xslFileName == null) || (xslFileName.equals(""))) { 
                return null;
            } else if(getProviderContext().isDebugMessageEnabled()) { 
                getProviderContext().debugMessage("XMLProvider.getXSL(): XSL File Name  = "+ xslFileName);
            }

        /*********************************************************
         * This block of code is not portable to win2k and is being rewritten
        // Check if it is an URL
        if (xslFileName.indexOf(':') == -1){
            // So it's either abs. pathname or just xsl filename.
            // In the 2nd case we will use the FileLookup API to
            // get the xsl file.

            if((xslFileName.charAt(0)) == '/') {
                xslFile = super.getFile(xslFileName);
            } else {
        // Now get the xsl file
                try {
            xslFile = getProviderContext().getTemplatePath(getName(), xslFileName);
                } catch (ProviderContextException pce ) {
                    throw new ProviderException( "XMLProvider.getXSL(): ", pce );
                }
        if ((xslFile != null) && (getProviderContext().isDebugMessageEnabled())) {
        getProviderContext().debugMessage("XMLProvider.getXSL: the file:" + xslFile.getName());
        }
            }
     return xslFile;
        } else {
            throw new ProviderException("XMLProvider.getXSL(): Unsupported URL type ");
        }
        *********************************************************/

            //Assume abs. pathname
            xslFile = super.getFile(xslFileName);
            if(xslFile == null){
                //Now assume that it is just xsl filename.
                //we will use the FileLookup API to
                // get the xsl file
                try {
                    xslFile = getProviderContext().getTemplatePath(getName(), xslFileName);
                } catch (ProviderContextException pce ) {
                    throw new ProviderException( "XMLProvider.getXSL(): ", pce );
                }
            }
            if ((xslFile != null) && (getProviderContext().isDebugMessageEnabled())) {
                    getProviderContext().debugMessage("XMLProvider.getXSL: the file:" + xslFile.getName());
            }
            if(xslFile == null){
                try{
                    URL xslFileUrl = new URL(xslFileName);
                    throw new ProviderException("XMLProvider.getXSL(): Unsupported URL type :" + xslFileUrl.getProtocol());
                } catch( MalformedURLException mex){}
            }
            return xslFile;
    	}
 
    /**
    * <P><P>Gets the XML file as a string buffer. 
    *
    * <P>This method calls the <code>getHttpContent</code> in class 
    * URLScraperProvider to get the XML file content as a StringBuffer
    * if the XML URL specified is a http or https url.
    *
    * @param req        An HttpServletRequest that contains information related
    *                   to this request for content.
    * @param res        An HttpServletResponse that allows the provider to
    *                   influence the overall response for the desktop page
    *                   (besides generating the content).
    * @return the XML file contents as a buffer
    * @exception ProviderException if the xml URL specified in the storage
    * is not a valid URL.
    * @see com.sun.portal.providers.urlscraper.URLScraperProvider#getHttpContent
    */
    protected StringBuffer getXML(HttpServletRequest req, HttpServletResponse res) throws ProviderException {

        String xmlURL = getURL();

        StringBuffer content = new StringBuffer();
        String proto = null;
        try {
            proto = xmlURL.substring(0, xmlURL.indexOf(':'));
        }
        catch (IndexOutOfBoundsException iobe) {
        }

        if (proto != null && proto.equalsIgnoreCase("file")) {
            String PathName = null;
            try {
                PathName = xmlURL.substring(xmlURL.indexOf('/'));
            }
            catch (IndexOutOfBoundsException iobe) {
                getProviderContext().debugError("XMLProvider.getXML(): Unsupported URL type : " 
					+ xmlURL, iobe);
                throw new ProviderException("XMLProvider.getXML():Getting XML Content failed. " +
					" Unsupported URL type : " + xmlURL, iobe);
            }
            try {
                content = getFileAsBuffer(PathName);
            } catch (UnsupportedEncodingException ue) {
                getProviderContext().debugError("XMLProvider.getContent(): UnsupportedEncoding specified : " 
					+ PathName, ue);
                throw new ProviderException("XMLProvider.getXML():Getting XML Content failed. " + 
					" UnsupportedEncoding specified : " + PathName, ue);
            } catch (IOException ioe) {
                getProviderContext().debugError("XMLProvider.getContent():IOException received : " + PathName, ioe);
                throw new ProviderException("XMLProvider.getXML():Getting XML Content failed. " +
					" IOException received : " + PathName, ioe);
            }
        } else {

            try {
                content = getHttpContent(req, res, xmlURL);
            } catch ( InterruptedException ie) {
                getProviderContext().debugError
                    ("XMLProvider.getXML(): " +
                                "fetcher did not finish : " + xmlURL, ie);
                throw new ProviderException("XMLProvider.getXML():Getting XML Content failed" +
					" fetcher did not finish : " + xmlURL, ie);

            } catch (MalformedURLException mue) {
                getProviderContext().debugError
                    ("XMLProvider.getXML(). Malformed URL : " + xmlURL, mue);
                throw new ProviderException("XMLProvider.getXML():Getting XML Content failed"
					+ " Malformed URL : " + xmlURL, mue); 
            }
        }
        return content;
    }

    /*
     * <P> Method that gets the XML File and the XSL file and which does the 
     * actual transformation
     *
     * @param XMLFile  - XML file to read 
     * @param xslFile  - XSL stylesheet for conversion
     *
     * @return StringBuffer containing the transformed XML data
     * 
     */

    private synchronized StringBuffer doTransform(StringBuffer XMLFile, String xslFileName)
        throws ProviderException {

        StringBuffer XformedContent = null;
	Transformer xslTransformer = null;
        
	try{
	    xslTransformer = getXslTransformer(xslFileName);
	} catch(TransformerConfigurationException tce) {
            getProviderContext().debugError("TransformerWrapperImpl.doTransform: " + xslFileName, tce);
	} catch(TransformerException te) {
            getProviderContext().debugError("TransformerWrapperImpl.doTransform: " + xslFileName, te);
	}

	if(xslTransformer != null) {
	    StreamSource xmlSource = new StreamSource(new StringReader(XMLFile.toString()));
	    
	    StringWriter contentWriter = new StringWriter();
	    StreamResult contentResult = new StreamResult(contentWriter);
	    
	    try {
		xslTransformer.transform(xmlSource,contentResult);
	    } catch(TransformerException te) {
		getProviderContext().debugError("XMLProvider.doTransform():"+
					      "Error in transforming xml : " + XMLFile + " using XSL file : " + xslFileName,te);
		throw new ProviderException("XMLProvider.doTransform():"+
					    "Error in transforming xml : " + XMLFile + " using XSL file : " + xslFileName);
	    }	    
	    XformedContent = contentWriter.getBuffer();
	} else {
	    getProviderContext().debugError("XMLProvider.doTransform():"+
					  "Error in getting XSL transformer to transform xml : " + XMLFile +
					   " using XSL file : " + xslFileName);
	    throw new ProviderException("XMLProvider.doTransform():"+
					" Error in getting XSL transformer to transform xml : " + XMLFile +
					   " using XSL file : " + xslFileName);
	}
	return XformedContent;
    }


    /**
    * <P><P>Gets and displays the XML file contents after converting the 
    * xml file according to the specified XSL stylesheet
    *
    * @param req        An HttpServletRequest that contains information related
    *                   to this request for content.
    * @param res        An HttpServletResponse that allows the provider to
    *                   influence the overall response for the desktop page
    *                   (besides generating the content).
    * @return 		Transformed XML contents.
    * @exception ProviderException
    */
    public StringBuffer getContent(HttpServletRequest req,
                                   HttpServletResponse res)
        throws ProviderException
    {
	

        StringBuffer content = new StringBuffer();
	String xslFileName   = getStringProperty("xslFileName");
	File   xslFile       = null;
	String xslFilePath   = null;       
	try {
	    xslFile = getXSL(xslFileName);
	} catch ( ProviderException xe) {
	    getProviderContext().debugError("XMLProvider.getContent():"+
					"Can not get XSL File : " + xslFileName, xe);
	}
	if( xslFile != null) {
	  xslFilePath = xslFile.getPath();
	}
	bundle = getResourceBundle();

	if ((xslFilePath == null) || (xslFilePath.equals(""))) { 
	    content = content.append(bundle.getString(EMPTY_XSL_FILENAME));
	    return content;
	}
	
        // Get the XML File
        StringBuffer Xmlfile = new StringBuffer();
        try {
            Xmlfile = getXML(req,res);
        } catch (ProviderException xe) {
            getProviderContext().debugError("XMLProvider.getContent():"+
					  "Can not get XML file from request", xe);
	    content = content.append(bundle.getString(XML_FETCH_ERROR));
	    return content;
        }
        if( Xmlfile == null ) {
            getProviderContext().debugError("XMLProvider.getContent(): Can not get XML File from request");
	    content = content.append(bundle.getString(XML_FETCH_ERROR));
            return content;
        } else {
            if(getProviderContext().isDebugMessageEnabled()) {
		getProviderContext().debugMessage("XMLProvider.getContent(): Got XML from request" );
	  }
	}
	try {
	    content = doTransform(Xmlfile, xslFilePath);	 
	} catch (ProviderException p) {
	    getProviderContext().debugError("XMLProvider.getContent():"+
					  "Error in XML transformation using XSL file : " + xslFileName, p);
	    content = content.append(bundle.getString(XML_TRANSFOMING_ERROR));
	}
        return content;
    }

    /**
     * Gets the charset from content
     *
     * This method determines the charset from the
     * XML Header
     * @param contentBytes Bytes from the scraped content
     * @return String charset or null if charset cannot be determined
     */
    protected String getContentEncodingFromContentBytes(byte[] contentBytes) {
        String charset = null;
        /* The character encoding info was not found in the inputEncoding
        * property. We have to parse through the content portion to
        * figure it out. It may be specified in the xml header
        * as the following;
        * 
        * <?xml version="1.0" encoding="utf-8" standalone="no"?>
        *  ...
        */
        int count = 1000;
        int len = contentBytes.length;
        if (len < count) {
            count = len;
        }
        String contentString = new String(contentBytes, 0, count);
        String str = contentString.toLowerCase();
        int start = str.indexOf("<?xml");
        if (start != -1) {
            int end = str.indexOf(">", start);
            if (end != -1) {
                String headerstr = contentString.substring(start, end);
                String header = headerstr.toLowerCase();
                int encoding = header.indexOf("encoding=");
                if (encoding != -1) {
                    int startencoding = header.indexOf("\"", encoding);
                    int endencoding = header.indexOf("\"", startencoding+1);
                    charset = headerstr.substring(startencoding+1, endencoding);
                }
            }
        }
        return charset;
    }

    private Transformer getXslTransformer(String xslFileName) 
	throws TransformerConfigurationException, TransformerException {

        ProviderContext pc = getProviderContext();

        // org.apache.xalan.processor.TransformerFactoryImpl.
        // org.apache.xalan.xsltc.trax.TransformerFactoryImpl.
        String transformerFactoryClassName = DEFAULT_TRANSFORMER_FACTORY;

        try {
            if( existsStringProperty( "transformerFactoryClassName" ) ) {
                transformerFactoryClassName = getStringProperty( "transformerFactoryClassName" );
                if( transformerFactoryClassName == null ||
                    transformerFactoryClassName.equals( "" ) ) {
                    transformerFactoryClassName = DEFAULT_TRANSFORMER_FACTORY;
                }
            }
        } catch( ProviderException pe ) {
            pc.debugWarning( "XMLProvider.getXslTransformer():", pe );
        }

        StringBuffer sb = new StringBuffer( xslFileName );
        sb.append( "|" );
        sb.append( transformerFactoryClassName );
        String cacheKey = sb.toString();

	// check if the Transformer object is already in cache 

	CacheEntry cacheEntry = (CacheEntry) xslTransformerMap.get(cacheKey);

	if (cacheEntry == null) {
	    // there is no cached Transformer for this xslFileName
	    File xslFile = null;
	    cacheEntry = new CacheEntry();
	    try {
		xslFile = getXSL(xslFileName);
		if (xslFile == null) {
		    pc.debugError("XMLProvider.getXslTransformer(): "+
						  "Getting XSL File " + xslFileName + " failed");
		    return null;
		}
	    } catch(ProviderException xpe) {
		pc.debugError("XMLProvider.getXslTransformer():"+
					      "Error in getting the XSLFile " + xslFileName, 
					      xpe);
		return null;
	    }
	    // set xslFile in CachedEntry and initialize lastModTime to -1 
	    // for this newly created CachedEntry
	    cacheEntry.xslFile = xslFile;
	    cacheEntry.lastModTime = -1;
	    synchronized(xslTransformerMap) {
		xslTransformerMap.put(cacheKey, cacheEntry);
	    }
	}
	
	if ((cacheEntry.lastModTime == -1) ||
	    (cacheEntry.lastModTime < cacheEntry.xslFile.lastModified())) {
	    // 1) the cached Transformer object is newly created OR
	    // 2) the cached Transformer object is outdated 
	    TransformerFactory factory = null;

            try {
                Class transformerFactoryClass = Class.forName( transformerFactoryClassName );
                factory = (TransformerFactory)transformerFactoryClass.newInstance();
            } catch( ClassCastException cce ) {
		pc.debugError("XMLProvider.getXslTransformer():"+
					      "Class cast exception", 
					      cce );
		return null;
            } catch( ClassNotFoundException cnfe ) {
		pc.debugError("XMLProvider.getXslTransformer():"+
					      "Class not found", 
					      cnfe);
		return null;
            } catch( InstantiationException ie ) {
		pc.debugError("XMLProvider.getXslTransformer():"+
					      "Instantiation exception ", 
					      ie);
		return null;
            } catch( IllegalAccessException iae ) {
		pc.debugError("XMLProvider.getXslTransformer():"+
					      "Illegal Access Excpetion", 
					      iae);
		return null;
            }
            
	    cacheEntry.xslTransformer = 
		    factory.newTransformer(new StreamSource(cacheEntry.xslFile));
	    // reset the lastModTime of the CachedEntry
	    cacheEntry.lastModTime = cacheEntry.xslFile.lastModified();
	}

	return cacheEntry.xslTransformer;
    }
    
    /**
     * This Inner Class representing an entry in the xslTransformerMap Cache
     */
    class CacheEntry {

        private Transformer xslTransformer;
	private File xslFile;
        private long lastModTime;

        /**
         * Constructor for an class representing an entry in the cache
         */
        public CacheEntry() {}
    }
}
