/* * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.user.rebind; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.RunAsyncCallback; 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.JMethod; import com.google.gwt.core.ext.typeinfo.JParameter; 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.TypeOracle; import com.google.gwt.user.client.AsyncProxy; import com.google.gwt.user.client.AsyncProxy.AllowNonVoid; import com.google.gwt.user.client.AsyncProxy.ConcreteType; import com.google.gwt.user.client.AsyncProxy.DefaultValue; import com.google.gwt.user.client.impl.AsyncProxyBase; import java.io.PrintWriter; import java.util.Arrays; import java.util.Iterator; /** * Generates implementation of AsyncProxy interfaces. */ public class AsyncProxyGenerator extends Generator { @Override public String generate(TreeLogger logger, GeneratorContext generatorContext, String typeName) throws UnableToCompleteException { // The TypeOracle knows about all types in the type system TypeOracle typeOracle = generatorContext.getTypeOracle(); // Get a reference to the type that the generator should implement JClassType asyncProxyType = typeOracle.findType(AsyncProxy.class.getName()); JClassType asyncProxyBaseType = typeOracle.findType(AsyncProxyBase.class.getName()); JClassType sourceType = typeOracle.findType(typeName); // Ensure that the requested type exists if (sourceType == null) { logger.log(TreeLogger.ERROR, "Could not find requested typeName"); throw new UnableToCompleteException(); } else if (sourceType.isInterface() == null) { // The incoming type wasn't a plain interface, we don't support // abstract base classes logger.log(TreeLogger.ERROR, sourceType.getQualifiedSourceName() + " is not an interface.", null); throw new UnableToCompleteException(); } JClassType concreteType = getConcreteType(logger, typeOracle, sourceType); JClassType paramType = getParamType(logger, asyncProxyType, sourceType); validate(logger, sourceType, concreteType, paramType); // com.foo.Bar$Proxy -> com_foo_Bar_ProxyImpl String generatedSimpleSourceName = sourceType.getQualifiedSourceName().replace( '.', '_').replace('$', '_') + "Impl"; // Begin writing the generated source. ClassSourceFileComposerFactory f = new ClassSourceFileComposerFactory( sourceType.getPackage().getName(), generatedSimpleSourceName); String createdClassName = f.getCreatedClassName(); // The generated class needs to be able to determine the module base URL f.addImport(GWT.class.getName()); f.addImport(RunAsyncCallback.class.getName()); // Used by the map methods f.setSuperclass(asyncProxyBaseType.getQualifiedSourceName() + "<" + paramType.getQualifiedSourceName() + ">"); // The whole point of this exercise f.addImplementedInterface(sourceType.getQualifiedSourceName()); // All source gets written through this Writer PrintWriter out = generatorContext.tryCreate(logger, sourceType.getPackage().getName(), generatedSimpleSourceName); // If an implementation already exists, we don't need to do any work if (out != null) { SourceWriter sw = f.createSourceWriter(generatorContext, out); // The invocation of runAsync, with a unique class for the split point sw.println("protected void doAsync0() {"); sw.indent(); sw.println("GWT.runAsync(new RunAsyncCallback() {"); sw.indentln("public void onFailure(Throwable caught) {doFailure0(caught);}"); sw.indentln("public void onSuccess() {setInstance0(doCreate0());}"); sw.println("});"); sw.outdent(); sw.println("}"); // Field for callback, plus getter and setter String proxyCallback = "ProxyCallback<" + paramType.getQualifiedSourceName() + ">"; sw.println("private " + proxyCallback + " callback;"); sw.println("public void setProxyCallback(" + proxyCallback + " callback) {this.callback = callback;}"); sw.println("protected " + proxyCallback + " getCallback0() {return callback;}"); // doCreate0() method to invoke GWT.create() sw.println("private " + paramType.getQualifiedSourceName() + " doCreate0() {"); sw.indent(); sw.println("return GWT.create(" + concreteType.getQualifiedSourceName() + ".class);"); sw.outdent(); sw.println("}"); boolean allowNonVoid = sourceType.getAnnotation(AllowNonVoid.class) != null; for (JMethod method : paramType.getOverridableMethods()) { DefaultValue defaults = getDefaultValue(sourceType, method); if (method.getReturnType() != JPrimitiveType.VOID && !allowNonVoid) { logger.log(TreeLogger.ERROR, "The method " + method.getName() + " returns a type other than void, but " + sourceType.getQualifiedSourceName() + " does not define the " + AllowNonVoid.class.getSimpleName() + " annotation."); throw new UnableToCompleteException(); } // Print the method decl sw.print("public " + method.getReturnType().getQualifiedSourceName() + " " + method.getName() + "("); for (Iterator<JParameter> i = Arrays.asList(method.getParameters()).iterator(); i.hasNext();) { JParameter param = i.next(); sw.print("final " + param.getType().getQualifiedSourceName() + " " + param.getName()); if (i.hasNext()) { sw.print(", "); } } sw.println(") {"); { sw.indent(); // Try a direct dispatch if we have a proxy instance sw.println("if (getProxiedInstance() != null) {"); { sw.indent(); if (method.getReturnType() != JPrimitiveType.VOID) { sw.print("return "); } writeInvocation(sw, "getProxiedInstance()", method); sw.outdent(); } // Otherwise queue up a parameterized command object sw.println("} else {"); { sw.indent(); sw.println("enqueue0(new ParamCommand<" + paramType.getQualifiedSourceName() + ">() {"); { sw.indent(); sw.println("public void execute(" + paramType.getQualifiedSourceName() + " t) {"); { sw.indent(); writeInvocation(sw, "t", method); sw.outdent(); } sw.println("}"); sw.outdent(); } sw.println("});"); if (method.getReturnType() != JPrimitiveType.VOID) { sw.println("return " + getDefaultExpression(defaults, method.getReturnType()) + ";"); } sw.outdent(); } sw.println("}"); sw.outdent(); } sw.println("}"); } sw.commit(logger); } // Return the name of the concrete class return createdClassName; } private JClassType getConcreteType(TreeLogger logger, TypeOracle typeOracle, JClassType sourceType) throws UnableToCompleteException { JClassType concreteType; ConcreteType concreteTypeAnnotation = sourceType.getAnnotation(ConcreteType.class); if (concreteTypeAnnotation == null) { logger.log(TreeLogger.ERROR, "AsnycProxy subtypes must specify a " + ConcreteType.class.getSimpleName() + " annotation."); throw new UnableToCompleteException(); } String concreteTypeName = concreteTypeAnnotation.value().getName().replace( '$', '.'); concreteType = typeOracle.findType(concreteTypeName); if (concreteType == null) { logger.log(TreeLogger.ERROR, "Unable to find concrete type; is it in the GWT source path?"); throw new UnableToCompleteException(); } return concreteType; } /** * Returns a useful default expression for a type. It is an error to pass * JPrimitiveType.VOID into this method. */ private String getDefaultExpression(DefaultValue defaultValue, JType type) { if (!(type instanceof JPrimitiveType)) { return "null"; } else if (type == JPrimitiveType.BOOLEAN) { return String.valueOf(defaultValue.booleanValue()); } else if (type == JPrimitiveType.BYTE) { return String.valueOf(defaultValue.byteValue()); } else if (type == JPrimitiveType.CHAR) { return String.valueOf((int) defaultValue.charValue()); } else if (type == JPrimitiveType.DOUBLE) { return String.valueOf(defaultValue.doubleValue()); } else if (type == JPrimitiveType.FLOAT) { return String.valueOf(defaultValue.floatValue()) + "F"; } else if (type == JPrimitiveType.INT) { return String.valueOf(defaultValue.intValue()); } else if (type == JPrimitiveType.LONG) { return String.valueOf(defaultValue.longValue()); } else if (type == JPrimitiveType.SHORT) { return String.valueOf(defaultValue.shortValue()); } else if (type == JPrimitiveType.VOID) { assert false : "Should not pass VOID into this method"; } assert false : "Should never reach here"; return null; } private DefaultValue getDefaultValue(JClassType sourceType, JMethod method) { DefaultValue toReturn = method.getAnnotation(DefaultValue.class); if (toReturn == null) { toReturn = sourceType.getAnnotation(DefaultValue.class); } if (toReturn == null) { JClassType proxyType = sourceType.getOracle().findType( AsyncProxy.class.getName()); toReturn = proxyType.getAnnotation(DefaultValue.class); } assert toReturn != null : "Could not find any DefaultValue instance"; return toReturn; } private JClassType getParamType(TreeLogger logger, JClassType asyncProxyType, JClassType sourceType) throws UnableToCompleteException { JClassType paramType = null; for (JClassType intr : sourceType.getImplementedInterfaces()) { JParameterizedType isParameterized = intr.isParameterized(); if (isParameterized == null) { continue; } if (isParameterized.getBaseType().equals(asyncProxyType)) { paramType = isParameterized.getTypeArgs()[0]; break; } } if (paramType == null) { logger.log(TreeLogger.ERROR, "Unable to determine parameterization type."); throw new UnableToCompleteException(); } return paramType; } private void validate(TreeLogger logger, JClassType sourceType, JClassType concreteType, JClassType paramType) throws UnableToCompleteException { // sourceType may not define any methods if (sourceType.getMethods().length > 0) { logger.log(TreeLogger.ERROR, "AsyncProxy subtypes may not define any additional methods"); throw new UnableToCompleteException(); } // Make sure that sourceType implements paramType to assignments work if (!sourceType.isAssignableTo(paramType)) { logger.log(TreeLogger.ERROR, "Expecting " + sourceType.getQualifiedSourceName() + " to implement " + paramType.getQualifiedSourceName()); throw new UnableToCompleteException(); } // Make sure that concreteType is assignable to paramType if (!concreteType.isAssignableTo(paramType)) { logger.log(TreeLogger.ERROR, "Expecting concrete type" + concreteType.getQualifiedSourceName() + " to implement " + paramType.getQualifiedSourceName()); throw new UnableToCompleteException(); } // Must be able to GWT.create() if (concreteType.isMemberType() && !concreteType.isStatic()) { logger.log(TreeLogger.ERROR, "Expecting concrete type " + concreteType.getQualifiedSourceName() + " to be static."); throw new UnableToCompleteException(); } } /** * Given a method and a qualifier expression, construct an invocation of the * method using its default parameter names. */ private void writeInvocation(SourceWriter sw, String qualifier, JMethod method) { sw.print(qualifier + "." + method.getName() + "("); for (Iterator<JParameter> i = Arrays.asList(method.getParameters()).iterator(); i.hasNext();) { JParameter param = i.next(); sw.print(param.getName()); if (i.hasNext()) { sw.print(", "); } } sw.println(");"); } }