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