blob: 211315e3ac588ec34b95f441b8f7394c7a6acb5c [file] [log] [blame]
/*
*
* 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.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
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.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));
private Elements elementUtils;
private Types typeUtils;
private Messager messager;
@Override
public SourceVersion getSupportedSourceVersion()
{
return SourceVersion.latest();
}
@Override
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv)
{
elementUtils = processingEnv.getElementUtils();
typeUtils = processingEnv.getTypeUtils();
messager = processingEnv.getMessager();
processAttributes(roundEnv, MANAGED_ATTRIBUTE_CLASS_NAME, false, false);
processAttributes(roundEnv, DERIVED_ATTRIBUTE_CLASS_NAME, true, true);
processStatistics(roundEnv, MANAGED_STATISTIC_CLASS_NAME);
return false;
}
public void processAttributes(final RoundEnvironment roundEnv,
String elementName,
final boolean allowedNamed,
final boolean allowAbstractManagedTypes)
{
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, allowedNamed);
checkTypeAgreesWithName(annotationElement, methodElement);
if(MANAGED_ATTRIBUTE_CLASS_NAME.equals(elementName))
{
checkValidValuesPatternOnAppropriateTypes(methodElement);
}
}
}
void checkValidValuesPatternOnAppropriateTypes(final ExecutableElement methodElement)
{
AnnotationMirror annotationMirror = getAnnotationMirror(methodElement, MANAGED_ATTRIBUTE_CLASS_NAME);
if (hasValidValuePattern(annotationMirror)
&& !isStringOrCollectionOfStrings(methodElement.getReturnType()))
{
messager.printMessage(Diagnostic.Kind.ERROR,
"@ManagedAttribute return type does not not support validValuePattern: "
+ methodElement.getReturnType().toString(),
methodElement);
}
}
private AnnotationMirror getAnnotationMirror(final ExecutableElement methodElement, final String annotationClassName)
{
for (AnnotationMirror annotationMirror : methodElement.getAnnotationMirrors())
{
Element annotationAsElement = annotationMirror.getAnnotationType().asElement();
if (annotationClassName.equals(getFullyQualifiedName(annotationAsElement)))
{
return annotationMirror;
}
}
return null;
}
private boolean hasValidValuePattern(final AnnotationMirror annotationMirror)
{
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : annotationMirror.getElementValues()
.entrySet())
{
if ("validValuePattern".equals(entry.getKey().getSimpleName().toString()))
{
return true;
}
}
return false;
}
private String getFullyQualifiedName(final Element element)
{
return elementUtils.getPackageOf(element).getQualifiedName().toString() + "." + element.getSimpleName().toString();
}
private boolean isStringOrCollectionOfStrings(TypeMirror type)
{
TypeMirror stringType = elementUtils.getTypeElement("java.lang.String").asType();
TypeMirror collectionType = elementUtils.getTypeElement("java.util.Collection").asType();
TypeElement typeAsElement = (TypeElement) typeUtils.asElement(type);
return typeUtils.isAssignable(type, stringType)
|| (typeAsElement != null
&& typeAsElement.getTypeParameters().size() == 1
&& "java.lang.String".equals(getFullyQualifiedName(getErasedParameterType((DeclaredType) type, 0)))
&& typeUtils.isAssignable(typeUtils.erasure(typeAsElement.asType()), collectionType));
}
private Element getErasedParameterType(final DeclaredType returnType, final int parameterIndex)
{
return typeUtils.asElement(typeUtils.erasure(returnType.getTypeArguments().get(parameterIndex)));
}
public void processStatistics(final RoundEnvironment roundEnv,
String elementName)
{
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);
checkMethodReturnTypeIsNumberOrDate(annotationElement, methodElement);
}
}
private void checkMethodReturnTypeIsNumberOrDate(final TypeElement annotationElement,
final ExecutableElement methodElement)
{
TypeMirror numberType = elementUtils.getTypeElement("java.lang.Number").asType();
TypeMirror dateType = elementUtils.getTypeElement("java.util.Date").asType();
if(!typeUtils.isAssignable(methodElement.getReturnType(),numberType)
&& !typeUtils.isSameType(methodElement.getReturnType(), dateType))
{
messager.printMessage(Diagnostic.Kind.ERROR,
"@"
+ annotationElement.getSimpleName()
+ " return type does not extend java.lang.Number"
+ " and is not java.util.Date : "
+ methodElement.getReturnType().toString(),
methodElement
);
}
}
public void checkTypeAgreesWithName(final TypeElement annotationElement, final ExecutableElement methodElement)
{
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())))
{
messager.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,
final boolean allowNamed)
{
if (!(isValidType(methodElement.getReturnType()) || (allowNamed && isNamed(methodElement.getReturnType()))))
{
messager.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")))
{
messager.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())
{
messager.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)
{
TypeMirror configuredObjectType = getErasure("org.apache.qpid.server.model.ConfiguredObject");
TypeElement parent = (TypeElement) e.getEnclosingElement();
if (!typeUtils.isAssignable(typeUtils.erasure(parent.asType()), configuredObjectType))
{
messager.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)
{
messager.printMessage(Diagnostic.Kind.ERROR,
"@"
+ annotationElement.getSimpleName()
+ " can only be applied to methods within an interface",
e
);
}
}
boolean isValidType(final TypeMirror type)
{
return isValidType(processingEnv, type, false);
}
static boolean isValidType(ProcessingEnvironment processingEnv,
final TypeMirror type, final boolean allowAbstractManagedTypes)
{
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 != null && typeElement.getKind()==ElementKind.ENUM)
{
return true;
}
String className = "org.apache.qpid.server.model.ConfiguredObject";
TypeMirror configuredObjectType = getErasure(processingEnv, className);
if(typeUtils.isAssignable(typeUtils.erasure(type), configuredObjectType))
{
return true;
}
final TypeElement managedAttributeTypeValueElement =
elementUtils.getTypeElement(ManagedAttributeValueTypeValidator.MANAGED_ATTRIBUTE_VALUE_TYPE_CLASS_NAME);
if(typeElement != null)
{
for (AnnotationMirror annotation : typeElement.getAnnotationMirrors())
{
if (annotation.getAnnotationType().asElement().equals(managedAttributeTypeValueElement))
{
if(allowAbstractManagedTypes)
{
return true;
}
else
{
final Map<? extends ExecutableElement, ? extends AnnotationValue> annotationValues =
elementUtils.getElementValuesWithDefaults(annotation);
for(Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> element : annotationValues.entrySet())
{
if("isAbstract".contentEquals(element.getKey().getSimpleName()))
{
return element.getValue().getValue().equals(Boolean.FALSE);
}
}
return false;
}
}
}
}
if(typeUtils.isSameType(type,elementUtils.getTypeElement("java.lang.Object").asType()))
{
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;
}
if(typeUtils.isSameType(type,elementUtils.getTypeElement("java.util.Date").asType()))
{
return true;
}
if(typeUtils.isSameType(type,elementUtils.getTypeElement("java.net.URI").asType()))
{
return true;
}
if(typeUtils.isSameType(type,elementUtils.getTypeElement("java.security.cert.Certificate").asType()))
{
return true;
}
if(typeUtils.isSameType(type,elementUtils.getTypeElement("java.security.Principal").asType()))
{
return true;
}
TypeMirror erasedType = typeUtils.erasure(type);
if(typeUtils.isSameType(erasedType, getErasure(processingEnv, "java.util.List"))
|| typeUtils.isSameType(erasedType, getErasure(processingEnv, "java.util.Set"))
|| typeUtils.isSameType(erasedType, getErasure(processingEnv, "java.util.Collection")))
{
for(TypeMirror paramType : ((DeclaredType)type).getTypeArguments())
{
if(!isValidType(processingEnv, paramType, allowAbstractManagedTypes))
{
return false;
}
}
return true;
}
if(typeUtils.isSameType(erasedType, getErasure(processingEnv, "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(processingEnv, args.get(0), false)
&& (isValidType(processingEnv, args.get(1), false)
|| typeUtils.isSameType(args.get(1), getErasure(processingEnv, "java.lang.Object")));
}
return false;
}
private boolean isNamed(final TypeMirror type)
{
return isNamed(processingEnv, type);
}
static boolean isNamed(ProcessingEnvironment processingEnv,
final TypeMirror type)
{
Types typeUtils = processingEnv.getTypeUtils();
String className = "org.apache.qpid.server.model.Named";
TypeMirror namedType = getErasure(processingEnv, className);
return typeUtils.isAssignable(typeUtils.erasure(type), namedType);
}
private TypeMirror getErasure(final String className)
{
return getErasure(processingEnv, className);
}
private static TypeMirror getErasure(ProcessingEnvironment processingEnv, final String className)
{
final Types typeUtils = processingEnv.getTypeUtils();
final Elements elementUtils = processingEnv.getElementUtils();
return typeUtils.erasure(elementUtils.getTypeElement(className).asType());
}
}