/* ========================================================================
 * JCommon : a free general purpose class library for the Java(tm) platform
 * ========================================================================
 *
 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
 * 
 * Project Info:  http://www.jfree.org/jcommon/index.html
 *
 * This library is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU Lesser General Public License as published by 
 * the Free Software Foundation; either version 2.1 of the License, or 
 * (at your option) any later version.
 *
 * This library 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.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
 * USA.  
 *
 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
 * in the United States and other countries.]
 * 
 * -------------------
 * AbstractModule.java
 * -------------------
 * (C)opyright 2003, 2004, by Thomas Morgner and Contributors.
 *
 * Original Author:  Thomas Morgner;
 * Contributor(s):   David Gilbert (for Object Refinery Limited);
 *
 * $Id: AbstractModule.java,v 1.3 2005/10/18 13:14:50 mungady Exp $
 *
 * Changes
 * -------
 * 05-Jul-2003 : Initial version
 * 07-Jun-2004 : Added JCommon header (DG);
 *
 */

package org.jfree.base.modules;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;

import org.jfree.util.ObjectUtilities;


/**
 * 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". This file is no real property file,
 * it follows a more complex rule set.
 * <p>
 * Lines starting with '#' are considered comments.
 * Section headers start at the beginning of the line, section properties
 * are indented with at least one whitespace.
 * <p>
 * The first section is always the module info and contains the basic module
 * properties like name, version and a short description.
 * <p>
 * <pre>
 * module-info:
 *   name: xls-export-gui
 *   producer: The JFreeReport project - www.jfree.org/jfreereport
 *   description: A dialog component for the Excel table export.
 *   version.major: 0
 *   version.minor: 84
 *   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>
 * depends:
 *   module: org.jfree.report.modules.output.table.xls.XLSTableModule
 *   version.major: 0
 *   version.minor: 84
 * </pre>
 * <p>
 * The property module references to the module implementation of the module package.
 *
 * @author Thomas Morgner
 */
public abstract class AbstractModule extends DefaultModuleInfo implements Module
{
  /**
   * The reader helper provides a pushback interface for the reader to read and
   * buffer  complete lines.
   * @author Thomas Morgner
   */
  private static class ReaderHelper
  {
    /** The line buffer containing the last line read. */
    private String buffer;
    /** The reader from which to read the text. */
    private final BufferedReader reader;

    /**
     * Creates a new reader helper for the given buffered reader.
     *
     * @param reader the buffered reader that is the source of the text.
     */
    public ReaderHelper(final BufferedReader reader)
    {
      this.reader = reader;
    }

    /**
     * Checks, whether the reader contains a next line. Returns false if the end
     * of the stream has been reached.
     *
     * @return true, if there is a next line to read, false otherwise.
     * @throws IOException if an error occures.
     */
    public boolean hasNext() throws IOException
    {
      if (this.buffer == null)
      {
        this.buffer = readLine();
      }
      return this.buffer != null;
    }

    /**
     * Returns the next line.
     *
     * @return the next line.
     */
    public String next()
    {
      final String line = this.buffer;
      this.buffer = null;
      return line;
    }

    /**
     * Pushes the given line back into the buffer. Only one line can be contained in
     * the buffer at one time.
     *
     * @param line the line that should be pushed back into the buffer.
     */
    public void pushBack(final String line)
    {
      this.buffer = line;
    }

    /**
     * Reads the next line skipping all comment lines.
     *
     * @return the next line, or null if no line can be read.
     * @throws IOException if an IO error occures.
     */
    protected String readLine() throws IOException
    {
      String line = this.reader.readLine();
      while (line != null && (line.length() == 0 || line.startsWith("#")))
      {
        // empty line or comment is ignored
        line = this.reader.readLine();
      }
      return line;
    }

    /**
     * Closes the reader.
     *
     * @throws IOException if an IOError occurs.
     */
    public void close() throws IOException
    {
      this.reader.close();
    }
  }

