Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Rushed inital commit. It's note even really anything useful at the mo…

…ment, just a proof-of-concept. I'm tired...
  • Loading branch information...
commit 79531a3ac31deb7dfa264022fd8fdc45e5e79649 0 parents
@TooTallNate authored
1  .gitignore
@@ -0,0 +1 @@
+*.class
67 README.md
@@ -0,0 +1,67 @@
+ModuleJS-rhino
+==============
+### Asynchronous [CommonJS][] Module Loader for [Rhino][] JavaScript.
+
+This library allows the use of "modules" within your Rhino-embedded Java programs.
+It's the Java version of the web-browser [ModuleJS][] library.
+
+Any top-level JavaScript module that provides direct JavaScript functionality
+may be loaded (like Underscore, for example). Additionally, this Java version
+allows for "native" Java modules to be loaded, and extend the functionality of the
+JavaScript environment.
+
+
+Writing a JavaScript Module
+---------------------------
+
+JavaScript modules are the same as the web-browser [ModuleJS][]. More than likely,
+your Java program is going to execute user-written JavaScript files, and they should
+be in the standard ModuleJS module format. For reference, here's a JavaScript
+module that exports a single function, 'hello', that returns the "Hello World!" string:
+
+ exports.hello = function() {
+ return "Hello World!";
+ }
+
+
+Writing a Java Module
+---------------------
+
+A "native" Java module works a little differently. Knowledge of [Rhino][]'s
+embedding APIs is a plus. A native module simply needs to define a public static "Init"
+method, which will be called when the class is loaded. The method should return
+a _Scriptable_ that will be the native module's _exports_ object.
+
+Let's see a Java module that exports a 'native' function called 'hello' that will
+return the "Hello World!" string:
+
+ import java.lang.reflect.Method;
+ import org.mozilla.javascript.*;
+
+ public class HelloModule {
+
+ // 'Init' sets up the module initially. It creates the 'exports' object
+ // and attaches the 'hello' function to the object.
+ public static Scriptable Init (Context cx, Scriptable thisObj, Object[] args, Function funObj) {
+
+ // Set up a new, empty, Object.
+ Scriptable exports = new ScriptableObject();
+
+ // Wrap the 'Hello' function defined below, and expose it on the exports object
+ Method helloMethod = HelloModule.class.getMethod("Hello", new Class[] { Context.class, Scriptable.class, Object[].class, Function.class });
+ Function helloFunction = new FunctionObject("hello", helloMethod, funObj);
+ ScriptableObject.putProperty(exports, "hello", helloFunction);
+
+ return exports;
+ }
+
+ public static String Hello (Context cx, Scriptable thisObj, Object[] args, Function funObj) {
+ return "Hello World!";
+ }
+ }
+
+
+
+[ModuleJS]: https://github.com/TooTallNate/ModuleJS
+[CommonJS]: http://wiki.commonjs.org/wiki/Modules
+[Rhino]: http://www.mozilla.org/rhino
BIN  lib/js.jar
Binary file not shown
199 src/net/tootallnate/modulejs/ModuleClassLoader.java
@@ -0,0 +1,199 @@
+import java.awt.event.*;
+import java.io.*;
+import java.lang.*;
+import java.net.*;
+import java.util.*;
+import java.util.jar.*;
+
+/**
+ * The 'ModuleClassLoader' is an asynchronous JAR class loader. It can
+ * retrieve and load classes from a JAR file over any valid java.net.URL
+ * instance that is compatible with the `openConnection()` method.
+ *
+ * It's async, so you must first add a "Load Listener", which can be any
+ * ActionListener instance, in order to be notified when it's ready to be
+ * used. Then call `retrieve()`, it returns immediately and begins
+ * asynchronously getting the JAR file contents in a new Thread.
+ *
+ * After the ActionListeners have been notified (i.e. `isLoaded()` returns
+ * true), it is safe to call the `loadClass()` method and do your bidding.
+ *
+ * For ModuleJS, there's a `getModuleClassName()` method, which returns
+ * the "Module-Class" attribute from the JAR's manifest file. This class
+ * name will be the entry point for a native Java ModuleJS module. The
+ * class must contain an initialization method with the signature:
+ *
+ */
+public class ModuleClassLoader extends ClassLoader implements Runnable {
+
+ private final URL jarUrl;
+ private final ArrayList<ActionListener> loadListeners;
+ private byte[] jarData;
+ private boolean loaded;
+
+ public ModuleClassLoader(URL jarUrl) {
+ if (jarUrl == null) {
+ throw new IllegalArgumentException ("A URL pointing to a ModuleJS JAR file is required");
+ }
+ this.jarUrl = jarUrl;
+ this.loadListeners = new ArrayList<ActionListener>();
+ this.loaded = false;
+ }
+
+ public void addLoadListener(ActionListener l) {
+ this.loadListeners.add(l);
+ }
+
+ public void removeLoadListener(ActionListener l) {
+ this.loadListeners.remove(l);
+ }
+
+ public boolean isLoaded() {
+ return this.loaded;
+ }
+
+ // Begins asynchronously getting the JAR file's byte data.
+ // Use `addLoadListener()` to be notified when the file has
+ // finished loading, and classes inside the JAR may begin being
+ // loaded and used.
+ public void retrieve() {
+ new Thread(this).start();
+ }
+
+ // The ModuleClassLoader implements Runnable in order to asynchronously
+ // retrieve the JAR file contents. ActionListeners may be attached via
+ // 'addLoadListener()' to be notified when the 'loadClass()' method may
+ // be safely called.
+ public void run() {
+
+ byte[] data = null;
+ try {
+ // First we have to open a connection to retrieve the file
+ URLConnection connection = this.jarUrl.openConnection();
+ // Since you get a URLConnection, use it to get the InputStream
+ InputStream in = connection.getInputStream();
+ // Now that the InputStream is open, get the content length
+ int contentLength = connection.getContentLength();
+
+ // To avoid having to resize the array over and over and over as
+ // bytes are written to the array, provide an accurate estimate of
+ // the ultimate size of the byte array
+ ByteArrayOutputStream tmpOut;
+ if (contentLength != -1) {
+ System.out.println("Content Length: " + contentLength);
+ tmpOut = new ByteArrayOutputStream(contentLength);
+ } else {
+ // If the server provided no Content-Length, allocate a
+ // byte array of 1 megabyte. Any bigger of a JAR file would be
+ // kind of ridiculous for a ModuleJS native module.
+ System.out.println("No content length!");
+ tmpOut = new ByteArrayOutputStream(1048576);
+ }
+
+ byte[] buf = new byte[512];
+ while (true) {
+ int len = in.read(buf);
+ if (len == -1) {
+ break;
+ }
+ tmpOut.write(buf, 0, len);
+ }
+ in.close();
+ tmpOut.close(); // No effect, but good to do anyway to keep the metaphor alive
+
+ data = tmpOut.toByteArray();
+ System.out.println("'data.length': " + data.length);
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ return; // TODO: Error reporting and handling
+ }
+
+ synchronized(this) {
+ this.loaded = true;
+ this.jarData = data;
+ for (ActionListener l : this.loadListeners) {
+ ActionEvent e = new ActionEvent(this, 1, "load");
+ l.actionPerformed(e);
+ }
+ }
+ }
+
+ /**
+ * Returns the "Module-Class" manifest attributes from the
+ * retrieved JAR file, or null if none was defined.
+ */
+ public String getModuleClassName() throws IOException {
+ System.out.println("Attempting to get 'Module-Class' manifest attribute");
+
+ JarInputStream jar = new JarInputStream(new ByteArrayInputStream(this.jarData));
+ Manifest manifest = jar.getManifest();
+ Attributes attr = manifest.getMainAttributes();
+ String rtn = attr != null ? attr.getValue("Module-Class") : null;
+ jar.close();
+ return rtn;
+ }
+
+ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ //System.out.println("Loading class: " + name + ", resolve: " + resolve);
+
+ // Since all support classes of loaded class use same class loader
+ // must check subclass cache of classes for things like Object
+ Class c = findLoadedClass(name);
+ if (c == null) {
+ try {
+ c = findSystemClass(name);
+ } catch (ClassNotFoundException e) {
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (c == null) {
+ try {
+ byte data[] = loadClassData(name);
+ c = defineClass (name, data, 0, data.length);
+ if (c == null)
+ throw new ClassNotFoundException (name);
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ }
+ }
+ if (resolve)
+ resolveClass (c);
+ return c;
+ }
+
+ private byte[] loadClassData (String classname) throws IOException {
+
+ JarInputStream jar = new JarInputStream(new ByteArrayInputStream(this.jarData));
+
+ // Convert class name argument to filename
+ // Convert package names into subdirectories
+ String filename = classname.replace ('.', File.separatorChar) + ".class";
+ System.out.println("Attempting to find: " + filename);
+
+ JarEntry entry = null;
+ ByteArrayOutputStream bos = null;
+ while ((entry = jar.getNextJarEntry()) != null) {
+ System.out.println("Next Jar entry: " + entry.getName());
+ if (!filename.equals(entry.getName())) continue;
+
+ bos = new ByteArrayOutputStream();
+ byte[] buff = new byte[8124];
+ while (true) {
+ int read = jar.read(buff, 0, buff.length);
+ if (read == -1) {
+ break;
+ }
+ bos.write(buff, 0, read);
+ }
+ break;
+
+ }
+ jar.close();
+
+ if (bos == null) throw new IOException("File not found in JAR: " + filename);
+
+ return bos.toByteArray();
+ }
+}
81 tests/CLTester.java
@@ -0,0 +1,81 @@
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.io.IOException;
+
+import org.mozilla.javascript.*;
+
+
+public class CLTester implements ActionListener {
+
+ private static ModuleClassLoader l;
+
+ public void actionPerformed(ActionEvent e) {
+ System.out.println("Got 'action' event!");
+ System.out.println("Source is the ModuleClassLoader instance? " + (l == e.getSource()));
+
+ String moduleClass = null;
+ try {
+ moduleClass = l.getModuleClassName();
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ return;
+ }
+ System.out.println("Module-Class: " + moduleClass);
+
+ // Creates and enters a Context. The Context stores information
+ // about the execution environment of a script.
+ Context cx = Context.enter();
+ try {
+ // Initialize the standard objects (Object, Function, etc.)
+ // This must be done before scripts can be executed. Returns
+ // a scope object that we use in later calls.
+ Scriptable scope = cx.initStandardObjects();
+
+
+ Class<?> c = l.loadClass(moduleClass);
+ Method m = c.getMethod("Init", new Class[] { Context.class, Scriptable.class });
+ m.setAccessible(true);
+ int mods = m.getModifiers();
+ if (m.getReturnType() != Scriptable.class || !Modifier.isStatic(mods) || !Modifier.isPublic(mods)) {
+ //throw new NoSuchMethodException("Init");
+ System.out.println("No 'Init' method found!");
+ return;
+ }
+ Scriptable exports = null;
+ try {
+ exports = (Scriptable)m.invoke(null, new Object[] { cx, scope });
+ } catch (Exception ex) {
+ // This should not happen, as we have disabled access checks in `setAccessible()`
+ ex.printStackTrace();
+ }
+
+
+ // Create the module
+ ScriptableObject.putProperty(scope, "module", exports);
+
+ // Now evaluate the string we've colected.
+ Object result = cx.evaluateString(scope, "module.hello.call({test:new Date()});", "<cmd>", 1, null);
+
+ // Convert the result to a string and print it.
+ System.err.println(Context.toString(result));
+
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ } finally {
+ // Exit from the context.
+ Context.exit();
+ }
+
+
+ }
+
+ public static void main (String args[]) throws Exception {
+ CLTester test = new CLTester();
+ l = new ModuleClassLoader(new URL(args[0]));
+ l.addLoadListener(test);
+ l.retrieve();
+ }
+}
30 tests/fixtures/HelloModule/HelloModule.java
@@ -0,0 +1,30 @@
+import java.lang.reflect.Method;
+import org.mozilla.javascript.*;
+
+public class HelloModule {
+
+ // 'Init' sets up the module initially. It creates the 'exports',
+ // which is what gets exposes back to the JavaScript script writer.
+ public static Scriptable Init (Context cx, Scriptable moduleScope) throws NoSuchMethodException {
+
+ // Set up a new, empty, Object.
+ Scriptable exports = cx.newObject(moduleScope);
+
+ // Wrap the 'Hello' function defined below, and expose it on the exports object
+ Method helloMethod = HelloModule.class.getMethod("Hello", new Class[] { Context.class, Scriptable.class, Object[].class, Function.class });
+ FunctionObject helloFunction = new FunctionObject("hello", helloMethod, moduleScope);
+ ScriptableObject.putProperty(exports, "hello", helloFunction);
+
+ // Demonstrating also exporting a String, "hw".
+ ScriptableObject.putProperty(exports, "helloStr", "hw");
+
+ return exports;
+ }
+
+ // The 'Hello' method is turned into a JavaScript Function instance in our
+ // 'Init' method. This makes it so the code in this method is called when the
+ // function is invoked.
+ public static String Hello (Context cx, Scriptable thisObj, Object[] args, Function funObj) {
+ return "Hello World";
+ }
+}
1  tests/fixtures/HelloModule/MANIFEST
@@ -0,0 +1 @@
+Module-Class: HelloModule
BIN  tests/fixtures/HelloModule/module.jar
Binary file not shown
Please sign in to comment.
Something went wrong with that request. Please try again.