cf949410ce
writeObject() as that handles nulls for us (and of course because that's how it's done when streaming using reflection). git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@4704 542714f4-19e9-0310-aa3c-eee0fc999fb1
356 lines
14 KiB
Java
356 lines
14 KiB
Java
//
|
|
// $Id$
|
|
//
|
|
// Narya library - tools for developing networked games
|
|
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
|
|
// http://www.threerings.net/code/narya/
|
|
//
|
|
// This library is free software; you can redistribute it and/or modify it
|
|
// under the terms of the GNU Lesser General Public License as published
|
|
// by the Free Software Foundation; either version 2.1 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
// Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public
|
|
// License along with this library; if not, write to the Free Software
|
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
package com.threerings.presents.tools;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
|
|
import javassist.ClassPool;
|
|
import javassist.CtClass;
|
|
import javassist.CtField;
|
|
import javassist.CtMethod;
|
|
import javassist.CtNewMethod;
|
|
import javassist.Modifier;
|
|
import javassist.NotFoundException;
|
|
import javassist.CannotCompileException;
|
|
|
|
import org.apache.tools.ant.BuildException;
|
|
import org.apache.tools.ant.DirectoryScanner;
|
|
import org.apache.tools.ant.Task;
|
|
import org.apache.tools.ant.types.FileSet;
|
|
import org.apache.tools.ant.types.Path;
|
|
|
|
import com.samskivert.io.StreamUtil;
|
|
import com.threerings.io.BasicStreamers;
|
|
import com.threerings.io.FieldMarshaller;
|
|
import com.threerings.io.Streamable;
|
|
|
|
/**
|
|
* Instruments compiled {@link Streamable} classes with public methods that can be used to stream
|
|
* protected and private members when running in a sandboxed JVM.
|
|
*/
|
|
public class InstrumentStreamableTask extends Task
|
|
{
|
|
/**
|
|
* Adds a nested <fileset> element which enumerates streamable class files.
|
|
*/
|
|
public void addFileset (FileSet set)
|
|
{
|
|
_filesets.add(set);
|
|
}
|
|
|
|
/**
|
|
* Adds a <path> element which defines our classpath.
|
|
*/
|
|
public void addPath (Path path)
|
|
{
|
|
_paths.add(path);
|
|
}
|
|
|
|
/**
|
|
* Configures the directory into which we write our instrumented class files.
|
|
*/
|
|
public void setOutdir (File outdir)
|
|
{
|
|
_outdir = outdir;
|
|
}
|
|
|
|
/**
|
|
* Performs the actual work of the task.
|
|
*/
|
|
public void execute () throws BuildException
|
|
{
|
|
// configure our ClassPool with our classpath
|
|
for (Path path : _paths) {
|
|
for (String element : path.list()) {
|
|
try {
|
|
_pool.appendClassPath(element);
|
|
} catch (NotFoundException nfe) {
|
|
System.err.println("Invalid classpath entry [path=" + element + "]: " + nfe);
|
|
}
|
|
}
|
|
}
|
|
|
|
// instantiate streamable
|
|
try {
|
|
_streamable = _pool.get(Streamable.class.getName());
|
|
} catch (Exception e) {
|
|
throw new BuildException("Unable to load " + Streamable.class.getName() + ": " + e);
|
|
}
|
|
|
|
// now process the files
|
|
for (FileSet fs : _filesets) {
|
|
DirectoryScanner ds = fs.getDirectoryScanner(getProject());
|
|
File fromDir = fs.getDir(getProject());
|
|
String[] srcFiles = ds.getIncludedFiles();
|
|
for (int f = 0; f < srcFiles.length; f++) {
|
|
processClass(new File(fromDir, srcFiles[f]));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes a class file.
|
|
*/
|
|
protected void processClass (File source)
|
|
{
|
|
CtClass clazz;
|
|
InputStream in = null;
|
|
try {
|
|
clazz = _pool.makeClass(in = new BufferedInputStream(new FileInputStream(source)));
|
|
} catch (IOException ioe) {
|
|
System.err.println("Failed to load " + source + ": " + ioe);
|
|
return;
|
|
} finally {
|
|
StreamUtil.close(in);
|
|
}
|
|
|
|
try {
|
|
if (clazz.subtypeOf(_streamable)) {
|
|
processStreamable(source, clazz);
|
|
}
|
|
} catch (NotFoundException nfe) {
|
|
System.err.println("Error processing class [class=" + clazz.getName() +
|
|
", error=" + nfe + "].");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Instruments the supplied {@link Streamable} implementing class.
|
|
*/
|
|
protected void processStreamable (File source, CtClass clazz)
|
|
throws NotFoundException
|
|
{
|
|
ArrayList<CtField> fields = new ArrayList<CtField>();
|
|
for (CtField field : clazz.getDeclaredFields()) {
|
|
int modifiers = field.getModifiers();
|
|
if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers) ||
|
|
!(Modifier.isProtected(modifiers) || Modifier.isPrivate(modifiers))) {
|
|
continue;
|
|
}
|
|
fields.add(field);
|
|
}
|
|
|
|
HashSet<String> methods = new HashSet<String>();
|
|
for (CtMethod method : clazz.getMethods()) {
|
|
methods.add(method.getName());
|
|
}
|
|
|
|
int added = 0;
|
|
for (CtField field : fields) {
|
|
String rname = FieldMarshaller.getReaderMethodName(field.getName());
|
|
if (!methods.contains(rname)) {
|
|
String reader =
|
|
"public void " + rname + " (com.threerings.io.ObjectInputStream ins) {\n" +
|
|
// " throws java.io.IOException, java.lang.ClassNotFoundException\n" +
|
|
" " + getFieldReader(field) + "\n" +
|
|
"}";
|
|
// System.out.println("Adding reader " + clazz.getName() + ":\n" + reader);
|
|
try {
|
|
clazz.addMethod(CtNewMethod.make(reader, clazz));
|
|
added++;
|
|
} catch (CannotCompileException cce) {
|
|
System.err.println("Unable to compile reader [class=" + clazz.getName() +
|
|
", error=" + cce + "]:");
|
|
System.err.println(reader);
|
|
}
|
|
}
|
|
|
|
String wname = FieldMarshaller.getWriterMethodName(field.getName());
|
|
if (!methods.contains(wname)) {
|
|
String writer =
|
|
"public void " + wname + " (com.threerings.io.ObjectOutputStream out) {\n" +
|
|
// " throws java.io.IOException\n" +
|
|
" " + getFieldWriter(field) + "\n" +
|
|
"}";
|
|
// System.out.println("Adding writer " + clazz.getName() + ":\n" + writer);
|
|
try {
|
|
clazz.addMethod(CtNewMethod.make(writer, clazz));
|
|
added++;
|
|
} catch (CannotCompileException cce) {
|
|
System.err.println("Unable to compile writer [class=" + clazz.getName() +
|
|
", error=" + cce + "]:");
|
|
System.err.println(writer);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (added > 0) {
|
|
try {
|
|
System.out.println("Instrumented '" + clazz.getName() + "'.");
|
|
clazz.writeFile(_outdir.getPath());
|
|
} catch (Exception e) {
|
|
System.err.println("Failed to write instrumented class [class=" + clazz +
|
|
", outdir=" + _outdir + "]: " + e);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected String getFieldReader (CtField field)
|
|
throws NotFoundException
|
|
{
|
|
CtClass type = field.getType();
|
|
String name = field.getName();
|
|
if (type.getName().equals("java.lang.String")) {
|
|
return readWrap(field, name + " = ins.readUTF();");
|
|
} else if (type.equals(CtClass.booleanType) || type.getName().equals("java.lang.Boolean")) {
|
|
return readWrap(field, name + " = ins.readBoolean();");
|
|
} else if (type.equals(CtClass.byteType) || type.getName().equals("java.lang.Byte")) {
|
|
return readWrap(field, name + " = ins.readByte();");
|
|
} else if (type.equals(CtClass.shortType) || type.getName().equals("java.lang.Short")) {
|
|
return readWrap(field, name + " = ins.readShort();");
|
|
} else if (type.equals(CtClass.intType) || type.getName().equals("java.lang.Integer")) {
|
|
return readWrap(field, name + " = ins.readInt();");
|
|
} else if (type.equals(CtClass.longType) || type.getName().equals("java.lang.Long")) {
|
|
return readWrap(field, name + " = ins.readLong();");
|
|
} else if (type.equals(CtClass.floatType) || type.getName().equals("java.lang.Float")) {
|
|
return readWrap(field, name + " = ins.readFloat();");
|
|
} else if (type.equals(CtClass.doubleType) || type.getName().equals("java.lang.Double")) {
|
|
return readWrap(field, name + " = ins.readDouble();");
|
|
}
|
|
|
|
if (type.isArray()) {
|
|
CtClass ctype = type.getComponentType();
|
|
if (ctype.equals(CtClass.booleanType)) {
|
|
return readWrap(field, name + " = " + BSNAME + ".readBooleanArray(ins);");
|
|
} else if (ctype.equals(CtClass.byteType)) {
|
|
return readWrap(field, name + " = " + BSNAME + ".readByteArray(ins);");
|
|
} else if (ctype.equals(CtClass.shortType)) {
|
|
return readWrap(field, name + " = " + BSNAME + ".readShortArray(ins);");
|
|
} else if (ctype.equals(CtClass.intType)) {
|
|
return readWrap(field, name + " = " + BSNAME + ".readIntArray(ins);");
|
|
} else if (ctype.equals(CtClass.longType)) {
|
|
return readWrap(field, name + " = " + BSNAME + ".readLongArray(ins);");
|
|
} else if (ctype.equals(CtClass.floatType)) {
|
|
return readWrap(field, name + " = " + BSNAME + ".readFloat(ins);");
|
|
} else if (ctype.equals(CtClass.doubleType)) {
|
|
return readWrap(field, name + " = " + BSNAME + ".readDoubleArray(ins);");
|
|
} else if (ctype.getName().equals("java.lang.Object")) {
|
|
return readWrap(field, name + " = " + BSNAME + ".readObjectArray(ins);");
|
|
}
|
|
}
|
|
|
|
// no need to wrap streamable instances
|
|
return (name + " = (" + type.getName() + ")ins.readObject();");
|
|
}
|
|
|
|
protected String getFieldWriter (CtField field)
|
|
throws NotFoundException
|
|
{
|
|
CtClass type = field.getType();
|
|
String name = field.getName();
|
|
|
|
if (type.equals(CtClass.booleanType) || type.getName().equals("java.lang.Boolean")) {
|
|
return writeWrap(field, "out.writeBoolean(" + name + ");");
|
|
} else if (type.equals(CtClass.byteType) || type.getName().equals("java.lang.Byte")) {
|
|
return writeWrap(field, "out.writeByte(" + name + ");");
|
|
} else if (type.equals(CtClass.shortType) || type.getName().equals("java.lang.Short")) {
|
|
return writeWrap(field, "out.writeShort(" + name + ");");
|
|
} else if (type.equals(CtClass.intType) || type.getName().equals("java.lang.Integer")) {
|
|
return writeWrap(field, "out.writeInt(" + name + ");");
|
|
} else if (type.equals(CtClass.longType) || type.getName().equals("java.lang.Long")) {
|
|
return writeWrap(field, "out.writeLong(" + name + ");");
|
|
} else if (type.equals(CtClass.floatType) || type.getName().equals("java.lang.Float")) {
|
|
return writeWrap(field, "out.writeFloat(" + name + ");");
|
|
} else if (type.equals(CtClass.doubleType) || type.getName().equals("java.lang.Double")) {
|
|
return writeWrap(field, "out.writeDouble(" + name + ");");
|
|
} else if (type.getName().equals("java.lang.String")) {
|
|
return writeWrap(field, "out.writeUTF(" + name + ");");
|
|
}
|
|
|
|
if (type.isArray()) {
|
|
CtClass ctype = type.getComponentType();
|
|
if (ctype.equals(CtClass.booleanType)) {
|
|
return writeWrap(field, BSNAME + ".writeBooleanArray(out, " + name + ");");
|
|
} else if (ctype.equals(CtClass.byteType)) {
|
|
return writeWrap(field, BSNAME + ".writeByteArray(out, " + name + ");");
|
|
} else if (ctype.equals(CtClass.shortType)) {
|
|
return writeWrap(field, BSNAME + ".writeShortArray(out, " + name + ");");
|
|
} else if (ctype.equals(CtClass.intType)) {
|
|
return writeWrap(field, BSNAME + ".writeIntArray(out, " + name + ");");
|
|
} else if (ctype.equals(CtClass.longType)) {
|
|
return writeWrap(field, BSNAME + ".writeLongArray(out, " + name + ");");
|
|
} else if (ctype.equals(CtClass.floatType)) {
|
|
return writeWrap(field, BSNAME + ".writeFloat(out, " + name + ");");
|
|
} else if (ctype.equals(CtClass.doubleType)) {
|
|
return writeWrap(field, BSNAME + ".writeDoubleArray(out, " + name + ");");
|
|
} else if (ctype.getName().equals("java.lang.Object")) {
|
|
return writeWrap(field, BSNAME + ".writeObjectArray(out, " + name + ");");
|
|
}
|
|
}
|
|
|
|
// no need to wrap streamable instances
|
|
return "out.writeObject(" + name + ");";
|
|
}
|
|
|
|
protected String readWrap (CtField field, String body)
|
|
throws NotFoundException
|
|
{
|
|
if (field.getType().isPrimitive()) {
|
|
return body;
|
|
} else {
|
|
return "if (ins.readBoolean()) {\n" +
|
|
" " + body + "\n" +
|
|
" } else {\n" +
|
|
" " + field.getName() + " = null;\n" +
|
|
" }";
|
|
}
|
|
}
|
|
|
|
protected String writeWrap (CtField field, String body)
|
|
throws NotFoundException
|
|
{
|
|
if (field.getType().isPrimitive()) {
|
|
return body;
|
|
} else {
|
|
return "if (" + field.getName() + " == null) {\n" +
|
|
" out.writeBoolean(false);\n" +
|
|
" } else {\n" +
|
|
" out.writeBoolean(true);\n" +
|
|
" " + body + "\n" +
|
|
" }";
|
|
}
|
|
}
|
|
|
|
/** A list of filesets that contain Streamable class files. */
|
|
protected ArrayList<FileSet> _filesets = new ArrayList<FileSet>();
|
|
|
|
/** A list of paths that make up our classpath. */
|
|
protected ArrayList<Path> _paths = new ArrayList<Path>();
|
|
|
|
/** The directory to which we write our instrumented class files. */
|
|
protected File _outdir;
|
|
|
|
/** Used to instrument class files. */
|
|
protected ClassPool _pool = ClassPool.getDefault();
|
|
|
|
/** Used to determine which classes implement {@link Streamable}. */
|
|
protected CtClass _streamable;
|
|
|
|
protected static final String BSNAME = BasicStreamers.class.getName();
|
|
}
|