| /* |
| * 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.apt.ClientToDomainMapper.UnmappedTypeException; |
| import com.google.web.bindery.requestfactory.shared.ProxyFor; |
| import com.google.web.bindery.requestfactory.shared.ProxyForName; |
| import com.google.web.bindery.requestfactory.shared.Service; |
| import com.google.web.bindery.requestfactory.shared.ServiceName; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import javax.lang.model.element.ElementKind; |
| import javax.lang.model.element.ExecutableElement; |
| import javax.lang.model.element.Modifier; |
| import javax.lang.model.element.Name; |
| import javax.lang.model.element.NestingKind; |
| import javax.lang.model.element.TypeElement; |
| import javax.lang.model.type.ExecutableType; |
| import javax.lang.model.type.MirroredTypeException; |
| import javax.lang.model.type.TypeKind; |
| import javax.lang.model.type.TypeMirror; |
| import javax.lang.model.util.ElementFilter; |
| |
| /** |
| * Checks client to domain mappings. |
| */ |
| class DomainChecker extends ScannerBase<Void> { |
| |
| /** |
| * Attempt to find the most specific method that conforms to a given |
| * signature. |
| */ |
| static class MethodFinder extends ScannerBase<ExecutableElement> { |
| private TypeElement domainType; |
| private ExecutableElement found; |
| private final boolean boxReturnType; |
| private final CharSequence name; |
| private final TypeMirror returnType; |
| private final List<TypeMirror> params; |
| |
| public MethodFinder(CharSequence name, TypeMirror returnType, List<TypeMirror> params, |
| boolean boxReturnType, State state) { |
| this.boxReturnType = boxReturnType; |
| this.name = name; |
| this.returnType = TypeSimplifier.simplify(returnType, boxReturnType, state); |
| List<TypeMirror> temp = new ArrayList<TypeMirror>(params.size()); |
| for (TypeMirror param : params) { |
| temp.add(TypeSimplifier.simplify(param, false, state)); |
| } |
| this.params = Collections.unmodifiableList(temp); |
| } |
| |
| @Override |
| public ExecutableElement visitExecutable(ExecutableElement domainMethodElement, State state) { |
| // Quick check for name, paramer count, and return type assignability |
| if (domainMethodElement.getSimpleName().contentEquals(name) |
| && domainMethodElement.getParameters().size() == params.size()) { |
| // Pick up parameterizations in domain type |
| ExecutableType domainMethod = viewIn(domainType, domainMethodElement, state); |
| |
| boolean returnTypeMatches; |
| if (returnType == null) { |
| /* |
| * This condition is for methods that we don't really care about the |
| * domain return types (for getId(), getVersion()). |
| */ |
| returnTypeMatches = true; |
| } else { |
| TypeMirror domainReturn = |
| TypeSimplifier.simplify(domainMethod.getReturnType(), boxReturnType, state); |
| // The isSameType handles the NONE case. |
| returnTypeMatches = state.types.isSubtype(domainReturn, returnType); |
| } |
| if (returnTypeMatches) { |
| boolean paramsMatch = true; |
| Iterator<TypeMirror> lookFor = params.iterator(); |
| Iterator<? extends TypeMirror> domainParam = domainMethod.getParameterTypes().iterator(); |
| while (lookFor.hasNext()) { |
| assert domainParam.hasNext(); |
| TypeMirror requestedType = lookFor.next(); |
| TypeMirror paramType = TypeSimplifier.simplify(domainParam.next(), false, state); |
| if (!state.types.isSubtype(requestedType, paramType)) { |
| paramsMatch = false; |
| } |
| } |
| |
| if (paramsMatch) { |
| // Keep most-specific method signature |
| if (found == null |
| || state.types.isSubsignature(domainMethod, (ExecutableType) found.asType())) { |
| found = domainMethodElement; |
| } |
| } |
| } |
| } |
| |
| return found; |
| } |
| |
| @Override |
| public ExecutableElement visitType(TypeElement domainType, State state) { |
| this.domainType = domainType; |
| return scanAllInheritedMethods(domainType, state); |
| } |
| } |
| |
| /** |
| * This is used as the target for errors since generic methods show up as |
| * synthetic elements that don't correspond to any source. |
| */ |
| private TypeElement checkedElement; |
| private boolean currentTypeIsProxy; |
| private TypeElement domainElement; |
| private boolean requireInstanceDomainMethods; |
| private boolean requireStaticDomainMethods; |
| |
| @Override |
| public Void visitExecutable(ExecutableElement clientMethodElement, State state) { |
| if (shouldIgnore(clientMethodElement, state)) { |
| return null; |
| } |
| // Ignore overrides of stableId() in proxies |
| Name name = clientMethodElement.getSimpleName(); |
| if (currentTypeIsProxy && name.contentEquals("stableId") |
| && clientMethodElement.getParameters().isEmpty()) { |
| return null; |
| } |
| |
| ExecutableType clientMethod = viewIn(checkedElement, clientMethodElement, state); |
| List<TypeMirror> lookFor = new ArrayList<TypeMirror>(); |
| // Convert client method signature to domain types |
| TypeMirror returnType; |
| try { |
| returnType = convertToDomainTypes(clientMethod, lookFor, clientMethodElement, state); |
| } catch (UnmappedTypeException e) { |
| /* |
| * Unusual: this would happen if a RequestContext for which we have a |
| * resolved domain service method uses unresolved proxy types. For |
| * example, the RequestContext uses a @Service annotation, while one or |
| * more proxy types use @ProxyForName("") and specify a domain type not |
| * available to the compiler. |
| */ |
| return null; |
| } |
| |
| ExecutableElement domainMethod; |
| if (currentTypeIsProxy && isSetter(clientMethodElement, state)) { |
| // Look for void setFoo(...) |
| domainMethod = |
| new MethodFinder(name, state.types.getNoType(TypeKind.VOID), lookFor, false, state).scan( |
| domainElement, state); |
| if (domainMethod == null) { |
| // Try a builder style |
| domainMethod = |
| new MethodFinder(name, domainElement.asType(), lookFor, false, state).scan( |
| domainElement, state); |
| } |
| } else { |
| /* |
| * The usual case for getters and all service methods. Only box return |
| * types when matching context methods since there's a significant |
| * semantic difference between a null Integer and 0. |
| */ |
| domainMethod = |
| new MethodFinder(name, returnType, lookFor, !currentTypeIsProxy, state).scan( |
| domainElement, state); |
| } |
| |
| if (domainMethod == null) { |
| // Did not find a service method |
| StringBuilder sb = new StringBuilder(); |
| sb.append(returnType).append(" ").append(name).append("("); |
| boolean first = true; |
| for (TypeMirror param : lookFor) { |
| if (!first) { |
| sb.append(", "); |
| } |
| sb.append(param); |
| first = false; |
| } |
| sb.append(")"); |
| |
| state.poison(clientMethodElement, Messages.domainMissingMethod(sb)); |
| return null; |
| } |
| |
| // Check if the method is public |
| if (!domainMethod.getModifiers().contains(Modifier.PUBLIC)) { |
| state.poison(clientMethodElement, Messages.domainMethodNotPublic( |
| domainMethod.getSimpleName())); |
| } |
| |
| /* |
| * Check the domain method for any requirements for it to be static. |
| * InstanceRequests assume instance methods on the domain type. |
| */ |
| boolean isInstanceRequest = |
| state.types.isSubtype(clientMethod.getReturnType(), state.instanceRequestType); |
| |
| if ((isInstanceRequest || requireInstanceDomainMethods) |
| && domainMethod.getModifiers().contains(Modifier.STATIC)) { |
| state.poison(clientMethodElement, Messages.domainMethodWrongModifier(false, domainMethod |
| .getSimpleName())); |
| } |
| if (!isInstanceRequest && requireStaticDomainMethods |
| && !domainMethod.getModifiers().contains(Modifier.STATIC)) { |
| state.poison(clientMethodElement, Messages.domainMethodWrongModifier(true, domainMethod |
| .getSimpleName())); |
| } |
| |
| // Record the mapping |
| state.addMapping(clientMethodElement, domainMethod); |
| return null; |
| } |
| |
| @Override |
| public Void visitType(TypeElement clientTypeElement, State state) { |
| TypeMirror clientType = clientTypeElement.asType(); |
| checkedElement = clientTypeElement; |
| boolean isEntityProxy = state.types.isSubtype(clientType, state.entityProxyType); |
| currentTypeIsProxy = isEntityProxy || state.types.isSubtype(clientType, state.valueProxyType); |
| domainElement = (TypeElement) state.getClientToDomainMap().get(clientTypeElement); |
| if (domainElement == null) { |
| // A proxy with an unresolved domain type (e.g. ProxyForName("")) |
| return null; |
| } |
| |
| requireInstanceDomainMethods = false; |
| requireStaticDomainMethods = false; |
| |
| if (currentTypeIsProxy) { |
| // Require domain property methods to be instance methods |
| requireInstanceDomainMethods = true; |
| if (!hasProxyLocator(clientTypeElement, state)) { |
| // Domain types without a Locator should be default-instantiable |
| if (!isDefaultInstantiable(domainElement)) { |
| state.warn(clientTypeElement, Messages.domainNotDefaultInstantiable(domainElement |
| .getSimpleName(), clientTypeElement.getSimpleName(), state.requestContextType |
| .asElement().getSimpleName())); |
| } |
| |
| /* |
| * Check for getId(), getVersion(), and findFoo() for any type that |
| * extends EntityProxy, but not on EntityProxy itself, since EntityProxy |
| * is mapped to java.lang.Object. |
| */ |
| if (isEntityProxy && !state.types.isSameType(clientType, state.entityProxyType)) { |
| checkDomainEntityMethods(state); |
| } |
| } |
| } else if (!hasServiceLocator(clientTypeElement, state)) { |
| /* |
| * Otherwise, we're looking at a RequestContext. If it doesn't have a |
| * ServiceLocator, all methods must be static. |
| */ |
| requireStaticDomainMethods = true; |
| } |
| |
| scanAllInheritedMethods(clientTypeElement, state); |
| return null; |
| } |
| |
| /** |
| * Check that {@code getId()} and {@code getVersion()} exist and that they are |
| * non-static. Check that {@code findFoo()} exists, is static, returns an |
| * appropriate type, and its parameter is assignable from the return value |
| * from {@code getId()}. |
| */ |
| private void checkDomainEntityMethods(State state) { |
| ExecutableElement getId = |
| new MethodFinder("getId", null, Collections.<TypeMirror> emptyList(), false, state).scan( |
| domainElement, state); |
| if (getId == null) { |
| state.poison(checkedElement, Messages.domainNoGetId(domainElement.asType())); |
| } else { |
| if (getId.getModifiers().contains(Modifier.STATIC)) { |
| state.poison(checkedElement, Messages.domainGetIdStatic()); |
| } |
| |
| // Can only check findFoo() if we have a getId |
| ExecutableElement find = |
| new MethodFinder("find" + domainElement.getSimpleName(), domainElement.asType(), |
| Collections.singletonList(getId.getReturnType()), false, state).scan(domainElement, |
| state); |
| if (find == null) { |
| state.warn(checkedElement, Messages.domainMissingFind(domainElement.asType(), domainElement |
| .getSimpleName(), getId.getReturnType(), checkedElement.getSimpleName())); |
| } else if (!find.getModifiers().contains(Modifier.STATIC)) { |
| state.poison(checkedElement, Messages.domainFindNotStatic(domainElement.getSimpleName())); |
| } |
| } |
| |
| ExecutableElement getVersion = |
| new MethodFinder("getVersion", null, Collections.<TypeMirror> emptyList(), false, state) |
| .scan(domainElement, state); |
| if (getVersion == null) { |
| state.poison(checkedElement, Messages.domainNoGetVersion(domainElement.asType())); |
| } else if (getVersion.getModifiers().contains(Modifier.STATIC)) { |
| state.poison(checkedElement, Messages.domainGetVersionStatic()); |
| } |
| } |
| |
| /** |
| * Converts a client method's types to their domain counterparts. |
| * |
| * @param clientMethod the RequestContext method to validate |
| * @param parameterAccumulator an out parameter that will be populated with |
| * the converted parameter types |
| * @param warnTo The element to which warnings should be posted if one or more |
| * client types cannot be converted to domain types for validation |
| * @param state the State object |
| * @throws UnmappedTypeException if one or more types used in |
| * {@code clientMethod} cannot be resolved to domain types |
| */ |
| private TypeMirror convertToDomainTypes(ExecutableType clientMethod, |
| List<TypeMirror> parameterAccumulator, ExecutableElement warnTo, State state) |
| throws UnmappedTypeException { |
| boolean error = false; |
| TypeMirror returnType; |
| try { |
| returnType = clientMethod.getReturnType().accept(new ClientToDomainMapper(), state); |
| } catch (UnmappedTypeException e) { |
| error = true; |
| returnType = null; |
| state.warn(warnTo, Messages.methodNoDomainPeer(e.getClientType(), false)); |
| } |
| for (TypeMirror param : clientMethod.getParameterTypes()) { |
| try { |
| parameterAccumulator.add(param.accept(new ClientToDomainMapper(), state)); |
| } catch (UnmappedTypeException e) { |
| parameterAccumulator.add(null); |
| error = true; |
| state.warn(warnTo, Messages.methodNoDomainPeer(e.getClientType(), true)); |
| } |
| } |
| if (error) { |
| throw new UnmappedTypeException(); |
| } |
| return returnType; |
| } |
| |
| private boolean hasProxyLocator(TypeElement x, State state) { |
| ProxyFor proxyFor = x.getAnnotation(ProxyFor.class); |
| if (proxyFor != null) { |
| // See javadoc on getAnnotation |
| try { |
| proxyFor.locator(); |
| throw new RuntimeException("Should not reach here"); |
| } catch (MirroredTypeException expected) { |
| TypeMirror locatorType = expected.getTypeMirror(); |
| return !state.types.asElement(locatorType).equals(state.locatorType.asElement()); |
| } |
| } |
| ProxyForName proxyForName = x.getAnnotation(ProxyForName.class); |
| return proxyForName != null && !proxyForName.locator().isEmpty(); |
| } |
| |
| private boolean hasServiceLocator(TypeElement x, State state) { |
| Service service = x.getAnnotation(Service.class); |
| if (service != null) { |
| // See javadoc on getAnnotation |
| try { |
| service.locator(); |
| throw new RuntimeException("Should not reach here"); |
| } catch (MirroredTypeException expected) { |
| TypeMirror locatorType = expected.getTypeMirror(); |
| return !state.types.asElement(locatorType).equals(state.serviceLocatorType.asElement()); |
| } |
| } |
| ServiceName serviceName = x.getAnnotation(ServiceName.class); |
| return serviceName != null && !serviceName.locator().isEmpty(); |
| } |
| |
| /** |
| * Looks for a no-arg constructor or no constructors at all. Instance |
| * initializers are ignored. |
| */ |
| private boolean isDefaultInstantiable(TypeElement x) { |
| if (x.getKind() != ElementKind.CLASS) { |
| return false; |
| } |
| if (x.getModifiers().contains(Modifier.ABSTRACT)) { |
| return false; |
| } |
| if (x.getNestingKind() == NestingKind.ANONYMOUS || x.getNestingKind() == NestingKind.LOCAL |
| || (x.getNestingKind() == NestingKind.MEMBER && !x.getModifiers().contains(Modifier.STATIC))) { |
| // anonymous and local shouldn't ever happen, but just in case... |
| return false; |
| } |
| List<ExecutableElement> constructors = ElementFilter.constructorsIn(x.getEnclosedElements()); |
| if (constructors.isEmpty()) { |
| return true; |
| } |
| for (ExecutableElement constructor : constructors) { |
| if (constructor.getParameters().isEmpty()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |