All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gwt.reflect.rebind.generators.GwtAnnotationGenerator Maven / Gradle / Ivy

package com.google.gwt.reflect.rebind.generators;

import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import xapi.dev.source.ClassBuffer;
import xapi.dev.source.FieldBuffer;
import xapi.dev.source.MethodBuffer;
import xapi.dev.source.SourceBuilder;
import xapi.source.read.JavaModel.IsNamedType;

import com.google.gwt.core.client.UnsafeNativeLong;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.jjs.UnifyAstListener;
import com.google.gwt.reflect.rebind.ReflectionUtilJava;
import com.google.gwt.reflect.shared.GwtReflect;


public class GwtAnnotationGenerator {

  private static final Type logLevel = Type.TRACE;

  /**
   * A cache of annotation types that have already been finished (seen during this compile).
   *

* This map is used to ensure we don't generate the same annotation twice, * and deterministically detect when a type has changed or not (i.e., we need to * generate and use a new wrapper type. *

* An annotation's method structure (probably) won't change during a single gwt compile, * so we can map from a seen annotation class to an instance of a {@link GeneratedAnnotation}. *

* If a type exists in this map, it has already been seen and can be immediately reused. *
* In the event that a cached type with our expected generated class name exists, * we can check that the existing class source exactly matched what we generate. *

* If a type is simply being re-used across super dev mode recompiles, * we will still have to generate the source for the annotation proxy the first time, * but we will only append _n to the class name and generate a new type * if indeed the annotation's structure has changed. *

* We will still need to updated the knownInstances method, * so we can easily generate the constructor methods to create an instance of an annotation proxy * (and reuse existing JMethod calls which create the exact annotation we need to reuse). * */ private static Map,GeneratedAnnotation> finished = new ConcurrentHashMap,GeneratedAnnotation>(); /** * An instance of a GeneratedAnnotation is used to cache two important bits of data we need: *
* 1) Mappings from a given instance of an annotation to a static noArg constructor method *
* 2) The class type of the annotation proxy, so we can verify generated source matches * the known source of the generated type name we want to use. *

* We cache these objects statically during the UnifyAst phase of the gwt compile; * This allows us to detect when we have already generated a proxy type for a given * annotation class, and more importantly, to detect if it changes, and rename * our proxy class accordingly. * * @author james.nelson * */ public static class GeneratedAnnotation { public GeneratedAnnotation(Annotation anno, String proxyName) { this.anno = anno; this.proxyName = proxyName; } final String proxyName; final Annotation anno; /** * The latest generated annotation provider */ IsNamedType latest; /** * A map from a configured instance of an annotation to a public static factory method. *

* These methods will be generated ad-hoc, and reused instead of regenerated. *

* They will also be placed into new classes, so we don't accidentally * reuse a method that will have to load an unrelated class full of dependencies. */ final Map knownInstances = new HashMap(); /** * The actual source type of the annotation proxy. *

* Whenever an annotation class is first seen, we will have to generate the source * for its proxy class (once per gwt compile). Then, when we ask gwt for the * PrintWriter to save this class, it may return null because a class w/ that name * already exists. So, then we would try to look that type up in the TypeOracle, * and then use it's .toSource() method to check what we have generated. *

* If our new source is different, the annotation has changed across compiles * (common for super dev mode), so we must update our proxy class. */ JClassType proxy; public String getAnnoName() { return anno.annotationType().getCanonicalName(); } public String providerPackage() { return latest.getPackage(); } public String providerClass() { return latest.getSimpleName(); } public String providerMethod() { return latest.getName(); } public String providerQualifiedName() { return latest.getQualifiedName(); } } /** * Called when the {@link UnifyAstListener#destroy()} methods are called. * */ public static void cleanup() { finished.clear(); } static void generateEmptyAnnotations(SourceBuilder out) { final MethodBuffer initAnnos = out.getClassBuffer() .createMethod("private static void enhanceAnnotations" + "(final Class cls)"); initAnnos .setUseJsni(true) .println("var map = [email protected]::annotations;") .println("if (map) return;") .println("map = [email protected]::annotations = {};"); } /** * @throws UnableToCompleteException */ static GeneratedAnnotation[] generateAnnotations( TreeLogger logger, SourceBuilder out, GeneratorContext context, Annotation ... annotations ) throws UnableToCompleteException { // logger.log(Type.INFO, "Generating annotations "+Arrays.asList(annotations)); final MethodBuffer initAnnos = out.getClassBuffer() .createMethod("private static void enhanceAnnotations" + "(final Class cls)"); initAnnos .setUseJsni(true) .addAnnotation(UnsafeNativeLong.class) .println("var map = [email protected]::annotations;") .println("if (map) return;") .println("map = [email protected]::annotations = {};"); if (annotations.length == 0){ return new GeneratedAnnotation[0]; } Map results = new LinkedHashMap(); for (int i = 0, max = annotations.length; i < max; i++ ) { Annotation anno = annotations[i]; GeneratedAnnotation gen = generateAnnotation(logger, context, anno); IsNamedType providerMethod; if (gen.knownInstances.containsKey(anno)) { // Reuse existing method providerMethod = gen.knownInstances.get(anno); } else { // Create new factory method. providerMethod = generateProvider(logger, out, anno, gen, context); initAnnos.addImport(providerMethod.getQualifiedName()); } results.put(gen, providerMethod); gen.knownInstances.put(anno, providerMethod); String annoCls = anno.annotationType().getCanonicalName(); out.getImports().addImport(annoCls+"Proxy"); initAnnos .println("var key" +i + " = @"+annoCls+"::[email protected]::getName()();") .println("map[key" +i + "] = function(){") .indent() .print("var anno = @") .print(providerMethod.getQualifiedName()) .print("::") .print(providerMethod.getName()) .println("()();") .println("map[key"+i+"] = function() { return anno; };") .println("map[key"+i+"].pub = true;") .println("return anno;") .outdent() .println("};") .println("map[key" +i + "].pub = true;"); }// end for // We don't set the providers for a given annotation type until after our loop; // because annotation can contain other annotations, this method can be // called recursively, AND we are running on multiple annotations at once, // the only way to avoid overwriting the .latest field is to set it just before return; for (Entry anno : results.entrySet()) { anno.getKey().latest = anno.getValue(); } return results.keySet().toArray(new GeneratedAnnotation[results.size()]); } protected static GeneratedAnnotation generateAnnotation(TreeLogger logger, GeneratorContext context, Annotation anno) throws UnableToCompleteException { final TypeOracle oracle = context.getTypeOracle(); final boolean doLog = logger.isLoggable(logLevel); Class annoType = anno.annotationType(); GeneratedAnnotation gen = finished.get(annoType); if (gen == null) { // Step one is to generate a class implementing the annotation. // This annotation type will likely have been seen before, // but it may have changed (across gwt compiles in super dev mode). String proxyPkg = annoType.getPackage().getName(); String proxyName = annoType.getCanonicalName().replace(proxyPkg+".", "").replace('.', '_')+"Proxy"; String proxyFQCN = (proxyPkg.length()==0 ? "" : proxyPkg + ".")+ proxyName; if (doLog) logger = logger.branch(logLevel, "Checking for existing "+proxyFQCN+" on classpath"); JClassType exists = oracle.findType(proxyFQCN); boolean mustGenerate = exists == null; // Now, just because the class exists does not mean it is correct. if (doLog) logger = logger.branch(logLevel, mustGenerate ? "No existing type "+ proxyFQCN+" on classpath; " : "Checking if existing "+proxyFQCN+" matches "+anno); if (!mustGenerate) { // If a type exists, make sure the method patterns match. mustGenerate = typeMatches(logger, anno, exists); } if (mustGenerate) { // Create a proxy type matching our annotation // Step one is to get a printwriter from the gwt generator context PrintWriter pw = context.tryCreate(logger, proxyPkg, proxyName); int inc=-1; if (pw == null) { // null means name's taken. Increment and try again. while (inc < 100) { if (doLog) logger.log(logLevel, "PrintWriter for "+proxyFQCN+" not available. Incrementing name"); String attempt = proxyName+"_"+(++inc); pw = context.tryCreate(logger, proxyPkg, attempt); if (pw != null) { proxyName = attempt; proxyFQCN = proxyFQCN+"_"+inc; break; } } } if (doLog) logger.log(logLevel, "Generating new annotation proxy "+proxyFQCN+"."); // We've got our class name, now create the proxy class implementation gen = new GeneratedAnnotation(anno, proxyFQCN); SourceBuilder sw = new SourceBuilder( "public class "+proxyName ).setPackage(proxyPkg); sw.setPayload(gen);// allow the source builder to access GeneratedAnnotation // cache this type _before_ we start generating, // as it is possible to recurse into the same type more than once // when generating annotations that have other annotations as members. finished.put(annoType, gen); // create this annotation proxy, and any proxies needed in its fields. generateProxy(logger, anno, sw.getClassBuffer(), proxyPkg, proxyName); // maybe log our generated contents String src = sw.toString(); if (logger.isLoggable(Type.DEBUG)) logger.log(Type.DEBUG, "Debug dump of generated annotation:\n"+src); // Actually save the file pw.println(src); context.commit(logger, pw); assert inc < 100 : "Generator context cannot create a printwriter; " + "check that your tmp / -out directory is not full."; } else { gen = new GeneratedAnnotation(anno, exists.getQualifiedSourceName()); finished.put(annoType, gen); } gen.proxy = exists; } return gen; } public static GeneratedAnnotation generateAnnotationProvider(TreeLogger logger, SourceBuilder out , Annotation anno, GeneratorContext context) throws UnableToCompleteException { GeneratedAnnotation gen = generateAnnotation(logger, context, anno); IsNamedType provider = generateProvider(logger, out, anno, gen, context); gen.latest = provider; if (logger.isLoggable(logLevel)) { logger.log(logLevel, "Generating annotation proxy "+gen.providerQualifiedName()+" for "+gen.getAnnoName()); } return gen; } private static IsNamedType generateProvider(TreeLogger logger, SourceBuilder out , Annotation anno, GeneratedAnnotation gen, GeneratorContext context) throws UnableToCompleteException { if (gen.knownInstances.containsKey(anno)) return gen.knownInstances.get(anno); String method = anno.annotationType().getCanonicalName().replace('.', '_') + gen.knownInstances.size(); // Cache the method name we're going to use, before we use it. IsNamedType type = new IsNamedType(method, out.getQualifiedName()); gen.knownInstances.put(anno, type); String proxyName = anno.annotationType().getSimpleName()+"Proxy"; out.getImports().addImport(anno.annotationType().getCanonicalName()+"Proxy"); MethodBuffer mb = out.getClassBuffer() .createMethod("public static "+proxyName+" "+method+"()") .print("return new "+proxyName+"("); Method[] methods = ReflectionUtilJava.getMethods(anno); int len = methods.length; for (int i = 0; i < len; i ++ ) { Method m = methods[i]; Class returnType = m.getReturnType(); Object value; try { value = m.invoke(anno); } catch (Exception e) { logger.log(Type.ERROR, "Error generating annotation proxy provider method." + "\nCould not invoke "+m+" on "+anno, e); throw new UnableToCompleteException(); } if (i > 0) mb.print(", "); if (Annotation.class.isAssignableFrom(returnType)) { Annotation asAnno = (Annotation)value; GeneratedAnnotation result = generateAnnotation(logger, context, asAnno); IsNamedType provider = generateProvider(logger, out, asAnno, result, context); mb.print(provider.getQualifiedName()+"."+provider.getName()+"()"); mb.addImport(asAnno.annotationType().getCanonicalName()+"Proxy"); } else if (returnType.isArray() && Annotation.class.isAssignableFrom(returnType.getComponentType())) { mb.println("new "+returnType.getComponentType().getCanonicalName()+"[]{"); for (int ind = 0, length = GwtReflect.arrayLength(value); ind < length; ind++ ) { if (ind > 0) mb.print(", "); Annotation asAnno = (Annotation)GwtReflect.arrayGet(value, ind); GeneratedAnnotation result = generateAnnotation(logger, context, asAnno); IsNamedType provider = generateProvider(logger, out, asAnno, result, context); mb.addImport(asAnno.annotationType().getCanonicalName()+"Proxy"); mb.print(provider.getQualifiedName()+"."+provider.getName()+"()"); } mb.println("}"); } else { // any other type, we can just generate raw source for now. mb.print(ReflectionUtilJava.sourceName(value)); } } mb.println(");"); return type; } private static void generateProxy(TreeLogger logger, Annotation anno, ClassBuffer cw, String proxyPkg, String proxyName) { assert proxyPkg.equals(anno.annotationType().getPackage().getName()); assert proxyName.equals(anno.annotationType().getCanonicalName().replace( anno.annotationType().getPackage().getName()+".","").replace('.','_')+"Proxy"); cw.addImport(Annotation.class); cw.addInterface(anno.annotationType()); MethodBuffer ctor = cw.createConstructor(Modifier.PUBLIC); // All public methods, include those from Object Method[] methods = anno.annotationType().getMethods(); Object[] defaults = new Object[methods.length]; for (int i = 0, m = methods.length; i < m; i++) { Method method = methods[i]; String clsName = method.getDeclaringClass().getName(); if (clsName.equals("java.lang.Object")) continue; if (clsName.equals("java.lang.annotation.Annotation")) { String name = method.getName(); if (name.equals("equals")) { // TODO copy basic structure from AbstractAnnotation } else if (name.equals("hashCode")) { } else if (name.equals("toString")) { } else if (name.equals("annotationType")) { cw .createMethod("public final Class annotationType()") .returnValue(anno.annotationType().getCanonicalName()+".class") ; } } else { // A method the client has declared Class returnType = (Class)method.getReturnType(); String simpleName = method.getName(); String paramName = ReflectionUtilJava.toSourceName(method.getGenericReturnType()) +" "+simpleName; Object defaultValue = defaults[i] = method.getDefaultValue(); FieldBuffer field = cw.createField(returnType, method.getName()) .setExactName(true) .makePrivate() .makeFinal() ; field.addGetter(Modifier.PUBLIC); ctor.addParameters(paramName); ctor.println("this."+simpleName+" = "+simpleName+";"); if (defaultValue != null) { if (returnType.isPrimitive()) { } else { assert returnType.isAssignableFrom(defaultValue.getClass()) : "Return type "+returnType.getName()+" is not assignable from "+defaultValue.getClass().getName(); } } } }// end for loop } private static boolean typeMatches(TreeLogger logger, Annotation anno, JClassType exists) throws UnableToCompleteException { final boolean doLog = logger.isLoggable(logLevel); if (doLog) { logger.log(logLevel, "Checking if annotation "+anno.getClass().getName()+" equals "+exists.getQualifiedSourceName()); logger.log(logLevel, anno.getClass().getName()+": "+ anno.toString()); logger.log(logLevel, exists.getQualifiedSourceName()+": "+ exists.toString()); } try { Method[] annoMethods = anno.annotationType().getDeclaredMethods(); // Filter and map existing types. Map existingMethods = new LinkedHashMap(); for (JMethod existingMethod : exists.getMethods()) { if (existingMethod.isPublic() && existingMethod.getEnclosingType() == exists) { existingMethods.put(existingMethod.getName(), existingMethod); } } // Now, our annotation methods must match our declared methods. for (Method m : annoMethods) { JMethod existing = existingMethods.get(m.getName()); if (!m.getName().equals(existing.getName())) { if (doLog) { logger.log(logLevel, "Annotations don't match for " +anno.annotationType().getName()+ "; "+ m.getName() +" != "+existing.getName()); } return false; } JParameter[] existingParams = existing.getParameters(); Class[] annoParams = m.getParameterTypes(); if (existingParams.length != annoParams.length) { if (doLog) { logger.log(logLevel, "Annotations don't match for " +anno.annotationType().getName()+ "; "+ "parameters for "+ m.getName() +" have changed."); } return false; } for (int i = existingParams.length; i --> 0; ) { JParameter existingParam = existingParams[i]; Class annoParam = annoParams[i]; if (!existingParam.getType().getQualifiedSourceName() .equals(annoParam.getCanonicalName())) { if (doLog) { logger.log(logLevel, "Annotations don't match for " + anno.annotationType().getName()+ "." + m.getName()+"(); "+ "parameter "+ existingParam.getName() +" type has changed " + "from " +existingParam.getType().getQualifiedSourceName()+" to " + annoParam.getCanonicalName()+"."); } return false; } } } logger.log(logLevel, "Annotations match for " + anno.annotationType().getName()+ "; reusing type."); return true; } catch (Exception e) { logger.log(Type.ERROR, "Error encountering comparing annotation class to generated proxy;"); logger.log(Type.ERROR, anno.getClass().getName() +" or "+exists.getName()+" is causing this error.", e); throw new UnableToCompleteException(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy