/*
* 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 {
Properties p = new Properties();
InputStream in = new FileInputStream(GLOBAL_PROPERTIES_FILE);
p.load(in);
in.close();
global = p;
} catch (IOException ioe) {
// nop, remains null
}
}
// --- instance members --------------------------------------------------
protected Collection<FrinikaScript> scripts;
protected ProjectContainer project;
protected Properties persistent = null; // init on first get (usual case will remain null)
/**
* Constructor. One instance per ProjectContainer (1:1).
*
* @param project
*/
public FrinikaScriptingEngine(ProjectContainer project) {
this.project = project;
scripts = new ArrayList<FrinikaScript>();
}
public Collection<FrinikaScript> getScripts() {
return scripts;
}
public void addScript(FrinikaScript script) {
if ( ! scripts.contains(script) ) {
scripts.add(script);
}
}
public void removeScript(FrinikaScript script) {
scripts.remove(script);
}
public Properties getPersistentProperties() {
if (persistent == null) {
persistent = new Properties();
}
return persistent;
}
public FrinikaScript loadScript(File file) throws IOException {
String source = loadString(file);
DefaultFrinikaScript script = new DefaultFrinikaScript();
if(file.getName().endsWith("groovy")) {
script.setLanguage(FrinikaScript.LANGUAGE_GROOVY);
}
else {
script.setLanguage(FrinikaScript.LANGUAGE_JAVASCRIPT);
}
script.setSource(source);
String name = file.getAbsolutePath();
script.setFilename(name);
this.addScript(script);
return script;
}
public void saveScript(FrinikaScript script, File file) throws IOException {
saveString(script.getSource(), file);
if (script instanceof DefaultFrinikaScript) {
((DefaultFrinikaScript)script).setFilename(file.getAbsolutePath());
}
}
public static String loadString(File file) throws IOException { // TODO move to global utilities-class
long len = file.length();
char[] c = new char[(int)len];
FileReader f = new FileReader(file);
f.read(c);
f.close();
return new String(c);
}
public static void saveString(String s, File file) throws IOException { // TODO move to global utilities-class
FileWriter f = new FileWriter(file);
f.write(s);
f.close();
}
/*private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
in.defaultReadObject();
}*/
private void writeObject(ObjectOutputStream out) throws ClassNotFoundException, IOException {
// remove all from open scripts which are not serializable (i.e. Preset-Scripts)
Collection<FrinikaScript> c = new ArrayList<FrinikaScript>( scripts );
for (FrinikaScript script : scripts) {
if ( ! (script instanceof Serializable) ) {
c.remove(script);
}
}
Collection<FrinikaScript> backup = scripts;
scripts = c;
out.defaultWriteObject();
scripts = backup;
}
}