/* * 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); } }