/* Copyright (c) 2001, Oracle Corporation. All rights reserved. */
package oracle.portal.provider.v2.http;

import java.io.*;
import java.util.*;
import java.net.URL;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServlet;
import org.w3c.dom.*;
import org.xml.sax.*;

import oracle.portal.log.LogManager;
import oracle.portal.provider.v2.*;
import oracle.portal.provider.v2.utils.ValidationError;
import oracle.portal.utils.xml.v2.*;
import oracle.portal.utils.xml.v2.XMLUtil;

import oracle.webdb.provider.v2.utils.soap.*;
import oracle.webdb.utils.SimpleStringBuffer;

import oracle.xml.parser.v2.XMLDocument;
import oracle.xml.parser.v2.XSLException;
import oracle.xml.parser.v2.XSLStylesheet;

/**
 * <code>DefaultProviderLoader</code> is the default implementation of a
 * <code>ProviderLoader</code>, the entity responsible for loading and
 * maintaining the integrity of a cached {@link ProviderDefinition}, and
 * returning {@link ProviderInstance} objects associated with this
 * definition.<p>
 *
 * <code>DefaultProviderLoader</code> makes use of the extensible {@link
 * DefaultNodeHandler} mechanism to allow <code>ProviderDefinition</code>s to
 * be loaded from XML files.<p>
 *
 * <code>DefaultProviderLoader</code> accepts a <code>Map</code> of String
 * parameters in its {@link #init init} method. The table below summarizes the
 * recognized parameters and their descriptions.<p>
 *
 * <table border="1" cellspacing="0">
 *
 * <tr><th>Parameter Name</th><th>Description</th></tr>
 *
 * <tr><td><code>definition</code></td><td>Context root-relative path to XML provider
 * definition file, e.g. <code>"providers/sample/provider.xml"</code></td>
 *
 * <tr><td><code>debugLevel</code></td><td>Setting this to <code>"2"</code> will
 * cause <code>DefaultProviderLoader</code> to dump parsed XML provider
 * definitions to the servlet log file</td></tr>
 *
 * <tr><td><code>showTestPage</code></td><td>Should the "test page" be
 * displayed if the provider is accessed through its URL in a browser? Value
 * should be either <code>"true"</code> or <code>"false"</code>.</td></tr>
 *
 * </table>
 */
