Permalink
Browse files

Introduce pluggable module loaders.

This adds a number of internal loaders and allows developers to add custom loaders at runtime.

Preconfigured internal loaders:

  * .js - loads from JavaScript, same as hardcoded loader in the past
  * .json - loads a JSON file and returns the parsed object
  * .class - loads a script compiled to a Java class using the Rhino jsc (JavaScript compiler) tool.
     Note that the class name and package must correspond to the module name.

 Plugging in custom module loaders:

   The require function now contains a "extension" property. To add a module loader add a property
   on `require.extensions` with the file extension you want to handle as property key and the
    function that compiles the module source as value. The function will be called with the resource
    as argument (an instance of org.ringojs.repository.Resource) and is expected to return either
    the exported module object or the module's JavaScript source code.

    For example, the following line shows how to load files with extension ".foo" as plain JavaScript modules:

         require.extensions[".foo"] = function(resource) {return resource.getContent()};

    Modules loaded through custom module loaders are subject to automatic reloading just like
     any other modules, i.e. they will be reloaded when the resource is modified unless running in
     production mode.
  • Loading branch information...
1 parent c978ee1 commit 20eee6739da7c69bf93c19d544abc2896df6b7a5 @hns hns committed Feb 8, 2012
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2012 Hannes Wallnoefer <hannes@helma.at>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.ringojs.engine;
+
+import org.mozilla.javascript.Context;
+import org.mozilla.javascript.Function;
+import org.mozilla.javascript.GeneratedClassLoader;
+import org.mozilla.javascript.Script;
+import org.mozilla.javascript.Scriptable;
+import org.mozilla.javascript.SecurityController;
+import org.mozilla.javascript.json.JsonParser;
+import org.ringojs.repository.Resource;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+
+public abstract class ModuleLoader {
+
+ private String extension;
+
+ public ModuleLoader(String extension) {
+ this.extension = extension;
+ }
+ public String getExtension() {
+ return extension;
+ }
+
+ public abstract Object load(Context cx, RhinoEngine engine,
+ Object securityDomain, String moduleName,
+ String charset, Resource resource) throws Exception;
+
+}
+
+class JsModuleLoader extends ModuleLoader {
+
+ public JsModuleLoader() {
+ super(".js");
+ }
+
+ @Override
+ public Object load(final Context cx, RhinoEngine engine, final Object securityDomain,
+ String moduleName, String charset, final Resource resource)
+ throws Exception {
+ return AccessController.doPrivileged(new PrivilegedAction<Object>() {
+ public Object run() {
+ try {
+ return cx.compileReader(resource.getReader(),
+ resource.getRelativePath(),
+ resource.getFirstLine(), securityDomain);
+ } catch (IOException iox) {
+ throw new RuntimeException(iox);
+ }
+ }
+ });
+ }
+}
+
+class JsonModuleLoader extends ModuleLoader {
+
+ public JsonModuleLoader() {
+ super(".json");
+ }
+
+ @Override
+ public Object load(Context cx, RhinoEngine engine, Object securityDomain,
+ String moduleName, String charset, Resource resource)
+ throws Exception {
+ JsonParser json = new JsonParser(cx, engine.getScope());
+ return json.parseValue(resource.getContent());
+ }
+}
+
+class ClassModuleLoader extends ModuleLoader {
+
+ public ClassModuleLoader() {
+ super(".class");
+ }
+
+ @Override
+ public Object load(Context cx, RhinoEngine engine, Object securityDomain,
+ String moduleName, String charset, Resource resource)
+ throws Exception {
+ long l = resource.getLength();
+ if (l > Integer.MAX_VALUE) {
+ throw new IOException("File too large: " + l);
+ }
+
+ int length = (int) l;
+ byte[] bytes = new byte[length];
+ InputStream input = resource.getInputStream();
+ int offset = 0, read;
+
+ while (offset < length) {
+ read = input.read(bytes, offset, length - offset);
+ if (read < 0) break;
+ offset += read;
+ }
+
+ if (offset < length) {
+ throw new IOException("Could not read file completely");
+ }
+ input.close();
+
+ String className = moduleName.replaceAll("/", ".");
+ ClassLoader rhinoLoader = getClass().getClassLoader();
+ GeneratedClassLoader loader;
+ loader = SecurityController.createLoader(rhinoLoader, securityDomain);
+
+ Class<?> clazz = loader.defineClass(className, bytes);
+ loader.linkClass(clazz);
+ if (!Script.class.isAssignableFrom(clazz)) {
+ throw new ClassCastException("Module must be a Rhino script class");
+ }
+
+ return clazz.newInstance();
+ }
+}
+
+class ScriptedModuleLoader extends ModuleLoader {
+
+ Function function;
+
+ public ScriptedModuleLoader(String extension, Function function) {
+ super(extension);
+ this.function = function;
+ }
+
+ @Override
+ public Object load(Context cx, RhinoEngine engine, Object securityDomain,
+ String moduleName, String charset, Resource resource)
+ throws Exception {
+ Scriptable scope = engine.getScope();
+ Object[] args = {cx.getWrapFactory().wrap(cx, scope, resource, null)};
+ Object source = function.call(cx, scope, scope, args);
+
+ if (source instanceof CharSequence) {
+ return cx.compileString(source.toString(), resource.getRelativePath(),
+ resource.getFirstLine(), securityDomain);
+ } else if (source instanceof Scriptable) {
+ return source;
+ } else {
+ throw new RuntimeException("Loader must return script or object");
+ }
+ }
+}
@@ -1,5 +1,5 @@
/*
- * Copyright 2004 Hannes Wallnoefer <hannes@helma.at>
+ * Copyright 2004-2012 Hannes Wallnoefer <hannes@helma.at>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,12 +20,10 @@
import org.mozilla.javascript.*;
import org.mozilla.javascript.tools.ToolErrorReporter;
+import java.io.FileNotFoundException;
import java.io.IOException;
-import java.io.Reader;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
import java.util.*;
import java.security.CodeSource;
import java.security.CodeSigner;
@@ -43,6 +41,8 @@
final Resource resource;
final RhinoEngine engine;
final String moduleName;
+ // the loader
+ ModuleLoader loader;
// true if we should reload modified source files
boolean reloading;
// the checksum of the underlying resource or repository when
@@ -72,6 +72,7 @@ public ReloadableScript(Resource source, RhinoEngine engine) {
source.setStripShebang(true);
this.resource = source;
this.engine = engine;
+ this.loader = engine.getModuleLoader(source);
reloading = engine.getConfig().isReloading();
moduleName = source.getModuleName();
}
@@ -92,14 +93,14 @@ public Resource getSource() {
* @throws IOException if an error occurred reading the script file
* @return the compiled and up-to-date script
*/
- public synchronized Script getScript(Context cx)
+ public synchronized Object getScript(Context cx)
throws JavaScriptException, IOException {
// only use shared code cache if optlevel >= 0
int optlevel = cx.getOptimizationLevel();
if (scriptref == null && optlevel > -1) {
scriptref = cache.get(resource);
}
- Script script = null;
+ Object script = null;
if (scriptref != null) {
script = scriptref.get();
checksum = scriptref.checksum;
@@ -110,7 +111,7 @@ public synchronized Script getScript(Context cx)
if ((script == null && exception == null)
|| (reloading && checksum != resource.getChecksum())) {
if (!resource.exists()) {
- throw new IOException(resource + " not found or not readable");
+ throw new FileNotFoundException(resource + " not found or not readable");
}
exception = null;
errors = null;
@@ -140,28 +141,17 @@ public synchronized Script getScript(Context cx)
* @throws IOException if an error occurred reading the script file
* @return the compiled and up-to-date script
*/
- protected synchronized Script compileScript(final Context cx)
+ protected synchronized Object compileScript(Context cx)
throws JavaScriptException, IOException {
- final String path = resource.getRelativePath();
- final ErrorReporter errorReporter = cx.getErrorReporter();
+ ErrorReporter errorReporter = cx.getErrorReporter();
cx.setErrorReporter(new ErrorCollector());
- Script script = null;
+ Object script = null;
String charset = engine.getCharset();
try {
- final CodeSource source = engine.isPolicyEnabled() ?
+ CodeSource securityDomain = engine.isPolicyEnabled() ?
new CodeSource(resource.getUrl(), (CodeSigner[]) null) : null;
- final Reader reader = resource.getReader(charset);
- script = AccessController.doPrivileged(new PrivilegedAction<Script>() {
- public Script run() {
- try {
- return cx.compileReader(reader, path,
- resource.getFirstLine(), source);
- } catch (Exception x) {
- exception = x;
- }
- return null;
- }
- });
+ script = loader.load(cx, engine, securityDomain, moduleName,
+ charset, resource);
} catch (Exception x) {
exception = x;
} finally {
@@ -181,11 +171,15 @@ public Script run() {
* @throws IOException if an error occurred reading the script file
*/
public Object evaluate(Scriptable scope, Context cx,
- Map<Resource,ModuleScope> modules)
+ Map<Resource, Scriptable> modules)
throws JavaScriptException, IOException {
- Script script = getScript(cx);
- ModuleScope module =
- scope instanceof ModuleScope ? (ModuleScope) scope : null;
+ Object obj = getScript(cx);
+ if (!(obj instanceof Script)) {
+ return obj;
+ }
+ Script script = (Script) obj;
+ ModuleScope module = scope instanceof ModuleScope ?
+ (ModuleScope) scope : null;
if (module != null) {
modules.put(resource, module);
}
@@ -202,37 +196,47 @@ public Object evaluate(Scriptable scope, Context cx,
*
* @param prototype the prototype for the module, usually the shared top level scope
* @param cx the rhino context
+ * @param module the preexisting module for this resource if available
* @param modules thread-local map for registering the module scope
* @return a new module scope
* @throws JavaScriptException if an error occurred evaluating the script file
* @throws IOException if an error occurred reading the script file
*/
- protected ModuleScope load(Scriptable prototype, Context cx,
- ModuleScope module,
- Map<Resource, ModuleScope> modules)
+ protected Scriptable load(Scriptable prototype, Context cx,
+ Scriptable module, Map<Resource, Scriptable> modules)
throws JavaScriptException, IOException {
- if (module != null && module.getChecksum() == getChecksum()) {
+ if (module instanceof ModuleScope &&
+ ((ModuleScope)module).getChecksum() == getChecksum()) {
// Module scope exists and is up to date
modules.put(resource, module);
return module;
}
- return exec(cx, getScript(cx), prototype, modules);
+ return exec(cx, prototype, modules);
}
- private synchronized ModuleScope exec(Context cx, Script script,
- Scriptable prototype,
- Map<Resource, ModuleScope> modules)
+ private synchronized Scriptable exec(Context cx, Scriptable prototype,
+ Map<Resource, Scriptable> modules)
throws IOException {
- ModuleScope module = new ModuleScope(moduleName, resource, prototype);
- // put module scope in map right away to make circular dependencies work
- modules.put(resource, module);
if (log.isLoggable(Level.FINE)) {
log.fine("Loading module: " + moduleName);
}
if (engine.getConfig().isVerbose()) {
System.err.println("Loading module: " + moduleName);
}
+ Object obj = getScript(cx);
+ if (!(obj instanceof Script)) {
+ if (!(obj instanceof Scriptable)) {
+ throw Context.reportRuntimeError("Module must be an object");
+ }
+ Scriptable scriptable = (Scriptable) obj;
+ modules.put(resource, scriptable);
+ return scriptable;
+ }
+ Script script = (Script) obj;
+ ModuleScope module = new ModuleScope(moduleName, resource, prototype);
+ // put module scope in map right away to make circular dependencies work
+ modules.put(resource, module);
// warnings are disabled in shell - enable warnings for module loading
ErrorReporter er = cx.getErrorReporter();
ToolErrorReporter reporter = er instanceof ToolErrorReporter ?
@@ -367,15 +371,15 @@ public EvaluatorException runtimeError(String message, String sourceName,
}
}
- static class ScriptReference extends SoftReference<Script> {
+ static class ScriptReference extends SoftReference<Object> {
Resource source;
long checksum;
List<ScriptError> errors;
Exception exception;
- ScriptReference(Resource source, Script script,
- ReloadableScript rescript, ReferenceQueue<Script> queue)
+ ScriptReference(Resource source, Object script,
+ ReloadableScript rescript, ReferenceQueue<Object> queue)
throws IOException {
super(script, queue);
this.source = source;
@@ -387,17 +391,17 @@ public EvaluatorException runtimeError(String message, String sourceName,
static class ScriptCache {
ConcurrentHashMap<Resource, ScriptReference> map;
- ReferenceQueue<Script> queue;
+ ReferenceQueue<Object> queue;
ScriptCache() {
map = new ConcurrentHashMap<Resource, ScriptReference>();
- queue = new ReferenceQueue<Script>();
+ queue = new ReferenceQueue<Object>();
}
- ScriptReference createReference(Resource source, Script script,
- ReloadableScript rescript)
+ ScriptReference createReference(Resource source, Object script,
+ ReloadableScript rlscript)
throws IOException {
- return new ScriptReference(source, script, rescript, queue);
+ return new ScriptReference(source, script, rlscript, queue);
}
ScriptReference get(Resource source) {
Oops, something went wrong.

0 comments on commit 20eee67

Please sign in to comment.