0,0 → 1,384 |
package net.outlyer.plugins; |
|
/* |
* Copyright (c) 2008, Toni Corvera. 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. |
* |
* THIS SOFTWARE IS PROVIDED "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. |
*/ |
|
// $Id$ |
|
// Related reference: |
// http://java.sun.com/developer/technicalArticles/J2SE/Desktop/scripting/ |
|
import java.io.IOException; |
import java.io.Reader; |
import java.lang.reflect.Method; |
import java.net.URI; |
import java.util.Collection; |
import java.util.HashMap; |
import java.util.LinkedList; |
import java.util.Map; |
import javax.script.Bindings; |
import javax.script.ScriptContext; |
import javax.script.ScriptEngine; |
import javax.script.ScriptEngineManager; |
import javax.script.ScriptException; |
|
/** |
* Implementation of the Sandbox interface |
*/ |
class SandboxImpl implements Sandbox { |
private final Bindings bindings; |
private final URI pluginUri; |
private static int nextUniqueVarNum; |
private Map<String, Collection<Runnable>> hooks; |
private final String boilerPlate; |
private int execution = 0; |
private final PluginProperties properties; |
private transient ScriptEngine currentEngine = null; |
private static final String END_KEY = "e"; |
private static final boolean debug; |
private final PluginEnvironment pE; |
|
private static class ExecResult { |
final ScriptEngine rhino; |
final Object evalResult; |
|
ExecResult(final ScriptEngine rhino_, final Object evalResult_) { |
rhino = rhino_; |
evalResult = evalResult_; |
} |
} |
|
static { |
nextUniqueVarNum = (int) (1193 * (1+Math.random())); |
debug = System.getProperty("debug", "0").equals("1"); |
} |
|
{ |
hooks = new HashMap<String, Collection<Runnable>>(); |
hooks.put(END_KEY, new LinkedList<Runnable>()); |
} |
|
protected SandboxImpl(final PluginEnvironment pe, final URI plugin, |
final Bindings bs, final PluginProperties pp) { |
this(pe, plugin, bs, pp, null); |
} |
|
protected SandboxImpl(final PluginEnvironment pe, final URI plugin, |
final Bindings bs, |
final PluginProperties pp, final String boilerPlate) { |
bindings = bs; |
pluginUri = plugin; // if null => sandbox without file |
this.boilerPlate = boilerPlate; |
properties = pp; |
pE = pe; |
} |
|
public void addEndHook(Runnable r) { |
hooks.get(END_KEY).add(r); |
} |
|
/** |
* Creates a unique variable name. |
*/ |
private static String uniqueVarName() { |
return new StringBuilder("$_").append(nextUniqueVarNum+=1193).append("_$").toString(); |
} |
|
/** |
* Creates a unique variable name including namespace |
* @see #namespace |
* @see #uniqueVarName |
*/ |
private static String uniqueFQVarName() { |
return Internal.getInternalObjectFieldName(uniqueVarName()); |
} |
|
/** |
* Gets an engine. |
* If the system property "debug" is set to "1" the returned engine |
* will dump extra information to the console |
* @return New engine |
*/ |
private static ScriptEngine getEngine() { |
final ScriptEngine se = new ScriptEngineManager().getEngineByName("rhino"); |
if (debug) { |
return new EngineSink(se); |
} |
return se; |
} |
|
private ExecResult execute(final String prependText, |
final String appendText, |
final Map<String,Object> extraBindings) |
throws PluginExecutionException { |
final ScriptEngine rhino = getEngine(); |
currentEngine = rhino; |
|
if (null != extraBindings) { |
bindings.putAll(extraBindings); |
} |
|
Object result = null; |
|
try { |
rhino.setBindings(bindings, ScriptContext.ENGINE_SCOPE); |
|
execution++; |
|
if (null != boilerPlate) { |
rhino.eval(boilerPlate); |
} |
|
// $net.outlyer.runtime.sandbox Contains a reference to this object |
// Implementation note: PluginEnvironment.EXPORTED_SANDBOX_VARIABLE |
// should have the same name |
final NamespaceContainer.$Net_Outlyer $nspc = new NamespaceContainer.$Net_Outlyer(pE, this); |
assert this == $nspc.runtime.sandbox; |
rhino.put(Internal.getReservedObjectName(), $nspc); |
// ....internal can be used to store random internal data, it's |
// a dynamic object so that fields can be created as needed |
rhino.eval(Internal.getInternalObjectName()+Internal.getInternalInitialisation()); |
|
if (null != prependText) { |
rhino.eval(prependText); |
} |
if (null != pluginUri) { |
result = rhino.eval(new PluginReader(pluginUri)); |
} |
else { |
result = null; |
} |
|
if (null != appendText) { |
rhino.eval(appendText); |
} |
|
// Execute any end hooks |
for (final Runnable hook : hooks.get(END_KEY)) { |
hook.run(); |
} |
} |
catch (final IOException e) { |
throw new PluginExecutionException("I/O Error: " + e.getMessage(), e); |
} |
catch (final ScriptException e) { |
throw new PluginExecutionException("Script exception: " + e.getMessage(), e); |
} |
finally { |
currentEngine = null; |
} |
|
return new ExecResult(rhino, result); |
} |
|
public void execute() throws PluginExecutionException { |
execute(null, null, null); |
} |
|
ScriptEngine executeAndGetEngine() throws PluginExecutionException { |
return execute(null, null, null).rhino; |
} |
|
public <T> T createDelayedImplementation(final Class<T> c, |
final String objectName) |
throws PluginExecutionException { |
if (null == c) { |
throw new IllegalArgumentException("Can't instantiate null"); |
} |
if (!c.isInterface()) { |
throw new IllegalArgumentException("Can only create delayed " + |
"implementations for interfaces"); |
} |
if (null == objectName) { |
throw new IllegalArgumentException("Implementor's name can't be null"); |
} |
final StringBuilder preCode = new StringBuilder(); |
|
if (debug) { |
preCode.append("// Forward declaration for delayed implementation:\n"); |
} |
preCode.append("var ").append(objectName).append(" = {\n"); |
for (final Method method : c.getMethods()) { |
if (method.getDeclaringClass() == Object.class) { // Implement everything, including hierarchies |
continue; |
} |
preCode.append(method.getName()).append(": null,\n"); |
} |
preCode.append("};\n"); |
|
final ScriptEngine rhino = execute(preCode.toString(), null, null).rhino; |
|
try { |
final StringBuilder retriever = new StringBuilder(); |
retriever.append("new ") |
.append(c.getCanonicalName()).append("(").append(objectName).append(");"); |
if (debug) { |
retriever.append("// <== Actual delayed implementation creation"); |
} |
|
// Could be safely instantiated before actually being assigned IF |
// plugins were required to define their methods with |
// objectName.methodName = function() {} ... |
// but would file with |
// objectName = { methodName: function() {}, ... } |
// Creating the instance as postCode does the job for both |
|
try { |
currentEngine = rhino; |
// eval() returns the value of the last evaluated line/command |
assert (c.isInstance(rhino.eval(retriever.toString()))); |
return (T) rhino.eval(retriever.toString()); |
} |
finally { |
currentEngine = null; |
} |
} |
catch (final ScriptException e) { |
throw new PluginExecutionException("Failed to create the delayed " + |
"implementation instance: " + e.getMessage(), e); |
} |
} |
|
/** |
* {@inheritDoc} |
*/ |
public <T> T createDelayedImplementation(final Class<T> c, |
final Map<Method, String> map) |
throws PluginExecutionException { |
if (null == c) { |
throw new IllegalArgumentException("Can't instantiate null"); |
} |
if (!c.isInterface()) { |
throw new IllegalArgumentException("Can only create delayed " + |
"implementations for interfaces"); |
} |
if (null == map || map.isEmpty()) { |
throw new IllegalArgumentException("No mappings; creating a delayed " + |
"implementation with no mappings " + |
"is equivalent to creating" + |
"the object from Java"); |
} |
|
final StringBuffer preCode = new StringBuffer(); |
|
final String var = uniqueFQVarName(); |
|
preCode.append(var).append(" = {\n"); |
|
for (final Method method : map.keySet()) { |
preCode.append(method.getName()).append(": ") |
.append(map.get(method)) |
.append("\n,\n"); |
} |
|
preCode.append("};\n"); |
|
final ScriptEngine rhino = execute(preCode.toString(), null, null).rhino; |
|
try { |
final StringBuilder retriever = new StringBuilder(); |
retriever.append("new ") |
.append(c.getCanonicalName()).append("(").append(var).append(");"); |
if (debug) { |
retriever.append("// <== Actual delayed implementation creation"); |
} |
|
// Could be safely instantiated before actually being assigned IF |
// plugins were required to define their methods with |
// objectName.methodName = function() {} ... |
// but would file with |
// objectName = { methodName: function() {}, ... } |
// Creating the instance as postCode does the job for both |
|
try { |
currentEngine = rhino; |
// eval() returns the value of the last evaluated line/command |
assert (c.isInstance(rhino.eval(retriever.toString()))); |
return (T) rhino.eval(retriever.toString()); |
} |
finally { |
currentEngine = null; |
} |
} |
catch (final ScriptException e) { |
throw new PluginExecutionException("Failed to create the delayed " + |
"implementation instance: " + e.getMessage(), e); |
} |
} |
|
public BasePluginObject getPluginObject() throws PluginExecutionException { |
execute(); |
assert bindings.get("plugin") instanceof BasePluginObject; |
return (BasePluginObject) bindings.get("plugin"); |
} |
|
public int getExecution() { |
return execution; |
} |
|
/** |
* {@inheritDoc} |
*/ |
public String getPluginName() { |
if (null == properties.name) { |
final String[] elements = loadedFrom().toString().split("/"); |
return elements[elements.length-1]; |
} |
return properties.name; |
} |
|
/** |
* {@inheritDoc} |
*/ |
public URI loadedFrom() { |
return pluginUri.normalize(); // FIXME: Return a copy |
} |
|
public ScriptEngine getCurrentEngine() { |
return currentEngine; |
} |
|
/** |
* {@inheritDoc} |
*/ |
public Object inject(final Reader code) throws IllegalStateException, PluginException { |
synchronized (currentEngine) { // Probably not needed |
if (null == currentEngine) { |
throw new IllegalStateException("Can only inject code while a sandbox is executing"); |
} |
try { |
return currentEngine.eval(code); |
} |
catch (final ScriptException e) { |
throw new PluginException("Failed to inject code: " + e.getMessage(), e); |
} |
} |
} |
|
@Override public String toString() { |
return "Sandbox("+getClass().getSimpleName()+")["+loadedFrom()+"]"; |
} |
|
|
} |
Property changes: |
Added: svn:keywords |
+Rev Id Date |
\ No newline at end of property |