/pluggablejs/trunk/src/net/outlyer/plugins/PluginLocator.java |
---|
31,7 → 31,6 |
import java.io.IOException; |
import java.net.JarURLConnection; |
import java.net.URI; |
import java.net.URISyntaxException; |
import java.util.Collection; |
import java.util.Collections; |
import java.util.Enumeration; |
78,14 → 77,14 |
unique = new HashSet<Sandbox>(); |
} |
void add(final String fileName, final PluginProperties pp, final Sandbox sbox) { |
byName.put(pp.name, sbox); |
void add(final String fileName, final BasePluginObject bpo, final Sandbox sbox) { |
byName.put(bpo.name, sbox); |
byFilename.put(fileName, sbox); |
if (byType.get(pp.type) == null) { |
byType.put(pp.type, new LinkedList<Sandbox>()); |
if (byType.get(bpo.type) == null) { |
byType.put(bpo.type, new LinkedList<Sandbox>()); |
} |
byType.get(pp.type).add(sbox); |
byType.get(bpo.type).add(sbox); |
unique.add(sbox); |
} |
276,21 → 275,18 |
for (final File candidateFile : dir.listFiles(jsFileFilter)) { |
final URI fileURI = candidateFile.toURI(); |
try { |
final Sandbox sb = environment.createSandbox(fileURI); |
final PluginProperties pp = PluginEnvironment.fetchPluginProperties(fileURI); |
if (null == pp) { |
continue; |
} |
final BasePluginObject bpo = sb.getPluginObject(); |
if (null == pp.name) { |
pp.name = candidateFile.getName(); |
if (bpo.name == null) { |
bpo.name = candidateFile.getName(); |
} |
library.add(candidateFile.getName(), pp, sb); |
library.add(candidateFile.getName(), bpo, sb); |
} |
catch (final PluginException e) { |
catch (final Exception e) { |
continue; |
} |
} |
326,22 → 322,16 |
final URI pluginUri = new URI("jar:"+jarFile+"!"+url); |
final Sandbox sb = environment.createSandbox(pluginUri); |
final PluginProperties pp = PluginEnvironment.fetchPluginProperties(jarUri); |
// JarEntry's getName() returns the full path |
final String fileName = new File(je.getName()).getName(); |
if (pp.name == null) { |
pp.name = fileName; |
final BasePluginObject bpo = sb.getPluginObject(); |
final String fileName = new File(jarURIElements[1]).getName(); |
if (bpo.name == null) { |
bpo.name = fileName; |
} |
library.add(fileName, pp, sb); |
library.add(fileName, bpo, sb); |
} |
catch (final PluginException e) { |
catch (final Exception e) { |
continue; |
} |
catch (final URISyntaxException e) { |
assert false; |
throw new IllegalStateException("Unexpected malformed " + |
"jar URI, implementation error (?)"); |
} |
} |
} |
} |
/pluggablejs/trunk/src/net/outlyer/plugins/PluginEnvironment.java |
---|
26,57 → 26,12 |
// $Id$ |
import java.io.IOException; |
import java.io.LineNumberReader; |
import java.lang.reflect.Field; |
import java.net.URI; |
import java.util.Arrays; |
import java.util.HashMap; |
import java.util.List; |
import java.util.Map; |
import javax.script.Bindings; |
import javax.script.ScriptEngine; |
import javax.script.ScriptEngineManager; |
import javax.script.ScriptException; |
/** |
* Provides the environment for {@link net.outlyer.plugins.Sandbox}es. |
*/ |
public class PluginEnvironment { |
static class UnsupportedAPIException extends PluginException { |
public UnsupportedAPIException(int providedApi) { |
super("Plugin has unsupported API ("+providedApi+")"); |
} |
} |
static class UnsupportedTypeException extends PluginException { |
public UnsupportedTypeException(String pluginType) { |
super("Plugin has unsupported type ("+pluginType+")"); |
} |
} |
private final Map<String, Object> exportedObjects; |
private BasePluginObject pluginObject; |
private StringBuilder boilerPlate; |
private List<String> acceptedTypes = null; |
private int maxApi=API.REVISION; |
// Since these lines are evaluated they'll be executed twice |
// so the lower the number the better to reduce unexpected side effects |
private static int linesToCheckForSupport = 1; |
static final String EXPORTED_SANDBOX_VARIABLE = "$net.outlyer.runtime.sandbox"; |
{ |
exportedObjects = new HashMap(); |
pluginObject = null; |
boilerPlate = new StringBuilder(); |
} |
//////////////////////////////////////////////////////// |
// Public interface |
//////////////////////////////////////////////////////// |
public abstract class PluginEnvironment { |
/** |
* Creates a sandbox associated to the script found in uri |
* @param uri Plugin uri |
83,34 → 38,8 |
* @return Sandbox associated to uri |
* @throws net.outlyer.plugins.PluginException |
*/ |
public Sandbox createSandbox(final URI forUri) throws PluginException { |
final ScriptEngine rhino = new ScriptEngineManager().getEngineByName("rhino"); |
final PluginProperties pp = checkForSupport(forUri); |
abstract public Sandbox createSandbox(final URI uri) throws PluginException; |
if (null == pluginObject) { |
pluginObject = new PluginObject(); |
} |
try { |
final PluginObject po = (PluginObject) pluginObject.clone(); |
final Bindings bindings = rhino.createBindings(); |
for (final String objectName : exportedObjects.keySet()) { |
bindings.put(objectName, exportedObjects.get(objectName)); |
} |
bindings.put("plugin", po); |
enableSandboxAccess("plugin", po); |
return new SandboxImpl(forUri, bindings, pp, boilerPlate.toString()); |
} |
catch (final CloneNotSupportedException e) { |
assert false; |
throw new IllegalStateException("Incorrect implementation"); |
} |
} |
/** |
* Makes an object accessible from the plugin. |
* Note the object will be shared amongst any sandboxes created after |
118,23 → 47,17 |
* @param name Name with which to access the object |
* @param object Object to export |
*/ |
public void exportGlobalObject(final String name, final Object object) { |
if (null == name) { |
throw new IllegalArgumentException("Can't export an object with" + |
"a null name"); |
} |
if (name.equals("plugin")) { |
throw new IllegalArgumentException("Can't export an object named " + |
"plugin, use setPluginObject() instead"); |
} |
exportedObjects.put(name, object); |
if (object instanceof SandboxAccessor) { |
enableSandboxAccess(name, (SandboxAccessor)object); |
} |
} |
abstract public void exportGlobalObject(final String name, final Object object); |
/** |
* Restricts the set of supported plugins to the ones with the given plugin |
* type (by default no restriction is applied). |
* @see PluginProperties#type |
* @param pluginTypes Set of types to allow; use null to allow all types |
*/ |
abstract public void setAcceptedTypes(final String... pluginTypes); |
/** |
* Defines the object to be exported as <code>plugin</code>. |
* The <code>plugin</code> object is the only one exported by default. |
* If none is set, a default one will be provided. |
143,147 → 66,9 |
* shared amongst sandboxes, a clone of the one provided will be used. |
* @param pluginObject Object to export, set to null to use a default one. |
*/ |
public void setPluginObject(BasePluginObject pObject) { |
pluginObject = pObject; |
} |
abstract public void setPluginObject(final BasePluginObject pluginObject); |
/** |
* Restricts the set of supported plugins to the ones with the given plugin |
* type (by default no restriction is applied). |
* @see PluginProperties#type |
* @param pluginTypes Set of types to allow; use null to allow all types |
*/ |
public void setAcceptedTypes(final String ... pluginTypes) { |
if (null != pluginTypes) { |
for (final String type : pluginTypes) { |
if (null == type) { |
throw new IllegalArgumentException("The set of plugin types" + |
" must be either null, or a set of non-null values"); |
} |
} |
} |
acceptedTypes = Arrays.asList(pluginTypes); |
} |
//////////////////////////////////////////////////////// |
// Non-public interface |
//////////////////////////////////////////////////////// |
private void enableSandboxAccess(final String name, final SandboxAccessor sa) { |
try { |
Field f = sa.getClass().getField("_getSandbox"); |
} |
catch (final NoSuchFieldException e) { |
throw new IllegalArgumentException("object " + name + " must comply " + |
"with the contract of SandboxAccessor"); |
} |
final String callback = name + "._getSandbox"; |
boilerPlate.append(callback).append("=function() {") |
.append(" return ") |
.append(EXPORTED_SANDBOX_VARIABLE).append(";\n};"); |
} |
PluginProperties checkForSupport(final URI uri) throws PluginException { |
assert null != uri; |
final PluginProperties pp = fetchPluginProperties(uri); |
if (null == pp) { |
throw new PluginExecutionException("Failed while inspecting plugin "+uri); |
} |
if (null != acceptedTypes) { |
if (!acceptedTypes.contains(pp.type)) { |
throw new UnsupportedTypeException(pp.type); |
} |
} |
if (pp.apiVersion > maxApi) { |
throw new UnsupportedAPIException(pp.apiVersion); |
} |
return pp; |
} |
// TODO: ? Possibly cache this, so that the first line is only eval'ed |
// once more than the # of executions |
static PluginProperties fetchPluginProperties(final URI uri) { |
assert null != uri; |
final ScriptEngine rhino = new ScriptEngineManager().getEngineByName("rhino"); |
try { |
// Define a Plugin object with a field named type |
rhino.eval("var plugin={ type: null, apiVersion: null };"); |
} |
catch (ScriptException e) { |
assert false; |
throw new IllegalStateException("Unknown error encountered"); |
} |
// First of all check the script for support |
Object type = null, name = null; |
Object apiVersion = null; |
// Try to eval each line, since it executes in a sandbox this is |
// *relatively* safe |
String line; |
try { |
final LineNumberReader script = new LineNumberReader(new PluginReader(uri)); |
try { |
while ((null == type || null == apiVersion || null == name) && |
(null != (line = script.readLine())) && |
script.getLineNumber() <= linesToCheckForSupport) { |
try { |
rhino.eval(line); |
// Note that undefined doesn't convert to null! |
if (null == type) { |
rhino.eval("$_1_$ = (undefined===plugin.type)?null:plugin.type;"); |
type = rhino.get("$_1_$"); |
} |
if (null == apiVersion) { |
rhino.eval("$_1_$ = (undefined===plugin.apiVersion)?null:plugin.apiVersion;"); |
apiVersion = rhino.get("$_1_$"); |
} |
if (null == name) { |
rhino.eval("$_1_$ = (undefined===plugin.name)?null:plugin.name;"); |
name = rhino.get("$_1_$"); |
} |
} |
catch (final ScriptException e) { |
// Exceptions are to be expected since none of the guaranteed |
// plugin tools are provided in this context they'll try to |
// access unexisting objects |
} |
} |
} |
finally { |
script.close(); |
} |
} |
catch (final IOException e) { |
// XXX: Evil silent failure |
return null; |
} |
if (null == type) { |
type = ""; |
} |
if (null == apiVersion) { |
apiVersion = 0; |
} |
// name can be null |
final PluginProperties pp = new PluginProperties(); |
try { |
pp.apiVersion = Double.valueOf(apiVersion.toString()).intValue(); |
pp.type = type.toString(); |
pp.name = (null != name) ? name.toString(): null; |
} |
catch (final NumberFormatException e) { |
// XXX: Evil silent failure |
return null; |
} |
return pp; |
} |
public static PluginEnvironment create() { |
return new PluginEnvironmentImpl(); |
} |
} |
/pluggablejs/trunk/src/net/outlyer/plugins/PluginEnvironmentImpl.java |
---|
0,0 → 1,250 |
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$ |
import java.io.IOException; |
import java.io.LineNumberReader; |
import java.lang.reflect.Field; |
import java.net.URI; |
import java.util.Arrays; |
import java.util.HashMap; |
import java.util.List; |
import java.util.Map; |
import javax.script.Bindings; |
import javax.script.ScriptEngine; |
import javax.script.ScriptEngineManager; |
import javax.script.ScriptException; |
/** |
* Default implementation of a PluginEnvironment. |
*/ |
final class PluginEnvironmentImpl extends PluginEnvironment { |
static class UnsupportedAPIException extends PluginException { |
public UnsupportedAPIException(int providedApi) { |
super("Plugin has unsupported API ("+providedApi+")"); |
} |
} |
static class UnsupportedTypeException extends PluginException { |
public UnsupportedTypeException(String pluginType) { |
super("Plugin has unsupported type ("+pluginType+")"); |
} |
} |
private final Map<String, Object> exportedObjects; |
private BasePluginObject pluginObject; |
private StringBuilder boilerPlate; |
private List<String> acceptedTypes = null; |
private int maxApi=API.REVISION; |
// Since these lines are evaluated they'll be executed twice |
// so the lower the number the better to reduce unexpected side effects |
int linesToCheckForSupport = 1; |
static final String EXPORTED_SANDBOX_VARIABLE = "$net.outlyer.runtime.sandbox"; |
{ |
exportedObjects = new HashMap(); |
pluginObject = null; |
boilerPlate = new StringBuilder(); |
} |
private void enableSandboxAccess(final String name, final SandboxAccessor sa) { |
try { |
Field f = sa.getClass().getField("_getSandbox"); |
} |
catch (final NoSuchFieldException e) { |
throw new IllegalArgumentException("object " + name + " must comply " + |
"with the contract of SandboxAccessor"); |
} |
final String callback = name + "._getSandbox"; |
boilerPlate.append(callback).append("=function() {") |
.append(" return ") |
.append(EXPORTED_SANDBOX_VARIABLE).append(";\n};"); |
} |
/** |
* {@inheritDoc} |
*/ |
public void exportGlobalObject(final String name, final Object object) { |
if (null == name) { |
throw new IllegalArgumentException("Can't export an object with" + |
"a null name"); |
} |
if (name.equals("plugin")) { |
throw new IllegalArgumentException("Can't export an object named " + |
"plugin, use setPluginObject() instead"); |
} |
exportedObjects.put(name, object); |
if (object instanceof SandboxAccessor) { |
enableSandboxAccess(name, (SandboxAccessor)object); |
} |
} |
/** |
* {@inheritDoc} |
*/ |
public void setPluginObject(BasePluginObject pObject) { |
pluginObject = pObject; |
} |
PluginProperties checkForSupport(final URI uri) throws PluginException { |
final ScriptEngine rhino = new ScriptEngineManager().getEngineByName("rhino"); |
try { |
// Define a Plugin object with a field named type |
rhino.eval("var plugin={ type: null, apiVersion: null };"); |
} |
catch (ScriptException e) { |
assert false; |
throw new IllegalStateException("Unknown error encountered"); |
} |
// First of all check the script for support |
Object type = null, name = null; |
Object apiVersion = null; |
// Try to eval each line, since it executes in a sandbox this is |
// *relatively* safe |
String line; |
try { |
final LineNumberReader script = new LineNumberReader(new PluginReader(uri)); |
try { |
while ((null == type || null == apiVersion || null == name) && |
(null != (line = script.readLine())) && |
script.getLineNumber() < linesToCheckForSupport) { |
try { |
rhino.eval(line); |
// Note that undefined doesn't convert to null! |
if (null == type) { |
rhino.eval("$_1_$ = (undefined===plugin.type)?null:plugin.type;"); |
type = rhino.get("$_1_$"); |
} |
if (null == apiVersion) { |
rhino.eval("$_1_$ = (undefined===plugin.apiVersion)?null:plugin.apiVersion;"); |
apiVersion = rhino.get("$_1_$"); |
} |
if (null == name) { |
rhino.eval("$_1_$ = (undefined===plugin.name)?null:plugin.name;"); |
name = rhino.get("$_1_$"); |
} |
} |
catch (final ScriptException e) { |
// Exceptions are to be expected since none of the guaranteed |
// plugin tools are provided in this context they'll try to |
// access unexisting objects |
} |
} |
} |
finally { |
script.close(); |
} |
} |
catch (final IOException e) { |
throw new PluginExecutionException("Failed while reading plugin: " + e.getMessage(), e); |
} |
if (null == type) { |
type = ""; |
} |
if (null == apiVersion) { |
apiVersion = 0; |
} |
// name can be null |
final PluginProperties pp = new PluginProperties(); |
try { |
pp.apiVersion = Double.valueOf(apiVersion.toString()).intValue(); |
pp.type = type.toString(); |
pp.name = (null != name) ? name.toString(): null; |
} |
catch (final NumberFormatException e) { |
throw new PluginException("Incorrect value for apiVersion provided, must be integer"); |
} |
if (null != acceptedTypes) { |
if (!acceptedTypes.contains(pp.type)) { |
throw new UnsupportedTypeException(pp.type); |
} |
} |
if (pp.apiVersion > maxApi) { |
throw new UnsupportedAPIException(pp.apiVersion); |
} |
return pp; |
} |
/** |
* {@inheritDoc} |
*/ |
public Sandbox createSandbox(final URI forUri) throws PluginException { |
final ScriptEngine rhino = new ScriptEngineManager().getEngineByName("rhino"); |
final PluginProperties pp = checkForSupport(forUri); |
if (null == pluginObject) { |
pluginObject = new PluginObject(); |
} |
try { |
final PluginObject po = (PluginObject) pluginObject.clone(); |
final Bindings bindings = rhino.createBindings(); |
for (final String objectName : exportedObjects.keySet()) { |
bindings.put(objectName, exportedObjects.get(objectName)); |
} |
bindings.put("plugin", po); |
enableSandboxAccess("plugin", po); |
return new SandboxImpl(forUri, bindings, pp, boilerPlate.toString()); |
} |
catch (final CloneNotSupportedException e) { |
assert false; |
throw new IllegalStateException("Incorrect implementation"); |
} |
} |
/** |
* {@inheritDoc} |
*/ |
public void setAcceptedTypes(final String ... pluginTypes) { |
if (null != pluginTypes) { |
for (final String type : pluginTypes) { |
if (null == type) { |
throw new IllegalArgumentException("The set of plugin types" + |
" must be either null, or a set of non-null values"); |
} |
} |
} |
acceptedTypes = Arrays.asList(pluginTypes); |
} |
} |
Property changes: |
Added: svn:keywords |
+Rev Id Date |
\ No newline at end of property |
/pluggablejs/trunk/src/net/outlyer/plugins/Shell.java |
---|
41,7 → 41,7 |
System.exit(1); |
} |
final PluginEnvironment pe = new PluginEnvironment(); |
final PluginEnvironment pe = PluginEnvironment.create(); |
pe.exportGlobalObject("input", new Input()); |
pe.exportGlobalObject("out", new Output()); |
pe.exportGlobalObject("err", new Output(System.err)); |
/pluggablejs/trunk/src/net/outlyer/plugins/SandboxImpl.java |
---|
26,6 → 26,8 |
// $Id$ |
import net.outlyer.plugins.Sandbox; |
import java.io.File; |
import java.io.IOException; |
import java.lang.reflect.Method; |
import java.net.URI; |
38,6 → 40,8 |
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 |
106,9 → 110,7 |
return new ScriptEngineManager().getEngineByName("rhino"); |
} |
private ScriptEngine execute(final String prependText, |
final String appendText, |
final Map<String,Object> extraBindings) |
private ScriptEngine execute(boolean appendText, String text, final Map<String,Object> extraBindings) |
throws PluginExecutionException { |
final ScriptEngine rhino = getEngine(); |
151,14 → 153,10 |
// Create the $net.outlyer.runtime pseudo-namespace... |
rhino.eval(runtime.toString()); |
if (null != prependText) { |
rhino.eval(prependText); |
} |
rhino.eval(new PluginReader(pluginUri)); |
if (null != appendText) { |
rhino.eval(appendText); |
if (appendText) { |
rhino.eval(text); |
} |
// Execute any end hooks |
177,36 → 175,31 |
} |
public void execute() throws PluginExecutionException { |
execute(null, null, null); |
execute(false, null, null); |
} |
public <T> T createDelayedImplementation(final Class<T> c, |
final String objectName) |
throws PluginExecutionException { |
System.err.println("createDelayedImple"); |
final String varImpl = uniqueVarName(); |
final StringBuilder preCode = new StringBuilder(); |
final StringBuilder code = new StringBuilder(); |
preCode.append("var ").append(objectName).append(" = {\n"); |
code.append("var ").append(objectName).append(" = {\n"); |
for (final Method method : c.getMethods()) { |
if (method.getDeclaringClass() != c) { |
continue; |
} |
preCode.append(method.getName()).append(": null,\n"); |
code.append(method.getName()).append(": null,"); |
} |
preCode.append("};\n"); |
code.append("};\n"); |
// 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 |
final StringBuilder postCode = new StringBuilder(); |
postCode.append(varImpl).append(" = new ") |
code.append(varImpl).append(" = new ") |
.append(c.getCanonicalName()).append("(").append(objectName).append(");"); |
System.err.println(code.toString()); |
final ScriptEngine rhino = execute(preCode.toString(), postCode.toString(), null); |
final ScriptEngine rhino = execute(true, code.toString(), null); |
assert (c.isInstance(rhino.get(varImpl))); |
return (T) rhino.get(varImpl); |
291,7 → 284,7 |
} |
//System.err.println(script.toString()); |
final ScriptEngine rhino = execute(null, script.toString(), bn); |
final ScriptEngine rhino = execute(true, script.toString(), bn); |
assert (c.isInstance(rhino.get(varImpl))); |
return (T) rhino.get(varImpl); |
} |
311,8 → 304,7 |
*/ |
public String getPluginName() { |
if (null == properties.name) { |
final String[] elements = loadedFrom().toString().split("/"); |
return elements[elements.length-1]; |
return new File(loadedFrom().getPath()).getName(); // FIXME: <= Is this safe? |
} |
return properties.name; |
} |