Rewrote the code that generates InvocationService marshalling and

dispatching classes in Java to eliminate annoying dependency on the output
format of JDK 1.4.1's javap.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@3238 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Michael Bayne
2004-11-25 04:46:19 +00:00
parent 90621e78de
commit a0d343ff04
6 changed files with 715 additions and 7 deletions
+7 -6
View File
@@ -31,20 +31,20 @@
<!-- generates marshaller and dispatcher classes for all invocation <!-- generates marshaller and dispatcher classes for all invocation
service declarations --> service declarations -->
<target name="genservice"> <target name="genservice">
<taskdef name="service"
classname="com.threerings.presents.tools.GenServiceTask"/>
<!-- make sure the service class files are all compiled --> <!-- make sure the service class files are all compiled -->
<javac srcdir="src/java" destdir="${classes.dir}" <javac srcdir="src/java" destdir="${classes.dir}"
debug="on" optimize="${build.optimize}" deprecation="on"> debug="on" optimize="${build.optimize}" deprecation="on">
<classpath refid="classpath"/> <classpath refid="classpath"/>
<include name="**/*Service.java"/> <include name="**/*Service.java"/>
<exclude name="**/InvocationService.java"/>
</javac> </javac>
<apply executable="bin/genservice" failonerror="true" parallel="true"> <!-- now generate the associated files -->
<arg value="--classpath"/> <service header="lib/SOURCE_HEADER">
<arg value="${classes.dir}"/>
<arg value="--sourcedir"/>
<arg value="src/java"/>
<fileset dir="src/java" includes="**/*Service.java" <fileset dir="src/java" includes="**/*Service.java"
excludes="**/InvocationService.java"/> excludes="**/InvocationService.java"/>
</apply> </service>
</target> </target>
<!-- generates sender and decoder classes for all invocation <!-- generates sender and decoder classes for all invocation
@@ -68,6 +68,7 @@
<mkdir dir="${javadoc.home}"/> <mkdir dir="${javadoc.home}"/>
<copy todir="${classes.dir}"> <copy todir="${classes.dir}">
<fileset dir="src/java" includes="**/*.properties"/> <fileset dir="src/java" includes="**/*.properties"/>
<fileset dir="src/java" includes="**/*.tmpl"/>
</copy> </copy>
<copy todir="${classes.dir}/rsrc"> <copy todir="${classes.dir}/rsrc">
<fileset dir="rsrc" includes="**/*" excludes="**/bundles/**"/> <fileset dir="rsrc" includes="**/*" excludes="**/bundles/**"/>
+1
View File
@@ -4,3 +4,4 @@ commons-io.jar
commons-lang.jar commons-lang.jar
samskivert.jar samskivert.jar
xml-writer.jar xml-writer.jar
velocity-1.5-dev.jar
+21
View File
@@ -0,0 +1,21 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 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
@@ -0,0 +1,567 @@
//
// $Id$
package com.threerings.presents.tools;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
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.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import com.samskivert.util.CollectionUtil;
import com.samskivert.util.ObjectUtil;
import com.samskivert.util.SortableArrayList;
import com.samskivert.util.StringUtil;
import com.samskivert.velocity.VelocityUtil;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService.InvocationListener;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.InvocationMarshaller;
import com.threerings.presents.dobj.InvocationResponseEvent;
import com.threerings.presents.server.InvocationDispatcher;
import com.threerings.presents.server.InvocationException;
/**
* An Ant task for generating invocation service marshalling and
* unmarshalling classes.
*/
public class GenServiceTask extends Task
{
/** Used to keep track of invocation service method listener arguments. */
public static class ListenerArgument
{
public int index;
public Class listener;
public ListenerArgument (int index, Class listener)
{
this.index = index+1;
this.listener = listener;
}
public String getMarshaller ()
{
String name = simpleName(listener);
// handle ye olde special case
if (name.equals("InvocationService.InvocationListener")) {
return "ListenerMarshaller";
}
name = StringUtil.replace(name, "Service", "Marshaller");
return StringUtil.replace(name, "Listener", "Marshaller");
}
}
/** Used to keep track of invocation service methods. */
public static class ServiceMethod
{
public Method method;
public ArrayList listenerArgs = new ArrayList();
public ServiceMethod (Class service, Method method, HashMap imports)
{
this.method = method;
// we need to look through our arguments and note any needed
// imports in the supplied table
Class[] args = method.getParameterTypes();
for (int ii = 0; ii < args.length; ii++) {
Class arg = args[ii];
while (arg.isArray()) {
arg = arg.getComponentType();
}
// if this is a listener, we need to add a listener
// argument info for it
if (InvocationListener.class.isAssignableFrom(arg)) {
listenerArgs.add(new ListenerArgument(ii, arg));
}
// if it's not primitive, global or in our package, we
// should add an import statement for it
if (arg.isPrimitive() ||
arg.getName().startsWith("java.lang") ||
ObjectUtil.equals(
arg.getPackage(), service.getPackage())) {
continue;
}
imports.put(importify(arg.getName()), Boolean.TRUE);
// if it's a listener and not one of the special
// InvocationService listeners, we need to import its
// marshaller as well
if (InvocationListener.class.isAssignableFrom(arg) &&
!simpleName(arg).startsWith("InvocationService")) {
String mname = arg.getName();
mname = StringUtil.replace(mname, "Service", "Marshaller");
mname = StringUtil.replace(mname, "Listener", "Marshaller");
mname = StringUtil.replace(mname, ".client.", ".data.");
imports.put(importify(mname), Boolean.TRUE);
}
}
}
public String getCode ()
{
return StringUtil.unStudlyName(method.getName()).toUpperCase();
}
public String getArgList ()
{
StringBuffer buf = new StringBuffer();
Class[] args = method.getParameterTypes();
for (int ii = 0; ii < args.length; ii++) {
if (buf.length() > 0) {
buf.append(", ");
}
buf.append(simpleName(args[ii])).append(" arg").append(ii+1);
}
return buf.toString();
}
public String getWrappedArgList (boolean skipFirst)
{
StringBuffer buf = new StringBuffer();
Class[] args = method.getParameterTypes();
for (int ii = (skipFirst ? 1 : 0); ii < args.length; ii++) {
if (buf.length() > 0) {
buf.append(", ");
}
buf.append(wrapArgument(args[ii], ii+1));
}
return buf.toString();
}
public boolean hasArgs ()
{
return (method.getParameterTypes().length > 1);
}
public String getUnwrappedArgList (boolean listenerMode)
{
StringBuffer buf = new StringBuffer();
Class[] args = method.getParameterTypes();
for (int ii = (listenerMode ? 0 : 1); ii < args.length; ii++) {
if (buf.length() > 0) {
buf.append(", ");
}
buf.append(unwrapArgument(args[ii], listenerMode ? ii : ii-1,
listenerMode));
}
return buf.toString();
}
protected String wrapArgument (Class clazz, int index)
{
if (clazz == Boolean.TYPE) {
return "new Boolean(arg" + index + ")";
} else if (clazz == Byte.TYPE) {
return "new Byte(arg" + index + ")";
} else if (clazz == Character.TYPE) {
return "new Character(arg" + index + ")";
} else if (clazz == Short.TYPE) {
return "new Short(arg" + index + ")";
} else if (clazz == Integer.TYPE) {
return "new Integer(arg" + index + ")";
} else if (clazz == Long.TYPE) {
return "new Long(arg" + index + ")";
} else if (clazz == Float.TYPE) {
return "new Float(arg" + index + ")";
} else if (clazz == Double.TYPE) {
return "new Double(arg" + index + ")";
} else if (InvocationListener.class.isAssignableFrom(clazz)) {
return "listener" + index;
} else {
return "arg" + index;
}
}
protected String unwrapArgument (
Class clazz, int index, boolean listenerMode)
{
if (clazz == Boolean.TYPE) {
return "((Boolean)args[" + index + "]).booleanValue()";
} else if (clazz == Byte.TYPE) {
return "((Byte)args[" + index + "]).byteValue()";
} else if (clazz == Character.TYPE) {
return "((Character)args[" + index + "]).charValue()";
} else if (clazz == Short.TYPE) {
return "((Short)args[" + index + "]).shortValue()";
} else if (clazz == Integer.TYPE) {
return "((Integer)args[" + index + "]).intValue()";
} else if (clazz == Long.TYPE) {
return "((Long)args[" + index + "]).longValue()";
} else if (clazz == Float.TYPE) {
return "((Float)args[" + index + "]).floatValue()";
} else if (clazz == Double.TYPE) {
return "((Double)args[" + index + "]).doubleValue()";
} else if (listenerMode &&
InvocationListener.class.isAssignableFrom(clazz)) {
return "listener" + index;
} else {
return "(" + simpleName(clazz) + ")args[" + index + "]";
}
}
}
/** Used to keep track of custom InvocationListener derivations. */
public static class ServiceListener
{
public Class listener;
public ArrayList methods = new ArrayList();
public ServiceListener (Class service, Class listener, HashMap imports)
{
this.listener = listener;
Method[] methdecls = listener.getDeclaredMethods();
for (int ii = 0; ii < methdecls.length; ii++) {
Method m = methdecls[ii];
// service interface methods must be public and abstract
if (!Modifier.isPublic(m.getModifiers()) &&
!Modifier.isAbstract(m.getModifiers())) {
continue;
}
methods.add(new ServiceMethod(service, m, imports));
}
}
public String getName ()
{
String name = simpleName(listener);
name = StringUtil.replace(name, "Listener", "");
int didx = name.indexOf(".");
return name.substring(didx+1);
}
}
/**
* Adds a nested &lt;fileset&gt; element which enumerates service
* declaration source files.
*/
public void addFileset (FileSet set)
{
_filesets.add(set);
}
/**
* 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());
}
}
/**
* Performs the actual work of the task.
*/
public void execute () throws BuildException
{
try {
_velocity = VelocityUtil.createEngine();
} catch (Exception e) {
throw new BuildException("Failure initializing Velocity", e);
}
ArrayList files = new ArrayList();
for (Iterator iter = _filesets.iterator(); iter.hasNext(); ) {
FileSet fs = (FileSet)iter.next();
DirectoryScanner ds = fs.getDirectoryScanner(getProject());
File fromDir = fs.getDir(getProject());
String[] srcFiles = ds.getIncludedFiles();
for (int f = 0; f < srcFiles.length; f++) {
processService(new File(fromDir, srcFiles[f]));
}
}
}
/**
* Processes an invocation service source file.
*/
protected void processService (File source)
{
// System.err.println("Processing " + source + "...");
// load up the file and determine it's package and classname
String pkgname = null, name = null;
try {
BufferedReader bin = new BufferedReader(new FileReader(source));
String line;
while ((line = bin.readLine()) != null) {
Matcher pm = PACKAGE_PATTERN.matcher(line);
if (pm.find()) {
pkgname = pm.group(1);
}
Matcher nm = NAME_PATTERN.matcher(line);
if (nm.find()) {
name = nm.group(1);
break;
}
}
bin.close();
// make sure we found something
if (name == null) {
System.err.println(
"Unable to locate interface name in " + source + ".");
return;
}
// prepend the package name to get a name we can Class.forName()
if (pkgname != null) {
name = pkgname + "." + name;
}
} catch (Exception e) {
System.err.println(
"Failed to parse " + source + ": " + e.getMessage());
}
try {
processService(source, Class.forName(name));
} catch (Exception e) {
System.err.println(
"Failed to load " + name + ": " + e.getMessage());
System.err.println("Make sure the classes for which you will " +
"be generating interfaces are");
System.err.println("in ant's classpath.");
}
}
/**
* Processes a resolved invocation service class instance.
*/
protected void processService (File source, Class service)
{
System.out.println("Processing " + service.getName() + "...");
String sname = service.getName();
String spackage = "";
int didx = sname.lastIndexOf(".");
if (didx != -1) {
spackage = sname.substring(0, didx);
sname = sname.substring(didx+1);
}
// verify that the service class name is as we expect it to be
if (!sname.endsWith("Service")) {
System.err.println("Cannot process '" + sname + "':");
System.err.println(
"Service classes must be named SomethingService.");
return;
}
HashMap imports = new HashMap();
ArrayList methods = new ArrayList();
ArrayList listeners = new ArrayList();
// we need to import the service itself
imports.put(importify(service.getName()), Boolean.TRUE);
// look through and locate our service methods, also locating any
// custom InvocationListener derivations along the way
Method[] methdecls = service.getDeclaredMethods();
for (int ii = 0; ii < methdecls.length; ii++) {
Method m = methdecls[ii];
// service interface methods must be public and abstract
if (!Modifier.isPublic(m.getModifiers()) &&
!Modifier.isAbstract(m.getModifiers())) {
continue;
}
// check this method for custom listener declarations
Class[] args = m.getParameterTypes();
for (int aa = 0; aa < args.length; aa++) {
if (InvocationListener.class.isAssignableFrom(args[aa]) &&
simpleName(args[aa]).startsWith(sname + ".")) {
listeners.add(new ServiceListener(
service, args[aa], imports));
}
}
methods.add(new ServiceMethod(service, m, imports));
}
// String dname = StringUtil.replace(sname, "Service", "Dispatcher");
// String dpackage = StringUtil.replace(spackage, ".client", ".server");
// String pname = StringUtil.replace(sname, "Service", "Provider");
// String ppackage = StringUtil.replace(spackage, ".client", ".server");
generateMarshaller(source, sname, spackage, methods, listeners,
imports.keySet().iterator());
generateDispatcher(source, sname, spackage, methods,
imports.keySet().iterator());
}
protected void generateMarshaller (
File source, String sname, String spackage, ArrayList methods,
ArrayList listeners, Iterator imports)
{
String name = StringUtil.replace(sname, "Service", "");
String mname = StringUtil.replace(sname, "Service", "Marshaller");
String mpackage = StringUtil.replace(spackage, ".client", ".data");
// construct our imports list
SortableArrayList implist = new SortableArrayList();
CollectionUtil.addAll(implist, imports);
checkedAdd(implist, Client.class.getName());
checkedAdd(implist, InvocationMarshaller.class.getName());
checkedAdd(implist, InvocationResponseEvent.class.getName());
implist.sort();
VelocityContext ctx = new VelocityContext();
ctx.put("name", name);
ctx.put("package", mpackage);
ctx.put("methods", methods);
ctx.put("listeners", listeners);
ctx.put("imports", implist);
try {
StringWriter sw = new StringWriter();
_velocity.mergeTemplate(MARSHALLER_TMPL, "UTF-8", ctx, sw);
// determine the path to our marshaller file
String mpath = source.getPath();
mpath = StringUtil.replace(mpath, "Service", "Marshaller");
mpath = StringUtil.replace(mpath, "/client/", "/data/");
System.out.println("Generating " + mname + "...");
writeFile(mpath, sw.toString());
} catch (Exception e) {
System.err.println("Failed processing template");
e.printStackTrace(System.err);
}
}
protected void generateDispatcher (
File source, String sname, String spackage, ArrayList methods,
Iterator imports)
{
String name = StringUtil.replace(sname, "Service", "");
String dname = StringUtil.replace(sname, "Service", "Dispatcher");
String dpackage = StringUtil.replace(spackage, ".client", ".server");
// construct our imports list
SortableArrayList implist = new SortableArrayList();
CollectionUtil.addAll(implist, imports);
checkedAdd(implist, ClientObject.class.getName());
checkedAdd(implist, InvocationMarshaller.class.getName());
checkedAdd(implist, InvocationDispatcher.class.getName());
checkedAdd(implist, InvocationException.class.getName());
String mname = StringUtil.replace(sname, "Service", "Marshaller");
String mpackage = StringUtil.replace(spackage, ".client", ".data");
checkedAdd(implist, mpackage + "." + mname);
implist.sort();
VelocityContext ctx = new VelocityContext();
ctx.put("name", name);
ctx.put("package", dpackage);
ctx.put("methods", methods);
ctx.put("imports", implist);
try {
StringWriter sw = new StringWriter();
_velocity.mergeTemplate(DISPATCHER_TMPL, "UTF-8", ctx, sw);
// determine the path to our marshaller file
String mpath = source.getPath();
mpath = StringUtil.replace(mpath, "Service", "Dispatcher");
mpath = StringUtil.replace(mpath, "/client/", "/server/");
System.out.println("Generating " + dname + "...");
writeFile(mpath, sw.toString());
} catch (Exception e) {
System.err.println("Failed processing template");
e.printStackTrace(System.err);
}
}
protected void writeFile (String path, String data)
throws IOException
{
if (_header != null) {
data = _header + data;
}
FileUtils.writeStringToFile(new File(path), data, "UTF-8");
}
protected static void checkedAdd (SortableArrayList list, String value)
{
if (!list.contains(value)) {
list.add(value);
}
}
protected static String simpleName (Class clazz)
{
if (clazz.isArray()) {
return simpleName(clazz.getComponentType()) + "[]";
} else {
Package pkg = clazz.getPackage();
int offset = (pkg == null) ? 0 : pkg.getName().length()+1;
String name = clazz.getName().substring(offset);
return StringUtil.replace(name, "$", ".");
}
}
protected static String importify (String name)
{
int didx = name.indexOf("$");
return (didx == -1) ? name : name.substring(0, didx);
}
/** 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;
/** Used to generate source files from templates. */
protected VelocityEngine _velocity;
/** A regular expression for matching the package declaration. */
protected static final Pattern PACKAGE_PATTERN =
Pattern.compile("^\\s*package\\s+(\\S+)\\W");
/** A regular expression for matching the interface declaration. */
protected static final Pattern NAME_PATTERN =
Pattern.compile("^\\s*public\\s+interface\\s+(\\S+)\\W");
/** Specifies the path to the marshaller template. */
protected static final String MARSHALLER_TMPL =
"com/threerings/presents/tools/marshaller.tmpl";
/** Specifies the path to the dispatcher template. */
protected static final String DISPATCHER_TMPL =
"com/threerings/presents/tools/dispatcher.tmpl";
}
@@ -0,0 +1,47 @@
package $package;
#foreach ($import in $imports)
import $import;
#end
/**
* Dispatches requests to the {@link ${name}Provider}.
*/
public class ${name}Dispatcher extends InvocationDispatcher
{
/**
* Creates a dispatcher that may be registered to dispatch invocation
* service requests for the specified provider.
*/
public ${name}Dispatcher (${name}Provider provider)
{
this.provider = provider;
}
// documentation inherited
public InvocationMarshaller createMarshaller ()
{
return new ${name}Marshaller();
}
// documentation inherited
public void dispatchRequest (
ClientObject source, int methodId, Object[] args)
throws InvocationException
{
switch (methodId) {
#foreach ($m in $methods)
case ${name}Marshaller.$m.code:
((${name}Provider)provider).${m.method.name}(
source#if ($m.hasArgs()),
#end
$m.getUnwrappedArgList(false)
);
return;
#end
default:
super.dispatchRequest(source, methodId, args);
}
}
}
@@ -0,0 +1,71 @@
package $package;
#foreach ($import in $imports)
import $import;
#end
/**
* Provides the implementation of the {@link ${name}Service} interface
* that marshalls the arguments and delivers the request to the provider
* on the server. Also provides an implementation of the response listener
* interfaces that marshall the response arguments and deliver them back
* to the requesting client.
*/
public class ${name}Marshaller extends InvocationMarshaller
implements ${name}Service
{
#foreach ($l in $listeners)
// documentation inherited
public static class ${l.name}Marshaller extends ListenerMarshaller
implements ${l.name}Listener
{
#foreach ($lm in $l.methods)
/** The method id used to dispatch {@link #$lm.method.name}
* responses. */
public static final int $lm.code = $velocityCount;
// documentation inherited from interface
public void $lm.method.name ($lm.argList)
{
omgr.postEvent(new InvocationResponseEvent(
callerOid, requestId, $lm.code,
new Object[] { $lm.getWrappedArgList(false) }));
}
#end
// documentation inherited
public void dispatchResponse (int methodId, Object[] args)
{
switch (methodId) {
#foreach ($lm in $l.methods)
case $lm.code:
((${l.name}Listener)listener).${lm.method.name}(
${lm.getUnwrappedArgList(true)});
return;
#end
default:
super.dispatchResponse(methodId, args);
}
}
}
#end
#foreach ($m in $methods)
/** The method id used to dispatch {@link #$m.method.name} requests. */
public static final int $m.code = $velocityCount;
// documentation inherited from interface
public void $m.method.name ($m.argList)
{
#foreach ($la in $m.listenerArgs)
$la.marshaller listener$la.index = new ${la.marshaller}();
listener${la.index}.listener = arg$la.index;
#end
sendRequest(arg1, $m.code, new Object[] {
$m.getWrappedArgList(true)
});
}
#end
}