/*
* IntrospectorGenerator.java
*
* Created on July 15, 2007, 2:21 PM
*
* 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package com.totsp.gwittir.rebind.beans;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.JTypeParameter;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.totsp.gwittir.client.beans.SelfDescribed;
import com.totsp.gwittir.client.beans.annotations.Introspectable;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
/**
*
* @author <a href="mailto:cooper@screaming-penguin.com">Robert "kebernet" Cooper</a>
*/
public class IntrospectorGenerator extends Generator {
private JType objectType = null;
private Map<MethodWrapper, Integer> methodWrapperLookup;
private String implementationName = com.totsp.gwittir.client.beans.Introspector.class.getSimpleName() + "_Impl";
private String methodsImplementationName = "MethodsList";
private String packageName = com.totsp.gwittir.client.beans.Introspector.class.getCanonicalName()
.substring(0,
com.totsp.gwittir.client.beans.Introspector.class.getCanonicalName().lastIndexOf("."));
/** Creates a new instance of IntrospectorGenerator */
public IntrospectorGenerator() {
}
public String generate(TreeLogger logger, GeneratorContext context, String typeName)
throws UnableToCompleteException {
//.println("Introspector Generate.");
try {
this.objectType = context.getTypeOracle()
.getType("java.lang.Object");
} catch (NotFoundException ex) {
logger.log(TreeLogger.ERROR, typeName, ex);
return null;
}
List<BeanResolver> introspectables = this.getIntrospectableTypes(logger, context.getTypeOracle());
MethodWrapper[] methods = this.findMethods(logger, introspectables);
ClassSourceFileComposerFactory mcf = new ClassSourceFileComposerFactory(this.packageName,
this.methodsImplementationName);
mcf.addImport(com.totsp.gwittir.client.beans.Method.class.getCanonicalName());
PrintWriter methodsPrintWriter = context.tryCreate(logger, this.packageName, this.methodsImplementationName);
if (methodsPrintWriter != null) {
SourceWriter methodsWriter = mcf.createSourceWriter(context, methodsPrintWriter);
this.writeMethods(logger, methods, methodsWriter);
methodsWriter.println("}");
context.commit(logger, methodsPrintWriter);
}
ClassSourceFileComposerFactory cfcf = new ClassSourceFileComposerFactory(this.packageName,
this.implementationName);
cfcf.addImplementedInterface(typeName);
cfcf.addImport("java.util.HashMap");
cfcf.addImport(com.totsp.gwittir.client.beans.Method.class.getCanonicalName());
cfcf.addImport(com.totsp.gwittir.client.beans.Property.class.getCanonicalName());
cfcf.addImport(com.totsp.gwittir.client.beans.BeanDescriptor.class.getCanonicalName());
PrintWriter printWriter = context.tryCreate(logger, packageName, implementationName);
if (printWriter == null) {
//.println( "Introspector Generate skipped.");
return packageName + "." + implementationName;
}
SourceWriter writer = cfcf.createSourceWriter(context, printWriter);
this.writeIntrospectables(logger, introspectables, methods, writer);
this.writeResolver(introspectables, writer);
writer.println(
"private HashMap<Class,BeanDescriptor> beanDescriptorLookup = new HashMap<Class,BeanDescriptor>();");
writer.println();
writer.println("public BeanDescriptor getDescriptor( Object object ){ ");
writer.indent();
writer.println("if( object == null ) throw new NullPointerException(\"Attempt to introspect null object\");");
writer.println("if( object instanceof " + SelfDescribed.class.getCanonicalName() +
" ) return ((SelfDescribed)object).__descriptor();");
writer.println("BeanDescriptor descriptor = beanDescriptorLookup.get(object.getClass());");
writer.println("if (descriptor!=null){");
writer.indentln("return descriptor;");
writer.outdent();
writer.println("}");
writer.println("descriptor=_getDescriptor(object);");
writer.println("beanDescriptorLookup.put(object.getClass(),descriptor);");
writer.println("return descriptor;");
writer.outdent();
writer.println("}");
writer.println("private BeanDescriptor _getDescriptor( Object object ){ ");
writer.indent();
for (BeanResolver resolver : introspectables) {
writer.println("if( object instanceof " + resolver.getType().getQualifiedSourceName() + " ) {");
writer.indent();
String name = resolver.getType()
.getQualifiedSourceName()
.replaceAll("\\.", "_");
logger.log(TreeLogger.DEBUG, "Writing : " + name, null);
writer.print("return " + name + " == null ? " + name + " = ");
this.writeBeanDescriptor(logger, resolver, methods, writer);
writer.print(": " + name + ";");
writer.outdent();
writer.println("}");
}
writer.println(" throw new IllegalArgumentException(\"Unknown type\" + object.getClass() ); ");
writer.outdent();
writer.println("}");
writer.outdent();
writer.println("}");
context.commit(logger, printWriter);
//.println( "Introspector Generate completed.");
return packageName + "." + implementationName;
}
protected List<BeanResolver> getIntrospectableTypes(TreeLogger logger, TypeOracle oracle) {
ArrayList<BeanResolver> results = new ArrayList<BeanResolver>();
HashSet<BeanResolver> resolvers = new HashSet<BeanResolver>();
HashSet<String> found = new HashSet<String>();
try {
JClassType[] types = oracle.getTypes();
//.println("Found "+types.length +" types.");
JClassType introspectable = oracle.getType(com.totsp.gwittir.client.beans.Introspectable.class.getCanonicalName());
for (JClassType type : types) {
// if(type.getQualifiedSourceName().endsWith("gwittir.client.ui.TextBox")){
// logger.log(
// TreeLogger.WARN,
// type.getQualifiedSourceName() + " is assignable to " + introspectable + " " +
// type.isAssignableTo(introspectable) + " isInterface = " + type.isInterface() +
// "isIntrospectable = "+isIntrospectable(logger,type),
// null
// );
// }
if (!found.contains(type.getQualifiedSourceName()) &&
(isIntrospectable(logger, type) || type.isAssignableTo(introspectable)) &&
(type.isInterface() == null)) {
found.add(type.getQualifiedSourceName());
resolvers.add(new BeanResolver(logger, type));
}
}
// Do a crazy assed sort to make sure least
// assignable types are at the bottom of the list
results.addAll(resolvers);
results.addAll(this.getFileDeclaredTypes(logger, oracle));
boolean swap = true;
//.print("Ordering "+results.size()+" by heirarchy ");
while (swap) {
//.print(".");
swap = false;
for (int i = results.size() - 1; i >= 0; i--) {
BeanResolver type = (BeanResolver) results.get(i);
for (int j = i - 1; j >= 0; j--) {
BeanResolver check = (BeanResolver) results.get(j);
if (type.getType()
.isAssignableTo(check.getType())) {
results.set(i, check);
results.set(j, type);
type = check;
swap = true;
}
}
}
}
//System.out.println();
} catch (Exception e) {
logger.log(TreeLogger.ERROR, "Unable to finad Introspectable types.", e);
}
// for(BeanResolver rs:results){
// logger.log(TreeLogger.ERROR, rs.toString());
// }
//System.out.println("Found "+results.size()+" introspectable types.");
return results;
}
private Set<BeanResolver> getFileDeclaredTypes(TreeLogger logger, TypeOracle oralce)
throws UnableToCompleteException {
HashSet<BeanResolver> results = new HashSet<BeanResolver>();
ClassLoader ctxLoader = Thread.currentThread()
.getContextClassLoader();
try {
Enumeration<URL> introspections = ctxLoader.getResources("gwittir-introspection.properties");
while (introspections.hasMoreElements()) {
URL propsUrl = introspections.nextElement();
logger.log(TreeLogger.Type.INFO, "Loading: " + propsUrl.toString());
Properties props = new Properties();
props.load(propsUrl.openStream());
for (Entry entry : props.entrySet()) {
String className = entry.getKey()
.toString();
String[] includedProps = entry.getValue()
.toString()
.split(",");
JClassType type = oralce.findType(className);
if (type == null) {
logger.log(TreeLogger.Type.ERROR,
"Unable to find type " + className + " declared in " + propsUrl);
throw new UnableToCompleteException();
}
results.add(new BeanResolver(logger, type, includedProps));
}
}
} catch (IOException ioe) {
logger.log(TreeLogger.Type.WARN, "Exception looking for properties files", ioe);
}
return results;
}
private boolean isIntrospectable(TreeLogger logger, JType type) {
if (type == null) {
return false;
}
JClassType ct = type.isClassOrInterface();
if (ct != null) {
if (ct.getAnnotation(Introspectable.class) != null) {
return true;
}
for (JClassType iface : ct.getImplementedInterfaces()) {
if (isIntrospectable(logger, iface)) {
return true;
}
}
if (isIntrospectable(logger, ct.getSuperclass())) {
return true;
}
}
return false;
}
private boolean box(JType type, SourceWriter writer) {
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.INT)) {
writer.print("new Integer( ");
return true;
}
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.LONG)) {
writer.print("new Long( ");
return true;
}
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.FLOAT)) {
writer.print("new Float( ");
return true;
}
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.DOUBLE)) {
writer.print("new Double( ");
return true;
}
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.CHAR)) {
writer.print("new Character( ");
return true;
}
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.BYTE)) {
writer.print("new Byte( ");
return true;
}
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.BOOLEAN)) {
writer.print("new Boolean( ");
return true;
}
return false;
}
/*because the call to MethodWrapper.toString() used in the comparison is _highly_ expensive
* (it wanders through the gwt jtype system), this optimisation gives something like a 30x
* speed improvement
*
*/
private int find(MethodWrapper[] search, MethodWrapper match) {
//the second check (!containsKey) will be hit if doing a recompile
//in hosted mode with new methods
if ((methodWrapperLookup == null) || !methodWrapperLookup.containsKey(match)) {
Map m = new HashMap<MethodWrapper, Integer>();
for (int i = 0; i < search.length; i++) {
m.put(search[i], i);
}
methodWrapperLookup = m;
}
return methodWrapperLookup.get(match);
}
private MethodWrapper[] findMethods(TreeLogger logger, List introspectables) {
HashSet methods = new HashSet();
for (Iterator it = introspectables.iterator(); it.hasNext();) {
BeanResolver info = (BeanResolver) it.next();
logger.branch(TreeLogger.DEBUG, "Method Scanning: " + info.getType().getQualifiedSourceName(), null);
try {
if (info.getProperties()
.size() == 0) {
continue;
}
Collection<RProperty> pds = info.getProperties()
.values();
for (RProperty p : pds) {
if (p.getReadMethod() != null) {
p.getReadMethod().hashWithType = true;
methods.add(p.getReadMethod());
}
if (p.getWriteMethod() != null) {
p.getWriteMethod().hashWithType = true;
methods.add(p.getWriteMethod());
}
}
} catch (Exception e) {
logger.log(TreeLogger.ERROR, "Unable to introspect class. Is class a bean?", e);
}
}
MethodWrapper[] results = new MethodWrapper[methods.size()];
Iterator it = methods.iterator();
for (int i = 0; it.hasNext(); i++) {
results[i] = (MethodWrapper) it.next();
}
return results;
}
private int findOld(MethodWrapper[] search, MethodWrapper match) {
for (int i = 0; i < search.length; i++) {
if (search[i].equals(match)) {
return i;
}
}
//System.out.println("NO MATCH FOR: " + match.toString());
throw new RuntimeException(match.toString());
}
private JType resolveType(final JType type) {
JType ret = type;
JParameterizedType pt = type.isParameterized();
if (pt != null) {
ret = pt.getRawType();
}
JTypeParameter tp = ret.isTypeParameter();
if (tp != null) {
ret = tp.getBaseType();
}
return ret;
}
private boolean unbox(JType type, String reference, SourceWriter writer) {
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.INT)) {
writer.print("((Integer) " + reference + ").intValue()");
return true;
}
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.LONG)) {
writer.print("((Long) " + reference + ").longValue()");
return true;
}
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.FLOAT)) {
writer.print("((Float) " + reference + ").floatValue()");
return true;
}
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.DOUBLE)) {
writer.print("((Double) " + reference + ").doubleValue()");
return true;
}
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.CHAR)) {
writer.print("((Character) " + reference + ").charValue()");
return true;
}
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.BYTE)) {
writer.print("((Byte) " + reference + ").byteValue()");
return true;
}
if ((type.isPrimitive() != null) && (type.isPrimitive() == JPrimitiveType.BOOLEAN)) {
writer.print("((Boolean) " + reference + ").booleanValue()");
return true;
}
writer.print("(" + type.getQualifiedSourceName() + ") " + reference);
return false;
}
private void writeBeanDescriptor(TreeLogger logger, BeanResolver info, MethodWrapper[] methods, SourceWriter writer) {
writer.println("new BeanDescriptor() { ");
writer.indent();
writer.println("private HashMap lookup;");
writer.println("private Property[] properties;");
writer.println("public Property[] getProperties(){");
writer.indent();
{
writer.println("if( this.properties != null ) ");
writer.indentln("return this.properties;");
writer.println("this.properties = new Property[" + (info.getProperties().size()) + "];");
Collection pds = info.getProperties()
.values();
String[] propertyNames = new String[pds.size()];
logger.log(TreeLogger.SPAM, "" + (pds == null), null);
if (pds != null) {
int i = 0;
for (Iterator it = pds.iterator(); it.hasNext(); i++) {
RProperty p = (RProperty) it.next();
propertyNames[i] = p.getName();
writer.println("{");
writer.indent();
writer.print("Method readMethod = ");
if (p.getReadMethod() == null) {
writer.println("null;");
} else {
writer.println(this.packageName + "." + this.methodsImplementationName + ".METHOD_" +
+this.find(methods, p.getReadMethod()) + ";");
}
writer.print("Method writeMethod = ");
if (p.getWriteMethod() == null) {
writer.println("null;");
} else {
writer.println(this.packageName + "." + this.methodsImplementationName + ".METHOD_" +
+this.find(methods, p.getWriteMethod()) + ";");
}
logger.log(TreeLogger.DEBUG, p.getName() + " " + p.getType().getQualifiedSourceName(), null);
JType ptype = this.resolveType(p.getType());
logger.log(TreeLogger.DEBUG, p.getName() + " (Erased) " + ptype.getQualifiedSourceName(), null);
writer.println("this.properties[" + (i) + "] = new Property( \"" + p.getName() + "\", " +
((p.getType() != null) ? ptype.getQualifiedSourceName() : "Object") +
".class, readMethod, writeMethod );");
writer.outdent();
writer.println("}");
}
}
writer.println("return this.properties;");
}
writer.outdent();
writer.println("} //end getProperties()");
writer.println("public Property getProperty( String name ) {");
writer.indent();
//TODO Rewrite this to a nested if loop using the propertyNames parameter.
writer.println("Property p = null;");
writer.println("if( this.lookup != null ) {");
writer.indentln("p = (Property) lookup.get(name); ");
writer.println("} else {");
writer.indent();
writer.println("this.lookup = new HashMap();");
writer.println("Property[] props = this.getProperties(); ");
writer.println("for( int i=0; i < props.length; i++ ) {");
writer.indent();
writer.println("this.lookup.put( props[i].getName(), props[i] );");
writer.outdent();
writer.println("}");
writer.println("p = (Property) this.lookup.get(name);");
writer.outdent();
writer.println("}");
writer.println("if( p == null ) throw new RuntimeException(\"Couldn't find property \"+name+\" for " +
info.getType().getQualifiedSourceName() + "\");");
writer.println("else return p;");
writer.outdent();
writer.println("}");
writer.outdent();
writer.print("}");
}
private void writeIntrospectables(TreeLogger logger, List introspectables, MethodWrapper[] methods,
SourceWriter writer) {
for (Iterator it = introspectables.iterator(); it.hasNext();) {
BeanResolver bean = (BeanResolver) it.next();
logger.branch(TreeLogger.DEBUG, "Introspecting: " + bean.getType().getQualifiedSourceName(), null);
try {
if (bean.getProperties()
.size() == 0) {
continue;
}
writer.print("private static BeanDescriptor ");
writer.print(bean.getType().getQualifiedSourceName().replaceAll("\\.", "_"));
writer.println(" = null;");
} catch (Exception e) {
logger.log(TreeLogger.ERROR, "Unable to introspect class. Is class a bean?", e);
}
}
}
private void writeMethod(TreeLogger logger, MethodWrapper method, SourceWriter writer) {
JType ptype = this.resolveType(method.getDeclaringType());
writer.println("new Method(){ ");
writer.indent();
writer.println("public String getName() {");
writer.indentln("return \"" + method.getBaseMethod().getName() + "\";");
writer.println(" }");
writer.println("public Object invoke( Object target, Object[] args ) throws Exception {");
writer.indent();
writer.println(ptype.getQualifiedSourceName() + " casted =");
writer.println("(" + ptype.getQualifiedSourceName() + ") target;");
logger.log(TreeLogger.SPAM,
"Method: " + method.getBaseMethod().getName() + " " +
method.getBaseMethod().getReturnType().getQualifiedSourceName(), null);
if (!(method.getBaseMethod()
.getReturnType()
.isPrimitive() == JPrimitiveType.VOID)) {
writer.print("return ");
}
JType type = this.resolveType(method.getBaseMethod().getReturnType());
boolean boxed = this.box(type, writer);
writer.print("casted." + method.getBaseMethod().getName() + "(");
if (method.getBaseMethod()
.getParameters() != null) {
for (int j = 0; j < method.getBaseMethod()
.getParameters().length; j++) {
JType arg = this.resolveType(method.getBaseMethod()
.getParameters()[j].getType());
this.unbox(arg, "args[" + j + "]", writer);
if (j != (method.getBaseMethod().getParameters().length - 1)) {
writer.print(", ");
}
}
}
writer.print(")");
if (boxed) {
writer.print(")");
}
writer.println(";");
if (method.getBaseMethod()
.getReturnType()
.getQualifiedSourceName()
.equals("void")) {
writer.println("return null;");
}
writer.outdent();
writer.println("}");
writer.outdent();
writer.println("};");
}
private void writeMethods(TreeLogger logger, MethodWrapper[] methods, SourceWriter writer) {
for (int i = 0; i < methods.length; i++) {
writer.print("public static final Method METHOD_" + i + " = ");
writeMethod(logger, methods[i], writer);
}
}
private void writeResolver(List introspectables, SourceWriter writer) {
writer.println("public Class resolveClass(Object object){");
writer.indent();
for (Iterator it = introspectables.iterator(); it.hasNext();) {
BeanResolver type = (BeanResolver) it.next();
writer.println("if( object instanceof " + type.getType().getQualifiedSourceName() + " ) return " +
type.getType().getQualifiedSourceName() + ".class;");
}
writer.println("throw new RuntimeException( \"Object \"+object+\"could not be resolved.\" );");
writer.outdent();
writer.println("}");
}
}