/*
 * 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) 2007 - 2009 Pentaho Corporation and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.base.boot;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;

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.pentaho.reporting.libraries.base.util.StringUtils;


/**
 * The abstract module provides a default implementation of the module interface.
 * <p/>
 * The module can be specified in an external property file. The file name of this
 * specification defaults to "module.properties". 
 * <p/>
 * The first and mandatory section is always the module info and contains the basic module
 * properties like name, version and a short description.
 * <p/>
 * <pre>
 * module.name: xls-export-gui
 * module.producer: The JFreeReport project - www.jfree.org/jfreereport
 * module.description: A dialog component for the Excel table export.
 * module.version.major: 0
 * module.version.minor: 84
 * module.version.patchlevel: 0
 * </pre>
 * The properties name, producer and description are simple strings. They may
 * span multiple lines, but may not contain a colon (':').
 * The version properties are integer values.
 * <p/>
 * This section may be followed by one or more "depends" sections. These
 * sections describe the base modules that are required to be active to make this
 * module work. The package manager will enforce this policy and will deactivate this
 * module if one of the base modules is missing.
 * <p/>
 * <pre>
 * dependency.module-id.module: org.pentaho.reporting.engine.classic.core.modules.output.table.xls.XLSTableModule
 * dependency.module-id.version.major: 0
 * dependency.module-id.version.minor: 84
 * </pre>
 * <p/>
 * The property *.module references to the module implementation of the module package.
 * The module-id is a per-module-definition-unique identifier and it is recommended to set it to the
 * referenced module's name for documentary purposes.
 *
 * @author Thomas Morgner
 */
public abstract class AbstractModule extends DefaultModuleInfo implements Module
{
  /**
   * An empty array for performance reasons.
   */
  private static final ModuleInfo[] EMPTY_MODULEINFO = new ModuleInfo[0];

  /**
   * The list of required modules.
   */
  private ModuleInfo[] requiredModules;
  /**
   * The list of optional modules.
   */
  private ModuleInfo[] optionalModules;

  /**
   * The name of the module.
   */
  private String name;
  /**
   * A short description of the module.
   */
  private String description;
  /**
   * The name of the module producer.
   */
  private String producer;
  /**
   * The modules subsystem.
   */
  private String subsystem;

  /**
   * Default Constructor.
   */
  protected AbstractModule()
  {
    setModuleClass(this.getClass().getName());
  }

  /**
   * Loads the default module description from the file "module.properties". This file
   * must be in the same package as the implementing class.
   *
   * @throws ModuleInitializeException if an error occurs.
   */
  protected void loadModuleInfo() throws ModuleInitializeException
  {
    final InputStream in = ObjectUtilities.getResourceRelativeAsStream
        ("module.properties", getClass());
    if (in == null)
    {
      throw new ModuleInitializeException
          ("File 'module.properties' not found in module package.");
    }

    loadModuleInfo(in);
  }

  /**
   * Loads the module descriptiong from the given input stream. The module description
   * must conform to the rules define in the class description. The file must be encoded
   * with "ISO-8859-1" (like property files).
   *
   * @param in the input stream from where to read the file
   * @throws ModuleInitializeException if an error occurs.
   */
  protected void loadModuleInfo(final InputStream in) throws ModuleInitializeException
  {
    if (in == null)
    {
      throw new NullPointerException
          ("Given InputStream is null.");
    }

    try
    {
      final DefaultConfiguration props = new DefaultConfiguration();
      props.load(in);

      readModuleInfo(props);

      final ArrayList optionalModules = new ArrayList();
      final ArrayList dependendModules = new ArrayList();
      final Iterator keys = props.findPropertyKeys("dependency.");
      while (keys.hasNext())
      {
        final String key = (String) keys.next();
        if (key.endsWith(".dependency-type"))
        {
          final String moduleHandle = key.substring(0, key.length() - ".dependency-type".length());
          final DefaultModuleInfo module = readExternalModule(props, moduleHandle);
          if ("optional".equals(props.getConfigProperty(key)))
          {
            optionalModules.add(module);
          }
          else
          {
            dependendModules.add(module);
          }
        }

      }

      this.optionalModules = (ModuleInfo[])
          optionalModules.toArray(new ModuleInfo[optionalModules.size()]);

      this.requiredModules = (ModuleInfo[])
          dependendModules.toArray(new ModuleInfo[dependendModules.size()]);
    }
    catch (IOException ioe)
    {
      throw new ModuleInitializeException("Failed to load properties", ioe);
    }
    finally
    {
      try
      {
        in.close();
      }
      catch (IOException e)
      {
        // ignore ..
      }
    }
  }