  /** 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.
   */
  public 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
  {
    try
    {
      if (in == null)
      {
        throw new NullPointerException
            ("Given InputStream is null.");
      }
      final ReaderHelper rh = new ReaderHelper(new BufferedReader
          (new InputStreamReader(in, "ISO-8859-1")));

      final ArrayList optionalModules = new ArrayList();
      final ArrayList dependendModules = new ArrayList();

      while (rh.hasNext())
      {
        final String lastLineRead = rh.next();
        if (lastLineRead.startsWith("module-info:"))
        {
          readModuleInfo(rh);
        }
        else if (lastLineRead.startsWith("depends:"))
        {
          dependendModules.add(readExternalModule(rh));
        }
        else if (lastLineRead.startsWith("optional:"))
        {
          optionalModules.add(readExternalModule(rh));
        }
        else
        {
          // we dont understand the current line, so we skip it ...
          // should we throw a parse exception instead?
        }
      }
      rh.close();

      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);
    }
  }

  /**
   * Reads a multiline value the stream. This will read the stream until
   * a new key is found or the end of the file is reached.
   *
   * @param reader the reader from where to read.
   * @param firstLine the first line (which was read elsewhere).
   * @return the complete value, never null
   * @throws IOException if an IO error occurs.
   */
  private String readValue(final ReaderHelper reader, String firstLine) throws IOException
  {
    final StringBuffer b = new StringBuffer(firstLine.trim());
    boolean newLine = true;
    while (isNextLineValueLine(reader))
    {
      firstLine = reader.next();
      final String trimedLine = firstLine.trim();
      if (trimedLine.length() == 0 && (newLine == false))
      {
        b.append ("\n");
        newLine = true;
      }
      else
      {
        if (newLine == false)
        {
          b.append(" ");
        }
        b.append(parseValue(trimedLine));
        newLine = false;
      }
    }
    return b.toString();
  }

  /**
   * Checks, whether the next line in the reader is a value line.
   * 
   * @param reader from where to read the lines.
   * @return true, if the next line is a value line, false otherwise.
   * @throws IOException if an IO error occurs.
   */
  private boolean isNextLineValueLine (final ReaderHelper reader) throws IOException
  {
    if (reader.hasNext() == false)
    {
      return false;
    }
    final String firstLine = reader.next();
    if (firstLine == null)
    {
      return false;
    }
    if (parseKey(firstLine) != null)
    {
      reader.pushBack(firstLine);
      return false;
    }
    reader.pushBack(firstLine);
    return true;
  }

  /**
   * Reads the module definition header. This header contains information about
   * the module itself.
   *
   * @param reader the reader from where to read the content.
   * @throws IOException if an error occures
   */
  private void readModuleInfo(final ReaderHelper reader) throws IOException
  {
    while (reader.hasNext())
    {
      final String lastLineRead = reader.next();

      if (Character.isWhitespace(lastLineRead.charAt(0)) == false)
      {
        // break if the current character is no whitespace ...
        reader.pushBack(lastLineRead);
        return;
      }

      final String line = lastLineRead.trim();
      final String key = parseKey(line);
      if (key != null)
      {
        // parse error: Non data line does not contain a colon
        final String b = readValue(reader, parseValue(line.trim()));

        if (key.equals("name"))
        {
          setName(b);
        }
        else if (key.equals("producer"))
        {
          setProducer(b);
        }
        else if (key.equals("description"))
        {
          setDescription(b);
        }
        else if (key.equals("subsystem"))
        {
          setSubSystem(b);
        }
        else if (key.equals("version.major"))
        {
          setMajorVersion(b);
        }
        else if (key.equals("version.minor"))
        {
          setMinorVersion(b);
        }
        else if (key.equals("version.patchlevel"))
        {
          setPatchLevel(b);
        }
      }
    }
  }

  /**
   * Parses an string to find the key section of the line. This section ends with
   * an colon.
   *
   * @param line the line which to parse
   * @return the key or null if no key is found.
   */
  private String parseKey(final String line)
  {
    final int idx = line.indexOf(':');
    if (idx == -1)
    {
      return null;
    }
    return line.substring(0, idx);
  }

  /**
   * Parses the value section of the given line.
   *
   * @param line the line that should be parsed
   * @return the value, never null
   */
  private String parseValue(final String line)
  {
    final int idx = line.indexOf(':');
    if (idx == -1)
    {
      return line;
    }
    if ((idx + 1) == line.length())
    {
      return "";
    }
    return line.substring(idx + 1);
  }

  /**
   * Reads an external module description. This describes either an optional or
   * a required module.
   *
   * @param reader the reader from where to read the module
   * @return the read module, never null
   * @throws IOException if an error occures.
   */
  private DefaultModuleInfo readExternalModule(final ReaderHelper reader)
      throws IOException
  {
    final DefaultModuleInfo mi = new DefaultModuleInfo();

    while (reader.hasNext())
    {
      final String lastLineRead = reader.next();

      if (Character.isWhitespace(lastLineRead.charAt(0)) == false)
      {
        // break if the current character is no whitespace ...
        reader.pushBack(lastLineRead);
        return mi;
      }

      final String line = lastLineRead.trim();
      final String key = parseKey(line);
      if (key != null)
      {
        final String b = readValue(reader, parseValue(line));
        if (key.equals("module"))
        {
          mi.setModuleClass(b);
        }
        else if (key.equals("version.major"))
        {
          mi.setMajorVersion(b);
        }
        else if (key.equals("version.minor"))
        {
          mi.setMinorVersion(b);
        }
        else if (key.equals("version.patchlevel"))
        {
          mi.setPatchLevel(b);
        }
      }
    }
    return mi;
  }

  /**
   * Returns the name of this module.
   *
   * @see Module#getName()
   *
   * @return the module name
   */
  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.
   * @see Module#getDescription()
   *
   * @return the description of the module.
   */
  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.
   *
   * @see Module#getProducer()
   *
   * @return the producer.
   */
  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.
   * @see Module#getRequiredModules()
   *
   * @return an array of all required modules.
   */
  public ModuleInfo[] getRequiredModules()
  {
    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.
   * @see Module#getRequiredModules()
   *
   * @return an array of all required modules.
   */
  public ModuleInfo[] getOptionalModules()
  {
    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.
   * @see java.lang.Object#toString()
   *
   * @return the string representation of this module for debugging purposes.
   */
  public String toString()
  {
    final StringBuffer buffer = new StringBuffer();
    buffer.append("Module : ");
    buffer.append(getName());
    buffer.append("\n");
    buffer.append("ModuleClass : ");
    buffer.append(getModuleClass());
    buffer.append("\n");
    buffer.append("Version: ");
    buffer.append(getMajorVersion());
    buffer.append(".");
    buffer.append(getMinorVersion());
    buffer.append(".");
    buffer.append(getPatchLevel());
    buffer.append("\n");
    buffer.append("Producer: ");
    buffer.append(getProducer());
    buffer.append("\n");
    buffer.append("Description: ");
    buffer.append(getDescription());
    buffer.append("\n");
    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.
   * @return true, if the class could be loaded, false otherwise.
   */
  protected static boolean isClassLoadable(final String name)
  {
    try
    {
      Thread.currentThread().getContextClassLoader().loadClass(name);
      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;
    }
    subSystem.getPackageManager().getPackageConfiguration().load(in);
  }

  /**
   * 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
   */
  protected void performExternalInitialize(final String classname)
      throws ModuleInitializeException
  {
    try
    {
      final Class c = Thread.currentThread().getContextClassLoader().loadClass(classname);
      final ModuleInitializer mi = (ModuleInitializer) c.newInstance();
      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.
   *
   * @param name the new name of the subsystem.
   */
  protected void setSubSystem (final String name)
  {
    this.subsystem = name;
  }
}
