static List<ListenerMethod> getListenerMethods(ListenerClass listener) { if (listener.method().length == 1) { return Arrays.asList(listener.method()); } try { List<ListenerMethod> methods = new ArrayList<ListenerMethod>(); Class<? extends Enum<?>> callbacks = listener.callbacks(); for (Enum<?> callbackMethod : callbacks.getEnumConstants()) { Field callbackField = callbacks.getField(callbackMethod.name()); ListenerMethod method = callbackField.getAnnotation(ListenerMethod.class); if (method == null) { throw new IllegalStateException( String.format( "@%s's %s.%s missing @%s annotation.", callbacks.getEnclosingClass().getSimpleName(), callbacks.getSimpleName(), callbackMethod.name(), ListenerMethod.class.getSimpleName())); } methods.add(method); } return methods; } catch (NoSuchFieldException e) { throw new AssertionError(e); } }
private void parseListenerAnnotation( Class<? extends Annotation> annotationClass, Element element, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) throws Exception { // This should be guarded by the annotation's @Target but it's worth a check for safe casting. if (!(element instanceof ExecutableElement) || element.getKind() != METHOD) { throw new IllegalStateException( String.format("@%s annotation must be on a method.", annotationClass.getSimpleName())); } ExecutableElement executableElement = (ExecutableElement) element; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Assemble information on the method. Annotation annotation = element.getAnnotation(annotationClass); Method annotationValue = annotationClass.getDeclaredMethod("value"); if (annotationValue.getReturnType() != int[].class) { throw new IllegalStateException( String.format("@%s annotation value() type not int[].", annotationClass)); } int[] ids = (int[]) annotationValue.invoke(annotation); String name = executableElement.getSimpleName().toString(); boolean required = isRequiredBinding(element); // Verify that the method and its containing class are accessible via generated code. boolean hasError = isInaccessibleViaGeneratedCode(annotationClass, "methods", element); hasError |= isBindingInWrongPackage(annotationClass, element); Integer duplicateId = findDuplicate(ids); if (duplicateId != null) { error( element, "@%s annotation for method contains duplicate ID %d. (%s.%s)", annotationClass.getSimpleName(), duplicateId, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } ListenerClass listener = annotationClass.getAnnotation(ListenerClass.class); if (listener == null) { throw new IllegalStateException( String.format( "No @%s defined on @%s.", ListenerClass.class.getSimpleName(), annotationClass.getSimpleName())); } for (int id : ids) { if (id == View.NO_ID) { if (ids.length == 1) { if (!required) { error( element, "ID-free binding must not be annotated with @Nullable. (%s.%s)", enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // Verify target type is valid for a binding without an id. String targetType = listener.targetType(); if (!isSubtypeOfType(enclosingElement.asType(), targetType) && !isInterface(enclosingElement.asType())) { error( element, "@%s annotation without an ID may only be used with an object of type " + "\"%s\" or an interface. (%s.%s)", annotationClass.getSimpleName(), targetType, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } } else { error( element, "@%s annotation contains invalid ID %d. (%s.%s)", annotationClass.getSimpleName(), id, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } } } ListenerMethod method; ListenerMethod[] methods = listener.method(); if (methods.length > 1) { throw new IllegalStateException( String.format( "Multiple listener methods specified on @%s.", annotationClass.getSimpleName())); } else if (methods.length == 1) { if (listener.callbacks() != ListenerClass.NONE.class) { throw new IllegalStateException( String.format( "Both method() and callback() defined on @%s.", annotationClass.getSimpleName())); } method = methods[0]; } else { Method annotationCallback = annotationClass.getDeclaredMethod("callback"); Enum<?> callback = (Enum<?>) annotationCallback.invoke(annotation); Field callbackField = callback.getDeclaringClass().getField(callback.name()); method = callbackField.getAnnotation(ListenerMethod.class); if (method == null) { throw new IllegalStateException( String.format( "No @%s defined on @%s's %s.%s.", ListenerMethod.class.getSimpleName(), annotationClass.getSimpleName(), callback.getDeclaringClass().getSimpleName(), callback.name())); } } // Verify that the method has equal to or less than the number of parameters as the listener. List<? extends VariableElement> methodParameters = executableElement.getParameters(); if (methodParameters.size() > method.parameters().length) { error( element, "@%s methods can have at most %s parameter(s). (%s.%s)", annotationClass.getSimpleName(), method.parameters().length, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // Verify method return type matches the listener. TypeMirror returnType = executableElement.getReturnType(); if (returnType instanceof TypeVariable) { TypeVariable typeVariable = (TypeVariable) returnType; returnType = typeVariable.getUpperBound(); } if (!returnType.toString().equals(method.returnType())) { error( element, "@%s methods must have a '%s' return type. (%s.%s)", annotationClass.getSimpleName(), method.returnType(), enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } if (hasError) { return; } Parameter[] parameters = Parameter.NONE; if (!methodParameters.isEmpty()) { parameters = new Parameter[methodParameters.size()]; BitSet methodParameterUsed = new BitSet(methodParameters.size()); String[] parameterTypes = method.parameters(); for (int i = 0; i < methodParameters.size(); i++) { VariableElement methodParameter = methodParameters.get(i); TypeMirror methodParameterType = methodParameter.asType(); if (methodParameterType instanceof TypeVariable) { TypeVariable typeVariable = (TypeVariable) methodParameterType; methodParameterType = typeVariable.getUpperBound(); } for (int j = 0; j < parameterTypes.length; j++) { if (methodParameterUsed.get(j)) { continue; } if (isSubtypeOfType(methodParameterType, parameterTypes[j]) || isInterface(methodParameterType)) { parameters[i] = new Parameter(j, methodParameterType.toString()); methodParameterUsed.set(j); break; } } if (parameters[i] == null) { StringBuilder builder = new StringBuilder(); builder .append("Unable to match @") .append(annotationClass.getSimpleName()) .append(" method arguments. (") .append(enclosingElement.getQualifiedName()) .append('.') .append(element.getSimpleName()) .append(')'); for (int j = 0; j < parameters.length; j++) { Parameter parameter = parameters[j]; builder .append("\n\n Parameter #") .append(j + 1) .append(": ") .append(methodParameters.get(j).asType().toString()) .append("\n "); if (parameter == null) { builder.append("did not match any listener parameters"); } else { builder .append("matched listener parameter #") .append(parameter.getListenerPosition() + 1) .append(": ") .append(parameter.getType()); } } builder .append("\n\nMethods may have up to ") .append(method.parameters().length) .append(" parameter(s):\n"); for (String parameterType : method.parameters()) { builder.append("\n ").append(parameterType); } builder.append( "\n\nThese may be listed in any order but will be searched for from top to bottom."); error(executableElement, builder.toString()); return; } } } MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required); BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement); for (int id : ids) { if (!bindingClass.addMethod(id, listener, method, binding)) { error( element, "Multiple listener methods with return value specified for ID %d. (%s.%s)", id, enclosingElement.getQualifiedName(), element.getSimpleName()); return; } } // Add the type-erased version to the valid binding targets set. erasedTargetNames.add(enclosingElement.toString()); }
private void emitListenerBindings(StringBuilder builder, ViewInjection injection) { Map<ListenerClass, Map<ListenerMethod, ListenerBinding>> bindings = injection.getListenerBindings(); if (bindings.isEmpty()) { return; } String extraIndent = ""; // We only need to emit the null check if there are zero required bindings. boolean needsNullChecked = injection.getRequiredBindings().isEmpty(); if (needsNullChecked) { builder.append(" if (view != null) {\n"); extraIndent = " "; } for (Map.Entry<ListenerClass, Map<ListenerMethod, ListenerBinding>> e : bindings.entrySet()) { ListenerClass listener = e.getKey(); Map<ListenerMethod, ListenerBinding> methodBindings = e.getValue(); // Emit: ((OWNER_TYPE) view).SETTER_NAME( boolean needsCast = !VIEW_TYPE.equals(listener.targetType()); builder.append(extraIndent).append(" "); if (needsCast) { builder.append("((").append(listener.targetType()); if (listener.genericArguments() > 0) { builder.append('<'); for (int i = 0; i < listener.genericArguments(); i++) { if (i > 0) { builder.append(", "); } builder.append('?'); } builder.append('>'); } builder.append(") "); } builder.append("view"); if (needsCast) { builder.append(')'); } builder.append('.').append(listener.setter()).append("(\n"); // Emit: new TYPE() { builder.append(extraIndent).append(" new ").append(listener.type()).append("() {\n"); for (ListenerMethod method : getListenerMethods(listener)) { // Emit: @Override public RETURN_TYPE METHOD_NAME( builder .append(extraIndent) .append(" @Override public ") .append(method.returnType()) .append(' ') .append(method.name()) .append("(\n"); // Emit listener method arguments, each on their own line. String[] parameterTypes = method.parameters(); for (int i = 0, count = parameterTypes.length; i < count; i++) { builder .append(extraIndent) .append(" ") .append(parameterTypes[i]) .append(" p") .append(i); if (i < count - 1) { builder.append(','); } builder.append('\n'); } // Emit end of parameters, start of body. builder.append(extraIndent).append(" ) {\n"); // Set up the return statement, if needed. builder.append(extraIndent).append(" "); boolean hasReturnType = !"void".equals(method.returnType()); if (hasReturnType) { builder.append("return "); } if (methodBindings.containsKey(method)) { ListenerBinding binding = methodBindings.get(method); builder.append("target.").append(binding.getName()).append('('); List<Parameter> parameters = binding.getParameters(); String[] listenerParameters = method.parameters(); for (int i = 0, count = parameters.size(); i < count; i++) { Parameter parameter = parameters.get(i); int listenerPosition = parameter.getListenerPosition(); emitCastIfNeeded(builder, listenerParameters[listenerPosition], parameter.getType()); builder.append('p').append(listenerPosition); if (i < count - 1) { builder.append(", "); } } builder.append(");"); } else if (hasReturnType) { builder.append(method.defaultReturn()).append(';'); } builder.append('\n'); // Emit end of listener method. builder.append(extraIndent).append(" }\n"); } // Emit end of listener class body and close the setter method call. builder.append(extraIndent).append(" });\n"); } if (needsNullChecked) { builder.append(" }\n"); } }