/*
* 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.getAnnotation;
import static org.jboss.errai.processor.AnnotationProcessors.getAnnotationParamValueWithoutDefaults;
import static org.jboss.errai.processor.AnnotationProcessors.getField;
import static org.jboss.errai.processor.AnnotationProcessors.hasAnnotation;
import static org.jboss.errai.processor.AnnotationProcessors.isBrowserEvent;
import java.util.List;
import java.util.Optional;
import java.util.Set;
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.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind;
/**
* Evaluates usage of the ErraiUI EventHandler annotation and emits errors and warnings when
* the annotation is not being used correctly.
*/
@SupportedAnnotationTypes(TypeNames.EVENT_HANDLER)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EventHandlerAnnotationChecker 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();
annotations
.stream()
.flatMap(annotation -> roundEnv.getElementsAnnotatedWith(annotation).stream())
.forEach(target -> {
validateVoidReturnType(target);
final AnnotationMirror eventHandlerAnnotation = getAnnotation(target, TypeNames.EVENT_HANDLER);
final TypeElement enclosingClassElement = (TypeElement) target.getEnclosingElement();
final boolean hasSinkNative = hasAnnotation(target, TypeNames.SINK_NATIVE);
final Optional<? extends VariableElement> oParam = validateSingleParam((ExecutableElement) target);
maybeWarnAboutMissingTemplated(target, eventHandlerAnnotation, enclosingClassElement);
oParam.ifPresent(param -> {
final boolean hasJsInteropEventParm = isBrowserEvent(param.asType(), elements);
final boolean isNativeEvent = hasSinkNative || hasJsInteropEventParm;
final AnnotationValue eventHandlerAnnotationValue =
getAnnotationParamValueWithoutDefaults(target, TypeNames.EVENT_HANDLER, "value");
// if there is no annotation parameter value, this method handles events from the templated widget itself:
// nothing more to check.
// if the method is also annotated with @SinkNative, the values refer to template elements and we can't
// (easily) check them
if (eventHandlerAnnotationValue != null && !isNativeEvent) {
validateFieldIsWidgetOrGwtElement(types, gwtWidgetType, gwtElementType, target, eventHandlerAnnotation,
enclosingClassElement, eventHandlerAnnotationValue);
}
if (hasSinkNative) {
final TypeMirror requiredParamType = elements.getTypeElement(TypeNames.GWT_OPAQUE_DOM_EVENT).asType();
if (!types.isAssignable(param.asType(), requiredParamType)) {
processingEnv.getMessager().printMessage(Kind.ERROR,
"@SinkNative event handling methods must take exactly one argument of type " + requiredParamType,
target);
}
}
else if (hasJsInteropEventParm) {
// TODO add validation of event parameter type and @ForEvent
}
else {
final TypeMirror gwtDomEvent = elements.getTypeElement(TypeNames.GWT_OPAQUE_DOM_EVENT).asType();
final TypeMirror gwtEvent = types.erasure(elements.getTypeElement(TypeNames.GWT_EVENT).asType());
if (!types.isAssignable(param.asType(), gwtEvent)) {
processingEnv.getMessager().printMessage(Kind.ERROR,
String.format(
"Event handling methods must take exactly one argument that is a [%s], [%s], or a native @BrowserEvent.",
gwtDomEvent, gwtEvent),
target);
}
}
});
});
return false;
}
private void maybeWarnAboutMissingTemplated(final Element target, final AnnotationMirror eventHandlerAnnotation,
final TypeElement enclosingClassElement) {
if (!hasAnnotation(enclosingClassElement, TypeNames.TEMPLATED)) {
processingEnv.getMessager().printMessage(Kind.WARNING,
"@EventHandler annotations have no effect outside of @Templated classes", target, eventHandlerAnnotation);
}
}
private Optional<? extends VariableElement> validateSingleParam(final ExecutableElement method) {
final List<? extends VariableElement> parameters = method.getParameters();
if (parameters.size() != 1) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Event handling methods must take exactly one argument.", method);
return Optional.empty();
}
else {
return Optional.of(parameters.get(0));
}
}
private void validateFieldIsWidgetOrGwtElement(final Types types, final TypeMirror gwtWidgetType,
final TypeMirror gwtElementType, final Element target, final AnnotationMirror eventHandlerAnnotation,
final TypeElement enclosingClassElement, final AnnotationValue eventHandlerAnnotationValue) {
@SuppressWarnings("unchecked")
final List<AnnotationValue> eventHandlerAnnotationValues = (List<AnnotationValue>) eventHandlerAnnotationValue
.getValue();
eventHandlerAnnotationValues.stream().forEach(av -> {
final String referencedFieldName = (String) av.getValue();
final Optional<Element> oReferencedField = getField(enclosingClassElement, referencedFieldName);
oReferencedField
.filter(field -> types.isAssignable(field.asType(), gwtWidgetType) || types.isAssignable(field.asType(), gwtElementType))
.isPresent();
if (!oReferencedField
.filter(field -> types.isAssignable(field.asType(), gwtWidgetType)
|| types.isAssignable(field.asType(), gwtElementType))
.isPresent()) {
processingEnv.getMessager().printMessage(Kind.ERROR,
"\"" + referencedFieldName
+ "\" must refer to a field of type Widget or GWT Element. To reference template elements directly, use @SinkNative or a @BrowserEvent.",
target, eventHandlerAnnotation, av);
}
});
}
private void validateVoidReturnType(final Element target){
if (((ExecutableElement) target).getReturnType().getKind() != TypeKind.VOID) {
processingEnv.getMessager().printMessage(Kind.ERROR, "@EventHandler methods must return void", target);
}
}
}