gt: 			Properties p = new Properties();
context: /*
   * Created on Feb 16, 2007
   *
   * Copyright (c) 2007 Jens Gulden
   * 
   * http://www.frinika.com
   * 
   * This file is part of Frinika.
   * 
   * Frinika 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.
  
   * Frinika 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 Frinika; if not, write to the Free Software
   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   */
  
  package com.frinika.project.scripting;
  
  import org.mozilla.javascript.*;
  
  import com.frinika.global.FrinikaConfig;
  import com.frinika.project.ProjectContainer;
  import com.frinika.project.gui.ProjectFrame;
  import com.frinika.project.scripting.gui.ScriptingDialog;
  import com.frinika.project.scripting.javascript.JavascriptScope;
  import com.frinika.sequencer.model.MultiEvent;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.FileReader;
  import java.io.FileWriter;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.ObjectOutputStream;
  import java.io.OutputStream;
  import java.io.Serializable;
  import java.io.Writer;
  import java.util.*;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  import javax.script.ScriptEngine;
  import javax.script.ScriptEngineManager;
  import javax.script.ScriptException;
  
  /**
   * Scriptig-engine for Frinika. The static elements of this class handle
   * application-wide script-execution. the instance elements of this class
   * provide a script container which can be (de)serialized along with
   * ProjectContainer. Although the execution of scripts is handled globally,
   * each script runs inside it's associated project's focus, and the set of
   * scripts which are currently loaded and thus available for execution is
   * project-specific.
   * 
   * The architecture allows for deploying multiple scripting languages, but
   * the only language used for scripting now is JavaScript, and there are no
   * plans to add support for any other languages. 
   * The actual interfacing between the 'Frinika world' and the 'JavaScript
   * world' is done by class com.frinika.project.scripting.javascript.JavascriptScope.
   * 
   * @see com.frinika.project.scripting.javascript.JavascriptScope
   * @author Jens Gulden
   */
  public class FrinikaScriptingEngine implements ScriptContainer, Serializable {
  	
  	private static final long serialVersionUID = 1L;
  	
  	public final static File GLOBAL_PROPERTIES_FILE = new File(FrinikaConfig.SCRIPTS_DIRECTORY, "scripting.properties");
  	
  	// --- static ---
  	
  	transient static Map<FrinikaScript, ScriptThread> runningScripts = new HashMap<FrinikaScript, ScriptThread>();
  	transient static Properties global = null;
  	transient private static Collection<ScriptListener> scriptListeners = new HashSet<ScriptListener>(); 
  	
  	public static void executeScript(FrinikaScript script, ProjectFrame frame, ScriptingDialog dialog) {
  		ScriptThread thread = new ScriptThread(script, frame, dialog);
  		thread.start();
  	}
  	
  	static Object runScript(FrinikaScript script, ProjectFrame frame, ScriptingDialog dialog) { // called from SriptThread
  		int language = script.getLanguage();
  		String name = script.getName();
  		
                  if (language != FrinikaScript.LANGUAGE_JAVASCRIPT
                          && language != FrinikaScript.LANGUAGE_GROOVY) {
  			System.out.println("cannot execute script " + name +": unsupported language " + language);
  			return null;
  		}
  		
                  System.out.println("Executing script '"+name+"'...");
  		String source = script.getSource();
  		frame.getProjectContainer().getEditHistoryContainer().mark("Script "+name);
  		
  		Collection<MultiEvent> events = frame.getProjectContainer().getMidiSelection().getSelected();
  		SortedSet<MultiEvent> clones = new TreeSet<MultiEvent>();
  		
  		// work on clones
  		if (events != null) {
  			for (MultiEvent ev : events) {
  				try {
  					MultiEvent clone = (MultiEvent) (ev.clone());
  					clones.add(clone);
  				} catch (CloneNotSupportedException cnse) {
  					cnse.printStackTrace();
  				}
  			}
  		}
  		
  		// do it
                  Object result = null;
                  switch(script.getLanguage()) {
                      case FrinikaScript.LANGUAGE_JAVASCRIPT:
                          result = executeJavascript(source, name, frame, clones, dialog);
                          break;
                      case FrinikaScript.LANGUAGE_GROOVY:
                          result = executeGroovyScript(source, name, frame, clones, dialog);
                          break;
                  }
  		
  		
  		if (events != null) {
  			Iterator<MultiEvent> clonesIterator = clones.iterator();
  			for (MultiEvent ev : events) {
  				ev.getPart().remove(ev);
  				ev.restoreFromClone(clonesIterator.next());
  				ev.getPart().add(ev);
  			}
  		}
  
  		if (result != null) {
  			System.out.println(result.toString());
  		}
  		frame.getProjectContainer().getEditHistoryContainer().notifyEditHistoryListeners();
  		
  		return result;
  	}
  	
  	public static void stopScript(FrinikaScript script) {
  		ScriptThread thread = runningScripts.get(script);
  		if (thread != null) {
  			thread.stop();
  		}
  	}
  	
  	protected static Object executeJavascript(String source, String name, ProjectFrame frame, SortedSet<MultiEvent> events, ScriptingDialog dialog) {
          Context cx = Context.enter();
          try {
          	JavascriptScope scope = new JavascriptScope(cx, frame, events, dialog);
              Object result;
              try {
              	result = cx.evaluateString(scope, source, name, 1, null);
              } catch (Throwable t) {
              	if (t instanceof ThreadDeath) {
              		frame.message("Script execution has been aborted.");
              		result = "";
              	} else {
                  	frame.error(t);
                  	result = null;
              	}
              }
  
              if (result != null) {
              	return cx.toString(result);
              } else {
              	return null;
              }
  
          } finally {
              Context.exit();
          }
  		
  	}
  	
          protected static Object executeGroovyScript(String source, String name, ProjectFrame frame, SortedSet<MultiEvent> events, final ScriptingDialog dialog) {
              ScriptEngineManager factory = new ScriptEngineManager();	                    
              ScriptEngine engine = factory.getEngineByName("groovy");
          
              Writer writer = new Writer() {
  
                  @Override
                  public void write(char[] cbuf, int off, int len) throws IOException {
                      dialog.print(new String(cbuf,off,len));
                  }
  
                  @Override
                  public void flush() throws IOException {                    
                  }
  
                  @Override
                  public void close() throws IOException {                    
                  }
              };
              
              engine.put("projectFrame", frame);        
              engine.getContext().setWriter(writer);
              engine.getContext().setErrorWriter(writer);            
              try {                        
                  return engine.eval(source);
              } catch (ScriptException ex) {
                  frame.message("Script execution has been aborted.");
                  Logger.getLogger(FrinikaScriptingEngine.class.getName()).log(Level.SEVERE, null, ex);
                  return null;
              }
  	}
  	
  	/**
  	 * Adds a ScriptListener. This method is static, as scripts can potentially affect the whole running system,
  	 * across projects, so it doesn't make sense to restrict references to scripts to individual projects.
  	 * 
  	 * @param l
  	 */
  	public static void addScriptListener(ScriptListener l) {
  		scriptListeners.add(l);
  	}
  	
  	public static void removeScriptListener(ScriptListener l) {
  		scriptListeners.remove(l);
  	}
  	
  	/**
  	 * 
  	 * @param script
  	 * @param returnValue if ==this, then 'script has started', else 'script as exited'
  	 */
  	protected static void notifyScriptListeners(FrinikaScript script, Object returnValue) {
  		for (ScriptListener l : scriptListeners) {
  			if (returnValue == script) { // start
  				l.scriptStarted(script);
  			} else {
  				l.scriptExited(script, returnValue); // returnValue may be null to indicate failure
  			}
  		}
  	}
  	
  	public static void globalPut(String variable, String value) {
  		if (global == null) {
  			loadGlobalProperties();
  		}
  		if (global == null) {
  			global = new Properties();
  		}
  		synchronized (global) {
  			global.put(variable, value);
  			try {
  				OutputStream out = new FileOutputStream(GLOBAL_PROPERTIES_FILE);
  				global. store(out, "Frinika Scripting - Global Properties");
  				out.close();
  			} catch (IOException ioe) {
  				ioe.printStackTrace();
  			}
  		}
  	}
  	
  	public static String globalGet(String variable) {
  		if (global == null) {
  			loadGlobalProperties();
  		}
  		if (global != null) {
  			return global.getProperty(variable);
  		} else {
  			return null;
  		}
  	}
  	
  	private static void loadGlobalProperties() {
  		try {
  