/* * 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.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.Name; 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("("); for (TypeMirror param : lookFor) { sb.append(param); } sb.append(")"); state.poison(clientMethodElement, Messages.domainMissingMethod(sb)); return null; } /* * 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(checkedElement, Messages.domainMethodWrongModifier(false, domainMethod .getSimpleName())); } if (!isInstanceRequest && requireStaticDomainMethods && !domainMethod.getModifiers().contains(Modifier.STATIC)) { state.poison(checkedElement, 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 have a no-arg constructor if (!hasNoArgConstructor(domainElement)) { state.warn(clientTypeElement, Messages.domainNoDefaultConstructor(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 paramater 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; } /** * Looks for a no-arg constructor or no constructors at all. Instance * initializers are ignored. */ private boolean hasNoArgConstructor(TypeElement x) { List<ExecutableElement> constructors = ElementFilter.constructorsIn(x.getEnclosedElements()); if (constructors.isEmpty()) { return true; } for (ExecutableElement constructor : constructors) { if (constructor.getParameters().isEmpty()) { return true; } } return false; } 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(); } }