WSJSはBigtable上に疑似ファイルシステムを持っている。
ここにスクリプトを置いて実行しているわけだけれども当然 Javaのクラスも置きたくなる。

まず URLClassLoader を使ってみる。

var qrcode = newQrcode();

function newQrcode() {
	var jarUrl = java.net.URL("jar:http://localhost:8080/qrcode/Qrcode.jar!/");
	var loader = java.net.URLClassLoader([jarUrl]);
	return loader.loadClass("com.swetake.util.Qrcode").newInstance();
}

撃沈。

access denied (java.net.SocketPermission localhost connect,accept,resolve)
  java.security.AccessControlException: access denied (java.net.SocketPermission localhost connect,accept,resolve)
  java.security.AccessControlContext.checkPermission(AccessControlContext.java:323)
  java.security.AccessController.checkPermission(AccessController.java:546)
  java.lang.SecurityManager.checkPermission(SecurityManager.java:532)
  com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkPermission(DevAppServerFactory.java:166)
            :

しょうが無いので疑似ファイルシステムから読み込むクラスローダを書いてみた。

RepositoryClassLoader.java:

package kotemaru.wsjs;

import java.io.* ;
import java.net.* ;
import java.util.* ;
import javax.servlet.*;
import javax.servlet.http.*;
import kotemaru.util.* ;
import kotemaru.auth.* ;
import java.util.jar.* ;
import kotemaru.wsjs.proc.ClassProc ;

/**
 * リポジトリからクラスをロードする。
 * 
 */
public class RepositoryClassLoader extends ClassLoader {
	private WsjsContext context;
	private Page basePage;

	public RepositoryClassLoader(WsjsContext ctx, String pageName, 
				ClassLoader parent) throws IOException {
		super(parent);
		this.context = ctx;
		this.basePage = ctx.getPage(pageName);
	}

	public Class findClass(String name) throws ClassNotFoundException {
		if (basePage.isDirectory()) {
			return findClassForPage(name);
		} else {
			return findClassForJarFile(name);
		}
	}

	private Class findClassForPage(String name) throws ClassNotFoundException {
		try {
			String path = basePage.getPageName()+"/"
								+name.replace('.','/')+".class";
			Processor proc = context.getProcessor(path);
			if (proc == null || !(proc instanceof ClassProc)) {
				throw new ClassNotFoundException("Not found "+name+" in "+basePage.getPageName());
			}

			ClassProc cproc = (ClassProc) proc;
			Class cls = cproc.getCacheClass();
			if (cls != null) return cls;

			byte[] data = cproc.getCacheBuffer(context);
			cls = defineClass(name, data, 0, data.length);
			cproc.setCacheClass(cls);
			return cls;
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}
	private Class findClassForJarFile(String name) throws ClassNotFoundException  {
		try {
			String path = name.replace('.','/')+".class";
			InputStream in = getJarStream(path);
			if (in == null) {
				throw new ClassNotFoundException("Not found "+name+" in "+basePage.getPageName());
			}
			byte[] data = IOUtil.streamToBytes(in);
			return defineClass(name, data, 0, data.length);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	public InputStream getResourceAsStream(String name) {
		try {
			if (basePage.isDirectory()) {
				Page page = context.getPage(basePage.getPageName()+"/"+name);
				if (!page.exists()) return null;
				byte[] data = page.getBodyBytes();
				return new ByteArrayInputStream(data);
			} else {
				return getJarStream(name);
			}
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	private InputStream getJarStream(String path) {
		try {
			ByteArrayInputStream bin = new ByteArrayInputStream(basePage.getBodyBytes());
			JarInputStream jarIn = new JarInputStream(bin);
			JarEntry file = jarIn.getNextJarEntry();
			while (file != null) {
				if (path.equals(file.getName())) return jarIn;
				file = jarIn.getNextJarEntry();
			}
			return null;
		} catch (IOException e) {
			throw new RuntimeException(e);
		//} finally {
		//	if (bin != null) bin.close();
		}
	}
}

実行スクリプト:

var qrcode = newQrcode();

function newQrcode() {
	var loader = Packages.kotemaru.wsjs.RepositoryClassLoader(
		__ENV__.wsjsContext, "/qrcode/Qrcode.jar", null);
	return loader.loadClass("com.swetake.util.Qrcode").newInstance();
}

動いた!!

Rhinoの実行環境にクラスパスを追加するメソッドを用意して普通に使える様にしてみる。

__ENV__の実装クラス:

	public void addClassPath(String pageName) throws Exception {
		pageName = getAbsPageName(pageName);
		ClassLoader parent = context.getApplicationClassLoader();
		ClassLoader loader =
			new RepositoryClassLoader(wsjsContext, pageName, parent);
		context.setApplicationClassLoader(loader);

		// NOTE: Rhino-1.7 の実装に依存。
		// これをしないと setApplicationClassLoader() が反映されない。
		NativeJavaTopPackage.init(context, scope, false);
	}

実行スクリプト

__ENV__.addClassPath("Qrcode.jar");
var Qrcode = Packages.com.swetake.util.Qrcode;
         :
		var qrcode = new Qrcode();

ちゃんと動く。
NativeJavaTopPackage.init()は非公開クラスで使いたく無いんだけど setApplicationClassLoader()が反映されないので仕方無く使ってる。

これで、GAE/Jアプリのアップロード無しに追加Javaライブラリが使えるようになった。 すげぇ便利かも。

流れをまとめとく

QrcodeライブラリのJarをアップロードする。

JavaScriptのClassPathにQrcodeライブラリ追加する。

実行結果。アップロードしたQrcodeライブラリが使えている。