diff --git a/build.xml b/build.xml
index 739631797..9ebb5da2a 100644
--- a/build.xml
+++ b/build.xml
@@ -85,6 +85,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/java/com/threerings/presents/tools/ActionScriptSource.java b/src/java/com/threerings/presents/tools/ActionScriptSource.java
new file mode 100644
index 000000000..7bf37d95b
--- /dev/null
+++ b/src/java/com/threerings/presents/tools/ActionScriptSource.java
@@ -0,0 +1,940 @@
+//
+// $Id$
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2006 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.BufferedReader;
+import java.io.FileReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.samskivert.util.StringUtil;
+
+/**
+ * Primitively parses an ActionScriptSource file, allows the addition and
+ * replacement of class members and handles writing out the file again.
+ */
+public class ActionScriptSource
+{
+ /**
+ * Contains the name and text of a particular member declaration.
+ */
+ public static class Member
+ {
+ public String name;
+ public String comment = "";
+ public String definition;
+ public String body = "";
+
+ public Member (String name, String definition) {
+ this.name = name;
+ this.definition = definition;
+ }
+
+ public void setInitialValue (String initValue) {
+ // do not-very-smart array conversion
+ initValue = initValue.replaceAll("\\{", "[");
+ initValue = initValue.replaceAll("\\}", "]");
+ // stick the initial value before the semicolon
+ definition = definition.substring(0, definition.length()-1) +
+ " = " + initValue + ";";
+ }
+
+ public void setComment (String comment) {
+ if (comment.indexOf("@Override") != -1) {
+ comment = comment.replaceAll("@Override\\s?", "");
+ definition = "override " + definition;
+ }
+ // trim blank lines from start
+ while (comment.startsWith("\n")) {
+ comment = comment.substring(1);
+ }
+ this.comment = comment;
+ }
+
+ public void write (PrintWriter writer) {
+ writer.print(comment);
+ writer.println(" " + definition);
+ writer.print(body);
+ }
+ }
+
+ public String preamble = "";
+
+ public String packageName;
+
+ public String imports = "";
+
+ public String classComment = "";
+
+ public boolean isAbstract;
+
+ public String className;
+
+ public String superClassName;
+
+ public String[] implementedInterfaces;
+
+ public ArrayList publicConstants = new ArrayList();
+
+ public ArrayList publicFields = new ArrayList();
+
+ public ArrayList publicStaticMethods = new ArrayList();
+
+ public ArrayList publicConstructors = new ArrayList();
+
+ public ArrayList publicMethods = new ArrayList();
+
+ public ArrayList protectedConstructors = new ArrayList();
+
+ public ArrayList protectedMethods = new ArrayList();
+
+ public ArrayList protectedStaticMethods = new ArrayList();
+
+ public ArrayList protectedFields = new ArrayList();
+
+ public ArrayList protectedConstants = new ArrayList();
+
+ public static String toSimpleName (String name)
+ {
+ name = name.substring(name.lastIndexOf(".")+1);
+ // inner classes are not supported by ActionScript so we _
+ name = name.replaceAll("\\$", "_");
+ return name;
+ }
+
+ /**
+ * Creates and returns a declaration for a field equivalent to the supplied
+ * Java field in ActionScript.
+ */
+ public static String createActionScriptDeclaration (Field field)
+ {
+ int mods = field.getModifiers();
+ StringBuilder builder = new StringBuilder();
+
+ // what's our visibility?
+ if (Modifier.isPublic(mods)) {
+ builder.append("public ");
+ } else if (Modifier.isProtected(mods)) {
+ builder.append("protected ");
+ } else if (Modifier.isPrivate(mods)) {
+ builder.append("private ");
+ }
+
+ // are we static const or variable?
+ if (Modifier.isStatic(mods)) {
+ builder.append("static const ");
+ } else {
+ builder.append("var ");
+ }
+
+ // next comes the name
+ builder.append(field.getName()).append(" :");
+
+ // now convert the type to an ActionScript type
+ builder.append(toActionScriptType(field.getType()));
+
+ builder.append(";");
+
+ return builder.toString();
+ }
+
+ public static String createActionScriptDeclaration (
+ Constructor ctor, boolean needsNoArg)
+ {
+ int mods = ctor.getModifiers();
+ StringBuilder builder = new StringBuilder();
+
+ // what's our visibility?
+ if (Modifier.isPublic(mods)) {
+ builder.append("public ");
+ } else if (Modifier.isProtected(mods)) {
+ builder.append("protected ");
+ } else if (Modifier.isPrivate(mods)) {
+ builder.append("private ");
+ }
+
+ // all constructors are functions
+ builder.append("function ");
+
+ // next comes the name
+ builder.append(toSimpleName(ctor.getName())).append(" (");
+
+ // now the parameters
+ int idx = 0;
+ for (Class> ptype : ctor.getParameterTypes()) {
+ if (idx++ > 0) {
+ builder.append(", ");
+ }
+ builder.append("arg").append(idx);
+ builder.append(" :").append(toActionScriptType(ptype));
+ if (needsNoArg) {
+ builder.append(" = undef");
+ }
+ }
+ builder.append(")");
+
+ return builder.toString();
+ }
+
+ public static String createActionScriptDeclaration (Method method)
+ {
+ int mods = method.getModifiers();
+ StringBuilder builder = new StringBuilder();
+
+ // TODO: override
+
+ // what's our visibility?
+ if (Modifier.isPublic(mods)) {
+ builder.append("public ");
+ } else if (Modifier.isProtected(mods)) {
+ builder.append("protected ");
+ } else if (Modifier.isPrivate(mods)) {
+ builder.append("private ");
+ }
+
+ // are we static const or variable?
+ if (Modifier.isStatic(mods)) {
+ builder.append("static ");
+ }
+ builder.append("function ");
+
+ // next comes the name
+ builder.append(getName(method)).append(" (");
+
+ // now the parameters
+ int idx = 0;
+ for (Class> ptype : method.getParameterTypes()) {
+ if (idx++ > 0) {
+ builder.append(", ");
+ }
+ builder.append("arg").append(idx);
+ builder.append(" :").append(toActionScriptType(ptype));
+ }
+ builder.append(") :");
+
+ builder.append(toActionScriptType(method.getReturnType()));
+
+ return builder.toString();
+ }
+
+ public static String toActionScriptType (Class type)
+ {
+ if (type.isArray()) {
+ return "Array";
+ }
+
+ String tname = type.getName();
+ if (tname.equals("java.lang.Integer") ||
+ tname.equals("byte")) {
+ return "int";
+ }
+
+ if (tname.equals("long")) {
+ return "Long";
+ }
+
+ if (tname.equals("boolean")) {
+ return "Boolean";
+ }
+
+ return toSimpleName(tname);
+ }
+
+ protected static String getName (Method method)
+ {
+ // yay for the hackery; we can't overload methods in ActionScript so we
+ // do some crazy conversions based on signature to handle some of our
+ // common patterns
+ String name = method.getName();
+ Class>[] ptypes = method.getParameterTypes();
+
+ // toString(StringBuilder) -> toStringBuilder(StringBuilder)
+ if (name.equals("toString") && ptypes.length == 1 &&
+ ptypes[0].getName().equals("java.lang.StringBuilder")) {
+ return "toStringBuilder";
+ }
+
+ return name;
+ }
+
+ public ActionScriptSource (Class jclass)
+ {
+ packageName = jclass.getPackage().getName();
+ className = jclass.getName().substring(packageName.length()+1);
+ Class sclass = jclass.getSuperclass();
+ if (sclass != null && !sclass.getName().equals("java.lang.Object")) {
+ superClassName = toSimpleName(sclass.getName());
+ }
+ isAbstract = ((jclass.getModifiers() & Modifier.ABSTRACT) != 0);
+
+ // note our implemented interfaces
+ ArrayList ifaces = new ArrayList();
+ for (Class iclass : jclass.getInterfaces()) {
+ ifaces.add(toSimpleName(iclass.getName()));
+ }
+ implementedInterfaces = ifaces.toArray(new String[ifaces.size()]);
+
+ // convert the field members
+ for (Field field : jclass.getDeclaredFields()) {
+ int mods = field.getModifiers();
+ ArrayList list;
+ if (Modifier.isStatic(mods)) {
+ list = Modifier.isPublic(mods) ?
+ publicConstants : protectedConstants;
+ } else {
+ list = Modifier.isPublic(mods) ? publicFields : protectedFields;
+ }
+ list.add(new Member(field.getName(),
+ createActionScriptDeclaration(field)));
+ }
+
+ // ActionScript only supports one constructor so we find the one with
+ // the most arguments and we make a note whether or not we need a no
+ // argument constructor which we create using default arguments
+ Constructor mainctor = null;
+ boolean needsNoArg = false;
+ for (Constructor ctor : jclass.getConstructors()) {
+ int params = ctor.getParameterTypes().length;
+ if (mainctor == null ||
+ params > mainctor.getParameterTypes().length) {
+ mainctor = ctor;
+ }
+ needsNoArg = needsNoArg || (params == 0);
+ }
+ if (mainctor != null) {
+ int mods = mainctor.getModifiers();
+ ArrayList list;
+ list = Modifier.isPublic(mods) ?
+ publicConstructors : protectedConstructors;
+ Member mem = new Member(
+ toSimpleName(mainctor.getName()),
+ createActionScriptDeclaration(mainctor, needsNoArg));
+ mem.body = " {\n TODO: IMPLEMENT ME\n }\n";
+ list.add(mem);
+ }
+
+ for (Method method : jclass.getDeclaredMethods()) {
+ int mods = method.getModifiers();
+ ArrayList list;
+ if (Modifier.isStatic(mods)) {
+ list = Modifier.isPublic(mods) ?
+ publicStaticMethods : protectedStaticMethods;
+ } else {
+ list = Modifier.isPublic(mods) ?
+ publicMethods : protectedMethods;
+ }
+
+ // see if we already have a member for this method name
+ String name = getName(method);
+ String decl = createActionScriptDeclaration(method);
+ Member mem = getMember(list, name);
+ if (mem == null) {
+ mem = new Member(name, decl);
+ mem.body = " {\n TODO: IMPLEMENT ME\n }\n";
+ list.add(mem);
+ } else {
+ // TODO: only overwrite if we have more arguments
+ mem.definition = decl;
+ }
+ }
+ }
+
+ public void absorbJava (File jsource)
+ throws IOException
+ {
+ // parse our Java source file for niggling bits
+ BufferedReader bin = new BufferedReader(new FileReader(jsource));
+ String line = null;
+ Mode mode = Mode.PREAMBLE;
+ Matcher m;
+ int braceCount = 0;
+ StringBuilder accum = new StringBuilder();
+ while ((line = bin.readLine()) != null) {
+ line = line.replaceAll("\\s+$", "");
+ switch (mode) {
+ case PREAMBLE:
+ // look for the package declaration
+ m = JPACKAGE.matcher(line);
+ if (m.matches()) {
+ preamble = accum.toString();
+ accum.setLength(0);
+ packageName = m.group(1);
+ mode = Mode.IMPORTS;
+ } else {
+ accum.append(line).append("\n");
+ }
+ break;
+
+ case IMPORTS:
+ // look for the start of the class comment
+ if (line.startsWith("/**")) {
+ imports = accum.toString();
+ accum.setLength(0);
+ mode = Mode.CLASSCOMMENT;
+ }
+ accum.append(line).append("\n");
+ break;
+
+ case CLASSCOMMENT:
+ // look for the class declaration
+ if (line.startsWith("public ")) {
+ classComment = accum.toString();
+ accum.setLength(0);
+ mode = line.endsWith("{") ? Mode.CLASSBODY : Mode.CLASSDECL;
+ } else {
+ accum.append(line).append("\n");
+ }
+ break;
+
+ case CLASSDECL:
+ // look for the open brace
+ if (line.equals("{")) {
+ mode = Mode.CLASSBODY;
+ }
+ break;
+
+ case CLASSBODY:
+ // see if we match a field declaration
+ if ((m = JFIELD.matcher(line)).matches()) {
+ // if this line does not end on a semicolon, keep sucking
+ // up lines until it does
+ if (line.indexOf(";") == -1) {
+ line = line + slurpUntil(bin, ";", true);
+ // now rematch it all on one line
+ m = JFIELD.matcher(line);
+ if (!m.matches()) {
+ System.err.println(
+ "J: Pants, no longer match field: " + line);
+ continue;
+ }
+ }
+
+ String name = m.group(1);
+ String comment = accum.toString();
+ accum.setLength(0);
+
+ // set the comment on the specified field
+ Member mem = updateComment(publicConstants, name, comment);
+ if (mem == null) {
+ mem = updateComment(publicFields, name, comment);
+ }
+ if (mem == null) {
+ mem = updateComment(protectedFields, name, comment);
+ }
+ if (mem == null) {
+ mem = updateComment(protectedConstants, name, comment);
+ }
+ if (mem == null) {
+ System.err.println("J: Matched field for which we " +
+ "have no bytecode version: " + name +
+ ": " + line);
+ continue;
+ }
+
+ // extract and clean up any default value
+ String defval = m.group(2).trim();
+ if (defval.startsWith("=")) {
+ defval = defval.substring(1);
+ }
+ if (defval.endsWith(";")) {
+ defval = defval.substring(0, defval.length()-1);
+ }
+ defval = defval.trim();
+ if (defval.length() > 0) {
+ mem.setInitialValue(defval);
+ }
+
+ // see if we match a constructor declaration
+ } else if ((m = JCONSTRUCTOR.matcher(line)).matches()) {
+ // if this line does not contain a close paren, keep
+ // sucking up lines until it does
+ if (line.indexOf(")") == -1) {
+ line = line + slurpUntil(bin, ")", true);
+ // now rematch it all on one line
+ m = JCONSTRUCTOR.matcher(line);
+ if (!m.matches()) {
+ System.err.println(
+ "J: Pants, no longer match ctor: " + line);
+ continue;
+ }
+ }
+
+ String name = m.group(1);
+ String comment = accum.toString();
+ accum.setLength(0);
+ // set the comment on the specified method
+ updateComment(publicConstructors, name, comment);
+ updateComment(protectedConstructors, name, comment);
+
+ // switch to METHODBODY to absorb the method
+ braceCount = (line.indexOf("{") == -1) ? 0 : 1;
+ mode = Mode.METHODBODY;
+
+ // see if we match a method declaration
+ } else if ((m = JMETHOD.matcher(line)).matches()) {
+ // if this line does not contain a close paren, keep
+ // sucking up lines until it does
+ if (line.indexOf(")") == -1) {
+ line = line + slurpUntil(bin, ")", true);
+ // now rematch it all on one line
+ m = JMETHOD.matcher(line);
+ if (!m.matches()) {
+ System.err.println(
+ "J: Pants, no longer match method: " + line);
+ continue;
+ }
+ }
+
+ String name = m.group(1);
+ String comment = accum.toString();
+ accum.setLength(0);
+ // set the comment on the specified method
+ updateComment(publicStaticMethods, name, comment);
+ updateComment(publicMethods, name, comment);
+ updateComment(protectedMethods, name, comment);
+ updateComment(protectedStaticMethods, name, comment);
+
+ // switch to METHODBODY to absorb the method
+ braceCount = (line.indexOf("{") == -1) ? 0 : 1;
+ mode = Mode.METHODBODY;
+
+ } else {
+ // make sure this is a comment (or annotation)
+ String tline = line.trim();
+ if (tline.length() == 0 || tline.startsWith("@") ||
+ tline.startsWith("//") || tline.startsWith("/*") ||
+ tline.startsWith("*")) {
+ accum.append(line).append("\n");
+
+ } else if (tline.equals("}")) {
+ mode = Mode.POSTCLASS;
+
+ } else {
+ System.err.println("J: Non-comment encountered " +
+ "between members: " + line);
+ }
+ }
+ break;
+
+ case METHODBODY:
+ if (line.indexOf("{") != -1) {
+ braceCount++;
+ }
+ if (line.indexOf("}") != -1) {
+ braceCount--;
+ }
+ if (braceCount == 0) {
+ mode = Mode.CLASSBODY;
+ }
+ break;
+
+ case POSTCLASS:
+ System.err.println("J: Post-class junk: " + line);
+ break;
+ }
+ }
+ bin.close();
+
+ if (accum.length() > 0) {
+ System.err.println("J: Leftover accumulated text: " + accum);
+ }
+ }
+
+ public void absorbActionScript (File assource)
+ throws IOException
+ {
+ // if our action script source file doesn't exist, we're done
+ if (!assource.exists()) {
+ return;
+ }
+
+ BufferedReader bin = new BufferedReader(new FileReader(assource));
+ String line = null;
+ Mode mode = Mode.PREAMBLE;
+ Matcher m;
+ int braceCount = 0;
+ StringBuilder accum = new StringBuilder();
+ Member curmem = null;
+ while ((line = bin.readLine()) != null) {
+ line = line.replaceAll("\\s+$", "");
+ switch (mode) {
+ case PREAMBLE:
+ // look for the package declaration
+ if (line.startsWith("package ")) {
+ preamble = accum.toString();
+ accum.setLength(0);
+ mode = Mode.IMPORTS;
+ } else {
+ accum.append(line).append("\n");
+ }
+ break;
+
+ case IMPORTS:
+ // look for the start of the class comment
+ if (line.startsWith("/**")) {
+ imports = accum.toString();
+ accum.setLength(0);
+ mode = Mode.CLASSCOMMENT;
+ }
+ accum.append(line).append("\n");
+ break;
+
+ case CLASSCOMMENT:
+ // look for the class declaration
+ if (line.startsWith("public ")) {
+ classComment = accum.toString();
+ accum.setLength(0);
+ mode = line.endsWith("{") ? Mode.CLASSBODY : Mode.CLASSDECL;
+ } else {
+ accum.append(line).append("\n");
+ }
+ break;
+
+ case CLASSDECL:
+ // look for the open brace
+ if (line.equals("{")) {
+ mode = Mode.CLASSBODY;
+ }
+ break;
+
+ case CLASSBODY:
+ // see if we match a field declaration
+ if ((m = ASFIELD.matcher(line)).matches()) {
+ // if this line does not contain a semicolon, keep sucking
+ // up lines until it does
+ if (line.indexOf(";") == -1) {
+ line = line + "\n" + slurpUntil(bin, ";", false);
+ }
+
+ String fieldName = m.group(1);
+
+ // TODO: update the comment?
+ accum.setLength(0);
+
+ // TODO: extract the name, replace that declaration
+
+ // see if we match a constructor declaration
+ } else if (line.indexOf("public function " + className) != -1) {
+ // if this line does not contain a close paren, keep
+ // sucking up lines until it does
+ if (line.indexOf(")") == -1) {
+ line = line + "\n" + slurpUntil(bin, ")", false);
+ }
+
+ // look up the public constructor
+ curmem = getMember(publicConstructors, className);
+ if (curmem == null) {
+ System.err.println("Missing public constructor for " +
+ "update " + className + "().");
+ } else {
+ // update the definition and comment with the
+ // ActionScript versions
+ curmem.definition = line.trim();
+ curmem.setComment(accum.toString());
+ }
+ accum.setLength(0);
+
+ // switch to METHODBODY to absorb the method
+ braceCount = (line.indexOf("{") == -1) ? 0 : 1;
+ mode = Mode.METHODBODY;
+
+ // see if we match a method declaration
+ } else if ((m = ASFUNCTION.matcher(line)).matches()) {
+ // if this line does not contain a close paren, keep
+ // sucking up lines until it does
+ if (line.indexOf(")") == -1) {
+ line = line + "\n" + slurpUntil(bin, ")", false);
+ }
+
+ // extract the name, replace the declaration
+ String funcName = m.group(1);
+ curmem = getMember(publicMethods, funcName);
+ if (curmem == null) {
+ curmem = getMember(protectedMethods, funcName);
+ }
+ if (curmem == null) {
+ System.err.println(
+ "Have ActionScript method with no " +
+ "Java equivalent: " + funcName + "().");
+ } else {
+ // update the definition and comment with the
+ // ActionScript versions (unless it's readObject or
+ // writeObject in which case we want always to use the
+ // generated versions)
+ if (!funcName.equals("readObject") &&
+ !funcName.equals("writeObject")) {
+ curmem.definition = line.trim();
+ }
+ curmem.setComment(accum.toString());
+ }
+ accum.setLength(0);
+
+ // switch to METHODBODY to absorb the method
+ braceCount = (line.indexOf("{") == -1) ? 0 : 1;
+ mode = Mode.METHODBODY;
+
+ } else {
+ // make sure this is a comment (or annotation)
+ String tline = line.trim();
+ if (tline.length() == 0 || tline.startsWith("@") ||
+ tline.startsWith("//") || tline.startsWith("/*") ||
+ tline.startsWith("*")) {
+ accum.append(line).append("\n");
+
+ } else if (tline.equals("}")) {
+ mode = Mode.POSTCLASS;
+
+ } else {
+ System.err.println("AS: Non-comment encountered " +
+ "between members: " + line);
+ }
+ }
+ break;
+
+ case METHODBODY:
+ if (line.indexOf("{") != -1) {
+ braceCount++;
+ }
+ if (line.indexOf("}") != -1) {
+ braceCount--;
+ }
+ accum.append(line).append("\n");
+ if (braceCount == 0) {
+ // update the method body of our currently matched member
+ if (curmem != null) {
+ curmem.body = accum.toString();
+ curmem = null;
+ } else {
+ System.err.println(
+ "AS: No matched method for body:\n" + accum);
+ }
+ accum.setLength(0);
+ mode = Mode.CLASSBODY;
+ }
+ break;
+
+ case POSTCLASS:
+ if (line.trim().equals("}")) {
+ mode = Mode.POSTPKG;
+ } else {
+ System.err.println("AS: Post-class junk: " + line);
+ }
+ break;
+
+ case POSTPKG:
+ System.err.println("AS: Post-package junk: " + line);
+ break;
+ }
+ }
+ bin.close();
+
+ if (accum.length() > 0) {
+ System.err.println("AS: Leftover accumulated text: " + accum);
+ }
+ }
+
+ public Member updateComment (
+ ArrayList list, String name, String comment)
+ {
+ Member mem = getMember(list, name);
+ if (mem != null) {
+ mem.setComment(comment);
+ }
+ return mem;
+ }
+
+ public Member getMember (ArrayList list, String name)
+ {
+ for (Member member : list) {
+ if (member.name.equals(name)) {
+ return member;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Writes our class definition to the supplied writer.
+ */
+ public void write (PrintWriter writer)
+ {
+ writer.print(preamble);
+ writer.println("package " + packageName + " {");
+ writer.print(imports);
+
+ writer.print(classComment);
+ writer.print("public ");
+ if (isAbstract) {
+ writer.print("/*abstract*/ ");
+ }
+ writer.print("class " + className);
+ if (superClassName != null) {
+ writer.print(" extends " + superClassName);
+ }
+ writer.println("");
+
+ if (implementedInterfaces.length > 0) {
+ writer.print(" implements ");
+ for (int ii = 0; ii < implementedInterfaces.length; ii++) {
+ if (ii > 0) {
+ writer.print(", ");
+ }
+ writer.print(implementedInterfaces[ii]);
+ }
+ writer.println("");
+ }
+
+ writer.println("{"); // start class block
+
+ boolean writtenAnything = false;
+ for (Member member : publicConstants) {
+ writtenAnything = writeBlank(writtenAnything, writer);
+ member.write(writer);
+ }
+
+ for (Member member : publicFields) {
+ writtenAnything = writeBlank(writtenAnything, writer);
+ member.write(writer);
+ }
+
+ for (Member member : publicStaticMethods) {
+ writtenAnything = writeBlank(writtenAnything, writer);
+ member.write(writer);
+ }
+
+ for (Member member : publicConstructors) {
+ writtenAnything = writeBlank(writtenAnything, writer);
+ member.write(writer);
+ }
+
+ for (Member member : publicMethods) {
+ writtenAnything = writeBlank(writtenAnything, writer);
+ member.write(writer);
+ }
+
+ for (Member member : protectedConstructors) {
+ writtenAnything = writeBlank(writtenAnything, writer);
+ member.write(writer);
+ }
+
+ for (Member member : protectedMethods) {
+ writtenAnything = writeBlank(writtenAnything, writer);
+ member.write(writer);
+ }
+
+ for (Member member : protectedStaticMethods) {
+ writtenAnything = writeBlank(writtenAnything, writer);
+ member.write(writer);
+ }
+
+ for (Member member : protectedFields) {
+ writtenAnything = writeBlank(writtenAnything, writer);
+ member.write(writer);
+ }
+
+ for (Member member : protectedConstants) {
+ writtenAnything = writeBlank(writtenAnything, writer);
+ member.write(writer);
+ }
+
+ writer.println("}"); // end class block
+ writer.println("}"); // end package block
+ writer.flush();
+ }
+
+ public String toString ()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append("public ");
+ if (isAbstract) {
+ builder.append("abstract ");
+ }
+ builder.append("class ").append(className);
+ if (superClassName != null) {
+ builder.append(" extends ").append(superClassName);
+ }
+ return builder.toString();
+ }
+
+ protected String slurpUntil (BufferedReader reader, String token,
+ boolean stripNewlines)
+ throws IOException
+ {
+ String text = "", line;
+ while ((line = reader.readLine()) != null) {
+ line = line.replaceAll("\\s+$", "");
+ text += line;
+ if (!stripNewlines) {
+ text += "\n";
+ }
+ if (line.indexOf(token) != -1) {
+ break;
+ }
+ }
+ return text;
+ }
+
+ protected boolean writeBlank (boolean writtenAnything, PrintWriter writer)
+ {
+ if (writtenAnything) {
+ writer.println("");
+ }
+ return true;
+ }
+
+ protected static enum Mode { PREAMBLE, IMPORTS, CLASSCOMMENT, CLASSDECL,
+ CLASSBODY, METHODBODY, POSTCLASS, POSTPKG };
+
+ protected static Pattern JPACKAGE = Pattern.compile("package\\s+(\\S+);");
+
+ protected static Pattern JFIELD = Pattern.compile(
+ "\\s+(?:public|protected|private)" +
+ "(?:\\s+static|\\s+final|\\s+transient)*" +
+ "\\s+(?:[a-zA-Z\\[\\]<>,]+)" + // type
+ "\\s+([_a-zA-Z]\\w*)(\\s+=.*|;)");
+
+ protected static Pattern JCONSTRUCTOR = Pattern.compile(
+ "\\s+(?:public|protected|private)\\s+([a-zA-Z]\\w*) \\(.*");
+
+ protected static Pattern JMETHOD = Pattern.compile(
+ "\\s+(?:public|protected|private).* ([a-zA-Z]\\w*) \\(.*");
+
+ protected static Pattern ASFIELD = Pattern.compile(
+ "\\s+(?:public|protected|private)" +
+ "(?:\\s+var|\\s+static\\s+const)?" +
+ "\\s+([_a-zA-Z]\\w*)" + // variable name
+ "\\s+(?::[a-zA-Z\\[\\]<>,]+)" + // type
+ "(\\s+=.*|;)");
+
+ protected static Pattern ASFUNCTION = Pattern.compile(
+ ".*(?:public|protected) function ([a-zA-Z]\\w*) \\(.*");
+}
diff --git a/src/java/com/threerings/presents/tools/GenActionScriptTask.java b/src/java/com/threerings/presents/tools/GenActionScriptTask.java
new file mode 100644
index 000000000..9040d370a
--- /dev/null
+++ b/src/java/com/threerings/presents/tools/GenActionScriptTask.java
@@ -0,0 +1,374 @@
+//
+// $Id$
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2006 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.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+
+import java.util.ArrayList;
+
+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.Reference;
+import org.apache.tools.ant.util.ClasspathUtils;
+
+import org.apache.commons.io.IOUtils;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+
+import com.samskivert.util.StringUtil;
+import com.samskivert.velocity.VelocityUtil;
+
+import com.threerings.io.Streamable;
+import com.threerings.presents.data.InvocationMarshaller;
+import com.threerings.presents.dobj.DObject;
+
+/**
+ * Generates ActionScript versions of {@link Streamable} classes and provides
+ * routines used by the {@link GenDObjectTask} to create ActionScript versions
+ * of distributed objects.
+ */
+public class GenActionScriptTask extends Task
+{
+ /**
+ * Adds a nested <fileset> element which enumerates streamable source
+ * files.
+ */
+ public void addFileset (FileSet set)
+ {
+ _filesets.add(set);
+ }
+
+ /**
+ * Configures the path to our ActionScript source files.
+ */
+ public void setAsroot (File asroot)
+ {
+ _asroot = asroot;
+ }
+
+ /**
+ * Configures us with a header file that we'll prepend to all
+ * generated source files.
+ */
+ public void setHeader (File header)
+ {
+ try {
+ _header = IOUtils.toString(new FileReader(header));
+ } catch (IOException ioe) {
+ System.err.println("Unabled to load header '" + header + ": " +
+ ioe.getMessage());
+ }
+ }
+
+ /**
+ * Configures the classpath that we'll use to load classes.
+ */
+ public void setClasspathref (Reference pathref)
+ {
+ _cloader = ClasspathUtils.getClassLoaderForPath(getProject(), pathref);
+ }
+
+ /**
+ * Performs the actual work of the task.
+ */
+ public void execute () throws BuildException
+ {
+ if (_cloader == null) {
+ String errmsg = "This task requires a 'classpathref' attribute " +
+ "to be set to the project's classpath.";
+ throw new BuildException(errmsg);
+ }
+
+ try {
+ _velocity = VelocityUtil.createEngine();
+ } catch (Exception e) {
+ throw new BuildException("Failure initializing Velocity", e);
+ }
+
+ // resolve the Streamable class using our classloader
+ try {
+ _sclass = _cloader.loadClass(Streamable.class.getName());
+ _doclass = _cloader.loadClass(DObject.class.getName());
+ _imclass = _cloader.loadClass(InvocationMarshaller.class.getName());
+ } catch (Exception e) {
+ throw new BuildException("Can't resolve Streamable", e);
+ }
+
+ 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 Streamable source file.
+ */
+ protected void processClass (File source)
+ {
+ // System.err.println("Processing " + source + "...");
+
+ // load up the file and determine it's package and classname
+ String name = null;
+ try {
+ name = GenUtil.readClassName(source);
+ } catch (Exception e) {
+ System.err.println(
+ "Failed to parse " + source + ": " + e.getMessage());
+ return;
+ }
+
+ try {
+ processClass(source, _cloader.loadClass(name));
+ } catch (ClassNotFoundException cnfe) {
+ System.err.println(
+ "Failed to load " + name + ".\n" +
+ "Missing class: " + cnfe.getMessage());
+ System.err.println(
+ "Be sure to set the 'classpathref' attribute to a classpath\n" +
+ "that contains your projects invocation service classes.");
+ } catch (Exception e) {
+ e.printStackTrace(System.err);
+ }
+ }
+
+ /**
+ * Processes a resolved Streamable class instance.
+ */
+ protected void processClass (File source, Class sclass)
+ throws IOException
+ {
+ // make sure we implement Streamable but don't extend DObject or
+ // InvocationMarshaller and that we're a class not an interface
+ if (!_sclass.isAssignableFrom(sclass) ||
+ _doclass.isAssignableFrom(sclass) ||
+ _imclass.isAssignableFrom(sclass) ||
+ ((sclass.getModifiers() & Modifier.INTERFACE) != 0)) {
+ // System.err.println("Skipping " + sclass.getName() + "...");
+ return;
+ }
+
+ // determine the path to the corresponding action script source file
+ String path = sclass.getPackage().getName();
+ path = path.replace(".", File.separator);
+ String name = sclass.getName();
+ name = name.substring(name.lastIndexOf(".")+1);
+ path = path + File.separator + name;
+ File asfile = new File(_asroot, path + ".as");
+
+ System.err.println("Converting " + sclass.getName() + "...");
+
+ // parse the existing ActionScript source and generate what we don't
+ // have from the Java class
+ ActionScriptSource assrc = new ActionScriptSource(sclass);
+ assrc.absorbJava(source);
+
+ // see if our parent also implements Streamable
+ boolean needSuper = false;
+ Class supster = sclass.getSuperclass();
+ do {
+ if (isStreamable(supster)) {
+ needSuper = true;
+ break;
+ }
+ supster = supster.getSuperclass();
+ } while (supster != null);
+
+ // add readObject() and writeObject() definitions
+ ActionScriptSource.Member member;
+ member = new ActionScriptSource.Member(
+ "readObject", (needSuper ? "override " : "") + READ_SIG);
+ member.comment = " // from interface Streamable\n";
+ StringBuilder body = new StringBuilder(" {\n");
+ if (needSuper) {
+ body.append(" super.readObject(ins);\n");
+ }
+ int added = 0;
+ for (Field field : sclass.getDeclaredFields()) {
+ if (!isStreamable(field)) {
+ continue;
+ }
+ body.append(" ");
+ body.append(field.getName()).append(" = ");
+ body.append(toReadObject(field.getType().getName()));
+ body.append(";\n");
+ added++;
+ }
+ member.body = body.append(" }\n").toString();
+ if (added > 0) {
+ assrc.publicMethods.add(member);
+ }
+
+ member = new ActionScriptSource.Member(
+ "writeObject", (needSuper ? "override " : "") + WRITE_SIG);
+ member.comment = " // from interface Streamable\n";
+ body = new StringBuilder(" {\n");
+ if (needSuper) {
+ body.append(" super.writeObject(out);\n");
+ }
+ added = 0;
+ for (Field field : sclass.getDeclaredFields()) {
+ if (!isStreamable(field)) {
+ continue;
+ }
+ body.append(" out.");
+ body.append(
+ toWriteObject(field.getType().getName(), field.getName()));
+ body.append(";\n");
+ added++;
+ }
+ member.body = body.append(" }\n").toString();
+ if (added > 0) {
+ assrc.publicMethods.add(member);
+ }
+
+ // now we can parse existing definitions from any extant ActionScript
+ // source file
+ assrc.absorbActionScript(asfile);
+
+ // make sure our parent directory exists
+ asfile.getParentFile().mkdirs();
+
+ // now write all that out to the target source file
+ BufferedWriter out = new BufferedWriter(new FileWriter(asfile));
+ assrc.write(new PrintWriter(out));
+ }
+
+ protected boolean isStreamable (Class clazz)
+ {
+ for (Class iface : clazz.getInterfaces()) {
+ if (_sclass.equals(iface)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected boolean isStreamable (Field field)
+ {
+ int mods = field.getModifiers();
+ return !Modifier.isStatic(mods) && !Modifier.isTransient(mods);
+ }
+
+ protected String toReadObject (String type)
+ {
+ if (type.equals("java.lang.String")) {
+ return "(ins.readField(String) as String)";
+
+ } else if (type.equals("java.lang.Integer")) {
+ type = ActionScriptSource.toSimpleName(type);
+ return "(ins.readField(" + type + ") as " + type + ").value";
+
+ } else if (type.equals("byte")) {
+ return "ins.readByte()";
+
+ } else if (type.equals("short")) {
+ return "ins.readShort()";
+
+ } else if (type.equals("int")) {
+ return "ins.readInt()";
+
+ } else if (type.equals("long")) {
+ return "new Long(ins.readInt(), ins.readInt())";
+
+ } else {
+ type = ActionScriptSource.toSimpleName(type);
+ return "(ins.readObject() as " + type + ")";
+ }
+ }
+
+ protected String toWriteObject (String type, String name)
+ {
+ if (type.equals("java.lang.Integer")) {
+ return "writeObject(new Integer(" + name + "))";
+
+ } else if (type.equals("byte")) {
+ return "writeByte(" + name + ")";
+
+ } else if (type.equals("short")) {
+ return "writeShort(" + name + ")";
+
+ } else if (type.equals("int")) {
+ return "writeInt(" + name + ")";
+
+ } else if (type.equals("long")) {
+ return "writeInt(" + name + " == null ? 0 : " + name + ".low);\n" +
+ " out.writeInt(" +
+ name + " == null ? 0 : " + name + ".high)";
+
+ } else if (type.equals("java.lang.String")) {
+ return "writeField(" + name + ")";
+
+ } else {
+ return "writeObject(" + name + ")";
+ }
+ }
+
+ /** A list of filesets that contain tile images. */
+ protected ArrayList _filesets = new ArrayList();
+
+ /** A header to put on all generated source files. */
+ protected String _header;
+
+ /** The path to our ActionScript source files. */
+ protected File _asroot;
+
+ /** Used to do our own classpath business. */
+ protected ClassLoader _cloader;
+
+ /** Used to generate source files from templates. */
+ protected VelocityEngine _velocity;
+
+ /** {@link Streamable} resolved with the proper classloader so that we can
+ * compare it to loaded derived classes. */
+ protected Class> _sclass;
+
+ /** {@link DObject} resolved with the proper classloader so that we
+ * can compare it to loaded derived classes. */
+ protected Class> _doclass;
+
+ /** {@link InvocationMarshaller} resolved with the proper classloader so
+ * that we can compare it to loaded derived classes. */
+ protected Class> _imclass;
+
+ protected static final String READ_SIG =
+ "public function readObject (ins :ObjectInputStream) :void";
+ protected static final String WRITE_SIG =
+ "public function writeObject (out :ObjectOutputStream) :void";
+}