/*
* Copyright (C) 2015 Red Hat, Inc. and/or its affiliates.
*
* 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 org.jboss.errai.processor;
import static org.jboss.errai.processor.AnnotationProcessors.extractAnnotationStringValue;
import static org.jboss.errai.processor.AnnotationProcessors.getAnnotation;
import static org.jboss.errai.processor.AnnotationProcessors.hasAnnotation;
import static org.jboss.errai.processor.AnnotationProcessors.isElementWrapper;
import static org.jboss.errai.processor.AnnotationProcessors.isNativeJsType;
import static org.jboss.errai.processor.AnnotationProcessors.propertyNameOfMethod;
import static org.jboss.errai.processor.AnnotationProcessors.resolveSingleTypeArgumentForGenericSuperType;
import static org.jboss.errai.processor.TypeNames.GWT_ELEMENT;
import static org.jboss.errai.processor.TypeNames.LIST_CHANGE_HANDLER;
import static org.jboss.errai.processor.TypeNames.NATIVE_HAS_VALUE;
import static org.jboss.errai.processor.TypeNames.TAKES_VALUE;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind;
/**
* Evaluates usage of the ErraiUI DataField annotation and emits errors and warnings when
* the annotation is not being used correctly.
*/
@SupportedAnnotationTypes(TypeNames.BOUND)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BoundAnnotationChecker extends AbstractProcessor {
@Override
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
final Types types = processingEnv.getTypeUtils();
final Elements elements = processingEnv.getElementUtils();
final TypeMirror gwtWidgetType = elements.getTypeElement(TypeNames.GWT_WIDGET).asType();
final TypeMirror gwtElementType = elements.getTypeElement(TypeNames.GWT_ELEMENT).asType();
final TypeMirror listChangeHandlerType = elements.getTypeElement(TypeNames.LIST_CHANGE_HANDLER).asType();
final Map<TypeElement, List<Element>> classesWithBoundThings =
annotations
.stream()
.flatMap(anno -> roundEnv.getElementsAnnotatedWith(anno).stream())
.peek(target -> validateBoundType(types, elements, gwtWidgetType, gwtElementType, listChangeHandlerType, target))
.collect(Collectors.groupingBy(AnnotationProcessors::getEnclosingTypeElement));
classesWithBoundThings
.entrySet()
.stream()
.forEach(entry -> {
final List<TypeMirror> modelTypes = findAllModelTypes(entry.getKey());
if (modelTypes.size() == 0) {
printMissingModelErrorsForBoundElements(entry.getValue());
}
else if (modelTypes.size() > 1) {
// TODO mark everything annotated with @AutoBound or @Model with an error
}
else {
validateBoundPropertyChains(elements, entry.getValue(), modelTypes.get(0));
}
});
return false;
}
private void validateBoundPropertyChains(final Elements elements, final List<Element> boundElements,
final TypeMirror modelType) {
boundElements
.stream()
.forEach(boundElement -> {
final String configuredProperty =
extractAnnotationStringValue(elements, getAnnotation(boundElement, TypeNames.BOUND), "property");
final boolean configured = configuredProperty != null && !configuredProperty.isEmpty();
final String boundProperty = configured ? configuredProperty
: getDefaultPropertyName(boundElement).orElseThrow(() -> new IllegalStateException(
String.format("Found a %s element [%s] annotated with @Bound.", boundElement.getKind(),
boundElement.getSimpleName())));
final TypeMirror boundComponentType = (boundElement instanceof ExecutableElement
? ((ExecutableElement) boundElement).getReturnType() : boundElement.asType());
if (!isValidPropertyChain(modelType, boundComponentType, boundProperty, configured)) {
processingEnv.getMessager().printMessage(Kind.ERROR,
"The model type " + ((DeclaredType) modelType).asElement().getSimpleName()
+ " does not have property \"" + boundProperty + "\"",
boundElement, getAnnotation(boundElement, TypeNames.BOUND));
}
});
}
private Optional<String> getDefaultPropertyName(final Element boundElement) {
switch (boundElement.getKind()) {
case FIELD:
case PARAMETER:
return Optional.of(boundElement.getSimpleName().toString());
case METHOD:
return Optional.of(propertyNameOfMethod(boundElement));
default:
return Optional.empty();
}
}
private void printMissingModelErrorsForBoundElements(final List<Element> value) {
value
.stream()
.forEach(boundElement ->
processingEnv
.getMessager()
.printMessage(Kind.ERROR,
"@Bound requires that this class also contains a @Model or @AutoBound DataBinder",
boundElement,
getAnnotation(boundElement, TypeNames.BOUND)));
}
private void validateBoundType(final Types types, final Elements elements, final TypeMirror gwtWidgetType,
final TypeMirror gwtElementType, final TypeMirror listChangeHandlerType, final Element target) {
final TypeMirror targetType = getTargetType(target);
if (!types.isAssignable(targetType, gwtWidgetType)
&& !types.isAssignable(targetType, gwtElementType)
&& !isNativeJsType(targetType, elements)
&& !types.isAssignable(targetType, types.erasure(listChangeHandlerType))) {
processingEnv.getMessager().printMessage(
Kind.ERROR, "@Bound must target a type assignable to Widget or Element, or be a JsType element wrapper.", target);
}
}
private TypeMirror getTargetType(final Element target) {
TypeMirror targetType;
if (target.getKind() == ElementKind.METHOD) {
targetType = ((ExecutableElement) target).getReturnType();
}
else {
targetType = target.asType();
}
return targetType;
}
/**
* Returns the set of all bindable property names in the given model.
*/
private Set<String> getPropertyNames(final TypeMirror modelType) {
final Elements elements = processingEnv.getElementUtils();
final Types types = processingEnv.getTypeUtils();
final Set<String> result = new HashSet<>();
for (final Element el : ElementFilter.methodsIn(elements.getAllMembers((TypeElement) types.asElement(modelType)))) {
final String propertyName = AnnotationProcessors.propertyNameOfMethod(el);
if (propertyName != null) {
result.add(propertyName);
}
}
return result;
}
/**
* Returns the type of the provided property in the given model type.
*/
private TypeMirror getPropertyType(final TypeMirror modelType, final String property) {
final Elements elements = processingEnv.getElementUtils();
final Types types = processingEnv.getTypeUtils();
TypeMirror result = null;
for (final Element el : ElementFilter.methodsIn(elements.getAllMembers((TypeElement) types.asElement(modelType)))) {
final String methodName = el.getSimpleName().toString();
if (methodName.toLowerCase().equals("get" + property.toLowerCase()) ||
methodName.toLowerCase().equals("is" + property.toLowerCase())) {
result = ((ExecutableElement) el).getReturnType();
break;
}
}
return result;
}
/**
* Returns the bindable model type of all things annotated with {@code @Model}
* and DataBinders annotated with {@code @AutoBound}. Legally, there should
* only be one; we return all of them as Elements so the caller can attach
* error/warning messages to them if we found multiples.
*
* @param classContainingBindableThings
* @return
*/
private List<TypeMirror> findAllModelTypes(final TypeElement classContainingBindableThings) {
final List<TypeMirror> result = new ArrayList<>();
final Elements elements = processingEnv.getElementUtils();
for (final Element el : elements.getAllMembers(classContainingBindableThings)) {
switch (el.getKind()) {
case METHOD:
case CONSTRUCTOR:
if (!hasAnnotation(el, TypeNames.JAVAX_INJECT)) continue;
for (final VariableElement param : ((ExecutableElement) el).getParameters()) {
if (hasAnnotation(param, TypeNames.MODEL)) {
result.add(param.asType());
}
else if (hasAnnotation(param, TypeNames.AUTO_BOUND)) {
result.add(typeOfDataBinder(param.asType()));
}
}
break;
case FIELD:
if (hasAnnotation(el, TypeNames.MODEL)) {
result.add(el.asType());
}
else if (hasAnnotation(el, TypeNames.AUTO_BOUND)) {
result.add(typeOfDataBinder(el.asType()));
}
break;
default:
break;
}
}
return result;
}
/**
* Returns the concrete type, type variable, or wildcard type of the given DataBinder declaration.
*
* @param dataBinderDeclaration
* @return
*/
private TypeMirror typeOfDataBinder(final TypeMirror dataBinderDeclaration) {
// in a superclass, this could return a type variable or a wildcard
return ((DeclaredType) dataBinderDeclaration).getTypeArguments().get(0);
}
private boolean isValidPropertyChain(final TypeMirror bindableType, final String propertyChain) {
final int dotPos = propertyChain.indexOf(".");
if (dotPos <= 0) {
return getPropertyNames(bindableType).contains(propertyChain);
}
else {
final String thisProperty = propertyChain.substring(0, dotPos);
final String moreProperties = propertyChain.substring(dotPos + 1);
if (!getPropertyNames(bindableType).contains(thisProperty)) {
return false;
}
final TypeMirror propertyType = getPropertyType(bindableType, thisProperty);
return isValidPropertyChain(propertyType, moreProperties);
}
}
private boolean isValidPropertyChain(final TypeMirror bindableType, final TypeMirror boundElementType, final String propertyChain, final boolean configured) {
return (!configured && bindsToType(boundElementType, bindableType))
|| isValidPropertyChain(bindableType, propertyChain);
}
private boolean bindsToType(final TypeMirror boundElementType, final TypeMirror bindableType) {
final Types types = processingEnv.getTypeUtils();
final Elements elements = processingEnv.getElementUtils();
final TypeMirror takesValue;
final TypeMirror nativeHasValue;
final TypeMirror listChangeHandler;
Optional<TypeMirror> oBoundPropertyType;
if (types.isAssignable(boundElementType, takesValue = types.erasure(elements.getTypeElement(TAKES_VALUE).asType()))) {
oBoundPropertyType = resolveSingleTypeArgumentForGenericSuperType(boundElementType, takesValue, types);
}
else if (types.isAssignable(boundElementType, nativeHasValue = types.erasure(elements.getTypeElement(NATIVE_HAS_VALUE).asType()))) {
oBoundPropertyType = resolveSingleTypeArgumentForGenericSuperType(boundElementType, nativeHasValue, types);
}
else if (types.isAssignable(boundElementType, listChangeHandler = types.erasure(elements.getTypeElement(LIST_CHANGE_HANDLER).asType()))) {
oBoundPropertyType =
resolveSingleTypeArgumentForGenericSuperType(boundElementType, listChangeHandler, types)
.map(listTypeArg -> types.getDeclaredType(elements.getTypeElement(List.class.getName()), listTypeArg));
}
else if (types.isAssignable(boundElementType, elements.getTypeElement(GWT_ELEMENT).asType())
|| isElementWrapper(boundElementType, elements)) {
oBoundPropertyType = Optional.of(elements.getTypeElement(String.class.getName()).asType());
}
else {
oBoundPropertyType = Optional.empty();
}
return oBoundPropertyType
.filter(propertyType -> types.isSameType(propertyType, bindableType))
.isPresent();
}
}