/*
* 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.
*/
/*
* Created on 8.12.2011
*
* Copyright (c) 2011 Et netera, a.s. All rights reserved.
* Intended for internal use only.
* http://www.etnetera.cz
*/
package net.formio.validation;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.MessageInterpolator;
import javax.validation.Path;
import javax.validation.Path.Node;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import net.formio.BasicListFormMapping;
import net.formio.Config;
import net.formio.FormElement;
import net.formio.FormMapping;
import net.formio.binding.BeanExtractor;
import net.formio.binding.HumanReadableType;
import net.formio.binding.ParseError;
import net.formio.internal.FormUtils;
import net.formio.upload.MaxRequestSizeExceededError;
/**
* Object validation using {@link ValidatorFactory} (bean validation API).
*
* @author Radek Beran
*/
public class DefaultBeanValidator implements BeanValidator {
private final ValidatorFactory validatorFactory;
private final BeanExtractor beanExtractor;
private final String messageBundleName;
public DefaultBeanValidator(ValidatorFactory validatorFactory, BeanExtractor beanExtractor, String messageBundleName) {
if (validatorFactory == null) throw new IllegalArgumentException("validatorFactory cannot be null");
if (beanExtractor == null) throw new IllegalArgumentException("beanExtractor cannot be null");
if (messageBundleName == null || messageBundleName.isEmpty()) throw new IllegalArgumentException("messageBundleName cannot be null or empty");
this.validatorFactory = validatorFactory;
this.beanExtractor = beanExtractor;
this.messageBundleName = messageBundleName;
}
public DefaultBeanValidator(ValidatorFactory validatorFactory, BeanExtractor beanExtractor) {
this(validatorFactory, beanExtractor, ResBundleMessageInterpolator.DEFAULT_VALIDATION_MESSAGES);
}
@Override
public <T> ValidationResult validate(
T mappingBoundValue,
String propPrefix,
FormMapping<T> mapping,
List<? extends InterpolatedMessage> customMessages,
Locale locale,
Class<?>... groups) {
if (mappingBoundValue == null) {
throw new IllegalArgumentException("Validated object cannot be null");
}
MessageInterpolator msgInterpolator = createMessageInterpolator(this.validatorFactory, this.messageBundleName, locale);
Validator beanValidator = createValidator(this.validatorFactory, msgInterpolator);
// Unfortunately, implementation of bean validation API can return violations
// in nondeterministic order as a HashSet (Hibernate validator)
final Set<ConstraintViolation<T>> violations = beanValidator.validate(mappingBoundValue, groups);
final List<ConstraintViolation<T>> violationsList = new ArrayList<ConstraintViolation<T>>(violations);
Collections.sort(violationsList, constraintViolationComparator);
List<InterpolatedMessage> allCustomMessages = new ArrayList<InterpolatedMessage>();
allCustomMessages.addAll(customMessages);
String pathSep = null;
if (mapping != null && !(mapping instanceof BasicListFormMapping<?>) && mapping.isVisible() && mapping.isEnabled()) {
pathSep = mapping.getConfig().getPathSeparator();
// Validate all nested elements
Map<String, Object> beanProperties = null;
for (FormElement<?> el : mapping.getElements()) {
if (el.getValidators() != null && !el.getValidators().isEmpty()) { // to avoid unnecessary visible/enabled checks
if (!(el instanceof BasicListFormMapping<?>) && el.isVisible() && el.isEnabled()) {
if (beanProperties == null) {
beanProperties = beanExtractor.extractBean(mappingBoundValue, gatherPropertyNames(mapping.getElements()));
}
Object elementValue = beanProperties.get(el.getPropertyName());
allCustomMessages.addAll(validateFormElement((FormElement<Object>)el, elementValue));
}
}
}
if (mapping.isRootMapping()) {
// validate also the root mapping (run global validators added to the root mapping itself)
for (net.formio.validation.Validator<T> validator : mapping.getValidators()) {
allCustomMessages.addAll(validator.validate(
new ValidationContext<T>(mapping.getName(), mappingBoundValue)));
}
}
} else {
pathSep = Config.DEFAULT_PATH_SEP;
}
return buildReport(msgInterpolator, violationsList, allCustomMessages, propPrefix, pathSep, locale);
}
@Override
public <T> ValidationResult validate(T inst,
String propPrefix,
List<? extends InterpolatedMessage> customMessages,
Locale locale,
Class<?>... groups) {
return validate(inst, propPrefix, (FormMapping<T>)null, customMessages, locale, groups);
}
@Override
public <T> ValidationResult validate(T inst, Locale locale, Class<?> ... groups) {
return this.validate(inst, (String)null, Collections.<InterpolatedMessage>emptyList(), locale, groups);
}
@Override
public <T> ValidationResult validate(T inst, Class<?> ... groups) {
return this.validate(inst, Locale.getDefault(), groups);
}
/**
* Returns message interpolator used in validation.
* Can be overriden in subclasses.
* @param validatorFactory
* @param locale
* @return
*/
protected MessageInterpolator createMessageInterpolator(ValidatorFactory validatorFactory, String messageBundleName, Locale locale) {
return new ResBundleMessageInterpolator(new PlatformResBundleLocator(messageBundleName), locale, true);
}
/**
* Translates given message by given created message interpolator, using given parameters
* and locale.
* @param msgInterpolator
* @param message
* @param parameters
* @param locale
* @return
*/
protected String interpolateMessage(MessageInterpolator msgInterpolator, String message, Map<String, Serializable> parameters, Locale locale) {
return ((ResBundleMessageInterpolator)msgInterpolator).interpolateMessage(message, parameters, locale);
}
protected Validator createValidator(ValidatorFactory validatorFactory, MessageInterpolator msgInterpolator) {
// for using specified locale
return validatorFactory
.usingContext()
.messageInterpolator(msgInterpolator)
.getValidator();
}
protected void processInterpolatedMessages(
MessageInterpolator msgInterpolator,
List<? extends InterpolatedMessage> interpolatedMessages,
String propPrefix,
String pathSep,
Map<String, List<ConstraintViolationMessage>> fieldMessages,
List<ConstraintViolationMessage> globalMessages,
Locale locale) {
for (InterpolatedMessage im : interpolatedMessages) {
if (im != null) {
if (im instanceof MaxRequestSizeExceededError) {
if (ValidationUtils.isTopLevelMapping(propPrefix, pathSep)) {
globalMessages.add(createConstraintViolationMessage(im, msgInterpolator, locale));
}
} else if (im instanceof ParseError) {
ParseError parseMsg = (ParseError)im;
String formPrefixedPropName = pathPrefixedName(propPrefix, parseMsg.getPropertyName(), pathSep);
List<ConstraintViolationMessage> msgs = getOrCreateFieldMessages(fieldMessages, formPrefixedPropName);
msgs.add(createConstraintViolationMessage(im, msgInterpolator, locale));
fieldMessages.put(formPrefixedPropName, msgs);
} else if (im.getElementName() != null && !im.getElementName().isEmpty()) {
// Also MaxFileSizeExceededError is processed here
List<ConstraintViolationMessage> msgs = getOrCreateFieldMessages(fieldMessages, im.getElementName());
msgs.add(createConstraintViolationMessage(im, msgInterpolator, locale));
fieldMessages.put(im.getElementName(), msgs);
} else {
globalMessages.add(createConstraintViolationMessage(im, msgInterpolator, locale));
}
}
}
}
private <T, U> List<InterpolatedMessage> validateFormElement(FormElement<T> element, T elementValue) {
List<InterpolatedMessage> messages = new ArrayList<InterpolatedMessage>();
for (net.formio.validation.Validator<T> validator : element.getValidators()) {
messages.addAll(validator.validate(new ValidationContext<T>(element.getName(), elementValue)));
}
return messages;
}
private ConstraintViolationMessage createConstraintViolationMessage(
InterpolatedMessage message, MessageInterpolator msgInterpolator, Locale locale) {
return new ConstraintViolationMessage(message.getSeverity(),
interpolateMessage(msgInterpolator, message, locale),
ValidationUtils.removeBraces(message.getMessageKey()),
message.getMessageParameters());
}
private String interpolateMessage(MessageInterpolator msgInterpolator, InterpolatedMessage msg, Locale locale) {
Map<String, Serializable> params = new LinkedHashMap<String, Serializable>();
params.putAll(msg.getMessageParameters());
if (msg instanceof ParseError) {
ParseError parseError = (ParseError)msg;
params.put("humanReadableTargetType", interpolateMessage(msgInterpolator,
humanReadableTypeToMsgTpl(parseError.getHumanReadableTargetType()),
Collections.<String, Serializable>emptyMap(), locale));
}
return ValidationUtils.removeBraces(interpolateMessage(msgInterpolator, msg.getMessageKey(), params, locale));
}
private String humanReadableTypeToMsgTpl(HumanReadableType hrt) {
return "{type." + hrt.name() + "}";
}
private List<ConstraintViolationMessage> getOrCreateFieldMessages(Map<String, List<ConstraintViolationMessage>> fieldMsgs, String fieldName) {
List<ConstraintViolationMessage> msgs = fieldMsgs.get(fieldName);
if (msgs == null) {
msgs = new ArrayList<ConstraintViolationMessage>();
}
return msgs;
}
private <T> ValidationResult buildReport(
MessageInterpolator msgInterpolator,
List<ConstraintViolation<T>> violations,
List<? extends InterpolatedMessage> customMessages,
String propPrefix,
String pathSep,
Locale locale) {
Map<String, List<ConstraintViolationMessage>> fieldMessages = new LinkedHashMap<String, List<ConstraintViolationMessage>>();
List<ConstraintViolationMessage> globalMessages = new ArrayList<ConstraintViolationMessage>();
// processing parse errors and request processing errors and other custom errors (in addition to bean validation API violations)
processInterpolatedMessages(msgInterpolator, customMessages, propPrefix, pathSep, fieldMessages, globalMessages, locale);
for (ConstraintViolation<T> v : violations) {
// Needed data should be taken from javax.ConstraintViolation,
// ConstraintViolationMessage should be javax.validation API independent
String formElementName = constructFormElementName(propPrefix, v, pathSep);
ConstraintViolationMessage msg = new ConstraintViolationMessage(v);
if (formElementName.length() == 0 || !formElementName.contains(pathSep)) {
globalMessages.add(msg);
} else {
appendFieldMsg(fieldMessages, formElementName, msg);
}
}
return new ValidationResult(fieldMessages, globalMessages);
}
private <T> String constructFormElementName(String propPrefix, ConstraintViolation<T> v, String pathSep) {
Path path = v.getPropertyPath();
StringBuilder nodePath = new StringBuilder();
if (propPrefix != null) {
nodePath.append(propPrefix);
}
for (Node node : path) {
if (node.getName() != null) {
if (nodePath.length() > 0) {
nodePath.append(pathSep);
}
nodePath.append(node.getName());
}
}
return FormUtils.removeTrailingBrackets(nodePath.toString());
}
private void appendFieldMsg(Map<String, List<ConstraintViolationMessage>> fieldMessages, String fieldName, ConstraintViolationMessage msg) {
if (fieldMessages.containsKey(fieldName)) {
fieldMessages.get(fieldName).add(msg);
} else {
List<ConstraintViolationMessage> msgs = new ArrayList<ConstraintViolationMessage>();
msgs.add(msg);
fieldMessages.put(fieldName, msgs);
}
}
private String pathPrefixedName(String pathPrefix, String name, String pathSep) {
if (name == null) return null;
if (pathPrefix == null || pathPrefix.isEmpty()) return name;
return pathPrefix + pathSep + name;
}
private Set<String> gatherPropertyNames(List<FormElement<?>> elements) {
Set<String> propertyNames = new LinkedHashSet<String>();
for (FormElement<?> el : elements) {
propertyNames.add(el.getPropertyName());
}
return propertyNames;
}
private static final ConstraintViolationComparator constraintViolationComparator = new ConstraintViolationComparator();
}