content: /*
   *    This program is free software; you can redistribute it and/or modify
   *    it under the terms of the GNU General Public License as published by
   *    the Free Software Foundation; either version 2 of the License, or
   *    (at your option) any later version.
   *
   *    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 General Public License for more details.
   *
   *    You should have received a copy of the GNU General Public License
   *    along with this program; if not, write to the Free Software
   *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
   */
  
  /*
   * Script.java
   * Copyright (C) 2009 University of Waikato, Hamilton, New Zealand
   */
  
  package weka.gui.scripting;
  
  import weka.core.Option;
  import weka.core.OptionHandler;
  import weka.core.SerializedObject;
  import weka.core.Utils;
  import weka.core.WekaException;
  import weka.gui.ExtensionFileFilter;
  import weka.gui.scripting.event.ScriptExecutionEvent;
  import weka.gui.scripting.event.ScriptExecutionListener;
  import weka.gui.scripting.event.ScriptExecutionEvent.Type;
  
  import java.io.File;
  import java.io.Serializable;
  import java.util.Enumeration;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.Vector;
  
  import javax.swing.event.DocumentEvent;
  import javax.swing.event.DocumentListener;
  import javax.swing.text.Document;
  
  /**
   * A simple helper class for loading, saving scripts.
   * 
   * @author  fracpete (fracpete at waikato dot ac dot nz)
   * @version $Revision: 5142 $
   */
  public abstract class Script
    implements OptionHandler, Serializable {
  
    /** for serialization. */
    private static final long serialVersionUID = 5053328052680586401L;
  
    /**
     * The Thread for running a script.
     * 
     * @author  fracpete (fracpete at waikato dot ac dot nz)
     * @version $Revision: 5142 $
     */
    public abstract static class ScriptThread
      extends Thread {
      
      /** the owning script. */
      protected Script m_Owner;
      
      /** commandline arguments. */
      protected String[] m_Args;
      
      /** whether the thread was stopped. */
      protected boolean m_Stopped;
      
      /**
       * Initializes the thread.
       * 
       * @param owner	the owning script
       * @param args	the commandline arguments
       */
      public ScriptThread(Script owner, String[] args) {
        super();
        
        m_Owner = owner;
        m_Args  = args.clone();
      }
  
      /**
       * Returns the owner.
       * 
       * @return		the owning script
       */
      public Script getOwner() {
        return m_Owner;
      }
      
      /**
       * Returns the commandline args.
       * 
       * @return		the arguments
       */
      public String[] getArgs() {
        return m_Args;
      }
      
      /**
       * Performs the actual run.
       */
      protected abstract void doRun();
      
      /**
       * Executes the script.
       */
      public void run() {
        m_Stopped = false;
        
        getOwner().notifyScriptFinishedListeners(new ScriptExecutionEvent(m_Owner, Type.STARTED));
        try {
  	doRun();
  	if (!m_Stopped)
  	  getOwner().notifyScriptFinishedListeners(new ScriptExecutionEvent(m_Owner, Type.FINISHED));
        }
        catch (Exception e) {
  	e.printStackTrace();
  	getOwner().notifyScriptFinishedListeners(new ScriptExecutionEvent(m_Owner, Type.ERROR, e));
        }
        getOwner().m_ScriptThread = null;
      }
      
      /**
       * Stops the script execution.
       */
      public void stopScript() {
        if (isAlive()) {
  	m_Stopped = true;
  	try {
  	  stop();
  	}
  	catch (Exception e) {
  	  // ignored
  	}
        }
      }
    }
  
    /** the backup extension. */
    public final static String BACKUP_EXTENSION = ".bak";
    
    /** the document this script is a wrapper around. */
    protected Document m_Document;
    
    /** the filename of the script. */
    protected File m_Filename;
    
    /** the newline used on this platform. */
    protected String m_NewLine;
    
    /** whether the script is modified. */
    protected boolean m_Modified;
    
    /** the current script thread. */
    protected transient ScriptThread m_ScriptThread;
    
    /** optional listeners when the script finishes. */
    protected HashSet<ScriptExecutionListener> m_FinishedListeners;
    
    /**
     * Initializes the script.
     */
    public Script() {
      this(null);
    }
    
    /**
     * Initializes the script.
     * 
     * @param doc		the document to use as basis
     */
    public Script(Document doc) {
      this(doc, null);
    }
    
    /**
     * Initializes the script. Automatically loads the specified file, if not
     * null.
     * 
     * @param doc		the document to use as basis
     * @param file	the file to load (if not null)
     */
    public Script(Document doc, File file) {
      initialize();
      
      m_Document = doc;
      
      if (m_Document != null) {
        m_Document.addDocumentListener(new DocumentListener() {
  	public void changedUpdate(DocumentEvent e) {
  	  m_Modified = true;
  	}
  	public void insertUpdate(DocumentEvent e) {
  	  m_Modified = true;
  	}
  	public void removeUpdate(DocumentEvent e) {
  	  m_Modified = true;
  	}
        });
      }
      
      if (file != null)
        open(file);
    }
    
    /**
     * Initializes the script.
     */
    protected void initialize() {
      m_Filename          = null;
      m_NewLine           = System.getProperty("line.separator");
      m_Modified          = false;
      m_ScriptThread      = null;
      m_FinishedListeners = new HashSet<ScriptExecutionListener>();
    }
  
    /**
     * Returns an enumeration describing the available options.
     *
     * @return an enumeration of all the available options
     */
    public Enumeration listOptions() {
      return new Vector().elements();
    }
  
    /**
     * Parses a given list of options. 
     *
     * @param options 	the list of options as an array of strings
     * @throws Exception 	if an option is not supported
     */
    public void setOptions(String[] options) throws Exception {
    }
  
    /**
     * Gets the current settings of the script.
     *
     * @return 		an array of strings suitable for passing to setOptions
     */
    public String[] getOptions() {
      return new String[0];
    }
    
    /**
     * Returns the extension filters for this type of script.
     * 
     * @return		the filters
     */
    public abstract ExtensionFileFilter[] getFilters();
    
    /**
     * Returns the default extension. Gets automatically added to files if
     * their name doesn't end with this.
     * 
     * @return		the default extension (incl. the dot)
     * @see		#saveAs(File)
     */
    public abstract String getDefaultExtension();
    
    /**
     * Returns the current filename.
     * 
     * @return		the filename, null if no file loaded/saved
     */
    public File getFilename() {
      return m_Filename;
    }
    
    /**
     * Returns the new line string in use.
     * 
     * @return		the new line string
     */
    public String getNewLine() {
      return m_NewLine;
    }
    
    /**
     * Returns whether the script is modified.
     * 
     * @return		true if the script is modified
     */
    public boolean isModified() {
      return m_Modified;
    }
    
    /**
     * Returns the content.
     * 
     * @return		the content or null in case of an error
     */
    public String getContent() {
      String	result;
      
      if (m_Document == null)
        return "";
      
      try {
        synchronized(m_Document) {
  	result = m_Document.getText(0, m_Document.getLength());
        }
      }
      catch (Exception e) {
        e.printStackTrace();
        result = null;
      }
      
      return result;
    }
    
    /**
     * Sets the content.
     * 
     * @param value	the new content
     */
    public void setContent(String value) {
      if (m_Document == null)
        return;
      
      try {
        m_Document.insertString(0, value, null);
      }
      catch (Exception e) {
        e.printStackTrace();
      }
    }
  
    /**
     * Checks whether the extension of the file is a known one.
     * 
     * @param file	the file to check
     * @return		true if the exetnsion is known
     */
    protected boolean checkExtension(File file) {
      boolean			result;
      int				i;
      int				n;
      ExtensionFileFilter[]	filters;
      String[]			exts;
  
      result = false;
      filters  = getFilters();
      for (i = 0; i < filters.length; i++) {
        exts = filters[i].getExtensions();
        for (n = 0; n < exts.length; n++) {
  	if (file.getName().endsWith(exts[n])) {
  	  result = true;
  	  break;
  	}
        }
        if (result)
  	break;
      }
      
      return result;
    }
    
    /**
     * Empties the document.
     */
    public void empty() {
      if (m_Document != null) {
        try {
  	m_Document.remove(0, m_Document.getLength());
        }
        catch (Exception e) {
  	// ignored
        }
      }
      
      m_Modified = false;
      m_Filename = null;
    }
    
    /**
     * Tries to open the file.
     * 
     * @param file	the file to open
     * @return		true if successfully read
     */
    public boolean open(File file) {
      boolean	result;
      String	content;
  
      if (m_Document == null)
        return true;
      
      // Warn if extension unwknown
      if (!checkExtension(file))
        System.err.println("Extension of file '" + file + "' is unknown!");
      
      try {
        // clear old content
        m_Document.remove(0, m_Document.getLength());
        
        // add new content
        content = ScriptUtils.load(file);
        if (content == null)
  	throw new WekaException("Error reading content of file '" + file + "'!");
        m_Document.insertString(0, content, null);
        
        m_Modified = false;
        m_Filename = file;
        result     = true;
      }
      catch (Exception e) {
        e.printStackTrace();
        try {
  	m_Document.remove(0, m_Document.getLength());
        }
        catch (Exception ex) {
  	// ignored
        }
        result     = false;
        m_Filename = null;
      }
      
      return result;
    }
    
    /**
     * Saves the file under with the current filename.
     * 
     * @return		true if successfully written
     */
    public boolean save() {
      if (m_Filename == null)
        return false;
      else
        return saveAs(m_Filename);
    }
    
    /**
     * Saves the file under with the given filename (and updates the internal
     * filename).
     * 
     * @param file	the filename to write the content to
     * @return		true if successfully written
     */
    public boolean saveAs(File file) {
      boolean	result;
      File	backupFile;
      
      if (m_Document == null)
        return true;
      
      // correct extension?
      if (!checkExtension(file))
        file = new File(file.getPath() + getDefaultExtension());
  
      // backup previous file
      if (file.exists()) {
        backupFile = new File(file.getPath() + BACKUP_EXTENSION);
        try {
  	ScriptUtils.copy(file, backupFile);
        }
        catch (Exception e) {
  	e.printStackTrace();
        }
      }
      
      // save current content
      try {
        result     = ScriptUtils.save(file, m_Document.getText(0, m_Document.getLength()));
        m_Filename = file;
        m_Modified = false;
      }
      catch (Exception e) {
        e.printStackTrace();
        result = false;
      }
      
      return result;
    }
  
    /**
     * Returns whether scripts can be executed.
     * 
     * @return		true if scripts can be executed
     */
    protected abstract boolean canExecuteScripts();
  
    /**
     * Returns a new thread to execute.
     * 
     * @param args	optional commandline arguments
     * @return		the new thread object
     */
    public abstract ScriptThread newThread(String[] args);
    
    /**
     * Performs pre-execution checks:
     * <ul>
     * 	<li>whether a script is currently running.</li>
     * 	<li>whether script has changed and needs saving</li>
     * 	<li>whether a filename is set (= empty content)</li>
     * </ul>
     * Throws exceptions if checks not met.
     * 
     * @param args	optional commandline arguments
     * @throws Exception	if checks fail
     */
    protected void preCheck(String[] args) throws Exception {
      if (m_ScriptThread != null)
        throw new Exception("A script is currently running!");
      if (m_Modified)
        throw new Exception("The Script has been modified!");
      if (m_Filename == null)
        throw new Exception("The Script contains no content?");
    }
    
    /**
     * Executes the script.
     * 
     * @param args	optional commandline arguments
     */
    protected void execute(String[] args) {
      m_ScriptThread = newThread(args);
      try {
        m_ScriptThread.start();
      }
      catch (Exception e) {
        e.printStackTrace();
      }
    }
    
    /**
     * Executes the script.
     * 
     * @param args	optional commandline arguments, can be null
     * @throws Exception	if checks or execution fail
     */
    public void start(String[] args) throws Exception {
      if (args == null)
        args = new String[0];
      
      preCheck(args);
      
      execute(args);
    }
    
    /**
     * Stops the execution of the script.
     */
    public void stop() {
      if (isRunning()) {
        m_ScriptThread.stopScript();
        m_ScriptThread = null;
        notifyScriptFinishedListeners(new ScriptExecutionEvent(this, Type.STOPPED));
      }
    }
    
    /**
     * Executes the script without loading it first.
     * 
     * @param file	the script to execute
     * @param args	the commandline parameters for the script
     */
    public void run(File file, String[] args) {
      Script	script;
      
      try {
        script = (Script) new SerializedObject(this).getObject();
        script.m_Filename = file;
        script.m_Modified = false;
        script.start(args);
      }
      catch (Exception e) {
        e.printStackTrace();
      }
    }
    
    /**
     * Returns whether the script is still running.
     * 
     * @return		true if the script is still running
     */
    public boolean isRunning() {
      return (m_ScriptThread != null);
    }
    
    /**
     * Adds the given listener to its internal list.
     * 
     * @param l		the listener to add
     */
    public void addScriptFinishedListener(ScriptExecutionListener l) {
      m_FinishedListeners.add(l);
    }
    
    /**
     * Removes the given listener from its internal list.
     * 
     * @param l		the listener to remove
     */
    public void removeScriptFinishedListener(ScriptExecutionListener l) {
      m_FinishedListeners.remove(l);
    }
    
    /**
     * Notifies all listeners.
     * 
     * @param e		the event to send to all listeners
     */
    protected void notifyScriptFinishedListeners(ScriptExecutionEvent e) {
      Iterator<ScriptExecutionListener>	iter;
      
      iter = m_FinishedListeners.iterator();
      while (iter.hasNext())
        iter.next().scriptFinished(e);
    }
    
    /**
     * Returns the content as string.
     * 
     * @return		the current content
     */
    public String toString() {
      String	result;
      
      try {
        if (m_Document == null)
  	result = "";
        else
  	result = m_Document.getText(0, m_Document.getLength());
      }
      catch (Exception e) {
        result = "";
      }
      
      return result.toString();
    }
  
    /**
     * Make up the help string giving all the command line options.
     *
     * @param script 	the script to include options for
     * @return 		a string detailing the valid command line options
     */
    protected static String makeOptionString(Script script) {
      StringBuffer 	result;
      Enumeration 	enm;
      Option 		option;
      
      result = new StringBuffer("");
  
      result.append("\nHelp requested:\n\n");
      result.append("-h or -help\n");
      result.append("\tDisplays this help screen.\n");
      result.append("-s <file>\n");
      result.append("\tThe script to execute.\n");
  
      enm = script.listOptions();
      while (enm.hasMoreElements()) {
        option = (Option) enm.nextElement();
        result.append(option.synopsis() + '\n');
        result.append(option.description() + "\n");
      }
  
      result.append("\n");
      result.append("Any additional options are passed on to the script as\n");
      result.append("command-line parameters.\n");
      result.append("\n");
      
      return result.toString();
    }
    
    /**
     * Runs the specified script. All options that weren't "consumed" (like 
     * "-s" for the script filename), will be used as commandline arguments for 
     * the actual script.
     * 
     * @param script	the script object to use
     * @param args	the commandline arguments
     * @throws Exception	if execution fails
     */
    public static void runScript(Script script, String[] args) throws Exception {
      String		tmpStr;
      File		scriptFile;
      Vector<String>	options;
      int			i;
      
      if (Utils.getFlag('h', args) || Utils.getFlag("help", args)) {
        System.out.println(makeOptionString(script));
      }
      else {
        // process options
        tmpStr = Utils.getOption('s', args);
        if (tmpStr.length() == 0)
          throw new WekaException("No script supplied!");
        else
  	scriptFile = new File(tmpStr);
        script.setOptions(args);
        
        // remove empty elements from array
        options = new Vector<String>();
        for (i = 0; i < args.length; i++) {
  	if (args[i].length() > 0)
  	  options.add(args[i]);
        }
        
        // run script
        script.run(scriptFile, options.toArray(new String[options.size()]));
      }
    }
  }
  