/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program 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.
 *
 * Copyright (c) 2006 - 2009 Object Refinery Ltd, Pentaho Corporation and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.xmlns.parser;

import java.io.IOException;
import java.net.URL;
import java.util.Iterator;
import java.util.Map;
import java.util.ArrayList;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.libraries.resourceloader.CompoundResource;
import org.pentaho.reporting.libraries.resourceloader.FactoryParameterKey;
import org.pentaho.reporting.libraries.resourceloader.Resource;
import org.pentaho.reporting.libraries.resourceloader.ResourceCreationException;
import org.pentaho.reporting.libraries.resourceloader.ResourceData;
import org.pentaho.reporting.libraries.resourceloader.ResourceFactory;
import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
import org.pentaho.reporting.libraries.resourceloader.ResourceKeyCreationException;
import org.pentaho.reporting.libraries.resourceloader.ResourceLoadingException;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
import org.pentaho.reporting.libraries.resourceloader.loader.raw.RawResourceData;
import org.pentaho.reporting.libraries.base.config.Configuration;
import org.pentaho.reporting.libraries.base.config.DefaultConfiguration;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

/**
 * A base-class for resource-factories that load their resources from XML files. This class provides a multiplexing
 * option. For this, the parser looks at the root-element of the document to be parsed and selects the most suitable
 * XmlFactoryModule implementation registered.
 *
 * @author Thomas Morgner
 */
public abstract class AbstractXmlResourceFactory implements ResourceFactory
{
  private static final Log logger = LogFactory.getLog(AbstractXmlResourceFactory.class);

  /**
   * A key for the content base.
   */
  public static final String CONTENTBASE_KEY = "content-base";
  private static final byte[] EMPTY_DATA = new byte[0];

  private ArrayList modules;
  private SAXParserFactory factory;

  /**
   * Default-Constructor.
   */
  protected AbstractXmlResourceFactory()
  {
    modules = new ArrayList();
  }


  /**
   * Returns a SAX parser.
   *
   * @return a SAXParser.
   * @throws ParserConfigurationException if there is a problem configuring the
   *                                      parser.
   * @throws SAXException                 if there is a problem with the parser
   *                                      initialisation
   */
  protected SAXParser getParser()
      throws ParserConfigurationException, SAXException
  {
    if (this.factory == null)
    {
      this.factory = SAXParserFactory.newInstance();
    }
    return this.factory.newSAXParser();
  }


  /**
   * Configures the xml reader. Use this to set features or properties before
   * the documents get parsed.
   *
   * @param handler the parser implementation that will handle the
   *                SAX-Callbacks.
   * @param reader  the xml reader that should be configured.
   */
  protected void configureReader(final XMLReader reader,
                                 final MultiplexRootElementHandler handler)
  {
    try
    {
      reader.setProperty
          ("http://xml.org/sax/properties/lexical-handler",
              handler.getCommentHandler());
    }
    catch (SAXException se)
    {
      logger.debug("Comments are not supported by this SAX implementation.");
    }

    try
    {
      reader.setFeature("http://xml.org/sax/features/xmlns-uris", true);
    }
    catch (SAXException e)
    {
      handler.setXmlnsUrisNotAvailable(true);
    }
    try
    {
      reader.setFeature("http://xml.org/sax/features/namespaces", true);
      reader.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
    }
    catch (SAXException e)
    {
      logger.warn("No Namespace features will be available. (Yes, this is serious)");
    }
  }

