/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package org.hibernate.validator.ap.checks;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.SimpleAnnotationValueVisitor6;
import javax.lang.model.util.TypeKindVisitor6;
import javax.lang.model.util.Types;
import org.hibernate.validator.ap.util.AnnotationApiHelper;
import org.hibernate.validator.ap.util.CollectionHelper;
import org.hibernate.validator.ap.util.TypeNames.BeanValidationTypes;
import static javax.lang.model.util.ElementFilter.methodsIn;
/**
* Checks, that each constraint annotation type declares the members message(), groups() and payload() as
* defined by the BV spec.
*
* @author Gunnar Morling
*/
public class AnnotationTypeMemberCheck extends AbstractConstraintCheck {
private final AnnotationApiHelper annotationApiHelper;
private final Types typeUtils;
public AnnotationTypeMemberCheck(AnnotationApiHelper annotationApiHelper, Types typeUtils) {
this.annotationApiHelper = annotationApiHelper;
this.typeUtils = typeUtils;
}
@Override
public Set<ConstraintCheckIssue> checkAnnotationType(TypeElement element, AnnotationMirror annotation) {
Set<ConstraintCheckIssue> theValue = CollectionHelper.newHashSet();
theValue.addAll( checkMessageAttribute( element ) );
theValue.addAll( checkGroupsAttribute( element ) );
theValue.addAll( checkPayloadAttribute( element ) );
return theValue;
}
/**
* Checks that the given type element
* <p/>
* <ul>
* <li>has a method with name "message",</li>
* <li>the return type of this method is {@link String}.</li>
* </ul>
*
* @param element The element of interest.
*
* @return A possibly non-empty set of constraint check errors, never null.
*/
private Set<ConstraintCheckIssue> checkMessageAttribute(TypeElement element) {
ExecutableElement messageMethod = getMethod( element, "message" );
if ( messageMethod == null ) {
return CollectionHelper.asSet(
ConstraintCheckIssue.error( element, null, "CONSTRAINT_TYPE_MUST_DECLARE_MESSAGE_MEMBER" )
);
}
if ( !typeUtils.isSameType(
annotationApiHelper.getMirrorForType( String.class ), messageMethod.getReturnType()
) ) {
return CollectionHelper.asSet(
ConstraintCheckIssue.error( messageMethod, null, "RETURN_TYPE_MUST_BE_STRING" )
);
}
return Collections.emptySet();
}
/**
* Checks that the given type element
* <p/>
* <ul>
* <li>has a method with name "groups",</li>
* <li>the return type of this method is <code>Class<?>[]</code>,</li>
* <li>the default value of this method is <code>{}</code>.</li>
* </ul>
*
* @param element The element of interest.
*
* @return A possibly non-empty set of constraint check errors, never null.
*/
private Set<ConstraintCheckIssue> checkGroupsAttribute(TypeElement element) {
ExecutableElement groupsMethod = getMethod( element, "groups" );
if ( groupsMethod == null ) {
return CollectionHelper.asSet(
ConstraintCheckIssue.error( element, null, "CONSTRAINT_TYPE_MUST_DECLARE_GROUPS_MEMBER" )
);
}
DeclaredType type = getComponentTypeOfArrayReturnType( groupsMethod );
if ( type == null ) {
return CollectionHelper.asSet(
ConstraintCheckIssue.error( groupsMethod, null, "RETURN_TYPE_MUST_BE_CLASS_ARRAY" )
);
}
boolean typeHasNameClass = type.asElement().getSimpleName().contentEquals( "Class" );
boolean typeHasExactlyOneTypeArgument = type.getTypeArguments().size() == 1;
boolean typeArgumentIsUnboundWildcard = validateWildcardBounds( type.getTypeArguments().get( 0 ), null, null );
if ( !( typeHasNameClass && typeHasExactlyOneTypeArgument && typeArgumentIsUnboundWildcard ) ) {
return CollectionHelper.asSet(
ConstraintCheckIssue.error( groupsMethod, null, "RETURN_TYPE_MUST_BE_CLASS_ARRAY" )
);
}
if ( !isEmptyArray( groupsMethod.getDefaultValue() ) ) {
return CollectionHelper.asSet(
ConstraintCheckIssue.error( groupsMethod, null, "DEFAULT_VALUE_MUST_BE_EMPTY_ARRAY" )
);
}
return Collections.emptySet();
}
/**
* Checks that the given type element
* <p/>
* <ul>
* <li>has a method with name "payload",</li>
* <li>the return type of this method is <code>Class<? extends Payload>[]</code>,</li>
* <li>the default value of this method is <code>{}</code>.</li>
* </ul>
*
* @param element The element of interest.
*
* @return A possibly non-empty set of constraint check errors, never null.
*/
private Set<ConstraintCheckIssue> checkPayloadAttribute(TypeElement element) {
ExecutableElement payloadMethod = getMethod( element, "payload" );
if ( payloadMethod == null ) {
return CollectionHelper.asSet(
ConstraintCheckIssue.error( element, null, "CONSTRAINT_TYPE_MUST_DECLARE_PAYLOAD_MEMBER" )
);
}
DeclaredType type = getComponentTypeOfArrayReturnType( payloadMethod );
if ( type == null ) {
return CollectionHelper.asSet(
ConstraintCheckIssue.error( payloadMethod, null, "PAYLOAD_RETURN_TYPE_MUST_BE_CLASS_ARRAY" )
);
}
boolean typeHasNameClass = type.asElement().getSimpleName().contentEquals( "Class" );
boolean typeHasExactlyOneTypeArgument = type.getTypeArguments().size() == 1;
boolean typeArgumentIsWildcardWithPayloadExtendsBound = validateWildcardBounds(
type.getTypeArguments().get( 0 ),
annotationApiHelper.getDeclaredTypeByName( BeanValidationTypes.PAYLOAD ),
null
);
if ( !( typeHasNameClass && typeHasExactlyOneTypeArgument && typeArgumentIsWildcardWithPayloadExtendsBound ) ) {
return CollectionHelper.asSet(
ConstraintCheckIssue.error( payloadMethod, null, "PAYLOAD_RETURN_TYPE_MUST_BE_CLASS_ARRAY" )
);
}
if ( !isEmptyArray( payloadMethod.getDefaultValue() ) ) {
return CollectionHelper.asSet(
ConstraintCheckIssue.error( payloadMethod, null, "PAYLOAD_DEFAULT_VALUE_MUST_BE_EMPTY_ARRAY" )
);
}
return Collections.emptySet();
}
/**
* Returns the method of the given type with the given name.
*
* @param element The type of interest.
* @param name The name of the method which should be returned.
*
* @return The method of the given type with the given name or null if no such method exists.
*/
private ExecutableElement getMethod(TypeElement element, String name) {
for ( ExecutableElement oneMethod : methodsIn( element.getEnclosedElements() ) ) {
if ( oneMethod.getSimpleName().contentEquals( name ) ) {
return oneMethod;
}
}
return null;
}
/**
* Returns the component type of the array-typed return value of the given method.
*
* @param method The method of interest.
*
* @return The component type of the array-typed return value of the given method or null,
* if the given method has no array-typed return value.
*/
private DeclaredType getComponentTypeOfArrayReturnType(ExecutableElement method) {
return method.getReturnType().accept(
new TypeKindVisitor6<DeclaredType, Void>() {
@Override
public DeclaredType visitArray(ArrayType t, Void p) {
return t.getComponentType().accept(
new TypeKindVisitor6<DeclaredType, Void>() {
@Override
public DeclaredType visitDeclared(DeclaredType t, Void p) {
return t;
}
}, null
);
}
}, null
);
}
/**
* Returns true, if the given type mirror is a wildcard type with the given extends and super bounds, false otherwise.
*
* @param type The type to check.
* @param expectedExtendsBound A mirror representing the expected extends bound.
* @param expectedSuperBound A mirror representing the expected super bound.
*
* @return True, if the given type mirror is a wildcard type with the given extends and super bounds, false otherwise.
*/
private boolean validateWildcardBounds(TypeMirror type, final TypeMirror expectedExtendsBound, final TypeMirror expectedSuperBound) {
Boolean theValue = type.accept(
new TypeKindVisitor6<Boolean, Void>() {
@Override
public Boolean visitWildcard(WildcardType t, Void p) {
boolean extendsBoundMatches = ( t.getExtendsBound() == null ? expectedExtendsBound == null : expectedExtendsBound != null && typeUtils
.isSameType( t.getExtendsBound(), expectedExtendsBound ) );
boolean superBoundMatches = ( t.getSuperBound() == null ? expectedSuperBound == null : expectedSuperBound != null && typeUtils
.isSameType( t.getSuperBound(), expectedSuperBound ) );
return extendsBoundMatches && superBoundMatches;
}
}, null
);
return Boolean.TRUE.equals( theValue );
}
/**
* Checks whether the given annotation value is an empty array or not.
*
* @param annotationValue The annotation value of interest.
*
* @return True, if the given annotation value is an empty array, false otherwise.
*/
private boolean isEmptyArray(AnnotationValue annotationValue) {
return annotationValue != null && Boolean.TRUE.equals(
annotationValue.accept(
new SimpleAnnotationValueVisitor6<Boolean, Void>() {
@Override
public Boolean visitArray(List<? extends AnnotationValue> values, Void p) {
return values.size() == 0;
}
}, null
)
);
}
}