public class DefaultProviderLoader extends ProviderLoader
    implements Initializable, Validateable
{
    private static final String PARAM_PATH = "definition";
    private static final String PARAM_DEBUG_LEVEL = "debugLevel";
    private static final String PARAM_TEST_PAGE = "showTestPage";
    /**
     * testPageURI property in <providerName>.properties file
     */
    private static final String PARAM_TEST_PAGE_URI  = "testPageURI";
    private static final String ELEMENT_PROVIDER = "provider";
    private static final String ATTRIBUTE_CLASS = "class";
    private static final String ATTRIBUTE_DATA_VERSION = "version";
    private static final String PI_PROVIDER = "providerDefinition";
    private static final String WEB_INF = "/WEB-INF";
    private static final String CURRENT_FORMAT_VERSION = "3.1";
    private static final String DEFAULT_FORMAT_VERSION = "1.0";

    private static final Map sUpgradeStyleSheets;

    static
    {
        sUpgradeStyleSheets = new HashMap();
        sUpgradeStyleSheets.put(DEFAULT_FORMAT_VERSION, "upgrade_v1_to_v2.xsl");
        sUpgradeStyleSheets.put("2.0", "upgrade_v2_to_v31.xsl");
    }

    private File mDefinitionFile;
    private long mDefinitionLoaded;
    private ProviderDefinition mLatestDefinition;
    private ProviderContext mProviderContext;
    private ServletContext mServletContext;
    private NodeHandler mNodeHandler;
    private Map mProviderInstances = new HashMap(10);
    private Object mLock = new Object();

    /**
     * Initializes this <code>DefaultProviderLoader</code>.
     *
     * @param soapContext object containing 'environmental' objects from SOAP
     * container.
     * @param parameters a <code>Map</code> containing String parameters that
     * will affect the {@link ProviderContext} that will be shared by all
     * <code>ProviderInstance</code>s created by this
     * <code>DefaultProviderLoader</code>. See class description for more
     * details.
     * @exception ProviderException if an error occurs whilst trying to initialize.
     */
    public void init(SOAPContext soapContext, Map parameters) throws ProviderException
    {
        HttpServlet servlet = soapContext.getServlet();
        mServletContext = servlet.getServletContext();

        // Make the Node Handler use the servlet's class loader, to make sure
        // that web app specific classes can be loaded.
        mNodeHandler = new DefaultNodeHandler(Thread.currentThread().getContextClassLoader());

        // Set the definition filename from its deployment parameter
        String definitionPath = (String)parameters.get(PARAM_PATH);

        if(definitionPath != null)
        {
            definitionPath = definitionPath.trim();

            // Make sure path starts with a separator character.
            if(!definitionPath.startsWith("/"))
            {
                definitionPath = "/" + definitionPath;
            }
            String realPath = mServletContext.getRealPath(WEB_INF +
                                                          definitionPath);
            // If the servlet context couldn't work out the real path to our
            // definition file, try just using a path relative to the current
            // directory.
            if(realPath == null)
            {
                mDefinitionFile = new File(definitionPath);
            }
            else
            {
                mDefinitionFile =  new File(realPath);
            }
        } // end if (definitionPath != null)

        // Set the debug level from its deployment parameter
        int debugLevel = 0;
        String temp = (String)parameters.get(PARAM_DEBUG_LEVEL);
        if (temp != null)
        {
            temp = temp.trim();
            debugLevel = Integer.parseInt(temp);
        }

        // Set the test page flag
        boolean testPage = true;
        String strTest = (String)parameters.get(PARAM_TEST_PAGE);
        if(strTest != null)
        {
            strTest = strTest.trim();
            testPage = XMLUtil.getBooleanValue(strTest);
        }

        // Set the test page flag
        String testPageURI = (String)parameters.get(PARAM_TEST_PAGE_URI);
        if(testPageURI != null)
        {
            testPageURI = testPageURI.trim();
        }

        // Context object for initializing new ProviderDefinitions with
        mProviderContext = new ServletProviderContext(mDefinitionFile.getParent(), 
                                     debugLevel,
                                     soapContext.getServletRequest(),
                                     servlet.getServletConfig(),
                                     testPage,
                                     testPageURI, parameters);
                                                      
    }
    

    /**
     * Gets the context object that describes the deployment environment of the
     * provider type handled by this <code>ProviderLoader</code>.
     * @return context object describing the deployment environment of the
     * provider.
     */
    public ProviderContext getProviderContext()
    {
        return mProviderContext;
    }
   

    /**
     * Returns a <code>ProviderInstance</code> object with the specified
     * providerId, optionally permitting reload of the cached
     * <code>ProviderDefinition</code>.<p>
     *
     * The first time this method is called, a new
     * <code>ProviderDefinition</code> is loaded from its XML file and cached
     * internally. This same cached <code>ProviderDefinition</code> is
     * associated with all <code>ProviderInstance</code>s returned by this
     * method, unless it is called again when the <code>refresh</code> flag is
     * <code>true</code>. If this is the case and the XML file has been
     * updated, the method will replace the cached
     * <code>ProviderDefinition</code> with a new version.
     *
     * @param providerId the providerId of the <code>ProviderInstance</code>
     * that is being requested.
     * @param refresh flag indicating whether the
     * <code>ProviderDefinition</code> should be reloaded or not. A value of
     * <code>true</code> will cause a refresh.
     *
     * @return the <code>ProviderInstance</code> object with the specified providerId
     */
    public ProviderInstance getProviderInstance(String providerId, 
                                                boolean refresh)
        throws ProviderException
    {
        ProviderInstance pi = null;
        
        // get the ProviderDefinition we want based on the refresh flag. This
        // may be the existing definition or a newly loaded on.
        ProviderDefinition pd = getProviderDefinition(refresh, true); 

        pi = (ProviderInstance)mProviderInstances.get(providerId); 
        if (pi != null) 
        {
            // ProviderInstance object exists in the cache - compare the
            // ProviderDefinition of the returned object with that of pd
            // ONLY if they are the same can this ProviderInstance be returned.
            // Otherwise the ProviderDefinition will not the refreshed one.                
            if (pi.isDefinitionEqual(pd))
            {
                LogManager.info("DefaultProviderLoader: found existing" +
                                "ProviderInstance for ProviderId: " +
                                providerId);
                return pi;
            }
        }

        // ProviderInstance matching the ProviderDefinition and ProviderId was
        // not found - create a new ProviderInstance using the new 
        // ProviderDefinition        
        pi = createProviderInstance(pd, providerId);
        return pi;
    }      

    /**
     * Validates the internal state of this <code>DefaultProviderLoader</code>
     * before initiating validation calls on the tree of meta-data objects.
     *
     * @param context the calling context.
     * @param errors current errors.
     */
    public void validate(String context, ValidationError errors)
    {
        String ctx = ValidationError.newContext(context,
                                                "DefaultProviderLoader", null);
        ProviderDefinition pd = null;
        try
        {
            pd = getProviderDefinition(true, false);
        }
        catch (ProviderException pe)
        {
            errors.addError(ctx, pe.getMessage(), null);
            LogManager.warning("Could not get ProviderDefinition", pe);
        }
        if (pd != null && pd instanceof Validateable)
        {
            ((Validateable)pd).validate(ctx, errors);
        }
    }

    /**
     * creates a new ProviderInstance object and adds it to the cache.
     *
     * Since this method modifies the cache, it must be synchronized. When a
     * new ProviderInstance object is created, it is given a reference to the
     * current ProviderDefinition. The ProviderInstance will use that definition
     * until it receives a refresh request from the portal it represents.
     *
     * If another ProviderInstance receives a new      
     *
     * @param providerId the providerId of the ProviderInstance that is being
     *        requested
     * @param refresh flag indicating whether the ProviderDefinition should be
     *        reloaded or not. A value of true will cause a refresh.
     *
     * @return the ProviderInstance object representing the specified providerId
     */
    private ProviderInstance createProviderInstance(ProviderDefinition newDef,
                                                      String providerId)
        throws ProviderException
    {
        // Even though this entire method is within the synchronized block, 
        // we cannot synchronize the method because we need to synchronize on
        // the SAME lock object (mLock) that is being used everywhere else in
        // this class.
        synchronized(mLock)
        {
            // check to see if another thread created the new ProviderInstance
            // before we got here
            ProviderInstance p = (ProviderInstance)mProviderInstances.get(providerId);
            if (p != null)
            {
                LogManager.info("DefaultProviderLoader: found existing" +
                                "ProviderInstance for ProviderId: " +
                                providerId);
                
                // compare the ProviderDefinition of the returned object with
                // that of newDef - only if they are the same can this instance
                // be returned, otherwise the ProviderDefinition will not the 
                // refreshed one.                
                if (p.isDefinitionEqual(newDef))
                {
                    return p;
                }
            }
            

            // we got this far, so either the ProviderInstance does not exist
            // or the instance in the cache has an old definition.
            // First remove the old instance from the cache and allow it to be
            // garbage collected when any executing requests have completed
            ProviderInstance oldInstance = 
                (ProviderInstance)mProviderInstances.remove(providerId);

            if (oldInstance != null)
            {
                LogManager.info("DefaultProviderLoader: Destroying old " +
                                "ProviderInstance for ProviderId: " + 
                                providerId);
                                
                // TODO
                // Ideally at this point, we would like to call a "destroy"
                // method on the oldInstance so it can persist any state it
                // might be holding. 
                // However, It could also be in use by another request & we
                // cannot destroy it until all requests using it have completed
                // This means adding some kind of locking mechanism to allow
                // exising requests to complete before destroying. This would
                // have to be synchronized with the creation of the new instance
                // because the new instance will probably need the state the
                // existing instance is carrying.
                // An alternative would be a transfer of state - but again, this
                // could be lossy if requests have not completed.
                //
                // For now, just free the object and let it be GC'ed once the
                // requests complete.
                oldInstance = null;
            }

            // Now get on with the business of creating a new instance using
            // the ProviderDefinition that was passed into this method
            LogManager.info("DefaultProviderLoader: Creating new " +
                            "ProviderInstance for Provider ID: " + 
                            providerId);
                            
            ProviderInstance newInstance = null;
            Class instanceClass = newDef.getProviderInstanceClass();
            try
            {
                newInstance = (ProviderInstance)instanceClass.newInstance();
                newInstance.init(providerId, newDef);
            }
            catch(Exception e)
            {
                throw new ProviderException("DefaultProviderDefinition:Unable " +
                                            "to create instance of class \"" +
                                            instanceClass.getName() + 
                                            "\": " + e.getMessage());
            }
            mProviderInstances.put(providerId, newInstance);
            return newInstance;

        } // end of synchronize
        
    }
    

    /**
     * Returns the ProviderDefinition, refreshing the existing definition
     * if necessary.
     *
     * This method should be the only way a ProviderDefinition is retrieved
     * within this class or its subclasses. This will ensure that only one
     * definition is loaded at once and the caller gets the definition they
     * requested. 
     *
     * @param refresh flag indicating whether the definition should be reloaded
     *        or not. A value of "true" indicates a reload is required.
     * @param validate flag indicating whether to initiate validation upon
     *        reload of the definition.
     * @return the ProviderDefinition.
     */
    private ProviderDefinition getProviderDefinition(boolean refresh,
                                                     boolean validate)
        throws ProviderException
    {
        // return the current definition if we can
        if(mLatestDefinition != null && !refresh) 
        {
            return mLatestDefinition;
        }

        if(!mDefinitionFile.exists())
        {
            throw new ProviderException("Unable to find provider definition file \"" +
                                        mDefinitionFile.getAbsolutePath() + "\"");
        }

        LogManager.info("DefaultProviderLoader: Refresh requested or " +
                        "definition not yet loaded"); 


        // Check whether a reload is necessary
        if(mLatestDefinition != null && 
           mDefinitionFile.lastModified() <= mDefinitionLoaded)
        {
            LogManager.info("DefaultProviderLoader: Refresh not required - " + 
                            "using existing definition");
            return mLatestDefinition;
        }

        
        // Definition needs to be loaded - only want to do this once
        LogManager.info("DefaultProviderLoader - loading new ProviderDefinition");
        
        ProviderDefinition newDefinition = null;
        synchronized (mLock)
        {

            // Try parsing the document and applying any necessary 
            // ttransformationso it. Log any exceptions that occur.
            
            XMLDocument registryXML = null;
            try
            {
                registryXML = parseRegistry(mDefinitionFile);
            }
            catch (ProviderException pe)
            {
                LogManager.severe(pe.getMessage(), pe);
                throw (ProviderException)pe.fillInStackTrace();
            }

            // Output the transformed XML document if debugLevel >= 2
            if (mProviderContext.getDebugLevel() >= 2)
            {
                OutputStream bos = new ByteArrayOutputStream();
                LogManager.info("Traverse provider.xml document tree: ");
                try
                {
                    registryXML.print(bos);
                    bos.close();
                }
                catch (IOException e)
                {
                }
                LogManager.info("DefaultProviderLoader: Transformed Provider " +
                                "Registry File: " + bos.toString());
            }            

            Element docEl = registryXML.getDocumentElement();
            String definitionClassName = docEl.getAttribute(ATTRIBUTE_CLASS);
            if(definitionClassName != null)
            {
                try
                {
                    // We have to use the the current thread's 'context class
                    // loader' (i.e. the web app's class loader) to load the
                    // class, so that it can be picked up from within the web
                    // app
                    Class definitionClass = Class.forName(definitionClassName,
                                                          true,
                                                          Thread.currentThread().getContextClassLoader());
                    newDefinition = 
                        (ProviderDefinition)definitionClass.newInstance();
                }
                catch (Exception e)
                {
                    SimpleStringBuffer msg = new SimpleStringBuffer(100);
                    msg.append("Unable to create ProviderDefinition of class \"");
                    msg.append(definitionClassName);
                    msg.append("\": ");
                    msg.append(e.getMessage());
                    LogManager.severe(msg.toString(), e);
                    throw new ProviderException(msg.toString());
                }                               
            }
            else
            {
                newDefinition = new DefaultProviderDefinition();
            }

            // initialize the new Object from the Provider Registry File
            try
            {
                newDefinition.init(mProviderContext);

                if(newDefinition instanceof InitializableXMLObject)
                {
                    // process the XML file calling pre & port init methods
                    InitializableXMLObject xmlObj = 
                        (InitializableXMLObject)newDefinition;
                    xmlObj.preInitialize(mProviderContext);
                    mNodeHandler.processNode(docEl, newDefinition);
                    xmlObj.postInitialize();
                }
                else
                {
                    // just process the file
                    mNodeHandler.processNode(docEl, newDefinition);
                }
             }
            catch(NodeHandlerException e)
            {
                LogManager.severe("DefaultProviderDefinition: Failed to load " +
                                  "ProviderDefinition", e);
                throw new ProviderException(e);
            }

            if (validate && newDefinition instanceof Validateable)
            {
                ValidationError errors = new ValidationError();
                ((Validateable)newDefinition).validate("ProviderLoader",
                                                       errors);
                if (errors.isEmpty())
                {
                    mLatestDefinition = newDefinition;
                    mDefinitionLoaded = System.currentTimeMillis();
                }
                else
                {
                    String msg = errors.toString();
                    LogManager.severe(msg);
                    throw new ProviderException(msg);
                }
            }
        } // end of synchronize
        
        return newDefinition;
    }



    private XMLDocument parseRegistry(File definitionFile) throws ProviderException
    {
        XMLDocument xmldoc = null;
        //Capture any XML / XSL parsing errors in the following block to this
        //output stream. A finally{} block at the end ensures it gets closed
        //afterwards
        ByteArrayOutputStream errOut = new ByteArrayOutputStream();
        try
        {
            // Make sure we release this InputStream when finished with it
            InputStream is = null;
            try
            {
                is = new FileInputStream(definitionFile);

                // Obtain the document.
                xmldoc = XMLUtil.parseDocument(is, errOut);
            }
            finally
            {
                if(is != null)
                {
                    try
                    {
                        is.close();
                    }
                    catch(Exception e1)
                    {
                    }
                    is = null;
                }
            }
            // While the document explicitly names a list of stylesheets, keep
            // applying them
            Iterator xsldocs;
            while((xsldocs = XMLUtil.getXslDocs(xmldoc)).hasNext())
            {
                //Apply the XSL transformations to the xml document.
                do
                {
                    xmldoc = XMLUtil.applyXSL(xmldoc,
                                              (XSLStylesheet)xsldocs.next(), errOut);
                }
                while(xsldocs.hasNext());
            }

            // Now check for a specific provider definition 'version', and upgrade
            // as necessary
            int compare = -1;
            do
            {
                String version = null;
                // Assume we have a version 1 format file, unless we can find a
                // providerDefinition processing instruction with a version
                // specified.
                int majorVersion = 1;
                Iterator pdkpis = XMLUtil.getTargetPIs(xmldoc, PI_PROVIDER);
                if(pdkpis.hasNext())
                {
                    ProcessingInstruction pi = (ProcessingInstruction)pdkpis.next();
                    Map piAttributes = XMLUtil.parsePI(pi);
                    // Try to get a version number out of the processing instruction
                    version = (String)piAttributes.get(ATTRIBUTE_DATA_VERSION);
                }
                if(version == null)
                {
                    version = DEFAULT_FORMAT_VERSION;
                }
                compare = XMLUtil.compareVersions(version, CURRENT_FORMAT_VERSION);
                // Upgrade the xml, if we have an upgrade stylesheet for it
                if (sUpgradeStyleSheets.containsKey(version))
                {
                    // Get the upgrade stylesheet from within the .jar file on
                    // the classpath
                    XMLDocument upgragedoc = null;
                    URL upgradeURL = getClass().getResource("resource/" + sUpgradeStyleSheets.get(version));
                    XSLStylesheet upgradeXSL = XMLUtil.parseStylesheet(upgradeURL, errOut);
                    xmldoc = XMLUtil.applyXSL(xmldoc, upgradeXSL, errOut);
                } // end if we have a stylesheet
                // If the version number is smaller, but we don't have a stylesheet for it, throw an exception
                else if(compare == -1)
                {
                    throw new ProviderException("Error: unsupported provider definition version \"" +
                                                version + "\"");
                }
            }
            while (compare == -1); // end do

            // Throw an exception if we have a data format we can't cope with
            if(compare == 1)
            {
                throw new ProviderException("Error: this provider loader does not support XML registries with a " +
                                            PI_PROVIDER + " version above \"" + CURRENT_FORMAT_VERSION + "\"");
            }

        } // End try
        catch(SAXParseException e1)
        {
            String msg = e1.getMessage() + ": in " + definitionFile + " line "
                + e1.getLineNumber() + ", column " + e1.getColumnNumber() +
                Character.LINE_SEPARATOR + errOut.toString();
            throw new ProviderException(msg);

        }
        catch (SAXException se)
        {
            String msg = se.getMessage() + Character.LINE_SEPARATOR + errOut.toString();
            throw new ProviderException(msg);
        }
        catch (XSLException xsle)
        {
            String msg = xsle.getMessage() + Character.LINE_SEPARATOR + errOut.toString();
            throw new ProviderException(msg);
        }
        catch (IOException ioe)
        {
            throw new ProviderException("IO Error parsing provider definition file: " + ioe.getMessage());
        }
        finally
        {
            if(errOut!= null)
            {
                try
                {
                    errOut.close();
                }
                catch(Exception e1)
                {
                }
            }
            errOut = null;
        }
        // Make sure that the XML we end up with actually has <provider> at the root!
        Element docEl = xmldoc.getDocumentElement();
        if(docEl == null || !docEl.getTagName().equalsIgnoreCase(ELEMENT_PROVIDER))
        {
            throw new ProviderException("No <provider> element found in provider definition file "
                                        + definitionFile);
        }
        return xmldoc;
    }

}