  /**
   * Reads the module definition header. This header contains information about
   * the module itself.
   *
   * @param config the properties from where to read the content.
   */
  private void readModuleInfo(final Configuration config)
  {
    setName(config.getConfigProperty("module.name"));
    setProducer(config.getConfigProperty("module.producer"));
    setDescription(config.getConfigProperty("module.description"));
    setMajorVersion(config.getConfigProperty("module.version.major"));
    setMinorVersion(config.getConfigProperty("module.version.minor"));
    setPatchLevel(config.getConfigProperty("module.version.patchlevel"));
    setSubSystem(config.getConfigProperty("module.subsystem"));
  }

  /**
   * Reads an external module description. This describes either an optional or
   * a required module.
   *
   * @param reader the reader from where to read the module
   * @param prefix the property-key prefix.
   * @return the read module, never null
   */
  private DefaultModuleInfo readExternalModule(final Configuration reader,
                                               final String prefix)
  {
    final DefaultModuleInfo mi = new DefaultModuleInfo();
    mi.setModuleClass(reader.getConfigProperty(prefix + ".module"));
    mi.setMajorVersion(reader.getConfigProperty(prefix + ".version.major"));
    mi.setMinorVersion(reader.getConfigProperty(prefix + ".version.minor"));
    mi.setPatchLevel(reader.getConfigProperty(prefix + ".version.patchlevel"));
    return mi;
  }

  /**
   * Returns the name of this module.
   *
   * @return the module name
   * @see Module#getName()
   */
  public String getName()
  {
    return this.name;
  }

  /**
   * Defines the name of the module.
   *
   * @param name the module name.
   */
  protected void setName(final String name)
  {
    this.name = name;
  }

  /**
   * Returns the module description.
   *
   * @return the description of the module.
   * @see Module#getDescription()
   */
  public String getDescription()
  {
    return this.description;
  }

  /**
   * Defines the description of the module.
   *
   * @param description the module's desciption.
   */
  protected void setDescription(final String description)
  {
    this.description = description;
  }

  /**
   * Returns the producer of the module.
   *
   * @return the producer.
   * @see Module#getProducer()
   */
  public String getProducer()
  {
    return this.producer;
  }

  /**
   * Defines the producer of the module.
   *
   * @param producer the producer.
   */
  protected void setProducer(final String producer)
  {
    this.producer = producer;
  }

  /**
   * Returns a copy of the required modules array. This array contains all
   * description of the modules that need to be present to make this module work.
   *
   * @return an array of all required modules.
   * @see Module#getRequiredModules()
   */
  public ModuleInfo[] getRequiredModules()
  {
    if (this.requiredModules == null)
    {
      return EMPTY_MODULEINFO;
    }
    final ModuleInfo[] retval = new ModuleInfo[this.requiredModules.length];
    System.arraycopy(this.requiredModules, 0, retval, 0, this.requiredModules.length);
    return retval;
  }

  /**
   * Returns a copy of the required modules array. This array contains all
   * description of the optional modules that may improve the modules functonality.
   *
   * @return an array of all required modules.
   * @see Module#getRequiredModules()
   */
  public ModuleInfo[] getOptionalModules()
  {
    if (this.optionalModules == null)
    {
      return EMPTY_MODULEINFO;
    }
    final ModuleInfo[] retval = new ModuleInfo[this.optionalModules.length];
    System.arraycopy(this.optionalModules, 0, retval, 0, this.optionalModules.length);
    return retval;
  }

  /**
   * Defines the required module descriptions for this module.
   *
   * @param requiredModules the required modules.
   */
  protected void setRequiredModules(final ModuleInfo[] requiredModules)
  {
    this.requiredModules = new ModuleInfo[requiredModules.length];
    System.arraycopy(requiredModules, 0, this.requiredModules, 0, requiredModules.length);
  }

  /**
   * Defines the optional module descriptions for this module.
   *
   * @param optionalModules the optional modules.
   */
  public void setOptionalModules(final ModuleInfo[] optionalModules)
  {
    this.optionalModules = new ModuleInfo[optionalModules.length];
    System.arraycopy(optionalModules, 0, this.optionalModules, 0, optionalModules.length);
  }