  /**
   * Creates a resource by interpreting the data given in the resource-data object. If additional datastreams need to
   * be parsed, the provided resource manager should be used. This method parses the given resource-data as XML stream.
   *
   * @param manager the resource manager used for all resource loading.
   * @param data    the resource-data from where the binary data is read.
   * @param context the resource context used to resolve relative resource paths.
   * @return the parsed result, never null.
   * @throws ResourceCreationException if the resource could not be parsed due to syntaxctial or logical errors in the data.
   * @throws ResourceLoadingException  if the resource could not be accessed from the physical storage.
   */
  public Resource create(final ResourceManager manager,
                         final ResourceData data,
                         final ResourceKey context)
      throws ResourceCreationException, ResourceLoadingException
  {
    try
    {
      final SAXParser parser = getParser();

      final XMLReader reader = parser.getXMLReader();
      final XmlFactoryModule[] rootHandlers = getModules();
      if (rootHandlers.length == 0)
      {
        throw new ResourceCreationException("There are no root-handlers registered for the factory for type " + getFactoryType());
      }

      final ResourceDataInputSource input = new ResourceDataInputSource(data, manager);

      final ResourceKey contextKey;
      final long version;
      final ResourceKey targetKey = data.getKey();
      if (context == null)
      {
        contextKey = targetKey;
        version = data.getVersion(manager);
      }
      else
      {
        contextKey = context;
        version = -1;
      }

      final MultiplexRootElementHandler handler =
          new MultiplexRootElementHandler(manager, targetKey,
              contextKey, version, rootHandlers);

      final DefaultConfiguration parserConfiguration = handler.getParserConfiguration();
      final URL value = manager.toURL(contextKey);
      if (value != null)
      {
        parserConfiguration.setConfigProperty(CONTENTBASE_KEY, value.toExternalForm());
      }

      configureReader(reader, handler);
      reader.setContentHandler(handler);
      reader.setDTDHandler(handler);
      reader.setEntityResolver(handler.getEntityResolver());
      reader.setErrorHandler(getErrorHandler());

      final Map parameters = targetKey.getFactoryParameters();
      final Iterator it = parameters.keySet().iterator();
      while (it.hasNext())
      {
        final Object o = it.next();
        if (o instanceof FactoryParameterKey)
        {
          final FactoryParameterKey fpk = (FactoryParameterKey) o;
          handler.setHelperObject(fpk.getName(), parameters.get(fpk));
        }
      }

      reader.parse(input);

      final Object createdProduct = finishResult
          (handler.getResult(), manager, data, contextKey);
      handler.getDependencyCollector().add(targetKey, data.getVersion(manager));
      return createResource(targetKey, handler, createdProduct, getFactoryType());
    }
    catch (ParserConfigurationException e)
    {
      throw new ResourceCreationException("Unable to initialize the XML-Parser", e);
    }
    catch (SAXException e)
    {
      throw new ResourceCreationException("Unable to parse the document: " + data.getKey(), e);
    }
    catch (IOException e)
    {
      throw new ResourceLoadingException("Unable to read the stream from document: " + data.getKey(), e);
    }
  }

  /**
   * A method to allow to invoke the parsing without accessing the LibLoader layer. The data to be parsed is held in
   * the given InputSource object.
   *
   * @param manager the resource manager used for all resource loading.
   * @param input    the raw-data given as SAX-InputSource.
   * @param context the resource context used to resolve relative resource paths.
   * @param parameters the parse parameters.
   * @return the parsed result, never null.
   * @throws ResourceCreationException if the resource could not be parsed due to syntaxctial or logical errors in the data.
   * @throws ResourceLoadingException  if the resource could not be accessed from the physical storage.
   * @throws ResourceKeyCreationException if creating the context key failed.
   */
  public Object parseDirectly(final ResourceManager manager,
                              final InputSource input,
                              final ResourceKey context,
                              final Map parameters)
      throws ResourceKeyCreationException, ResourceCreationException, ResourceLoadingException
  {
    try
    {
      final SAXParser parser = getParser();

      final XMLReader reader = parser.getXMLReader();
      final XmlFactoryModule[] rootHandlers = getModules();

      final ResourceKey targetKey = manager.createKey(EMPTY_DATA);
      final ResourceKey contextKey;
      if (context == null)
      {
        contextKey = targetKey;
      }
      else
      {
        contextKey = context;
      }

      final MultiplexRootElementHandler handler =
          new MultiplexRootElementHandler(manager, targetKey, contextKey, -1, rootHandlers);

      final DefaultConfiguration parserConfiguration = handler.getParserConfiguration();
      final URL value = manager.toURL(contextKey);
      if (value != null)
      {
        parserConfiguration.setConfigProperty(CONTENTBASE_KEY, value.toExternalForm());
      }

      configureReader(reader, handler);
      reader.setContentHandler(handler);
      reader.setDTDHandler(handler);
      reader.setEntityResolver(handler.getEntityResolver());
      reader.setErrorHandler(getErrorHandler());

      final Iterator it = parameters.keySet().iterator();
      while (it.hasNext())
      {
        final Object o = it.next();
        if (o instanceof FactoryParameterKey)
        {
          final FactoryParameterKey fpk = (FactoryParameterKey) o;
          handler.setHelperObject(fpk.getName(), parameters.get(fpk));
        }
      }

      reader.parse(input);

      return finishResult(handler.getResult(), manager, new RawResourceData(targetKey), contextKey);
    }
    catch (ParserConfigurationException e)
    {
      throw new ResourceCreationException
          ("Unable to initialize the XML-Parser", e);
    }
    catch (SAXException e)
    {
      throw new ResourceCreationException("Unable to parse the document", e);
    }
    catch (IOException e)
    {
      throw new ResourceLoadingException("Unable to read the stream", e);
    }

  }

