diff --git a/src/java/com/threerings/presents/tools/GenActionScriptStreamableTask.java b/src/java/com/threerings/presents/tools/GenActionScriptStreamableTask.java new file mode 100644 index 000000000..e12113d9b --- /dev/null +++ b/src/java/com/threerings/presents/tools/GenActionScriptStreamableTask.java @@ -0,0 +1,102 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2010 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.lang.reflect.Field; + +import java.util.List; +import java.util.Set; + +import java.io.File; +import com.google.common.base.Charsets; +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.io.Files; + +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; + +public class GenActionScriptStreamableTask extends GenActionScriptTask +{ + @Override + protected void convert (File javaSource, Class sclass, File outputLocation) + throws Exception + { + // Generate the current version of the streamable + StreamableClassRequirements reqs = new StreamableClassRequirements(sclass); + + Set imports = Sets.newLinkedHashSet(); + imports.add(ObjectInputStream.class.getName()); + imports.add(ObjectOutputStream.class.getName()); + String extendsName = ""; + if (!sclass.getSuperclass().equals(Object.class)) { + extendsName = addImportAndGetShortType(sclass.getSuperclass(), false, imports); + } + + Set implemented = Sets.newLinkedHashSet(); + for (Class iface : sclass.getInterfaces()) { + implemented.add(addImportAndGetShortType(iface, false, imports)); + } + List fields = Lists.newArrayList(); + for (Field f : reqs.streamedFields) { + fields.add(new ASField(f, imports)); + } + String output = mergeTemplate("com/threerings/presents/tools/streamable_as.tmpl", + "header", _header, + "package", sclass.getPackage().getName(), + "classname", sclass.getSimpleName(), + "imports", imports, + "extends", extendsName, + "implements", Joiner.on(", ").join(implemented), + "superclassStreamable", reqs.superclassStreamable, + "fields", fields); + + if (outputLocation.exists()) { + // Merge in the previously generated version + String existing = Files.toString(outputLocation, Charsets.UTF_8); + output = new GeneratedSourceMerger().merge(output, existing); + } else if (!outputLocation.getParentFile().exists()) { + // Make sure the directory exists before trying to write there + Preconditions.checkArgument(outputLocation.getParentFile().mkdirs(), + "Unable to create directory to write '%s'", outputLocation.getAbsolutePath()); + } + Files.write(output, outputLocation, Charsets.UTF_8); + } + + protected static class ASField + { + public final String name; + public final String simpleType; + public final String reader; + public final String writer; + + public ASField (Field f, Set imports) + { + this.name = f.getName(); + this.simpleType = addImportAndGetShortType(f.getType(), true, imports); + this.reader = toReadObject(f.getType()); + this.writer = toWriteObject(f.getType(), name); + } + } +} diff --git a/src/java/com/threerings/presents/tools/GenActionScriptTask.java b/src/java/com/threerings/presents/tools/GenActionScriptTask.java index 484950d82..eaa5603a7 100644 --- a/src/java/com/threerings/presents/tools/GenActionScriptTask.java +++ b/src/java/com/threerings/presents/tools/GenActionScriptTask.java @@ -25,21 +25,23 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Set; import java.io.BufferedWriter; import java.io.File; -import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; +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 com.google.common.base.Charsets; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; - -import com.samskivert.io.StreamUtil; +import com.google.common.io.Files; import com.threerings.io.ObjectInputStream; import com.threerings.io.ObjectOutputStream; @@ -54,7 +56,7 @@ 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 +public class GenActionScriptTask extends GenTask { /** * Adds a nested <fileset> element which enumerates streamable source files. @@ -78,7 +80,7 @@ public class GenActionScriptTask extends Task public void setHeader (File header) { try { - _header = StreamUtil.toString(new FileReader(header)); + _header = Files.toString(header, Charsets.UTF_8); } catch (IOException ioe) { System.err.println("Unabled to load header '" + header + ": " + ioe.getMessage()); } @@ -116,17 +118,11 @@ public class GenActionScriptTask extends Task return; } + Class sclass = loadClass(name); try { - // in order for annotations to work, this task and all the classes it uses must be - // loaded from the same class loader as the classes on which we are going to - // introspect; this is non-ideal but unavoidable - processClass(source, getClass().getClassLoader().loadClass(name)); - } catch (ClassNotFoundException cnfe) { - System.err.println("Failed to load " + name + ".\nMissing class: " + cnfe.getMessage()); - System.err.println("Be sure to set the 'classpathref' attribute to a classpath\n" + - "that contains your projects invocation service classes."); + processClass(source, sclass); } catch (Exception e) { - e.printStackTrace(System.err); + throw new BuildException(e); } } @@ -134,7 +130,7 @@ public class GenActionScriptTask extends Task * Processes a resolved Streamable class instance. */ protected void processClass (File source, Class sclass) - throws IOException + throws Exception { // make sure we implement Streamable but don't extend DObject or InvocationMarshaller and // that we're a class not an interface @@ -158,19 +154,20 @@ public class GenActionScriptTask extends Task } while (cclass != null); // 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; + String path = toActionScriptType(sclass, false).replace(".", File.separator); File asfile = new File(_asroot, path + ".as"); System.err.println("Converting " + sclass.getName() + "..."); + convert(source, sclass, asfile); + } + protected void convert (File javaSource, Class sclass, File output) + throws Exception + { // parse the existing ActionScript source and generate what we don't // have from the Java class ActionScriptSource assrc = new ActionScriptSource(sclass); - assrc.absorbJava(source); + assrc.absorbJava(javaSource); assrc.imports.add(ObjectInputStream.class.getName()); assrc.imports.add(ObjectOutputStream.class.getName()); @@ -193,7 +190,7 @@ public class GenActionScriptTask extends Task continue; } body.append(" "); - body.append(field.getName()).append(" = "); + body.append(field.getName()).append(" = ins."); body.append(toReadObject(field.getType())); body.append(";\n"); added++; @@ -227,13 +224,13 @@ public class GenActionScriptTask extends Task } // now we can parse existing definitions from any extant ActionScript source file - assrc.absorbActionScript(asfile); + assrc.absorbActionScript(output); // make sure our parent directory exists - asfile.getParentFile().mkdirs(); + output.getParentFile().mkdirs(); // now write all that out to the target source file - BufferedWriter out = new BufferedWriter(new FileWriter(asfile)); + BufferedWriter out = new BufferedWriter(new FileWriter(output)); assrc.write(new PrintWriter(out)); } @@ -243,65 +240,123 @@ public class GenActionScriptTask extends Task return !Modifier.isStatic(mods) && !Modifier.isTransient(mods); } - protected String toReadObject (Class type) + protected static String addImportAndGetShortType (Class type, boolean isField, + Set imports) + { + String full = toActionScriptType(type, isField); + if (needsActionScriptImport(type, isField)) { + imports.add(full); + } + return getSimpleType(full); + } + + protected static boolean needsActionScriptImport (Class type, boolean isField) + { + if (type.isArray()) { + return Byte.TYPE.equals(type.getComponentType()) || isField; + } + return (Long.TYPE.equals(type) || !type.isPrimitive()) && !String.class.equals(type); + } + + protected static String toActionScriptType (Class type, boolean isField) + { + if (type.isArray()) { + if (Byte.TYPE.equals(type.getComponentType())) { + return "flash.utils.ByteArray"; + } + if (isField) { + return "com.threerings.io.TypedArray"; + } + return "Array"; + } + + if (Integer.TYPE.equals(type) || + Byte.TYPE.equals(type) || + Short.TYPE.equals(type) || + Character.TYPE.equals(type)) { + return "int"; + } + + if (Float.TYPE.equals(type) || + Double.TYPE.equals(type)) { + return "Number"; + } + + if (Long.TYPE.equals(type)) { + return "com.threerings.util.Long"; + } + + if (Boolean.TYPE.equals(type)) { + return "Boolean"; + } + + return type.getName().replaceAll("\\$", "_"); + } + + public static String toReadObject (Class type) { if (type.equals(String.class)) { - return "ins.readField(String)"; + return "readField(String)"; } else if (type.equals(Integer.class) || type.equals(Short.class) || type.equals(Byte.class)) { String name = ActionScriptSource.toSimpleName(type.getName()); - return "ins.readField(" + name + ").value"; + return "readField(" + name + ").value"; } else if (type.equals(Long.class)) { String name = ActionScriptSource.toSimpleName(type.getName()); - return "ins.readField(" + name + ")"; + return "readField(" + name + ")"; } else if (type.equals(Boolean.TYPE)) { - return "ins.readBoolean()"; + return "readBoolean()"; } else if (type.equals(Byte.TYPE)) { - return "ins.readByte()"; + return "readByte()"; - } else if (type.equals(Short.TYPE)) { - return "ins.readShort()"; + } else if (type.equals(Short.TYPE) || type.equals(Character.TYPE)) { + return "readShort()"; } else if (type.equals(Integer.TYPE)) { - return "ins.readInt()"; + return "readInt()"; } else if (type.equals(Long.TYPE)) { - return "new Long(ins.readInt(), ins.readInt())"; + return "readLong()"; } else if (type.equals(Float.TYPE)) { - return "ins.readFloat()"; + return "readFloat()"; } else if (type.equals(Double.TYPE)) { - return "ins.readDouble()"; + return "readDouble()"; } else if (type.isArray()) { if (!type.getComponentType().isPrimitive()) { - return "ins.readObject(TypedArray)"; + return "readObject(TypedArray)"; } else { if (Double.TYPE.equals(type.getComponentType())) { - return "ins.readField(TypedArray.getJavaType(Number))"; + return "readField(TypedArray.getJavaType(Number))"; } else if (Boolean.TYPE.equals(type.getComponentType())) { - return "ins.readField(TypedArray.getJavaType(Boolean))"; + return "readField(TypedArray.getJavaType(Boolean))"; } else if (Integer.TYPE.equals(type.getComponentType())) { - return "ins.readField(TypedArray.getJavaType(int))"; + return "readField(TypedArray.getJavaType(int))"; } else if (Byte.TYPE.equals(type.getComponentType())) { - return "ins.readField(ByteArray)"; + return "readField(ByteArray)"; } else { throw new IllegalArgumentException(type + " isn't supported to stream to actionscript"); } } } else { - return "ins.readObject(" + ActionScriptSource.toSimpleName(type.getName()) + ")"; + return "readObject(" + ActionScriptSource.toSimpleName(type.getName()) + ")"; } } - protected String toWriteObject (Class type, String name) + public static String getSimpleType(String fullType) + { + return Iterables.getLast(DOT_SPLITTER.split(fullType)); + } + + public static String toWriteObject (Class type, String name) { if (type.equals(Integer.class)) { return "writeObject(new Integer(" + name + "))"; @@ -315,16 +370,14 @@ public class GenActionScriptTask extends Task } else if (type.equals(Byte.TYPE)) { return "writeByte(" + name + ")"; - } else if (type.equals(Short.TYPE)) { + } else if (type.equals(Short.TYPE) || type.equals(Character.TYPE)) { return "writeShort(" + name + ")"; } else if (type.equals(Integer.TYPE)) { return "writeInt(" + name + ")"; } else if (type.equals(Long.TYPE)) { - return "writeInt(" + name + " == null ? 0 : " + name + ".low);\n" + - " out.writeInt(" + - name + " == null ? 0 : " + name + ".high)"; + return "writeLong(" + name + ")"; } else if (type.equals(Float.TYPE)) { return "writeFloat(" + name + ")"; @@ -354,4 +407,6 @@ public class GenActionScriptTask extends Task "public function readObject (ins :ObjectInputStream) :void"; protected static final String WRITE_SIG = "public function writeObject (out :ObjectOutputStream) :void"; + + protected static final Splitter DOT_SPLITTER = Splitter.on('.'); } diff --git a/src/java/com/threerings/presents/tools/GenStreamableTask.java b/src/java/com/threerings/presents/tools/GenStreamableTask.java index 8da052cb7..36370208e 100644 --- a/src/java/com/threerings/presents/tools/GenStreamableTask.java +++ b/src/java/com/threerings/presents/tools/GenStreamableTask.java @@ -22,8 +22,6 @@ package com.threerings.presents.tools; import java.lang.reflect.Field; -import java.lang.reflect.Modifier; - import java.util.ArrayList; import java.io.File; @@ -35,7 +33,6 @@ import org.apache.tools.ant.types.FileSet; import com.google.common.collect.Lists; -import com.threerings.io.SimpleStreamableObject; import com.threerings.io.Streamable; import com.threerings.presents.dobj.DObject; @@ -104,9 +101,11 @@ public class GenStreamableTask extends Task protected void processClass (File source, Class sclass) throws IOException { - // we must implement Streamable, not be a DObject and not be an interface ourselves + StreamableClassRequirements reqs = new StreamableClassRequirements(sclass); + // we must implement Streamable, not be a DObject and have some fields that need to be + // streamed if (!Streamable.class.isAssignableFrom(sclass) || DObject.class.isAssignableFrom(sclass) || - Modifier.isInterface(sclass.getModifiers())) { + reqs.streamedFields.isEmpty()) { // System.err.println("Skipping " + sclass.getName() + "..."); return; } @@ -115,22 +114,11 @@ public class GenStreamableTask extends Task StringBuilder readbuf = new StringBuilder(READ_OPEN); StringBuilder writebuf = new StringBuilder(WRITE_OPEN); - // see if our parent also implements Streamable - Class supster = sclass.getSuperclass(); - do { - if (isStreamableClass(supster)) { - readbuf.append(" super.readObject(ins);\n"); - writebuf.append(" super.writeObject(out);\n"); - break; - } - supster = supster.getSuperclass(); - } while (supster != null); - - int added = 0; - for (Field field : sclass.getDeclaredFields()) { - if (!isStreamableField(field)) { - continue; - } + if (reqs.superclassStreamable) { + readbuf.append(" super.readObject(ins);\n"); + writebuf.append(" super.writeObject(out);\n"); + } + for (Field field : reqs.streamedFields) { readbuf.append(" "); readbuf.append(field.getName()).append(" = "); readbuf.append(toReadObject(field)); @@ -139,10 +127,6 @@ public class GenStreamableTask extends Task writebuf.append(" out."); writebuf.append(toWriteObject(field)); writebuf.append(";\n"); - added++; - } - if (added == 0) { - return; // nothing to do } readbuf.append(READ_CLOSE); @@ -179,22 +163,6 @@ public class GenStreamableTask extends Task } } - protected boolean isStreamableClass (Class clazz) - { - for (Class iface : clazz.getInterfaces()) { - if (Streamable.class.equals(iface)) { - return !SimpleStreamableObject.class.equals(clazz); - } - } - return false; - } - - protected boolean isStreamableField (Field field) - { - int mods = field.getModifiers(); - return !Modifier.isStatic(mods) && !Modifier.isTransient(mods); - } - protected String toReadObject (Field field) { Class type = field.getType(); diff --git a/src/java/com/threerings/presents/tools/GenTask.java b/src/java/com/threerings/presents/tools/GenTask.java index b3a9f1153..c66e76640 100644 --- a/src/java/com/threerings/presents/tools/GenTask.java +++ b/src/java/com/threerings/presents/tools/GenTask.java @@ -21,16 +21,15 @@ package com.threerings.presents.tools; -import java.io.StringWriter; -import java.util.HashMap; import java.util.Map; +import java.io.StringWriter; + import org.apache.tools.ant.AntClassLoader; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Task; import org.apache.tools.ant.types.Reference; import org.apache.tools.ant.util.ClasspathUtils; - import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; diff --git a/src/java/com/threerings/presents/tools/GeneratedSourceMerger.java b/src/java/com/threerings/presents/tools/GeneratedSourceMerger.java new file mode 100644 index 000000000..b901fccd9 --- /dev/null +++ b/src/java/com/threerings/presents/tools/GeneratedSourceMerger.java @@ -0,0 +1,117 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2010 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.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import com.samskivert.util.Tuple; + +/** + * Merges updates to a generated source file into a previously generated version of that source + * while leaving changes outside marked sections alone. + */ +public class GeneratedSourceMerger +{ + /** + * Returns previouslyGenerated with marked sections updated from the same marked + * sections in newlyGenerated Everything outside these sections in + * previouslyGenerated is returned as is. A marked section starts with + * "// GENERATED START" and ends with "// GENERATED END" + */ + public String merge (String newlyGenerated, String previouslyGenerated) + throws Exception + { + // Extract the generated section names from the output and make sure they're all matched + Map sections = Maps.newLinkedHashMap(); + Matcher m = _sectionDelimiter.matcher(newlyGenerated); + while (m.find()) { + Tuple section = extractGeneratedSection(m, newlyGenerated); + Preconditions.checkArgument(!sections.containsKey(section.left), + "Section '%s' used more than once", section.left); + sections.put(section.left, section.right); + } + + // Merge with the previously generated source + StringBuilder merged = new StringBuilder(); + m = _sectionDelimiter.matcher(previouslyGenerated); + Iterator> secs = sections.entrySet().iterator(); + int currentStart = 0; + while (m.find()) { + merged.append(previouslyGenerated.substring(currentStart, m.start())); + Tuple existingSection = + extractGeneratedSection(m, previouslyGenerated); + // Allow generated sections to be dropped in the template, but warn in case something + // odd's happening + if (!secs.hasNext()) { + System.err.println("Dropping previously generated section '" + m.group(1) + + "' that's no longer generated by the template"); + } + // However, we don't allow sections to be removed or reordered in the generated code + Entry newSection = secs.next(); + Preconditions.checkArgument(newSection.getKey().equals(existingSection.left), + "Generated code expected '%s' section, but encountered '%s'", newSection.getKey(), + existingSection.left); + // Pop in the newly generated code in the place of what was there before + merged.append(newSection.getValue()); + currentStart = m.end(); + } + // Add generated sections that weren't present in the old output before the last + // non-generated code. It's a 50-50 shot, so warn when this happens + while(secs.hasNext()) { + Entry newSection = secs.next(); + System.err.println("Adding previously missing generated section '" + + newSection.getKey() + "' before the last non-generated text"); + merged.append(newSection.getValue()); + } + // Add any text past the last previously generated section + merged.append(previouslyGenerated.substring(currentStart)); + + return merged.toString(); + } + + /** + * Returns a section name and its contents from the given matcher pointing to the start of a + * section. m is at the end of the section when this returns. + */ + protected Tuple extractGeneratedSection (Matcher m, String input) + { + int startIdx = m.start(); + String startName = m.group(1); + Preconditions.checkArgument(m.group(2).equals("START"), "'%s' END without START", + startName); + Preconditions.checkArgument(m.find(), "'%s' START without END", startName); + String endName = m.group(1); + Preconditions.checkArgument(m.group(2).equals("END"), + "'%s' START after '%s' START", endName, startName); + Preconditions.checkArgument(endName.equals(startName), + "'%s' END after '%s' START", endName, startName); + return Tuple.newTuple(startName, input.substring(startIdx, m.end())); + } + + protected final Pattern _sectionDelimiter = Pattern.compile(" *// GENERATED (\\w+) (START|END)\n"); +} diff --git a/src/java/com/threerings/presents/tools/StreamableClassRequirements.java b/src/java/com/threerings/presents/tools/StreamableClassRequirements.java new file mode 100644 index 000000000..54cd6bcc2 --- /dev/null +++ b/src/java/com/threerings/presents/tools/StreamableClassRequirements.java @@ -0,0 +1,54 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2010 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.lang.reflect.Field; +import java.lang.reflect.Modifier; + +import java.util.List; + +import com.google.common.collect.Lists; + +import com.threerings.io.Streamable; + +public class StreamableClassRequirements +{ + public final List streamedFields = Lists.newArrayList(); + + public final boolean superclassStreamable; + + public final Class klass; + + public StreamableClassRequirements(Class klass) + { + this.klass = klass; + superclassStreamable = + klass.getSuperclass() != null && Streamable.class.isAssignableFrom(klass.getSuperclass()); + for (Field field : klass.getDeclaredFields()) { + int mods = field.getModifiers(); + if (Modifier.isStatic(mods) || Modifier.isTransient(mods)) { + continue; + } + streamedFields.add(field); + } + } +} diff --git a/src/java/com/threerings/presents/tools/streamable_as.tmpl b/src/java/com/threerings/presents/tools/streamable_as.tmpl new file mode 100644 index 000000000..fe07d1066 --- /dev/null +++ b/src/java/com/threerings/presents/tools/streamable_as.tmpl @@ -0,0 +1,48 @@ +// GENERATED PREAMBLE START +${header} +package ${package} { + +#foreach ($import in $imports) +import $import; +#end +// GENERATED PREAMBLE END + +// GENERATED CLASSDECL START +public class ${classname} #if (!${extends.isEmpty()})extends $extends +#end +#if (!${implements.isEmpty()}) + implements $implements +#end +{ + public function ${classname} () + {} +// GENERATED CLASSDECL END + +// GENERATED STREAMING START +#foreach ($field in $fields) + public var $field.name :${field.simpleType}; +#end + + #if ($superclassStreamable)override #{end}public function readObject (ins :ObjectInputStream) :void + { +#if ($superclassStreamable) + super.readObject(ins); +#end +#foreach ($field in $fields) + $field.name = ins.${field.reader}; +#end + } + + #if ($superclassStreamable)override #{end}public function writeObject (out :ObjectOutputStream) :void + { +#if ($superclassStreamable) + super.writeObject(out); +#end +#foreach ($field in $fields) + out.${field.writer}; +#end + } +// GENERATED STREAMING END + +} +} \ No newline at end of file