blob: 69273fb9e6b5f3af87c2a0764e0b4dc6b384b271 [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.bval.jsr303.xml;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.Payload;
import javax.validation.ValidationException;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import org.apache.bval.jsr303.ApacheValidatorFactory;
import org.apache.bval.jsr303.ConstraintAnnotationAttributes;
import org.apache.bval.jsr303.util.EnumerationConverter;
import org.apache.bval.jsr303.util.IOUtils;
import org.apache.bval.jsr303.util.SecureActions;
import org.apache.bval.util.FieldAccess;
import org.apache.bval.util.MethodAccess;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.lang3.StringUtils;
/**
* Uses JAXB to parse constraints.xml based on validation-mapping-1.0.xsd.<br>
*/
@SuppressWarnings("restriction")
public class ValidationMappingParser {
// private static final Log log = LogFactory.getLog(ValidationMappingParser.class);
private static final String VALIDATION_MAPPING_XSD = "META-INF/validation-mapping-1.0.xsd";
private static final Set<ConstraintAnnotationAttributes> RESERVED_PARAMS = Collections.unmodifiableSet(EnumSet.of(
ConstraintAnnotationAttributes.GROUPS, ConstraintAnnotationAttributes.MESSAGE,
ConstraintAnnotationAttributes.PAYLOAD));
private final Set<Class<?>> processedClasses;
private final ApacheValidatorFactory factory;
/**
* Create a new ValidationMappingParser instance.
* @param factory
*/
public ValidationMappingParser(ApacheValidatorFactory factory) {
this.factory = factory;
this.processedClasses = new HashSet<Class<?>>();
}
/**
* Parse files with constraint mappings and collect information in the factory.
*
* @param xmlStreams - one or more contraints.xml file streams to parse
*/
public void processMappingConfig(Set<InputStream> xmlStreams) throws ValidationException {
for (InputStream xmlStream : xmlStreams) {
ConstraintMappingsType mapping = parseXmlMappings(xmlStream);
String defaultPackage = mapping.getDefaultPackage();
processConstraintDefinitions(mapping.getConstraintDefinition(), defaultPackage);
for (BeanType bean : mapping.getBean()) {
Class<?> beanClass = loadClass(bean.getClazz(), defaultPackage);
if (!processedClasses.add(beanClass)) {
// spec: A given class must not be described more than once amongst all
// the XML mapping descriptors.
throw new ValidationException(
beanClass.getName() + " has already be configured in xml.");
}
factory.getAnnotationIgnores()
.setDefaultIgnoreAnnotation(beanClass, bean.isIgnoreAnnotations());
processClassLevel(bean.getClassType(), beanClass, defaultPackage);
processFieldLevel(bean.getField(), beanClass, defaultPackage);
processPropertyLevel(bean.getGetter(), beanClass, defaultPackage);
processedClasses.add(beanClass);
}
}
}
/** @param in XML stream to parse using the validation-mapping-1.0.xsd */
private ConstraintMappingsType parseXmlMappings(InputStream in) {
ConstraintMappingsType mappings;
try {
JAXBContext jc = JAXBContext.newInstance(ConstraintMappingsType.class);
Unmarshaller unmarshaller = jc.createUnmarshaller();
unmarshaller.setSchema(getSchema());
StreamSource stream = new StreamSource(in);
JAXBElement<ConstraintMappingsType> root =
unmarshaller.unmarshal(stream, ConstraintMappingsType.class);
mappings = root.getValue();
} catch (JAXBException e) {
throw new ValidationException("Failed to parse XML deployment descriptor file.",
e);
} finally {
IOUtils.closeQuietly(in);
}
return mappings;
}
/** @return validation-mapping-1.0.xsd based schema */
private Schema getSchema() {
return ValidationParser.getSchema(VALIDATION_MAPPING_XSD);
}
private void processClassLevel(ClassType classType, Class<?> beanClass,
String defaultPackage) {
if (classType == null) {
return;
}
// ignore annotation
if (classType.isIgnoreAnnotations() != null) {
factory.getAnnotationIgnores()
.setIgnoreAnnotationsOnClass(beanClass, classType.isIgnoreAnnotations());
}
// group sequence
Class<?>[] groupSequence =
createGroupSequence(classType.getGroupSequence(), defaultPackage);
if (groupSequence != null) {
factory.addDefaultSequence(beanClass, groupSequence);
}
// constraints
for (ConstraintType constraint : classType.getConstraint()) {
MetaConstraint<?, ?> metaConstraint =
createConstraint(constraint, beanClass, null, defaultPackage);
factory.addMetaConstraint(beanClass, metaConstraint);
}
}
@SuppressWarnings("unchecked")
private <A extends Annotation, T> MetaConstraint<?, ?> createConstraint(
ConstraintType constraint, Class<T> beanClass, Member member,
String defaultPackage) {
Class<A> annotationClass =
(Class<A>) loadClass(constraint.getAnnotation(), defaultPackage);
AnnotationProxyBuilder<A> annoBuilder = new AnnotationProxyBuilder<A>(annotationClass);
if (constraint.getMessage() != null) {
annoBuilder.setMessage(constraint.getMessage());
}
annoBuilder.setGroups(getGroups(constraint.getGroups(), defaultPackage));
annoBuilder.setPayload(getPayload(constraint.getPayload(), defaultPackage));
for (ElementType elementType : constraint.getElement()) {
String name = elementType.getName();
checkValidName(name);
Class<?> returnType = getAnnotationParameterType(annotationClass, name);
Object elementValue = getElementValue(elementType, returnType, defaultPackage);
annoBuilder.putValue(name, elementValue);
}
return new MetaConstraint<T, A>(beanClass, member, annoBuilder.createAnnotation());
}
private void checkValidName(String name) {
for (ConstraintAnnotationAttributes attr : RESERVED_PARAMS) {
if (attr.getAttributeName().equals(name)) {
throw new ValidationException(name + " is a reserved parameter name.");
}
}
}
private <A extends Annotation> Class<?> getAnnotationParameterType(
final Class<A> annotationClass, final String name) {
final Method m = doPrivileged(SecureActions.getPublicMethod(annotationClass, name));
if (m == null) {
throw new ValidationException("Annotation of type " + annotationClass.getName() +
" does not contain a parameter " + name + ".");
}
return m.getReturnType();
}
private Object getElementValue(ElementType elementType, Class<?> returnType,
String defaultPackage) {
removeEmptyContentElements(elementType);
boolean isArray = returnType.isArray();
if (!isArray) {
if (elementType.getContent().size() != 1) {
throw new ValidationException(
"Attempt to specify an array where single value is expected.");
}
return getSingleValue(elementType.getContent().get(0), returnType, defaultPackage);
} else {
List<Object> values = new ArrayList<Object>();
for (Serializable s : elementType.getContent()) {
values.add(getSingleValue(s, returnType.getComponentType(), defaultPackage));
}
return values.toArray(
(Object[]) Array.newInstance(returnType.getComponentType(), values.size()));
}
}
private void removeEmptyContentElements(ElementType elementType) {
List<Serializable> contentToDelete = new ArrayList<Serializable>();
for (Serializable content : elementType.getContent()) {
if (content instanceof String && ((String) content).matches("[\\n ].*")) {
contentToDelete.add(content);
}
}
elementType.getContent().removeAll(contentToDelete);
}
@SuppressWarnings("unchecked")
private Object getSingleValue(Serializable serializable, Class<?> returnType,
String defaultPackage) {
Object returnValue;
if (serializable instanceof String) {
String value = (String) serializable;
returnValue = convertToResultType(returnType, value, defaultPackage);
} else if (serializable instanceof JAXBElement<?> &&
((JAXBElement<?>) serializable).getDeclaredType()
.equals(String.class)) {
JAXBElement<?> elem = (JAXBElement<?>) serializable;
String value = (String) elem.getValue();
returnValue = convertToResultType(returnType, value, defaultPackage);
} else if (serializable instanceof JAXBElement<?> &&
((JAXBElement<?>) serializable).getDeclaredType()
.equals(AnnotationType.class)) {
JAXBElement<?> elem = (JAXBElement<?>) serializable;
AnnotationType annotationType = (AnnotationType) elem.getValue();
try {
Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) returnType;
returnValue =
createAnnotation(annotationType, annotationClass, defaultPackage);
} catch (ClassCastException e) {
throw new ValidationException("Unexpected parameter value");
}
} else {
throw new ValidationException("Unexpected parameter value");
}
return returnValue;
}
private Object convertToResultType(Class<?> returnType, String value,
String defaultPackage) {
/**
* Class is represented by the fully qualified class name of the class.
* spec: Note that if the raw string is unqualified,
* default package is taken into account.
*/
if (returnType.equals(Class.class)) {
value = toQualifiedClassName(value, defaultPackage);
}
/* Converter lookup */
Converter converter = ConvertUtils.lookup(returnType);
if (converter == null && returnType.isEnum()) {
converter = EnumerationConverter.getInstance();
}
if (converter != null) {
return converter.convert(returnType, value);
} else {
return converter;
}
}
private <A extends Annotation> Annotation createAnnotation(AnnotationType annotationType,
Class<A> returnType,
String defaultPackage) {
AnnotationProxyBuilder<A> metaAnnotation = new AnnotationProxyBuilder<A>(returnType);
for (ElementType elementType : annotationType.getElement()) {
String name = elementType.getName();
Class<?> parameterType = getAnnotationParameterType(returnType, name);
Object elementValue = getElementValue(elementType, parameterType, defaultPackage);
metaAnnotation.putValue(name, elementValue);
}
return metaAnnotation.createAnnotation();
}
private Class<?>[] getGroups(GroupsType groupsType, String defaultPackage) {
if (groupsType == null) {
return new Class[]{};
}
List<Class<?>> groupList = new ArrayList<Class<?>>();
for (JAXBElement<String> groupClass : groupsType.getValue()) {
groupList.add(loadClass(groupClass.getValue(), defaultPackage));
}
return groupList.toArray(new Class[groupList.size()]);
}
@SuppressWarnings("unchecked")
private Class<? extends Payload>[] getPayload(PayloadType payloadType,
String defaultPackage) {
if (payloadType == null) {
return new Class[]{};
}
List<Class<? extends Payload>> payloadList = new ArrayList<Class<? extends Payload>>();
for (JAXBElement<String> groupClass : payloadType.getValue()) {
Class<?> payload = loadClass(groupClass.getValue(), defaultPackage);
if (!Payload.class.isAssignableFrom(payload)) {
throw new ValidationException("Specified payload class " + payload.getName() +
" does not implement javax.validation.Payload");
} else {
payloadList.add((Class<? extends Payload>) payload);
}
}
return payloadList.toArray(new Class[payloadList.size()]);
}
private Class<?>[] createGroupSequence(GroupSequenceType groupSequenceType,
String defaultPackage) {
if (groupSequenceType != null) {
Class<?>[] groupSequence = new Class<?>[groupSequenceType.getValue().size()];
int i=0;
for (JAXBElement<String> groupName : groupSequenceType.getValue()) {
Class<?> group = loadClass(groupName.getValue(), defaultPackage);
groupSequence[i++] = group;
}
return groupSequence;
} else {
return null;
}
}
private void processFieldLevel(List<FieldType> fields, Class<?> beanClass,
String defaultPackage) {
List<String> fieldNames = new ArrayList<String>();
for (FieldType fieldType : fields) {
String fieldName = fieldType.getName();
if (fieldNames.contains(fieldName)) {
throw new ValidationException(fieldName +
" is defined more than once in mapping xml for bean " +
beanClass.getName());
} else {
fieldNames.add(fieldName);
}
final Field field = doPrivileged(SecureActions.getDeclaredField(beanClass, fieldName));
if (field == null) {
throw new ValidationException(
beanClass.getName() + " does not contain the fieldType " + fieldName);
}
// ignore annotations
boolean ignoreFieldAnnotation = fieldType.isIgnoreAnnotations() == null ? false :
fieldType.isIgnoreAnnotations();
if (ignoreFieldAnnotation) {
factory.getAnnotationIgnores().setIgnoreAnnotationsOnMember(field);
}
// valid
if (fieldType.getValid() != null) {
factory.addValid(beanClass, new FieldAccess(field));
}
// constraints
for (ConstraintType constraintType : fieldType.getConstraint()) {
MetaConstraint<?, ?> constraint =
createConstraint(constraintType, beanClass, field, defaultPackage);
factory.addMetaConstraint(beanClass, constraint);
}
}
}
private void processPropertyLevel(List<GetterType> getters, Class<?> beanClass,
String defaultPackage) {
List<String> getterNames = new ArrayList<String>();
for (GetterType getterType : getters) {
String getterName = getterType.getName();
if (getterNames.contains(getterName)) {
throw new ValidationException(getterName +
" is defined more than once in mapping xml for bean " +
beanClass.getName());
} else {
getterNames.add(getterName);
}
final Method method = getGetter(beanClass, getterName);
if (method == null) {
throw new ValidationException(
beanClass.getName() + " does not contain the property " + getterName);
}
// ignore annotations
boolean ignoreGetterAnnotation = getterType.isIgnoreAnnotations() == null ? false :
getterType.isIgnoreAnnotations();
if (ignoreGetterAnnotation) {
factory.getAnnotationIgnores().setIgnoreAnnotationsOnMember(method);
}
// valid
if (getterType.getValid() != null) {
factory.addValid(beanClass, new MethodAccess(getterName, method));
}
// constraints
for (ConstraintType constraintType : getterType.getConstraint()) {
MetaConstraint<?, ?> metaConstraint =
createConstraint(constraintType, beanClass, method, defaultPackage);
factory.addMetaConstraint(beanClass, metaConstraint);
}
}
}
@SuppressWarnings("unchecked")
private void processConstraintDefinitions(
List<ConstraintDefinitionType> constraintDefinitionList, String defaultPackage) {
for (ConstraintDefinitionType constraintDefinition : constraintDefinitionList) {
String annotationClassName = constraintDefinition.getAnnotation();
Class<?> clazz = loadClass(annotationClassName, defaultPackage);
if (!clazz.isAnnotation()) {
throw new ValidationException(annotationClassName + " is not an annotation");
}
Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) clazz;
ValidatedByType validatedByType = constraintDefinition.getValidatedBy();
List<Class<? extends ConstraintValidator<?, ?>>> classes =
new ArrayList<Class<? extends ConstraintValidator<?, ?>>>();
/*
If include-existing-validator is set to false,
ConstraintValidator defined on the constraint annotation are ignored.
*/
if (validatedByType.isIncludeExistingValidators() != null &&
validatedByType.isIncludeExistingValidators()) {
/*
If set to true, the list of ConstraintValidators described in XML
are concatenated to the list of ConstraintValidator described on the
annotation to form a new array of ConstraintValidator evaluated.
*/
classes.addAll(findConstraintValidatorClasses(annotationClass));
}
for (JAXBElement<String> validatorClassName : validatedByType.getValue()) {
Class<? extends ConstraintValidator<?, ?>> validatorClass;
validatorClass = (Class<? extends ConstraintValidator<?, ?>>)
loadClass(validatorClassName.getValue());
if (!ConstraintValidator.class.isAssignableFrom(validatorClass)) {
throw new ValidationException(
validatorClass + " is not a constraint validator class");
}
/*
Annotation based ConstraintValidator come before XML based
ConstraintValidator in the array. The new list is returned
by ConstraintDescriptor.getConstraintValidatorClasses().
*/
if (!classes.contains(validatorClass)) classes.add(validatorClass);
}
if (factory.getConstraintsCache().containsConstraintValidator(annotationClass)) {
throw new ValidationException("Constraint validator for " +
annotationClass.getName() + " already configured.");
} else {
factory.getConstraintsCache().putConstraintValidator(annotationClass,
classes.toArray(new Class[classes.size()]));
}
}
}
private List<Class<? extends ConstraintValidator<? extends Annotation, ?>>> findConstraintValidatorClasses(
Class<? extends Annotation> annotationType) {
List<Class<? extends ConstraintValidator<? extends Annotation, ?>>> classes =
new ArrayList<Class<? extends ConstraintValidator<? extends Annotation, ?>>>();
Class<? extends ConstraintValidator<?, ?>>[] validator =
factory.getDefaultConstraints().getValidatorClasses(annotationType);
if (validator != null) {
classes
.addAll(Arrays.asList(validator));
} else {
Class<? extends ConstraintValidator<?, ?>>[] validatedBy = annotationType
.getAnnotation(Constraint.class)
.validatedBy();
classes.addAll(Arrays.asList(validatedBy));
}
return classes;
}
private Class<?> loadClass(String className, String defaultPackage) {
return loadClass(toQualifiedClassName(className, defaultPackage));
}
private String toQualifiedClassName(String className, String defaultPackage) {
if (!isQualifiedClass(className)) {
className = defaultPackage + "." + className;
}
return className;
}
private boolean isQualifiedClass(String clazz) {
return clazz.contains(".");
}
private static <T> T doPrivileged(final PrivilegedAction<T> action) {
if (System.getSecurityManager() != null) {
return AccessController.doPrivileged(action);
} else {
return action.run();
}
}
private static Method getGetter(final Class<?> clazz, final String propertyName) {
return doPrivileged(new PrivilegedAction<Method>() {
public Method run() {
try {
final String p = StringUtils.capitalize(propertyName);
try {
return clazz.getMethod("get" + p);
} catch (NoSuchMethodException e) {
return clazz.getMethod("is" + p);
}
} catch (NoSuchMethodException e) {
return null;
}
}
});
}
private Class<?> loadClass(final String className) {
ClassLoader loader = doPrivileged(SecureActions.getContextClassLoader());
if (loader == null)
loader = getClass().getClassLoader();
try {
return Class.forName(className, true, loader);
} catch (ClassNotFoundException ex) {
throw new ValidationException("Unable to load class: " + className, ex);
}
}
}