  /**
   * Returns the registered XmlFactoryModules as array.
   *
   * @return the modules as array.
   */
  private XmlFactoryModule[] getModules()
  {
    return (XmlFactoryModule[]) modules.toArray(new XmlFactoryModule[modules.size()]);
  }

  /**
   * Creates a Resource object for the given product. By default this returns a compound-resource that holds
   * all the key that identify the resources used during the content production.
   *
   * @param targetKey the target key.
   * @param handler the root handler used for the parsing.
   * @param createdProduct the created product.
   * @return the product wrapped into a resource object.
   */
  protected Resource createResource(final ResourceKey targetKey,
                                    final MultiplexRootElementHandler handler,
                                    final Object createdProduct,
                                    final Class createdType)
  {
    return new CompoundResource
        (targetKey, handler.getDependencyCollector(), createdProduct, createdType);
  }

  /**
   * Finishes up the result. This can be used for general clean up and post-parse initializaion of the result.
   * The default implementation does nothing and just returns the object itself.
   *
   * @param res the parsed resource.
   * @param manager the resource manager that was used to load the resource.
   * @param data the data object from where the resource is loaded.
   * @param context the context that resolves relative resource paths.
   * @return the parsed resource.
   * @throws ResourceCreationException if the post initialization fails.
   * @throws ResourceLoadingException if loading external resources failed with an IO error.
   */
  protected Object finishResult(final Object res,
                                final ResourceManager manager,
                                final ResourceData data,
                                final ResourceKey context)
      throws ResourceCreationException, ResourceLoadingException
  {
    return res;
  }

  /**
   * Returns the configuration that should be used to initialize this factory.
   *
   * @return the configuration for initializing the factory.
   */
  protected abstract Configuration getConfiguration();

  /**
   * Loads all XmlFactoryModule-implementations from the given configuration.
   *
   * @see #getConfiguration() 
   */
  public void initializeDefaults()
  {
    final String type = getFactoryType().getName();
    final String prefix = ResourceFactory.CONFIG_PREFIX + type;
    final Configuration config = getConfiguration();
    final Iterator itType = config.findPropertyKeys(prefix);
    while (itType.hasNext())
    {
      final String key = (String) itType.next();
      final String modClass = config.getConfigProperty(key);
      final Object maybeFactory = ObjectUtilities.loadAndInstantiate
          (modClass, AbstractXmlResourceFactory.class, XmlFactoryModule.class);
      if (maybeFactory instanceof XmlFactoryModule == false)
      {
        continue;
      }
      registerModule((XmlFactoryModule) maybeFactory);
    }
  }

  /**
   * Registers a factory module for being used during the parsing. If the factory module does not return a
   * result that matches the factory's type, the parsing will always fail.
   *
   * @param factoryModule the factory module.
   * @throws NullPointerException if the module given is null.
   */
  public void registerModule(final XmlFactoryModule factoryModule)
  {
    if (factoryModule == null)
    {
      throw new NullPointerException();
    }
    modules.add(factoryModule);
  }

  /**
   * Returns the XML-Error handler that should be registered with the XML parser. By default, this returns
   * a logger.
   *
   * @return the error handler.
   */
  protected ErrorHandler getErrorHandler()
  {
    return new LoggingErrorHandler();
  }
}