  /**
   * Returns a string representation of this module.
   *
   * @return the string representation of this module for debugging purposes.
   * @see Object#toString()
   */
  public String toString()
  {
    final String lineSeparator = StringUtils.getLineSeparator();
    final StringBuffer buffer = new StringBuffer(120);
    buffer.append("Module : ");
    buffer.append(getName());
    buffer.append(lineSeparator);
    buffer.append("ModuleClass : ");
    buffer.append(getModuleClass());
    buffer.append(lineSeparator);
    buffer.append("Version: ");
    buffer.append(getMajorVersion());
    buffer.append('.');
    buffer.append(getMinorVersion());
    buffer.append('.');
    buffer.append(getPatchLevel());
    buffer.append(lineSeparator);
    buffer.append("Producer: ");
    buffer.append(getProducer());
    buffer.append(lineSeparator);
    buffer.append("Description: ");
    buffer.append(getDescription());
    buffer.append(lineSeparator);
    return buffer.toString();
  }

  /**
   * Tries to load a class to indirectly check for the existence
   * of a certain library.
   *
   * @param name    the name of the library class.
   * @param context the context class to get a classloader from.
   * @return true, if the class could be loaded, false otherwise.
   */
  protected static boolean isClassLoadable(final String name, final Class context)
  {
    try
    {
      final ClassLoader loader = ObjectUtilities.getClassLoader(context);
      Class.forName(name, false, loader);
      return true;
    }
    catch (Exception e)
    {
      return false;
    }
  }

  /**
   * Configures the module by loading the configuration properties and
   * adding them to the package configuration.
   *
   * @param subSystem the subsystem.
   */
  public void configure(final SubSystem subSystem)
  {
    final InputStream in = ObjectUtilities.getResourceRelativeAsStream
        ("configuration.properties", getClass());
    if (in == null)
    {
      return;
    }
    try
    {
      subSystem.getPackageManager().getPackageConfiguration().load(in);
    }
    finally
    {
      try
      {
        in.close();
      }
      catch (IOException e)
      {
        // can be ignored ...
      }
    }
  }

  /**
   * Tries to load an module initializer and uses this initializer to initialize
   * the module.
   *
   * @param classname the class name of the initializer.
   * @throws ModuleInitializeException if an error occures
   * @deprecated Use the method that provides a class-context instead.
   */
  protected void performExternalInitialize(final String classname)
      throws ModuleInitializeException
  {
    try
    {
      final ModuleInitializer mi =
          (ModuleInitializer) ObjectUtilities.loadAndInstantiate(classname, AbstractModule.class, ModuleInitializer.class);
      if (mi == null)
      {
        throw new ModuleInitializeException("Failed to load specified initializer class.");
      }
      mi.performInit();
    }
    catch (ModuleInitializeException mie)
    {
      throw mie;
    }
    catch (Exception e)
    {
      throw new ModuleInitializeException("Failed to load specified initializer class.", e);
    }
  }

  /**
   * Executes an weakly referenced external initializer. The initializer will be loaded using reflection and will
   * be executed once. If the initializing fails with any exception, the module will become unavailable.
   *
   * @param classname the classname of the <code>ModuleInitializer</code> implementation
   * @param context the class-loader context from where to load the module's classes.
   * @throws ModuleInitializeException if an error occured or the initializer could not be found.
   */
  protected void performExternalInitialize(final String classname, final Class context)
      throws ModuleInitializeException
  {
    try
    {
      final ModuleInitializer mi =
          (ModuleInitializer) ObjectUtilities.loadAndInstantiate(classname, context, ModuleInitializer.class);
      if (mi == null)
      {
        throw new ModuleInitializeException("Failed to load specified initializer class.");
      }
      mi.performInit();
    }
    catch (ModuleInitializeException mie)
    {
      throw mie;
    }
    catch (Exception e)
    {
      throw new ModuleInitializeException("Failed to load specified initializer class.", e);
    }
  }

  /**
   * Returns the modules subsystem. If this module is not part of an subsystem
   * then return the modules name, but never null.
   *
   * @return the name of the subsystem.
   */
  public String getSubSystem()
  {
    if (this.subsystem == null)
    {
      return getName();
    }
    return this.subsystem;
  }

  /**
   * Defines the subsystem name for this module. If no sub-system name is set, the sub-system defaults to the
   * module's name.
   *
   * @param name the new name of the subsystem.
   */
  protected void setSubSystem(final String name)
  {
    this.subsystem = name;
  }
}