blob: 2b2a2c4eb1533b5e1e615bacc178dc8cff83362f [file] [log] [blame]
/*
* Copyright 2011 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.web.bindery.requestfactory.apt;
import com.google.web.bindery.requestfactory.shared.SkipInterfaceValidation;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind;
class State {
/**
* Slightly tweaked implementation used when running tests.
*/
static class ForTesting extends State {
public ForTesting(ProcessingEnvironment processingEnv) {
super(processingEnv);
}
@Override
boolean respectAnnotations() {
return false;
}
}
/**
* Implements comparable for priority ordering.
*/
private static class Job implements Comparable<Job> {
private static long count;
public final TypeElement element;
public final ScannerBase<?> scanner;
private final long order = count++;
private final int priority;
public Job(TypeElement element, ScannerBase<?> scanner, int priority) {
this.element = element;
this.priority = priority;
this.scanner = scanner;
}
@Override
public int compareTo(Job o) {
int c = priority - o.priority;
if (c != 0) {
return c;
}
return Long.signum(order - o.order);
}
@Override
public String toString() {
return scanner.getClass().getSimpleName() + " " + element.getSimpleName();
}
}
/**
* Used to take a {@code FooRequest extends Request<Foo>} and find the
* {@code Request<Foo>} type.
*/
static TypeMirror viewAs(DeclaredType desiredType, TypeMirror searchFrom, State state) {
if (!desiredType.getTypeArguments().isEmpty()) {
throw new IllegalArgumentException("Expecting raw type, received " + desiredType.toString());
}
Element searchElement = state.types.asElement(searchFrom);
switch (searchElement.getKind()) {
case CLASS:
case INTERFACE:
case ENUM: {
TypeMirror rawSearchFrom = state.types.getDeclaredType((TypeElement) searchElement);
if (state.types.isSameType(desiredType, rawSearchFrom)) {
return searchFrom;
}
for (TypeMirror s : state.types.directSupertypes(searchFrom)) {
TypeMirror maybe = viewAs(desiredType, s, state);
if (maybe != null) {
return maybe;
}
}
break;
}
case TYPE_PARAMETER: {
// Search <T extends Foo> as Foo
return viewAs(desiredType, ((TypeVariable) searchElement).getUpperBound(), state);
}
}
return null;
}
final TypeMirror baseProxyType;
final Elements elements;
final DeclaredType entityProxyIdType;
final DeclaredType entityProxyType;
final DeclaredType extraTypesAnnotation;
final Filer filer;
final DeclaredType instanceRequestType;
final DeclaredType locatorType;
final DeclaredType objectType;
final DeclaredType requestContextType;
final DeclaredType requestFactoryType;
final DeclaredType requestType;
final DeclaredType serviceLocatorType;
final Set<TypeElement> seen;
final Types types;
final DeclaredType valueProxyType;
private final Map<Element, Element> clientToDomainMain;
private final SortedSet<Job> jobs = new TreeSet<Job>();
private final Messager messager;
private boolean poisoned;
private boolean requireAllMappings;
private final boolean suppressErrors;
private final boolean suppressWarnings;
private final boolean verbose;
/**
* Prevents duplicate messages from being emitted.
*/
private final Map<Element, Set<String>> previousMessages = new HashMap<Element, Set<String>>();
private final Set<TypeElement> typesRequiringMapping = new LinkedHashSet<TypeElement>();
private boolean clientOnly;
public State(ProcessingEnvironment processingEnv) {
clientToDomainMain = new HashMap<Element, Element>();
elements = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
types = processingEnv.getTypeUtils();
suppressErrors = Boolean.parseBoolean(processingEnv.getOptions().get("suppressErrors"));
suppressWarnings = Boolean.parseBoolean(processingEnv.getOptions().get("suppressWarnings"));
verbose = Boolean.parseBoolean(processingEnv.getOptions().get("verbose"));
baseProxyType = findType("BaseProxy");
entityProxyType = findType("EntityProxy");
entityProxyIdType = findType("EntityProxyId");
extraTypesAnnotation = findType("ExtraTypes");
instanceRequestType = findType("InstanceRequest");
locatorType = findType("Locator");
objectType = findType(Object.class);
requestType = findType("Request");
requestContextType = findType("RequestContext");
requestFactoryType = findType("RequestFactory");
seen = new HashSet<TypeElement>();
serviceLocatorType = findType("ServiceLocator");
valueProxyType = findType("ValueProxy");
}
/**
* Add a mapping from a client method to a domain method.
*/
public void addMapping(ExecutableElement clientMethod, ExecutableElement domainMethod) {
if (domainMethod == null) {
debug(clientMethod, "No domain mapping");
} else {
debug(clientMethod, "Found domain method %s", domainMethod.toString());
}
clientToDomainMain.put(clientMethod, domainMethod);
}
/**
* Add a mapping from a client type to a domain type.
*/
public void addMapping(TypeElement clientType, TypeElement domainType) {
if (domainType == null) {
debug(clientType, "No domain mapping");
} else {
debug(clientType, "Found domain type %s", domainType.toString());
}
clientToDomainMain.put(clientType, domainType);
}
/**
* Check an element for an {@code ExtraTypes} annotation. Handles both methods
* and types.
*/
public void checkExtraTypes(Element x) {
(new ExtraTypesScanner<Void>() {
@Override
public Void visitExecutable(ExecutableElement x, State state) {
checkForAnnotation(x, state);
return null;
}
@Override
public Void visitType(TypeElement x, State state) {
checkForAnnotation(x, state);
return null;
}
@Override
protected void scanExtraType(TypeElement extraType) {
maybeScanProxy(extraType);
}
}).scan(x, this);
}
/**
* Print a warning message if verbose mode is enabled. A warning is used to
* ensure that the message shows up in Eclipse's editor (a note only makes it
* into the error console).
*/
public void debug(Element elt, String message, Object... args) {
if (verbose) {
messager.printMessage(Kind.WARNING, String.format(message, args), elt);
}
}
public void executeJobs() {
while (!jobs.isEmpty()) {
Job job = jobs.first();
jobs.remove(job);
debug(job.element, "Scanning");
try {
job.scanner.scan(job.element, this);
} catch (HaltException ignored) {
// Already reported
} catch (Throwable e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
poison(job.element, sw.toString());
}
}
if (clientOnly) {
// Don't want to check for mappings in client-only mode
return;
}
for (TypeElement element : typesRequiringMapping) {
if (!getClientToDomainMap().containsKey(element)) {
if (types.isAssignable(element.asType(), requestContextType)) {
poison(element, Messages.contextMustBeAnnotated(element.getSimpleName()));
} else {
poison(element, Messages.proxyMustBeAnnotated(element.getSimpleName()));
}
}
}
}
/**
* Utility method to look up raw types from class literals.
*/
public DeclaredType findType(Class<?> clazz) {
return types.getDeclaredType(elements.getTypeElement(clazz.getCanonicalName()));
}
/**
* Returns a map of client elements to their domain counterparts. The keys may
* be RequestContext or Proxy types or methods within those types.
*/
public Map<Element, Element> getClientToDomainMap() {
return Collections.unmodifiableMap(clientToDomainMain);
}
public boolean isClientOnly() {
return clientOnly;
}
public boolean isMappingRequired(TypeElement element) {
return typesRequiringMapping.contains(element);
}
public boolean isPoisoned() {
return poisoned;
}
/**
* Verifies that the given type may be used with RequestFactory.
*
* @see TransportableTypeVisitor
*/
public boolean isTransportableType(TypeMirror asType) {
return asType.accept(new TransportableTypeVisitor(), this);
}
public void maybeScanContext(TypeElement requestContext) {
// Also ignore RequestContext itself
if (fastFail(requestContext) || types.isSameType(requestContextType, requestContext.asType())) {
return;
}
jobs.add(new Job(requestContext, new RequestContextScanner(), 0));
if (!clientOnly) {
jobs.add(new Job(requestContext, new DomainChecker(), 1));
}
}
public void maybeScanFactory(TypeElement factoryType) {
if (fastFail(factoryType) || types.isSameType(requestFactoryType, factoryType.asType())) {
return;
}
jobs.add(new Job(factoryType, new RequestFactoryScanner(), 0));
jobs.add(new Job(factoryType, new DeobfuscatorBuilder(), 2));
}
public void maybeScanProxy(TypeElement proxyType) {
if (fastFail(proxyType)) {
return;
}
jobs.add(new Job(proxyType, new ProxyScanner(), 0));
if (!clientOnly) {
jobs.add(new Job(proxyType, new DomainChecker(), 1));
}
}
public boolean mustResolveAllAnnotations() {
return requireAllMappings;
}
/**
* Emits a fatal error message attached to an element. If the element or an
* eclosing type is annotated with {@link SkipInterfaceValidation} the message
* will be dropped.
*/
public void poison(Element elt, String message) {
if (suppressErrors) {
return;
}
if (squelchMessage(elt, message)) {
return;
}
if (respectAnnotations()) {
Element check = elt;
while (check != null) {
if (check.getAnnotation(SkipInterfaceValidation.class) != null) {
return;
}
check = check.getEnclosingElement();
}
}
poisoned = true;
if (elt == null) {
messager.printMessage(Kind.ERROR, message);
} else {
messager.printMessage(Kind.ERROR, message, elt);
}
}
public void requireMapping(TypeElement interfaceElement) {
typesRequiringMapping.add(interfaceElement);
}
/**
* Set to {@code true} to indicate that only JVM-client support code needs to
* be generated.
*/
public void setClientOnly(boolean clientOnly) {
this.clientOnly = clientOnly;
}
/**
* Set to {@code true} if it is an error for unresolved ProxyForName and
* ServiceName annotations to be left over.
*/
public void setMustResolveAllMappings(boolean requireAllMappings) {
this.requireAllMappings = requireAllMappings;
}
/**
* Emits a warning message, unless the element or an enclosing element are
* annotated with a {@code @SuppressWarnings("requestfactory")}.
*/
public void warn(Element elt, String message) {
if (suppressWarnings) {
return;
}
if (squelchMessage(elt, message)) {
return;
}
if (respectAnnotations()) {
SuppressWarnings suppress;
Element check = elt;
while (check != null) {
if (check.getAnnotation(SkipInterfaceValidation.class) != null) {
return;
}
suppress = check.getAnnotation(SuppressWarnings.class);
if (suppress != null) {
if (Arrays.asList(suppress.value()).contains("requestfactory")) {
return;
}
}
check = check.getEnclosingElement();
}
}
messager.printMessage(Kind.WARNING, message + Messages.warnSuffix(), elt);
}
/**
* This switch allows the RfValidatorTest code to be worked on in the IDE
* without causing compilation failures.
*/
boolean respectAnnotations() {
return true;
}
private boolean fastFail(TypeElement element) {
return !seen.add(element);
}
/**
* Utility method to look up raw types from the requestfactory.shared package.
* This method is used instead of class literals in order to minimize the
* number of dependencies that get packed into {@code requestfactoy-apt.jar}.
* If the requested type cannot be found, the State will be poisoned.
*/
private DeclaredType findType(String simpleName) {
TypeElement element =
elements.getTypeElement("com.google.web.bindery.requestfactory.shared." + simpleName);
if (element == null) {
poison(null, "Unable to find RequestFactory built-in type. "
+ "Is requestfactory-[client|server].jar on the classpath?");
return null;
}
return types.getDeclaredType(element);
}
/**
* Prevents duplicate messages from being emitted.
*/
private boolean squelchMessage(Element elt, String message) {
Set<String> set = previousMessages.get(elt);
if (set == null) {
set = new HashSet<String>();
// HashMap allows the null key
previousMessages.put(elt, set);
}
return !set.add(message);
}
}