From 8015764c61bd573d958a1de7e3b8083c12088642 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Wed, 4 Oct 2006 02:05:15 +0000 Subject: [PATCH] Don't look now kids, but it's automatic generation of ActionScript code from Java. Of course this is a very limited translation facility that is mainly focused on auto-generating Streamable ActionScript classes from their Java originals, and it doesn't actually convert method bodies, just field and method declarations, initial values, and such. Most of our Streamable classes don't have much in the way of real methods, and the autogenerator will stick new methods in when they show up on the Java side so at least we'll *know* that they need to be implemented. And the main thing: readObject and writeObject are in fact implemented by the code generator so at least we won't be plagued by annoying streaming errors when we change something on the Java side and forget to change the ActionScript side. Next up, auto-generating InvocationService interfaces and InvocationMarshaller implementations in ActionScript and ActionScript versions of DObjects. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@4394 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- build.xml | 11 + .../presents/tools/ActionScriptSource.java | 940 ++++++++++++++++++ .../presents/tools/GenActionScriptTask.java | 374 +++++++ 3 files changed, 1325 insertions(+) create mode 100644 src/java/com/threerings/presents/tools/ActionScriptSource.java create mode 100644 src/java/com/threerings/presents/tools/GenActionScriptTask.java 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"; +}