/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.qpid.server.model.validation;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
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.type.DeclaredType;
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;
@SupportedAnnotationTypes({AttributeAnnotationValidator.MANAGED_ATTRIBUTE_CLASS_NAME,
AttributeAnnotationValidator.DERIVED_ATTRIBUTE_CLASS_NAME,
AttributeAnnotationValidator.MANAGED_STATISTIC_CLASS_NAME})
public class AttributeAnnotationValidator extends AbstractProcessor
{
public static final String MANAGED_ATTRIBUTE_CLASS_NAME = "org.apache.qpid.server.model.ManagedAttribute";
public static final String DERIVED_ATTRIBUTE_CLASS_NAME = "org.apache.qpid.server.model.DerivedAttribute";
public static final String MANAGED_STATISTIC_CLASS_NAME = "org.apache.qpid.server.model.ManagedStatistic";
private static final Set<TypeKind> VALID_PRIMITIVE_TYPES = new HashSet<>(Arrays.asList(TypeKind.BOOLEAN,
TypeKind.BYTE,
TypeKind.CHAR,
TypeKind.DOUBLE,
TypeKind.FLOAT,
TypeKind.INT,
TypeKind.LONG,
TypeKind.SHORT));
@Override
public SourceVersion getSupportedSourceVersion()
{
return SourceVersion.latest();
}
@Override
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv)
{
processAttributes(roundEnv, MANAGED_ATTRIBUTE_CLASS_NAME);
processAttributes(roundEnv, DERIVED_ATTRIBUTE_CLASS_NAME);
processStatistics(roundEnv, MANAGED_STATISTIC_CLASS_NAME);
return false;
}
public void processAttributes(final RoundEnvironment roundEnv,
String elementName)
{
Elements elementUtils = processingEnv.getElementUtils();
TypeElement annotationElement = elementUtils.getTypeElement(elementName);
for (Element e : roundEnv.getElementsAnnotatedWith(annotationElement))
{
checkAnnotationIsOnMethodInInterface(annotationElement, e);
ExecutableElement methodElement = (ExecutableElement) e;
checkInterfaceExtendsConfiguredObject(annotationElement, methodElement);
checkMethodTakesNoArgs(annotationElement, methodElement);
checkMethodName(annotationElement, methodElement);
checkMethodReturnType(annotationElement, methodElement);
checkTypeAgreesWithName(annotationElement, methodElement);
}
}
public void processStatistics(final RoundEnvironment roundEnv,
String elementName)
{
Elements elementUtils = processingEnv.getElementUtils();
TypeElement annotationElement = elementUtils.getTypeElement(elementName);
for (Element e : roundEnv.getElementsAnnotatedWith(annotationElement))
{
checkAnnotationIsOnMethodInInterface(annotationElement, e);
ExecutableElement methodElement = (ExecutableElement) e;
checkInterfaceExtendsConfiguredObject(annotationElement, methodElement);
checkMethodTakesNoArgs(annotationElement, methodElement);
checkMethodName(annotationElement, methodElement);
checkTypeAgreesWithName(annotationElement, methodElement);
checkMethodReturnTypeIsNumber(annotationElement, methodElement);
}
}
private void checkMethodReturnTypeIsNumber(final TypeElement annotationElement,
final ExecutableElement methodElement)
{
Types typeUtils = processingEnv.getTypeUtils();
Elements elementUtils = processingEnv.getElementUtils();
TypeMirror numberType = elementUtils.getTypeElement("java.lang.Number").asType();
if(!typeUtils.isAssignable(methodElement.getReturnType(),numberType))
{
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR,
"@"
+ annotationElement.getSimpleName()
+ " return type does not extend Number: "
+ methodElement.getReturnType().toString(),
methodElement
);
}
}
public void checkTypeAgreesWithName(final TypeElement annotationElement, final ExecutableElement methodElement)
{
Types typeUtils = processingEnv.getTypeUtils();
String methodName = methodElement.getSimpleName().toString();
if((methodName.startsWith("is") || methodName.startsWith("has"))
&& !(methodElement.getReturnType().getKind() == TypeKind.BOOLEAN
|| typeUtils.isSameType(typeUtils.boxedClass(typeUtils.getPrimitiveType(TypeKind.BOOLEAN)).asType(), methodElement.getReturnType())))
{
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR,
"@"
+ annotationElement.getSimpleName()
+ " return type is not boolean or Boolean: "
+ methodElement.getReturnType().toString(),
methodElement
);
}
}
public void checkMethodReturnType(final TypeElement annotationElement, final ExecutableElement methodElement)
{
if (!isValidType(methodElement.getReturnType()))
{
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR,
"@"
+ annotationElement.getSimpleName()
+ " cannot be applied to methods with return type "
+ methodElement.getReturnType().toString(),
methodElement
);
}
}
public void checkMethodName(final TypeElement annotationElement, final ExecutableElement methodElement)
{
String methodName = methodElement.getSimpleName().toString();
if (methodName.length() < 3
|| (methodName.length() < 4 && !methodName.startsWith("is"))
|| !(methodName.startsWith("is") || methodName.startsWith("get") || methodName.startsWith("has")))
{
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR,
"@"
+ annotationElement.getSimpleName()
+ " can only be applied to methods which of the form getXXX(), isXXX() or hasXXX()",
methodElement
);
}
}
public void checkMethodTakesNoArgs(final TypeElement annotationElement, final ExecutableElement methodElement)
{
if (!methodElement.getParameters().isEmpty())
{
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR,
"@"
+ annotationElement.getSimpleName()
+ " can only be applied to methods which take no parameters",
methodElement
);
}
}
public void checkInterfaceExtendsConfiguredObject(final TypeElement annotationElement, final Element e)
{
Types typeUtils = processingEnv.getTypeUtils();
TypeMirror configuredObjectType = getErasure("org.apache.qpid.server.model.ConfiguredObject");
TypeElement parent = (TypeElement) e.getEnclosingElement();
if (!typeUtils.isAssignable(typeUtils.erasure(parent.asType()), configuredObjectType))
{
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR,
"@"
+ annotationElement.getSimpleName()
+ " can only be applied to methods within an interface which extends "
+ configuredObjectType.toString()
+ " which does not apply to "
+ parent.asType().toString(),
e);
}
}
public void checkAnnotationIsOnMethodInInterface(final TypeElement annotationElement, final Element e)
{
if (e.getKind() != ElementKind.METHOD || e.getEnclosingElement().getKind() != ElementKind.INTERFACE)
{
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR,
"@"
+ annotationElement.getSimpleName()
+ " can only be applied to methods within an interface",
e
);
}
}
private boolean isValidType(final TypeMirror type)
{
Types typeUtils = processingEnv.getTypeUtils();
Elements elementUtils = processingEnv.getElementUtils();
Element typeElement = typeUtils.asElement(type);
if (VALID_PRIMITIVE_TYPES.contains(type.getKind()))
{
return true;
}
for(TypeKind primitive : VALID_PRIMITIVE_TYPES)
{
if(typeUtils.isSameType(type, typeUtils.boxedClass(typeUtils.getPrimitiveType(primitive)).asType()))
{
return true;
}
}
if(typeElement.getKind()==ElementKind.ENUM)
{
return true;
}
String className = "org.apache.qpid.server.model.ConfiguredObject";
TypeMirror configuredObjectType = getErasure(className);
if(typeUtils.isAssignable(typeUtils.erasure(type), configuredObjectType))
{
return true;
}
if(typeUtils.isSameType(type, elementUtils.getTypeElement("java.lang.String").asType()))
{
return true;
}
if(typeUtils.isSameType(type,elementUtils.getTypeElement("java.util.UUID").asType()))
{
return true;
}
TypeMirror erasedType = typeUtils.erasure(type);
if(typeUtils.isSameType(erasedType, getErasure("java.util.List"))
|| typeUtils.isSameType(erasedType, getErasure("java.util.Set"))
|| typeUtils.isSameType(erasedType, getErasure("java.util.Collection")))
{
for(TypeMirror paramType : ((DeclaredType)type).getTypeArguments())
{
if(!isValidType(paramType))
{
return false;
}
}
return true;
}
if(typeUtils.isSameType(erasedType, getErasure("java.util.Map")))
{
List<? extends TypeMirror> args = ((DeclaredType) type).getTypeArguments();
if (args.size() != 2)
{
throw new IllegalArgumentException("Map types " + type + " must have exactly two type arguments");
}
return isValidType(args.get(0)) && (isValidType(args.get(1)) || typeUtils.isSameType(args.get(1), getErasure("java.lang.Object")));
}
return false;
}
private TypeMirror getErasure(final String className)
{
final Types typeUtils = processingEnv.getTypeUtils();
final Elements elementUtils = processingEnv.getElementUtils();
return typeUtils.erasure(elementUtils.getTypeElement(className).asType());
}
}