diff --git a/build.xml b/build.xml
index ab930c3d0..3eb7dc410 100644
--- a/build.xml
+++ b/build.xml
@@ -88,9 +88,12 @@
+ classname="com.threerings.presents.tools.InstrumentStreamableTask"/>
-
+
+
+
+
diff --git a/src/java/com/threerings/presents/tools/InstrumentStreamableTask.java b/src/java/com/threerings/presents/tools/InstrumentStreamableTask.java
new file mode 100644
index 000000000..5eed6a58a
--- /dev/null
+++ b/src/java/com/threerings/presents/tools/InstrumentStreamableTask.java
@@ -0,0 +1,282 @@
+//
+// $Id$
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.presents.tools;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashSet;
+
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.CtField;
+import javassist.CtMethod;
+import javassist.CtNewMethod;
+import javassist.Modifier;
+import javassist.NotFoundException;
+import javassist.CannotCompileException;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.DirectoryScanner;
+import org.apache.tools.ant.Task;
+import org.apache.tools.ant.types.FileSet;
+import org.apache.tools.ant.types.Path;
+
+import com.samskivert.io.StreamUtil;
+import com.threerings.io.Streamable;
+
+/**
+ * Instruments compiled {@link Streamable} classes with public methods that can be used to stream
+ * protected and private members when running in a sandboxed JVM.
+ */
+public class InstrumentStreamableTask extends Task
+{
+ /**
+ * Adds a nested <fileset> element which enumerates streamable class files.
+ */
+ public void addFileset (FileSet set)
+ {
+ _filesets.add(set);
+ }
+
+ /**
+ * Adds a <path> element which defines our classpath.
+ */
+ public void addPath (Path path)
+ {
+ _paths.add(path);
+ }
+
+ /**
+ * Configures the directory into which we write our instrumented class files.
+ */
+ public void setOutdir (File outdir)
+ {
+ _outdir = outdir;
+ }
+
+ /**
+ * Performs the actual work of the task.
+ */
+ public void execute () throws BuildException
+ {
+ // configure our ClassPool with our classpath
+ for (Path path : _paths) {
+ for (String element : path.list()) {
+ try {
+ _pool.appendClassPath(element);
+ } catch (NotFoundException nfe) {
+ System.err.println("Invalid classpath entry [path=" + element + "]: " + nfe);
+ }
+ }
+ }
+
+ // now process the files
+ for (FileSet fs : _filesets) {
+ DirectoryScanner ds = fs.getDirectoryScanner(getProject());
+ File fromDir = fs.getDir(getProject());
+ String[] srcFiles = ds.getIncludedFiles();
+ for (int f = 0; f < srcFiles.length; f++) {
+ processClass(new File(fromDir, srcFiles[f]));
+ }
+ }
+ }
+
+ /**
+ * Processes a class file.
+ */
+ protected void processClass (File source)
+ {
+ CtClass clazz;
+ InputStream in = null;
+ try {
+ clazz = _pool.makeClass(in = new BufferedInputStream(new FileInputStream(source)));
+ } catch (IOException ioe) {
+ System.err.println("Failed to load " + source + ": " + ioe);
+ return;
+ } finally {
+ StreamUtil.close(in);
+ }
+
+ try {
+ if (isStreamable(clazz)) {
+ processStreamable(source, clazz);
+ }
+ } catch (NotFoundException nfe) {
+ System.err.println("Error processing class [class=" + clazz.getName() +
+ ", error=" + nfe + "].");
+ }
+ }
+
+ /**
+ * Returns true if the supplied class or any of its parents implements {@link Streamable}.
+ */
+ protected boolean isStreamable (CtClass clazz)
+ throws NotFoundException
+ {
+ if (clazz == null) {
+ return false;
+ }
+
+ for (CtClass iface : clazz.getInterfaces()) {
+ if (iface.getName().equals(Streamable.class.getName())) {
+ return true;
+ }
+ }
+ return isStreamable(clazz.getSuperclass());
+ }
+
+ /**
+ * Instruments the supplied {@link Streamable} implementing class.
+ */
+ protected void processStreamable (File source, CtClass clazz)
+ throws NotFoundException
+ {
+ // System.err.println("Processing " + clazz.getName() + "...");
+
+ ArrayList fields = new ArrayList();
+ for (CtField field : clazz.getDeclaredFields()) {
+ int modifiers = field.getModifiers();
+ if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers) ||
+ !(Modifier.isProtected(modifiers) || Modifier.isPrivate(modifiers))) {
+ continue;
+ }
+ fields.add(field);
+ }
+
+ HashSet methods = new HashSet();
+ for (CtMethod method : clazz.getMethods()) {
+ methods.add(method.getName());
+ }
+
+ int added = 0;
+ for (CtField field : fields) {
+ String rname = "readField_" + field.getName();
+ if (!methods.contains(rname)) {
+ String reader =
+ "public void " + rname + " (com.threerings.io.ObjectInputStream ins) {\n" +
+ " " + field.getName() + " = " + getFieldReader(field) + ";\n" +
+ "}";
+ // System.err.println("Adding reader " + clazz.getName() + "." + rname);
+ try {
+ clazz.addMethod(CtNewMethod.make(reader, clazz));
+ added++;
+ } catch (CannotCompileException cce) {
+ System.err.println("Unable to compile reader [class=" + clazz.getName() +
+ ", error=" + cce + "]:");
+ System.err.println(reader);
+ }
+ }
+ String wname = "writeField_" + field.getName();
+ if (!methods.contains(wname)) {
+ // System.err.println("Adding writer " + clazz.getName() + "." + wname);
+ String writer =
+ "public void " + wname + " (com.threerings.io.ObjectOutputStream out) {\n" +
+ " out." + getFieldWriter(field) + ";\n" +
+ "}";
+ try {
+ clazz.addMethod(CtNewMethod.make(writer, clazz));
+ added++;
+ } catch (CannotCompileException cce) {
+ System.err.println("Unable to compile writer [class=" + clazz.getName() +
+ ", error=" + cce + "]:");
+ System.err.println(writer);
+ }
+ }
+ }
+
+ if (added > 0) {
+ try {
+ clazz.writeFile(_outdir.getPath());
+ } catch (Exception e) {
+ System.err.println("Failed to write instrumented class [class=" + clazz +
+ ", outdir=" + _outdir + "]: " + e);
+ }
+ }
+ }
+
+ protected String getFieldReader (CtField field)
+ throws NotFoundException
+ {
+ CtClass type = field.getType();
+ if (type.getName().equals("java.lang.String")) {
+ return "ins.readUTF()";
+ } else if (type.equals(CtClass.booleanType) || type.getName().equals("java.lang.Boolean")) {
+ return "ins.readBoolean()";
+ } else if (type.equals(CtClass.byteType) || type.getName().equals("java.lang.Byte")) {
+ return "ins.readByte()";
+ } else if (type.equals(CtClass.shortType) || type.getName().equals("java.lang.Short")) {
+ return "ins.readShort()";
+ } else if (type.equals(CtClass.intType) || type.getName().equals("java.lang.Integer")) {
+ return "ins.readInt()";
+ } else if (type.equals(CtClass.longType) || type.getName().equals("java.lang.Long")) {
+ return "ins.readLong()";
+ } else if (type.equals(CtClass.floatType) || type.getName().equals("java.lang.Float")) {
+ return "ins.readFloat()";
+ } else if (type.equals(CtClass.doubleType) || type.getName().equals("java.lang.Double")) {
+ return "ins.readDouble()";
+ } else {
+ return "(" + type.getSimpleName() + ")ins.readObject()";
+ }
+ }
+
+ protected String getFieldWriter (CtField field)
+ throws NotFoundException
+ {
+ CtClass type = field.getType();
+ String name = field.getName();
+ if (type.equals(CtClass.booleanType) || type.getName().equals("java.lang.Boolean")) {
+ return "writeBoolean(" + name + ")";
+ } else if (type.equals(CtClass.byteType) || type.getName().equals("java.lang.Byte")) {
+ return "writeByte(" + name + ")";
+ } else if (type.equals(CtClass.shortType) || type.getName().equals("java.lang.Short")) {
+ return "writeShort(" + name + ")";
+ } else if (type.equals(CtClass.intType) || type.getName().equals("java.lang.Integer")) {
+ return "writeInt(" + name + ")";
+ } else if (type.equals(CtClass.longType) || type.getName().equals("java.lang.Long")) {
+ return "writeLong(" + name + ")";
+ } else if (type.equals(CtClass.floatType) || type.getName().equals("java.lang.Float")) {
+ return "writeFloat(" + name + ")";
+ } else if (type.equals(CtClass.doubleType) || type.getName().equals("java.lang.Double")) {
+ return "writeDouble(" + name + ")";
+ } else if (type.getName().equals("java.lang.String")) {
+ return "writeUTF(" + name + ")";
+ } else {
+ return "writeObject(" + name + ")";
+ }
+ }
+
+ /** A list of filesets that contain Streamable class files. */
+ protected ArrayList _filesets = new ArrayList();
+
+ /** A list of paths that make up our classpath. */
+ protected ArrayList _paths = new ArrayList();
+
+ /** The directory to which we write our instrumented class files. */
+ protected File _outdir;
+
+ /** Used to instrument class files. */
+ protected ClassPool _pool = ClassPool.getDefault();
+}