0,0 → 1,302 |
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. |
*/ |
|
import net.outlyer.plugins.Sandbox; |
import java.io.File; |
import java.io.IOException; |
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; |
import net.outlyer.plugins.BasePluginObject; |
import net.outlyer.plugins.PluginProperties; |
|
/** |
* 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 static final String END_KEY = "e"; |
|
static { |
nextUniqueVarNum = (int) (1193 * (1+Math.random())); |
} |
|
{ |
hooks = new HashMap<String, Collection<Runnable>>(); |
hooks.put(END_KEY, new LinkedList<Runnable>()); |
} |
|
protected SandboxImpl(final URI plugin, final Bindings bs, final PluginProperties pp) { |
this(plugin, bs, pp, null); |
} |
|
protected SandboxImpl(final URI plugin, final Bindings bs, |
final PluginProperties pp, final String boilerPlate) { |
bindings = bs; |
pluginUri = plugin; |
this.boilerPlate = boilerPlate; |
properties = pp; |
} |
|
public void addEndHook(Runnable r) { |
hooks.get(END_KEY).add(r); |
} |
|
/** |
* Namespace for variables used by the enviroment |
*/ |
private static String namespace() { |
return "$net.outlyer.runtime"; |
} |
|
/** |
* 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 new StringBuilder(namespace()).append(".") |
.append(uniqueVarName()).toString(); |
} |
|
private static ScriptEngine getEngine() { |
return new ScriptEngineManager().getEngineByName("rhino"); |
} |
|
private ScriptEngine execute(boolean appendText, String text, final Map<String,Object> extraBindings) |
throws PluginExecutionException { |
final ScriptEngine rhino = getEngine(); |
|
// First, export in the global namespace the reference to this |
// sandbox. This variable isn't meant to be used directly, it's only |
// set to refer to it from the boilerplate code (AFAIK there's no |
// way to add a binding with a FQ name). |
// TODO: Can a fully qualified name be bound directly? |
final String globalSanboxVarName = uniqueVarName(); |
bindings.put(globalSanboxVarName, this); |
if (null != extraBindings) { |
for (final String key : extraBindings.keySet()) { |
bindings.put(key, extraBindings.get(key)); |
} |
} |
|
// Will contain some biolerplate code used to provide access |
// to the runtime wrapped-environment |
final StringBuilder runtime = new StringBuilder(); |
// $net.outlyer.runtime.sandbox Contains a reference to this object |
// Implementation note: PluginEnvironment.EXPORTED_SANDBOX_VARIABLE should have the same name |
// TODO: ? $net.outlyer.runtime.pluginEnvironment |
runtime.append("var $net = {\n") |
.append(" outlyer : {\n") |
.append(" runtime : {\n") |
.append(" sandbox : ").append(globalSanboxVarName).append("\n") |
.append(" }\n") |
.append(" }\n") |
.append("};\n"); |
|
try { |
rhino.setBindings(bindings, ScriptContext.ENGINE_SCOPE); |
|
execution++; |
|
if (null != boilerPlate) { |
rhino.eval(boilerPlate); |
} |
|
// Create the $net.outlyer.runtime pseudo-namespace... |
rhino.eval(runtime.toString()); |
|
rhino.eval(new PluginReader(pluginUri)); |
|
if (appendText) { |
rhino.eval(text); |
} |
|
// 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); |
} |
|
return rhino; |
} |
|
public void execute() throws PluginExecutionException { |
execute(false, null, null); |
} |
|
public <T> T createDelayedImplementation(Class<T> c, String objectName) |
throws PluginExecutionException { |
final String varImpl = uniqueVarName(); |
|
final StringBuilder code = new StringBuilder(); |
|
code.append(varImpl).append(" = new ") |
.append(c.getCanonicalName()).append("(").append(objectName).append(");"); |
//System.err.println(code.toString()); |
|
final ScriptEngine rhino = execute(true, code.toString(), null); |
|
assert (c.isInstance(rhino.get(varImpl))); |
return (T) rhino.get(varImpl); |
} |
|
|
|
public <T> T createDelayedImplementation(Class<T> c) throws PluginExecutionException { |
return createDelayedImplementation(c, false); |
} |
|
public <T> T createDelayedImplementation(Class<T> c, boolean allowPartial) throws PluginExecutionException { |
return createDelayedImplementation(c, null, allowPartial); |
} |
|
public <T> T createDelayedImplementation(Class<T> interfaceClass, final T fallbackObject) throws PluginExecutionException { |
if (null == fallbackObject) { |
throw new IllegalArgumentException("Can't use a null fallback object"); |
} |
return createDelayedImplementation(interfaceClass, fallbackObject, true); |
} |
|
private <T> T createDelayedImplementation(Class<T> c, T fallback, boolean allowPartial) throws PluginExecutionException { |
final StringBuilder hack = new StringBuilder(); |
|
final String var = uniqueFQVarName(); |
// Apparently only global namespace objects can be retrieved with get() |
// so to simplify retrieval a global variable is used |
final String varImpl = uniqueVarName(); |
|
hack.append(var).append("={\n"); |
|
final StringBuilder script = new StringBuilder(); |
|
final String varFallback = uniqueVarName(); |
|
for (final Method m : c.getMethods()) { |
if (m.getDeclaringClass() != c) { |
continue; |
} |
|
final String implFuncName = c.getSimpleName()+"_"+m.getName(); |
|
if (allowPartial) { |
// This makes undefined functions to be mapped to undefined |
// variables (otherwise the interface instantation would fail) |
// Apparently defining a var with the same name as a function |
// produces no error, while checking for an undefined |
// variable/method does fail. |
// FIXME: Might be cleaner by handling "undefined" |
script.append("var ").append(implFuncName).append(";\n"); |
|
if (null != fallback) { |
script.append("if (!").append(implFuncName).append(")\n\t") |
.append(implFuncName).append("=") |
.append(varFallback).append(".").append(m.getName()) |
.append("\n"); |
} |
// script.append("if (!").append(implFuncName).append(") ") |
// .append(implFuncName).append("=null;\n"); |
} |
|
hack.append("\t").append(m.getName()).append(" : ").append(implFuncName).append(",\n"); |
} |
|
hack.append("};\nvar ") |
.append(varImpl).append("=new ") |
.append(c.getCanonicalName()).append("(").append(var).append(");"); |
|
script.append(hack.toString()); |
|
final HashMap<String, Object> bn = new HashMap(); |
bn.put(varFallback, fallback); |
|
if (null != fallback) { |
script.append("\n// " + bn.get(varFallback).getClass()); |
} |
//System.err.println(script.toString()); |
|
final ScriptEngine rhino = execute(true, script.toString(), bn); |
assert (c.isInstance(rhino.get(varImpl))); |
return (T) rhino.get(varImpl); |
} |
|
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) { |
return new File(loadedFrom().getPath()).getName(); // FIXME: <= Is this safe? |
} |
return properties.name; |
} |
|
/** |
* {@inheritDoc} |
*/ |
public URI loadedFrom() { |
return pluginUri.normalize(); // FIXME: Return a copy |
} |
|
|
} |