/* * $Id$ * * Copyright (c) 2013-2013 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'jMonkeyEngine' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.simsilica.script; import com.google.common.io.Files; import com.google.common.io.Resources; import com.jme3.app.Application; import com.simsilica.lemur.event.BaseAppState; import groovy.lang.GroovyRuntimeException; import groovy.lang.GroovyShell; import groovy.ui.Console; import java.awt.Component; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; import javax.script.Bindings; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import javax.swing.JFrame; import javax.swing.SwingUtilities; import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.control.CompilerConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author PSpeed */ public class GroovyConsoleState extends BaseAppState { static Logger log = LoggerFactory.getLogger(GroovyConsoleState.class); private static final String PREF_LAST_SCRIPT = "lastScript"; private Console console; private JFrame frame; private Component outputWindow; /** * The script engine to which the console will delegate its * evals. This is setup upon state initialization and is kept * intact across enable/disable. */ private ScriptEngine engine; /** * The bindings that will live as long as the console * state itself. If the script engine is reset these are * the bindings that will be reapplied automatically to the * new engine. */ private Map<String, Object> initialBindings = new HashMap<String, Object>(); /** * These are the live global bindings for the script engine * session. It will live across enable/disable until foreably * reset and these bindings will include anything that the scripts * (API-level or otherwise) have dropped into the environment. */ private Bindings globalBindings = null; /** * The set of scripts to run on engine startup. This sets up the * base environment and API that the console will have available. */ private Map<Object, String> initScripts = new LinkedHashMap<Object, String>(); /** * Default imports that will be added to every script run. */ private List<String> imports = new ArrayList<String>(); private String importString = null; public GroovyConsoleState() { } public void setInitBinding( String key, Object value ) { initialBindings.put(key, value); } public Map<String, Object> getInitBindings() { return initialBindings; } public void toFront() { if( frame != null ) { frame.toFront(); } } public void toggleEnabled() { setEnabled(!isEnabled()); } public void addDefaultImports( String... array ) { for( String s : array ) { imports.add(s); } importString = null; } public List<String> getDefaultImports() { return imports; } protected String getImportString() { if( importString == null ) { StringBuilder sb = new StringBuilder(); for( String s : imports ) { sb.append( "import " + s + ";\n" ); } importString = sb.toString(); } return importString; } protected void resetScriptEngine() { ScriptEngineManager factory = new ScriptEngineManager(); this.engine = factory.getEngineByName("groovy"); globalBindings = engine.createBindings(); engine.setBindings(globalBindings, ScriptContext.ENGINE_SCOPE); // Clear the old imports imports.clear(); importString = null; // Provide direct access to the bindings as a binding. // This can be useful for debugging 'not found' errors // inside scripts. globalBindings.put( "bindings", globalBindings ); // Put all of the caller provided preset bindings globalBindings.putAll(initialBindings); // Run the API Scripts for( Map.Entry<Object, String> e : initScripts.entrySet() ) { try { String script = e.getValue(); script = getImportString() + script; engine.eval(script); } catch( ScriptException ex ) { throw new GroovyRuntimeException("Error executing initialization script:" + e.getKey(), ex); } } } protected void initialize( Application app ) { resetScriptEngine(); } @Override protected void cleanup( Application app ) { globalBindings = null; engine = null; } public void addInitializationScript( File f ) { try { String script = Files.toString(f, Charset.forName("UTF-8")); script = getImportString() + script; initScripts.put(f, script); } catch( IOException e ) { throw new RuntimeException("Error reading:" + f, e); } } public void addInitializationScript( URL resource ) { try { String script = Resources.toString( resource, Charset.forName("UTF-8")); initScripts.put(resource, script); } catch( IOException e ) { throw new RuntimeException("Error reading:" + resource, e); } } @Override public void update( float tpf ) { if( frame != null && !frame.isDisplayable() ) { setEnabled(false); } } protected void enable() { console = new Console(); console.setShell(new EnhancedShell(console.getShell())); //, scriptList)); console.run(); // See if we have any script text from last time Preferences prefs = Preferences.userNodeForPackage(getClass()); final String lastText = prefs.get(PREF_LAST_SCRIPT, null); if( lastText != null ) { SwingUtilities.invokeLater( new Runnable() { public void run() { console.getInputArea().setText(lastText); } }); } outputWindow = console.getOutputWindow(); frame = (JFrame)console.getFrame(); GroovyShell shell = console.getShell(); // So now that the console has been "run" we need to set the script // engine's stdout to the latest stdout. This is done through // jsr223's ScriptContext. Many Bothans died to bring us this // information. ScriptContext context = engine.getContext(); context.setWriter(new PrintWriter(System.out)); } protected void disable() { // See if we can grab the text String text = console.getInputArea().getText(); if( text.trim().length() > 0 ) { log.info("Saving for next time:\n" + text); // Save it for next time Preferences prefs = Preferences.userNodeForPackage(getClass()); prefs.put(PREF_LAST_SCRIPT, text); try { prefs.flush(); } catch( BackingStoreException e ) { log.warn( "Error saving last script to preferences", e ); } } if( frame != null && frame.isDisplayable() ) { console.exit(null); } } public class ScriptCallable implements Callable { private String scriptText; public ScriptCallable( String scriptText ) { this.scriptText = getImportString() + scriptText; } public Object call() throws ScriptException { return engine.eval(scriptText); } } public class EnhancedShell extends GroovyShell { public EnhancedShell( GroovyShell inherit ) { super( inherit.getClassLoader().getParent(), inherit.getContext(), new CompilerConfiguration() ); } @Override public Object run(String scriptText, String fileName, List list) throws CompilationFailedException { // Evaluate the script on the render thread Future future = getApplication().enqueue(new ScriptCallable(scriptText)); try { // And we wait for it. Object result = future.get(); //System.out.println( " done-------" ); //System.out.println( " globalBindings:" + globalBindings ); //System.out.println( " contextVariables:" + getContext().getVariables() ); return result; } catch( InterruptedException e ) { throw new GroovyRuntimeException("Interrupted executing script", e); } catch( ExecutionException e ) { throw new GroovyRuntimeException("Error executing script", e.getCause()); } } } }