Factor the logic of genrecord from the Ant harness.
This commit is contained in:
@@ -0,0 +1,483 @@
|
||||
//
|
||||
// Depot library - a Java relational persistence library
|
||||
// http://code.google.com/p/depot/source/browse/trunk/LICENSE
|
||||
|
||||
package com.samskivert.depot.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.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.GenericArrayType;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.GeneratedValue;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.annotation.Transient;
|
||||
import com.samskivert.depot.impl.DepotUtil;
|
||||
import com.samskivert.io.StreamUtil;
|
||||
import com.samskivert.util.ClassUtil;
|
||||
import com.samskivert.util.GenUtil;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
public abstract class GenRecord
|
||||
{
|
||||
public GenRecord (ClassLoader cloader) {
|
||||
_cloader = cloader;
|
||||
// resolve the PersistentRecord class using our classloader
|
||||
try {
|
||||
_prclass = _cloader.loadClass(PersistentRecord.class.getName());
|
||||
} catch (Exception e) {
|
||||
throw mkFail("Can't resolve PersistentRecord", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a persistent record source file.
|
||||
*/
|
||||
public void processRecord (File source)
|
||||
{
|
||||
// System.err.println("Processing " + source + "...");
|
||||
|
||||
// load up the file and determine it's package and classname
|
||||
String name = null;
|
||||
try {
|
||||
name = readClassName(source);
|
||||
} catch (Exception e) {
|
||||
logWarn("Failed to parse " + source, e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
processRecord(source, _cloader.loadClass(name));
|
||||
} catch (ClassNotFoundException cnfe) {
|
||||
logWarn("Failed to load " + name + ".\nMissing class: " + cnfe.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
logWarn("Failed to process " + source, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void logInfo (String msg);
|
||||
protected abstract void logWarn (String msg, Exception e);
|
||||
protected abstract RuntimeException mkFail (String msg, Exception e);
|
||||
|
||||
/** Processes a resolved persistent record class instance. */
|
||||
protected void processRecord (File source, Class<?> rclass)
|
||||
{
|
||||
// make sure we extend persistent record
|
||||
if (!_prclass.isAssignableFrom(rclass)) {
|
||||
logInfo("Skipping non-record '" + rclass.getName() + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
// determine our primary key fields for getKey() generation (if we're not an abstract)
|
||||
List<Field> kflist = Lists.newArrayList();
|
||||
if (!Modifier.isAbstract(rclass.getModifiers())) {
|
||||
// determine which fields make up our primary key; we'd just use Class.getFields() but
|
||||
// that returns things in a random order whereas ClassUtil returns fields in
|
||||
// declaration order starting from the top-most class and going down the line
|
||||
for (Field field : ClassUtil.getFields(rclass)) {
|
||||
if (hasAnnotation(field, Id.class)) kflist.add(field);
|
||||
else if (hasAnnotation(field, GeneratedValue.class)) {
|
||||
logWarn("Skipping " + rclass.getName() + ". Field '" + field.getName() +
|
||||
"' has @GeneratedValue, which may only used on primary keys with @Id.",
|
||||
null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// determine which fields we need to generate constants for
|
||||
List<Field> flist = Lists.newArrayList();
|
||||
for (Field field : rclass.getFields()) {
|
||||
if (isPersistentField(field)) {
|
||||
flist.add(field);
|
||||
}
|
||||
}
|
||||
Set<Field> declared = Sets.newHashSet();
|
||||
for (Field field : rclass.getDeclaredFields()) {
|
||||
if (isPersistentField(field)) {
|
||||
declared.add(field);
|
||||
}
|
||||
}
|
||||
|
||||
// slurp our source file into newline separated strings
|
||||
String[] lines = null;
|
||||
try {
|
||||
BufferedReader bin = new BufferedReader(new FileReader(source));
|
||||
List<String> llist = Lists.newArrayList();
|
||||
String line = null;
|
||||
while ((line = bin.readLine()) != null) {
|
||||
llist.add(line);
|
||||
}
|
||||
lines = llist.toArray(new String[llist.size()]);
|
||||
bin.close();
|
||||
} catch (IOException ioe) {
|
||||
logWarn("Error reading '" + source + "'", ioe);
|
||||
return;
|
||||
}
|
||||
|
||||
// now determine where to insert our static field declarations
|
||||
int bstart = -1, bend = -1;
|
||||
int nstart = -1, nend = -1;
|
||||
int mstart = -1, mend = -1;
|
||||
for (int ii = 0; ii < lines.length; ii++) {
|
||||
String line = lines[ii].trim();
|
||||
|
||||
// look for the start of the class body
|
||||
if (NAME_PATTERN.matcher(line).find()) {
|
||||
if (line.endsWith("{")) {
|
||||
bstart = ii+1;
|
||||
} else {
|
||||
// search down a few lines for the open brace
|
||||
for (int oo = 1; oo < 10; oo++) {
|
||||
if (get(lines, ii+oo).trim().endsWith("{")) {
|
||||
bstart = ii+oo+1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// track the last } on a line by itself and we'll call that the end of the class body
|
||||
} else if (line.equals("}")) {
|
||||
bend = ii;
|
||||
|
||||
// look for our field and method markers
|
||||
} else if (line.equals(FIELDS_START)) {
|
||||
nstart = ii;
|
||||
} else if (line.equals(FIELDS_END)) {
|
||||
nend = ii+1;
|
||||
} else if (line.equals(METHODS_START)) {
|
||||
mstart = ii;
|
||||
} else if (line.equals(METHODS_END)) {
|
||||
mend = ii+1;
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check the markers
|
||||
if (check(source, "fields start", nstart, "fields end", nend) ||
|
||||
check(source, "fields end", nend, "fields start", nstart) ||
|
||||
check(source, "methods start", mstart, "methods end", mend) ||
|
||||
check(source, "methods end", mend, "methods start", mstart)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we have no previous markers then stuff the fields at the top of the class body and the
|
||||
// methods at the bottom
|
||||
if (nstart == -1) {
|
||||
nstart = bstart;
|
||||
nend = bstart;
|
||||
}
|
||||
if (mstart == -1) {
|
||||
mstart = bend;
|
||||
mend = bend;
|
||||
}
|
||||
|
||||
// get the unqualified class name
|
||||
String rname = DepotUtil.justClassName(rclass);
|
||||
|
||||
// generate our fields section
|
||||
StringBuilder fsection = new StringBuilder();
|
||||
|
||||
// add our prototype declaration
|
||||
Map<String, String> subs = Maps.newHashMap();
|
||||
// We could add the @Generated annotation, but
|
||||
// - we'd need to add the import for javax.annotation.Generated
|
||||
// - it adds a lot of boilerplate to the source file
|
||||
// - we can't annotate the static initializer that registers the key fields
|
||||
// So it was decided to omit it and the few benefits:
|
||||
// - marked as Generated in javadoc
|
||||
// - timestamp of last generation
|
||||
// - a reference back to this class
|
||||
// subs.put("generated", "@Generated(value={\"" + GenRecordTask.class.getName() + "\"}, " +
|
||||
// "date=\"" + // ISO 8601 date
|
||||
// new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(new Date()) + "\")");
|
||||
subs.put("record", rname);
|
||||
fsection.append(mergeTemplate(PROTO_TMPL, subs));
|
||||
|
||||
// add our ColumnExp constants
|
||||
for (int ii = 0; ii < flist.size(); ii++) {
|
||||
Field f = flist.get(ii);
|
||||
String fname = f.getName();
|
||||
|
||||
// create our substitution mappings
|
||||
Map<String, String> fsubs = Maps.newHashMap(subs);
|
||||
fsubs.put("type", getTypeName(f.getGenericType()));
|
||||
fsubs.put("field", fname);
|
||||
fsubs.put("capfield", StringUtil.unStudlyName(fname).toUpperCase());
|
||||
|
||||
// now generate our bits
|
||||
fsection.append(mergeTemplate(COL_TMPL, fsubs));
|
||||
}
|
||||
|
||||
// generate our methods section
|
||||
StringBuilder msection = new StringBuilder();
|
||||
|
||||
// add a getKey() method, if applicable
|
||||
if (kflist.size() > 0) {
|
||||
StringBuilder argList = new StringBuilder();
|
||||
StringBuilder argNameList = new StringBuilder();
|
||||
StringBuilder fieldNameList = new StringBuilder();
|
||||
for (Field keyField : kflist) {
|
||||
if (argList.length() > 0) {
|
||||
argList.append(", ");
|
||||
argNameList.append(", ");
|
||||
fieldNameList.append(", ");
|
||||
}
|
||||
String name = keyField.getName();
|
||||
argList.append(GenUtil.simpleName(keyField)).append(" ").append(name);
|
||||
argNameList.append(name);
|
||||
fieldNameList.append(StringUtil.unStudlyName(name));
|
||||
}
|
||||
|
||||
subs.put("argList", argList.toString());
|
||||
subs.put("argNameList", argNameList.toString());
|
||||
subs.put("fieldNameList", fieldNameList.toString());
|
||||
|
||||
// generate our bits and append them as appropriate to the string buffers
|
||||
msection.append(mergeTemplate(KEY_TMPL, subs));
|
||||
}
|
||||
|
||||
// now bolt everything back together into a class declaration
|
||||
try {
|
||||
BufferedWriter bout = new BufferedWriter(new FileWriter(source));
|
||||
for (int ii = 0; ii < nstart; ii++) {
|
||||
writeln(bout, lines[ii]);
|
||||
}
|
||||
|
||||
if (fsection.length() > 0) {
|
||||
String prev = get(lines, nstart-1);
|
||||
if (!StringUtil.isBlank(prev) && !prev.equals("{")) {
|
||||
bout.newLine();
|
||||
}
|
||||
writeln(bout, " " + FIELDS_START);
|
||||
bout.write(fsection.toString());
|
||||
writeln(bout, " " + FIELDS_END);
|
||||
if (!StringUtil.isBlank(get(lines, nend))) {
|
||||
bout.newLine();
|
||||
}
|
||||
}
|
||||
for (int ii = nend; ii < mstart; ii++) {
|
||||
writeln(bout, lines[ii]);
|
||||
}
|
||||
|
||||
if (msection.length() > 0) {
|
||||
if (!StringUtil.isBlank(get(lines, mstart-1))) {
|
||||
bout.newLine();
|
||||
}
|
||||
writeln(bout, " " + METHODS_START);
|
||||
bout.write(msection.toString());
|
||||
writeln(bout, " " + METHODS_END);
|
||||
String next = get(lines, mend);
|
||||
if (!StringUtil.isBlank(next) && !next.equals("}")) {
|
||||
bout.newLine();
|
||||
}
|
||||
}
|
||||
for (int ii = mend; ii < lines.length; ii++) {
|
||||
writeln(bout, lines[ii]);
|
||||
}
|
||||
|
||||
bout.close();
|
||||
} catch (IOException ioe) {
|
||||
logWarn("Error writing to '" + source + "'", ioe);
|
||||
}
|
||||
logInfo("Processed '" + source + "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the supplied field is part of a persistent record (is a public, non-static,
|
||||
* non-@Transient field).
|
||||
*/
|
||||
protected boolean isPersistentField (Field field)
|
||||
{
|
||||
int mods = field.getModifiers();
|
||||
return Modifier.isPublic(mods) && !Modifier.isStatic(mods) &&
|
||||
!hasAnnotation(field, Transient.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely gets the <code>index</code>th line, returning the empty string if we exceed the
|
||||
* length of the array.
|
||||
*/
|
||||
protected String get (String[] lines, int index)
|
||||
{
|
||||
return (index < lines.length) ? lines[index] : "";
|
||||
}
|
||||
|
||||
/** Helper function for sanity checking marker existence. */
|
||||
protected boolean check (File source, String mname, int mline, String fname, int fline)
|
||||
{
|
||||
if (mline == -1 && fline != -1) {
|
||||
logWarn("Found " + fname + " marker (at line " + (fline+1) + ") but no " +
|
||||
mname + " marker in '" + source + "'.", null);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Helper function for writing a string and a newline to a writer. */
|
||||
protected void writeln (BufferedWriter bout, String line)
|
||||
throws IOException
|
||||
{
|
||||
bout.write(line);
|
||||
bout.newLine();
|
||||
}
|
||||
|
||||
/** Helper function for generating our boilerplate code. */
|
||||
protected String mergeTemplate (String tmpl, Map<String, String> subs)
|
||||
{
|
||||
try {
|
||||
String text = StreamUtil.toString(
|
||||
getClass().getClassLoader().getResourceAsStream(tmpl), "UTF-8");
|
||||
text = text.replace("\n", System.getProperty("line.separator"));
|
||||
for (Map.Entry<String, String> entry : subs.entrySet()) {
|
||||
text = text.replaceAll("@"+entry.getKey()+"@", entry.getValue());
|
||||
}
|
||||
return text;
|
||||
} catch (Exception e) {
|
||||
throw mkFail("Failed processing template [tmpl=" + tmpl + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected static boolean hasAnnotation (Field field, Class<?> annotation)
|
||||
{
|
||||
// iterate because getAnnotation() fails if we're dealing with multiple classloaders
|
||||
for (Annotation a : field.getAnnotations()) {
|
||||
if (annotation.getName().equals(a.annotationType().getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads in the supplied source file and locates the package and class or interface name and
|
||||
* returns a fully qualified class name.
|
||||
*/
|
||||
protected static String readClassName (File source)
|
||||
throws IOException
|
||||
{
|
||||
// load up the file and determine it's package and classname
|
||||
String pkgname = null, name = null;
|
||||
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(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
bin.close();
|
||||
|
||||
// make sure we found something
|
||||
if (name == null) {
|
||||
throw new IOException("Unable to locate class or interface name in " + source + ".");
|
||||
}
|
||||
|
||||
// prepend the package name to get a name we can Class.forName()
|
||||
if (pkgname != null) {
|
||||
name = pkgname + "." + name;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
protected static String getTypeName (Type type)
|
||||
{
|
||||
if (type instanceof Class) {
|
||||
Class<?> clazz = (Class<?>)type;
|
||||
if (clazz.isArray()) {
|
||||
Class<?> cclass = clazz.getComponentType();
|
||||
return (cclass.isPrimitive() ? cclass.getSimpleName() : getTypeName(cclass)) + "[]";
|
||||
} else {
|
||||
Class<?> nclass = clazz.isPrimitive() ? BOXES.get(clazz) : clazz;
|
||||
Class<?> eclass = nclass.getEnclosingClass();
|
||||
return (eclass == null) ? nclass.getSimpleName() :
|
||||
getTypeName(eclass) + "." + nclass.getSimpleName();
|
||||
}
|
||||
|
||||
} else if (type instanceof ParameterizedType) {
|
||||
ParameterizedType paramType = (ParameterizedType)type;
|
||||
Type[] typeArgs = paramType.getActualTypeArguments();
|
||||
String tparams = (typeArgs == null || typeArgs.length == 0) ? "" :
|
||||
"<" + Joiner.on(", ").join(
|
||||
Lists.transform(Lists.newArrayList(typeArgs), new Function<Type, String>() {
|
||||
@Override public String apply (Type input) {
|
||||
return getTypeName(input);
|
||||
}
|
||||
})) + ">";
|
||||
return getTypeName(paramType.getRawType()) + tparams;
|
||||
|
||||
} else if (type instanceof GenericArrayType) {
|
||||
return getTypeName(((GenericArrayType)type).getGenericComponentType()) + "[]";
|
||||
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown kind of type '" + type + "'");
|
||||
}
|
||||
}
|
||||
|
||||
/** Used to do our own classpath business. */
|
||||
protected final ClassLoader _cloader;
|
||||
|
||||
/** {@link PersistentRecord} resolved with the proper classloader so that we can compare it to
|
||||
* loaded derived classes. */
|
||||
protected final Class<?> _prclass;
|
||||
|
||||
/** Specifies the path to the name code template. */
|
||||
protected static final String PROTO_TMPL = "com/samskivert/depot/tools/record_proto.tmpl";
|
||||
|
||||
/** Specifies the path to the column code template. */
|
||||
protected static final String COL_TMPL = "com/samskivert/depot/tools/record_column.tmpl";
|
||||
|
||||
/** Specifies the path to the key code template. */
|
||||
protected static final String KEY_TMPL = "com/samskivert/depot/tools/record_key.tmpl";
|
||||
|
||||
// markers
|
||||
protected static final String MARKER = "// AUTO-GENERATED: ";
|
||||
protected static final String FIELDS_START = MARKER + "FIELDS START";
|
||||
protected static final String FIELDS_END = MARKER + "FIELDS END";
|
||||
protected static final String METHODS_START = MARKER + "METHODS START";
|
||||
protected static final String METHODS_END = MARKER + "METHODS END";
|
||||
|
||||
/** 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 class or interface declaration. */
|
||||
protected static final Pattern NAME_PATTERN = Pattern.compile(
|
||||
"^\\s*public\\s+(?:abstract\\s+)?(interface|class)\\s+(\\w+)(\\W|$)");
|
||||
|
||||
protected static final Map<Class<?>, Class<?>> BOXES =
|
||||
ImmutableMap.<Class<?>,Class<?>>builder().
|
||||
put(Boolean.TYPE, Boolean.class).
|
||||
put(Byte.TYPE, Byte.class).
|
||||
put(Short.TYPE, Short.class).
|
||||
put(Character.TYPE, Character.class).
|
||||
put(Integer.TYPE, Integer.class).
|
||||
put(Long.TYPE, Long.class).
|
||||
put(Float.TYPE, Float.class).
|
||||
put(Double.TYPE, Double.class).build();
|
||||
}
|
||||
@@ -4,23 +4,8 @@
|
||||
|
||||
package com.samskivert.depot.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.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.GenericArrayType;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.tools.ant.BuildException;
|
||||
import org.apache.tools.ant.DirectoryScanner;
|
||||
@@ -30,22 +15,7 @@ import org.apache.tools.ant.types.FileSet;
|
||||
import org.apache.tools.ant.types.Reference;
|
||||
import org.apache.tools.ant.util.ClasspathUtils;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.GeneratedValue;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.annotation.Transient;
|
||||
import com.samskivert.depot.impl.DepotUtil;
|
||||
import com.samskivert.io.StreamUtil;
|
||||
import com.samskivert.util.ClassUtil;
|
||||
import com.samskivert.util.GenUtil;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* An ant task that updates the column constants for a persistent record.
|
||||
@@ -77,453 +47,32 @@ public class GenRecordTask extends Task
|
||||
throw new BuildException(errmsg);
|
||||
}
|
||||
|
||||
// resolve the PersistentRecord class using our classloader
|
||||
try {
|
||||
_prclass = _cloader.loadClass(PersistentRecord.class.getName());
|
||||
} catch (Exception e) {
|
||||
throw new BuildException("Can't resolve PersistentRecord", e);
|
||||
}
|
||||
GenRecord genner = new GenRecord(_cloader) {
|
||||
protected void logInfo (String msg) {
|
||||
log(msg, Project.MSG_VERBOSE);
|
||||
}
|
||||
protected void logWarn (String msg, Exception e) {
|
||||
if (e == null) log(msg, Project.MSG_WARN);
|
||||
else log(msg, e, Project.MSG_WARN);
|
||||
}
|
||||
protected RuntimeException mkFail (String msg, Exception e) {
|
||||
return new BuildException(msg, e);
|
||||
}
|
||||
};
|
||||
|
||||
for (FileSet fs : _filesets) {
|
||||
DirectoryScanner ds = fs.getDirectoryScanner(getProject());
|
||||
File fromDir = fs.getDir(getProject());
|
||||
String[] srcFiles = ds.getIncludedFiles();
|
||||
for (String srcFile : srcFiles) {
|
||||
processRecord(new File(fromDir, srcFile));
|
||||
genner.processRecord(new File(fromDir, srcFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a distributed object source file.
|
||||
*/
|
||||
protected void processRecord (File source)
|
||||
{
|
||||
// System.err.println("Processing " + source + "...");
|
||||
|
||||
// load up the file and determine it's package and classname
|
||||
String name = null;
|
||||
try {
|
||||
name = readClassName(source);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to parse " + source + ": " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
processRecord(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 persistent record class instance. */
|
||||
protected void processRecord (File source, Class<?> rclass)
|
||||
{
|
||||
// make sure we extend persistent record
|
||||
if (!_prclass.isAssignableFrom(rclass)) {
|
||||
log("Skipping non-record '" + rclass.getName() + "'", Project.MSG_VERBOSE);
|
||||
return;
|
||||
}
|
||||
|
||||
// determine our primary key fields for getKey() generation (if we're not an abstract)
|
||||
List<Field> kflist = Lists.newArrayList();
|
||||
if (!Modifier.isAbstract(rclass.getModifiers())) {
|
||||
// determine which fields make up our primary key; we'd just use Class.getFields() but
|
||||
// that returns things in a random order whereas ClassUtil returns fields in
|
||||
// declaration order starting from the top-most class and going down the line
|
||||
for (Field field : ClassUtil.getFields(rclass)) {
|
||||
if (hasAnnotation(field, Id.class)) {
|
||||
kflist.add(field);
|
||||
} else if (hasAnnotation(field, GeneratedValue.class)) {
|
||||
log("Skipping " + rclass.getName() + ". Field '" +
|
||||
field.getName() + "' has @GeneratedValue, which may only used on primary " +
|
||||
"keys with @Id.", Project.MSG_WARN);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// determine which fields we need to generate constants for
|
||||
List<Field> flist = Lists.newArrayList();
|
||||
for (Field field : rclass.getFields()) {
|
||||
if (isPersistentField(field)) {
|
||||
flist.add(field);
|
||||
}
|
||||
}
|
||||
Set<Field> declared = Sets.newHashSet();
|
||||
for (Field field : rclass.getDeclaredFields()) {
|
||||
if (isPersistentField(field)) {
|
||||
declared.add(field);
|
||||
}
|
||||
}
|
||||
|
||||
// slurp our source file into newline separated strings
|
||||
String[] lines = null;
|
||||
try {
|
||||
BufferedReader bin = new BufferedReader(new FileReader(source));
|
||||
List<String> llist = Lists.newArrayList();
|
||||
String line = null;
|
||||
while ((line = bin.readLine()) != null) {
|
||||
llist.add(line);
|
||||
}
|
||||
lines = llist.toArray(new String[llist.size()]);
|
||||
bin.close();
|
||||
} catch (IOException ioe) {
|
||||
log("Error reading '" + source + "'", ioe, Project.MSG_WARN);
|
||||
return;
|
||||
}
|
||||
|
||||
// now determine where to insert our static field declarations
|
||||
int bstart = -1, bend = -1;
|
||||
int nstart = -1, nend = -1;
|
||||
int mstart = -1, mend = -1;
|
||||
for (int ii = 0; ii < lines.length; ii++) {
|
||||
String line = lines[ii].trim();
|
||||
|
||||
// look for the start of the class body
|
||||
if (NAME_PATTERN.matcher(line).find()) {
|
||||
if (line.endsWith("{")) {
|
||||
bstart = ii+1;
|
||||
} else {
|
||||
// search down a few lines for the open brace
|
||||
for (int oo = 1; oo < 10; oo++) {
|
||||
if (get(lines, ii+oo).trim().endsWith("{")) {
|
||||
bstart = ii+oo+1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// track the last } on a line by itself and we'll call that the end of the class body
|
||||
} else if (line.equals("}")) {
|
||||
bend = ii;
|
||||
|
||||
// look for our field and method markers
|
||||
} else if (line.equals(FIELDS_START)) {
|
||||
nstart = ii;
|
||||
} else if (line.equals(FIELDS_END)) {
|
||||
nend = ii+1;
|
||||
} else if (line.equals(METHODS_START)) {
|
||||
mstart = ii;
|
||||
} else if (line.equals(METHODS_END)) {
|
||||
mend = ii+1;
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check the markers
|
||||
if (check(source, "fields start", nstart, "fields end", nend) ||
|
||||
check(source, "fields end", nend, "fields start", nstart) ||
|
||||
check(source, "methods start", mstart, "methods end", mend) ||
|
||||
check(source, "methods end", mend, "methods start", mstart)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we have no previous markers then stuff the fields at the top of the class body and the
|
||||
// methods at the bottom
|
||||
if (nstart == -1) {
|
||||
nstart = bstart;
|
||||
nend = bstart;
|
||||
}
|
||||
if (mstart == -1) {
|
||||
mstart = bend;
|
||||
mend = bend;
|
||||
}
|
||||
|
||||
// get the unqualified class name
|
||||
String rname = DepotUtil.justClassName(rclass);
|
||||
|
||||
// generate our fields section
|
||||
StringBuilder fsection = new StringBuilder();
|
||||
|
||||
// add our prototype declaration
|
||||
Map<String, String> subs = Maps.newHashMap();
|
||||
// We could add the @Generated annotation, but
|
||||
// - we'd need to add the import for javax.annotation.Generated
|
||||
// - it adds a lot of boilerplate to the source file
|
||||
// - we can't annotate the static initializer that registers the key fields
|
||||
// So it was decided to omit it and the few benefits:
|
||||
// - marked as Generated in javadoc
|
||||
// - timestamp of last generation
|
||||
// - a reference back to this class
|
||||
// subs.put("generated", "@Generated(value={\"" + GenRecordTask.class.getName() + "\"}, " +
|
||||
// "date=\"" + // ISO 8601 date
|
||||
// new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(new Date()) + "\")");
|
||||
subs.put("record", rname);
|
||||
fsection.append(mergeTemplate(PROTO_TMPL, subs));
|
||||
|
||||
// add our ColumnExp constants
|
||||
for (int ii = 0; ii < flist.size(); ii++) {
|
||||
Field f = flist.get(ii);
|
||||
String fname = f.getName();
|
||||
|
||||
// create our substitution mappings
|
||||
Map<String, String> fsubs = Maps.newHashMap(subs);
|
||||
fsubs.put("type", getTypeName(f.getGenericType()));
|
||||
fsubs.put("field", fname);
|
||||
fsubs.put("capfield", StringUtil.unStudlyName(fname).toUpperCase());
|
||||
|
||||
// now generate our bits
|
||||
fsection.append(mergeTemplate(COL_TMPL, fsubs));
|
||||
}
|
||||
|
||||
// generate our methods section
|
||||
StringBuilder msection = new StringBuilder();
|
||||
|
||||
// add a getKey() method, if applicable
|
||||
if (kflist.size() > 0) {
|
||||
StringBuilder argList = new StringBuilder();
|
||||
StringBuilder argNameList = new StringBuilder();
|
||||
StringBuilder fieldNameList = new StringBuilder();
|
||||
for (Field keyField : kflist) {
|
||||
if (argList.length() > 0) {
|
||||
argList.append(", ");
|
||||
argNameList.append(", ");
|
||||
fieldNameList.append(", ");
|
||||
}
|
||||
String name = keyField.getName();
|
||||
argList.append(GenUtil.simpleName(keyField)).append(" ").append(name);
|
||||
argNameList.append(name);
|
||||
fieldNameList.append(StringUtil.unStudlyName(name));
|
||||
}
|
||||
|
||||
subs.put("argList", argList.toString());
|
||||
subs.put("argNameList", argNameList.toString());
|
||||
subs.put("fieldNameList", fieldNameList.toString());
|
||||
|
||||
// generate our bits and append them as appropriate to the string buffers
|
||||
msection.append(mergeTemplate(KEY_TMPL, subs));
|
||||
}
|
||||
|
||||
// now bolt everything back together into a class declaration
|
||||
try {
|
||||
BufferedWriter bout = new BufferedWriter(new FileWriter(source));
|
||||
for (int ii = 0; ii < nstart; ii++) {
|
||||
writeln(bout, lines[ii]);
|
||||
}
|
||||
|
||||
if (fsection.length() > 0) {
|
||||
String prev = get(lines, nstart-1);
|
||||
if (!StringUtil.isBlank(prev) && !prev.equals("{")) {
|
||||
bout.newLine();
|
||||
}
|
||||
writeln(bout, " " + FIELDS_START);
|
||||
bout.write(fsection.toString());
|
||||
writeln(bout, " " + FIELDS_END);
|
||||
if (!StringUtil.isBlank(get(lines, nend))) {
|
||||
bout.newLine();
|
||||
}
|
||||
}
|
||||
for (int ii = nend; ii < mstart; ii++) {
|
||||
writeln(bout, lines[ii]);
|
||||
}
|
||||
|
||||
if (msection.length() > 0) {
|
||||
if (!StringUtil.isBlank(get(lines, mstart-1))) {
|
||||
bout.newLine();
|
||||
}
|
||||
writeln(bout, " " + METHODS_START);
|
||||
bout.write(msection.toString());
|
||||
writeln(bout, " " + METHODS_END);
|
||||
String next = get(lines, mend);
|
||||
if (!StringUtil.isBlank(next) && !next.equals("}")) {
|
||||
bout.newLine();
|
||||
}
|
||||
}
|
||||
for (int ii = mend; ii < lines.length; ii++) {
|
||||
writeln(bout, lines[ii]);
|
||||
}
|
||||
|
||||
bout.close();
|
||||
} catch (IOException ioe) {
|
||||
log("Error writing to '" + source + "'", ioe, Project.MSG_WARN);
|
||||
}
|
||||
log("Processed '" + source + "'", Project.MSG_VERBOSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the supplied field is part of a persistent record (is a public, non-static,
|
||||
* non-@Transient field).
|
||||
*/
|
||||
protected boolean isPersistentField (Field field)
|
||||
{
|
||||
int mods = field.getModifiers();
|
||||
return Modifier.isPublic(mods) && !Modifier.isStatic(mods) &&
|
||||
!hasAnnotation(field, Transient.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely gets the <code>index</code>th line, returning the empty string if we exceed the
|
||||
* length of the array.
|
||||
*/
|
||||
protected String get (String[] lines, int index)
|
||||
{
|
||||
return (index < lines.length) ? lines[index] : "";
|
||||
}
|
||||
|
||||
/** Helper function for sanity checking marker existence. */
|
||||
protected boolean check (File source, String mname, int mline, String fname, int fline)
|
||||
{
|
||||
if (mline == -1 && fline != -1) {
|
||||
System.err.println("Found " + fname + " marker (at line " + (fline+1) + ") but no " +
|
||||
mname + " marker in '" + source + "'.");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Helper function for writing a string and a newline to a writer. */
|
||||
protected void writeln (BufferedWriter bout, String line)
|
||||
throws IOException
|
||||
{
|
||||
bout.write(line);
|
||||
bout.newLine();
|
||||
}
|
||||
|
||||
/** Helper function for generating our boilerplate code. */
|
||||
protected String mergeTemplate (String tmpl, Map<String, String> subs)
|
||||
{
|
||||
try {
|
||||
String text = StreamUtil.toString(
|
||||
getClass().getClassLoader().getResourceAsStream(tmpl), "UTF-8");
|
||||
text = text.replace("\n", System.getProperty("line.separator"));
|
||||
for (Map.Entry<String, String> entry : subs.entrySet()) {
|
||||
text = text.replaceAll("@"+entry.getKey()+"@", entry.getValue());
|
||||
}
|
||||
return text;
|
||||
} catch (Exception e) {
|
||||
throw new BuildException("Failed processing template [tmpl=" + tmpl + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected static boolean hasAnnotation (Field field, Class<?> annotation)
|
||||
{
|
||||
// iterate because getAnnotation() fails if we're dealing with multiple classloaders
|
||||
for (Annotation a : field.getAnnotations()) {
|
||||
if (annotation.getName().equals(a.annotationType().getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads in the supplied source file and locates the package and class or interface name and
|
||||
* returns a fully qualified class name.
|
||||
*/
|
||||
protected static String readClassName (File source)
|
||||
throws IOException
|
||||
{
|
||||
// load up the file and determine it's package and classname
|
||||
String pkgname = null, name = null;
|
||||
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(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
bin.close();
|
||||
|
||||
// make sure we found something
|
||||
if (name == null) {
|
||||
throw new IOException("Unable to locate class or interface name in " + source + ".");
|
||||
}
|
||||
|
||||
// prepend the package name to get a name we can Class.forName()
|
||||
if (pkgname != null) {
|
||||
name = pkgname + "." + name;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
protected static String getTypeName (Type type)
|
||||
{
|
||||
if (type instanceof Class) {
|
||||
Class<?> clazz = (Class<?>)type;
|
||||
if (clazz.isArray()) {
|
||||
Class<?> cclass = clazz.getComponentType();
|
||||
return (cclass.isPrimitive() ? cclass.getSimpleName() : getTypeName(cclass)) + "[]";
|
||||
} else {
|
||||
Class<?> nclass = clazz.isPrimitive() ? BOXES.get(clazz) : clazz;
|
||||
Class<?> eclass = nclass.getEnclosingClass();
|
||||
return (eclass == null) ? nclass.getSimpleName() :
|
||||
getTypeName(eclass) + "." + nclass.getSimpleName();
|
||||
}
|
||||
|
||||
} else if (type instanceof ParameterizedType) {
|
||||
ParameterizedType paramType = (ParameterizedType)type;
|
||||
Type[] typeArgs = paramType.getActualTypeArguments();
|
||||
String tparams = (typeArgs == null || typeArgs.length == 0) ? "" :
|
||||
"<" + Joiner.on(", ").join(
|
||||
Lists.transform(Lists.newArrayList(typeArgs), new Function<Type, String>() {
|
||||
@Override public String apply (Type input) {
|
||||
return getTypeName(input);
|
||||
}
|
||||
})) + ">";
|
||||
return getTypeName(paramType.getRawType()) + tparams;
|
||||
|
||||
} else if (type instanceof GenericArrayType) {
|
||||
return getTypeName(((GenericArrayType)type).getGenericComponentType()) + "[]";
|
||||
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown kind of type '" + type + "'");
|
||||
}
|
||||
}
|
||||
|
||||
/** A list of filesets that contain tile images. */
|
||||
protected List<FileSet> _filesets = Lists.newArrayList();
|
||||
|
||||
/** Used to do our own classpath business. */
|
||||
protected ClassLoader _cloader;
|
||||
|
||||
/** {@link PersistentRecord} resolved with the proper classloader so that we can compare it to
|
||||
* loaded derived classes. */
|
||||
protected Class<?> _prclass;
|
||||
|
||||
/** Specifies the path to the name code template. */
|
||||
protected static final String PROTO_TMPL = "com/samskivert/depot/tools/record_proto.tmpl";
|
||||
|
||||
/** Specifies the path to the column code template. */
|
||||
protected static final String COL_TMPL = "com/samskivert/depot/tools/record_column.tmpl";
|
||||
|
||||
/** Specifies the path to the key code template. */
|
||||
protected static final String KEY_TMPL = "com/samskivert/depot/tools/record_key.tmpl";
|
||||
|
||||
// markers
|
||||
protected static final String MARKER = "// AUTO-GENERATED: ";
|
||||
protected static final String FIELDS_START = MARKER + "FIELDS START";
|
||||
protected static final String FIELDS_END = MARKER + "FIELDS END";
|
||||
protected static final String METHODS_START = MARKER + "METHODS START";
|
||||
protected static final String METHODS_END = MARKER + "METHODS END";
|
||||
|
||||
/** 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 class or interface declaration. */
|
||||
protected static final Pattern NAME_PATTERN = Pattern.compile(
|
||||
"^\\s*public\\s+(?:abstract\\s+)?(interface|class)\\s+(\\w+)(\\W|$)");
|
||||
|
||||
protected static final Map<Class<?>, Class<?>> BOXES =
|
||||
ImmutableMap.<Class<?>,Class<?>>builder().
|
||||
put(Boolean.TYPE, Boolean.class).
|
||||
put(Byte.TYPE, Byte.class).
|
||||
put(Short.TYPE, Short.class).
|
||||
put(Character.TYPE, Character.class).
|
||||
put(Integer.TYPE, Integer.class).
|
||||
put(Long.TYPE, Long.class).
|
||||
put(Float.TYPE, Float.class).
|
||||
put(Double.TYPE, Double.class).build();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user