Java tutorial
/* * Copyright 2010-2011 Duplichien, Wicksell, Springjutsu.org * * Licensed 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.springjutsu.validation; import java.beans.PropertyDescriptor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceResolvable; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.core.NamedThreadLocal; import org.springframework.validation.Errors; import org.springframework.validation.beanvalidation.CustomValidatorBean; import org.springframework.webflow.execution.RequestContextHolder; import org.springjutsu.validation.executors.RuleExecutor; import org.springjutsu.validation.executors.RuleExecutorContainer; import org.springjutsu.validation.rules.ValidationRule; import org.springjutsu.validation.rules.ValidationRulesContainer; import org.springjutsu.validation.spel.WebContextSPELResolver; import org.springjutsu.validation.util.RequestUtils; /** * Registerable as a JSR-303 @link{CustomValidatorBean}, this * ValidationManager class is instead responsible for reading * XML-driven nested validation rules. * However, it populates a standard Errors object as expected. * Logic is divided into two main portions: * First context rules are read from the <context-rules> * defined for a given path. These rules are loaded based on * the current path the user is accessing, with implementation * handled by subclasses. * Context rules are typically those rules which are specific * to a given form: the fields which are required, and also * conditional validation logic based on other fields or * variables defined in EL or otherwise. These are the * conditional per-form validation rules not handled by JSR-303 * Second, model rules are read from the <model-rules> * defined for a given class. These rules are loaded directly * from the per-class definitions provided in the XML rules. * Model rules are those rules which do not change for a * given class and include type checking, length checks, * and so on. These are the more typical JSR-303 type rules. * * @author Clark Duplichien * @author Taylor Wicksell * *@see CustomValidatorBean */ public class ValidationManager extends CustomValidatorBean { /** * Validate a lot of requests. A log is fine, too. */ protected static Log log = LogFactory.getLog(ValidationManager.class); /** * Configurable message code prefix for discovering error messages. */ private String errorMessagePrefix = "errors."; /** * Configurable message code prefix for discovering field labels. */ private String fieldLabelPrefix = ""; /** * Holds the validation rules which have been * parsed from the XML rule sets. * @see ValidationRulesContainer * @see ValidationRule */ @Autowired protected ValidationRulesContainer rulesContainer; /** * Holds the implementations of the validation * rule executors. * @see RuleExecutorContainer * @see RuleExecutor */ @Autowired protected RuleExecutorContainer ruleExecutorContainer; /** * We'll load error message definitions from * the spring message source. * @see MessageSource */ @Autowired protected MessageSource messageSource; /** * We delegate to rule container executor, * in order to see if rules have been mapped for this * class. If none have, then we don't support it. * @see #RuleExecutorContainer.supportsClass(Class) * @see #javax.validation.Validator.supports(Class) */ @Override public boolean supports(Class<?> clazz) { return rulesContainer.supportsClass(clazz); } /** * Use one per request to evaluate SPEL Expressions, * as creation is somewhat expensive. */ private static final ThreadLocal<WebContextSPELResolver> spelResolver = new NamedThreadLocal<WebContextSPELResolver>( "Validation SPEL Resolver"); /** * We perform actual validation in the order * of context rules followed by model rules. */ @Override public void validate(Object model, Errors errors) { spelResolver.set(new WebContextSPELResolver(model)); try { validateContextRules(model, errors); validateModelRules(model, errors, new ArrayList<Object>()); } finally { spelResolver.set(null); } } @Override public void validate(Object target, Errors errors, Object... validationHints) { // TODO Support Validation Hints validate(target, errors); } /** * Responsible for testing all XML-defined per-class model rules. * We will check recursively: using a BeanWrapper to get a * @link(PropertyDescriptor) for each field, and then checking to * see if any of the fields are supported by validation rules. * If so, we will test those nested paths using that class's * model rules as well. This ensures that sub beans are properly * validated using their standard model rules. * @param model the model object to validate. May be a recursed sub bean. * @param errors standard Errors object to record validation errors to. * @param checkedModels A list of model objects we have already validated, * in order to prevent unneeded or infinite recursion */ protected void validateModelRules(Object model, Errors errors, List<Object> checkedModels) { if (model == null) { return; } BeanWrapperImpl beanWrapper = new BeanWrapperImpl(model); Object validateMe = null; String beanPath = appendPath(errors.getNestedPath(), ""); // get sub bean to validate if (beanPath.isEmpty()) { validateMe = model; } else { validateMe = beanWrapper.getPropertyValue(beanPath); } //TODO: Refactor and re-enable recursion check // Infinite recursion check if (validateMe == null) { return; // } else { // checkedModels.add(validateMe.hashCode()); } List<ValidationRule> modelRules = rulesContainer.getModelRules(validateMe.getClass()); callModelRules(model, errors, modelRules); // Get fields for subbeans and iterate BeanWrapperImpl subBeanWrapper = new BeanWrapperImpl(validateMe); PropertyDescriptor[] propertyDescriptors = subBeanWrapper.getPropertyDescriptors(); for (PropertyDescriptor property : propertyDescriptors) { if (rulesContainer.supportsClass(property.getPropertyType())) { errors.pushNestedPath(property.getName()); validateModelRules(model, errors, checkedModels); errors.popNestedPath(); } else if (List.class.isAssignableFrom(property.getPropertyType()) || property.getPropertyType().isArray()) { Object potentialList = subBeanWrapper.getPropertyValue(property.getName()); List list = (List) (property.getPropertyType().isArray() && potentialList != null ? Arrays.asList(potentialList) : potentialList); if (list == null || list.isEmpty()) { continue; } else if (list.get(0) == null || !supports(list.get(0).getClass())) { continue; } for (int i = 0; i < list.size(); i++) { errors.pushNestedPath(property.getName() + "[" + i + "]"); validateModelRules(model, errors, checkedModels); errors.popNestedPath(); } } } } /** * Responsible for delegating each actual model rule * to the appropriate @link{RuleExecutor}. * Errors are recorded if no previous error has been * recorded for the given path. * @param model The object being validated * @param errors Standard errors object to record validation errors. * @param modelRules A list of ValidationRules parsed from * the <model-rules> section of the validation XML. */ protected void callModelRules(Object model, Errors errors, List<ValidationRule> modelRules) { if (modelRules == null) { return; } for (ValidationRule rule : modelRules) { // get full path to current model String fullPath = appendPath(errors.getNestedPath(), rule.getPath()); // if this field is not on the page we're checking, // or the field already has errors, skip it. //TODO: refactor this out into another method or provider class that can be made configurable boolean containedInRequestParams = false; for (Object key : RequestUtils.getRequestParameters().keySet()) { if (key instanceof String && (key.equals(fullPath) || ((String) key).replaceAll("\\(.*\\)", "").equals(fullPath))) { containedInRequestParams = true; } } if (!rule.isValidateWhenNotInRequest() && !fullPath.isEmpty() && !containedInRequestParams || errors.hasFieldErrors(rule.getPath())) { continue; } // update rule for full path ValidationRule modelRule = rule.cloneWithPath(fullPath); if (passes(modelRule, model)) { // If the rule passes and it has children, // it is a condition for nested elements. // Call children instead. if (modelRule.hasChildren()) { callModelRules(model, errors, modelRule.getRules()); } } else { // If the rule fails and it has children, // it is a condition for nested elements. // Skip nested elements. if (modelRule.hasChildren()) { continue; } else { // If the rule has no children and fails, // perform fail action. logError(modelRule, model, errors); } } } } /** * Responsible for running context rules for the current uri path. * Fetches context rules from one of two methods depending on * the request type. * @see #getWebflowContextRules(Object) * @see #getMVCContextRules(Object) * @param model Object to be validated * @param errors standard errors object for recording errors. */ protected void validateContextRules(Object model, Errors errors) { List<ValidationRule> contextRules = RequestContextHolder.getRequestContext() != null ? getWebflowContextRules(model) : getMVCContextRules(model); callContextRules(model, errors, contextRules); } /** * * Looks up context rules for an MVC controller. * Just cleans up a Servlet path URL for rule resolving by * the rules container. * Restful URL paths may be used, with \{variable} path support. * As of 0.6.1, ant paths like * and ** may also be used. */ protected List<ValidationRule> getMVCContextRules(Object model) { String requestString = RequestUtils .removeLeadingAndTrailingSlashes(RequestUtils.getRequest().getServletPath()); List<ValidationRule> contextRules = rulesContainer.getContextRules(model.getClass(), requestString); return contextRules; } /** * Gets a identifier of the current state that needs validating in * order to determine what rules to load from the validation definition. * For webflow, this is the flow ID appended with a colon, and then the * state id. * For example /accounts/account-creation:basicInformation * @return the context rules associated with this identifier. */ protected List<ValidationRule> getWebflowContextRules(Object model) { StringBuffer flowStateId = new StringBuffer(); flowStateId.append(RequestContextHolder.getRequestContext().getCurrentState().getOwner().getId()); flowStateId.append(":"); flowStateId.append(RequestContextHolder.getRequestContext().getCurrentState().getId()); String flowStateIdString = RequestUtils.removeLeadingAndTrailingSlashes(flowStateId.toString()); return rulesContainer.getContextRules(model.getClass(), flowStateIdString); } /** * Responsible for delegating each actual context rule * to the appropriate @link{RuleExecutor}. * Errors are recorded if no previous error has been * recorded for the given path. * @param model The object being validated * @param errors Standard errors object to record validation errors. * @param contextRules A list of ValidationRules parsed from * the <context-rules> section of the validation XML. */ protected void callContextRules(Object model, Errors errors, List<ValidationRule> contextRules) { if (contextRules == null || contextRules.isEmpty()) { return; } for (ValidationRule rule : contextRules) { if (errors.hasFieldErrors(rule.getPath())) { continue; } if (passes(rule, model)) { // If the rule passes and it has children, // it is a condition for nested elements. // Call children instead. if (rule.hasChildren()) { callContextRules(model, errors, rule.getRules()); } } else { // If the rule fails and it has children, // it is a condition for nested elements. // Skip nested elements. if (rule.hasChildren()) { continue; } else { // If the rule has no children and fails, // perform fail action. logError(rule, model, errors); } } } } /** * Determines if the validation rule passes * by calling the rule executor. * Delegates to extract the model and arguments from * the sub path defined on the XML rule. * @param rule The validation rule to run * @param rootModel The model to run the rule on. * @return */ protected boolean passes(ValidationRule rule, Object rootModel) { // get args Object ruleModel = getContextModel(rootModel, rule.getPath()); Object ruleArg = getContextArgument(rootModel, rule.getValue()); // call method boolean isValid; RuleExecutor executor = ruleExecutorContainer.getRuleExecutorByName(rule.getType()); try { isValid = executor.validate(ruleModel, ruleArg); } catch (Exception ve) { throw new RuntimeException("Error occured during validation: ", ve); } return isValid; } /** * Responsible for discovering the path-described model which * is to be validated by the current rule. This path may contain * EL, and if it does, we delegate to @link(#resolveEL(String, Object)) * to resolve that EL. * @param model Object to be validated * @param expression The string path expression for the model. * @return the Object to validate. */ protected Object getContextModel(Object model, String expression) { Object result = null; if (expression == null || expression.isEmpty()) { return model; } if (hasEL(expression)) { result = resolveSPEL(expression, model); } else { BeanWrapperImpl beanWrapper = new BeanWrapperImpl(model); if (model != null && beanWrapper.isReadableProperty(expression)) { result = beanWrapper.getPropertyValue(expression); } } return result; } /** * Responsible for determining the argument to be passed to the rule. * If the argument expression string contains EL, it will be resolved, * otherwise, the expression string is taken as a literal argument. * @param model Object to be validated * @param expression The string path expression for the model. * @return the Object to serve as a rule argument */ protected Object getContextArgument(Object model, String expression) { Object result = null; if (expression == null || expression.isEmpty()) { return null; } if (hasEL(expression)) { result = resolveSPEL(expression, model); } else { result = expression; } return result; } /** * In the event that a validation rule fails, this method is responsible * for recording an error message on the affected path of the Errors object. * The error message is gathered in three parts: * First the base message, if not provided is based on the rule executor class. * This is a message like "\{0} should be longer than \{1} chars." * Next, the first argument \{0} is the model descriptor. This will resolve to a * label for the path that failed, based on second to last path subBean and the * field that failed. So, if "account.accountOwner.username" had an error, * it would look for a message based on the class name of accountOwner, and the * field username: like "user.username". If the message files contained a * "user.username=User name", then the message would now read something like * "User name should be longer than \{1} chars." * Finally, the argument is resolved. * If the argument is just a flat string, like "16", then you would get * "User name should be longer than 16 chars." * If the argument contained EL that resolved on the model, it would perform * the same model lookup detailed above, so you could potentially have something * like "User name should be longer than First name", which is a bit weird, but * has its uses. * For either the model or argument lookup, if EL is used in the path * which resolves off the model, the literal value of the evaluated * EL expression is used. * @param rule the rule which failed * @param rootModel the root model (not failed bean) * @param errors standard Errors object to record error on. */ protected void logError(ValidationRule rule, Object rootModel, Errors errors) { String errorMessageKey = rule.getMessage(); if (errorMessageKey == null || errorMessageKey.isEmpty()) { errorMessageKey = errorMessagePrefix + rule.getType(); } String defaultError = rule.getPath() + " " + rule.getType(); String modelMessageKey = getMessageResolver(rootModel, rule.getPath(), true); String ruleArg = getMessageResolver(rootModel, rule.getValue(), false); MessageSourceResolvable modelMessageResolvable = new DefaultMessageSourceResolvable( new String[] { modelMessageKey }, modelMessageKey); MessageSourceResolvable argumentMessageResolvable = new DefaultMessageSourceResolvable( new String[] { ruleArg }, ruleArg); // get the local path to error, in case errors object is on nested path. String errorMessagePath = rule.getErrorPath(); if (errorMessagePath == null || errorMessagePath.isEmpty()) { errorMessagePath = rule.getPath(); } if (!errors.getNestedPath().isEmpty() && errorMessagePath.startsWith(errors.getNestedPath())) { errorMessagePath = appendPath(errorMessagePath.substring(errors.getNestedPath().length()), ""); } errors.rejectValue(errorMessagePath, errorMessageKey, new Object[] { modelMessageResolvable, argumentMessageResolvable }, defaultError); } /** * This method is responsible for getting the the String used * to resolve the message that should be recorded as the error message. * This proceeds as described in the logError message: * If EL is utilized, and the EL path resolves on the bean, use a string * like owningClassName.fieldName to resolve a message. * If EL is utilized, and the EL path does not resolve on the bean, * use the literal value of the evaluated EL. * IF EL is not utilized, and we're evaluating for the model, use the * model field path like owningClassName.fieldName to resolve message. * If EL is not utilized, and we're evaluating for the argument, use the * literal string that's passed in as the argument. * @param model The root model on which the path describes the error location. * @param rulePath The path which was given to the rule. The path that was * validated using the rule and failed. * @param resolveAsModel if true, use the behavior to resolve the model. * Otherwise, use the behavior to resolve the argument. * @return A string used to look up the message to resolve as the model * or argument of a failed validation rule, as determined by resolveAsModel. */ protected String getMessageResolver(Object model, String rulePath, boolean resolveAsModel) { // if there is no path, return. if (rulePath == null || rulePath.length() < 1) { return rulePath; } else if (hasEL(rulePath)) { // If the path is actually an expression language statement // Need to check if it resolves to a path on the model. // trim off EL denotation #{} String expressionlessValue = rulePath.substring(2, rulePath.length() - 1); // trim off any possible model prefix e.g. model.path.field if (expressionlessValue.startsWith("model.")) { expressionlessValue = expressionlessValue.substring(6); } // check if path matches a path on the model. if (new BeanWrapperImpl(model).isReadableProperty(expressionlessValue)) { // Since this matched a model path, get the label // for the resolved model. return getModelMessageKey(expressionlessValue, model); } else { // It's not a model object, so we don't need the label message key. // Instead, use the value of the expression as a label. // If the expression fails, just use the expression itself. return String.valueOf(getContextArgument(model, rulePath)); } } else { if (resolveAsModel) { // not an expression, just get the model message key. return getModelMessageKey(rulePath, model); } else { // not an expression, return literal return rulePath; } } } /** * If we're trying to resolve the message key for a path on the model, * this method will unwrap that message key. * For instance, consider our model is a Account instance, which has a * field accountOwner of type User, and that User object has a * username field of type String: * If rulePath was "accountOwner.username", then it would return a * message key of "user.username", which is the simple classname of the * owning object of the failed validation path, and the field name. * This is so we can display the label of the field that failed validation * in the error message. For instance "User Name must be 8 chars" instead * of something cryptic like "accountOwner.username must be 8 chars". * @param rulePath Validation rule path to the failed field. * @param rootModel The root model owning the field that failed. * @return A message key used to resolve a message describing the field * that failed. */ protected String getModelMessageKey(String rulePath, Object rootModel) { if (rulePath == null || rulePath.length() < 1) { return rulePath; } Object parent = null; String fieldPath = null; if (rulePath.contains(".")) { fieldPath = rulePath.substring(rulePath.lastIndexOf(".") + 1); String parentPath = rulePath.substring(0, rulePath.lastIndexOf(".")); BeanWrapperImpl beanWrapper = new BeanWrapperImpl(rootModel); parent = beanWrapper.getPropertyValue(parentPath); } else { fieldPath = rulePath; parent = rootModel; } return fieldLabelPrefix + StringUtils.uncapitalize(parent.getClass().getSimpleName()) + "." + fieldPath; } /** * @param expression A string expression * @return returns true if the expression string is EL. */ protected boolean hasEL(String expression) { return expression.matches(".*\\$\\{.+\\}.*"); } /** * Responsible for resolving a SPEL expression. * Unwraps the EL string, creates an instance of a * @link{SPELReadyRequestContext}, adds all the needed * property accessors, and runs the SPEL evaluation. * TODO: find a better way to return null if not found on any scope. * * @param el The EL expression to resolve. * @param model The model on which the EL-described field MAY lie. * @return The object described by the EL expression. */ protected Object resolveSPEL(String elContaining, Object model) { // if the whole thing is a single EL string, try to get the object. if (elContaining.matches("\\$\\{(.(?!\\$\\{))+\\}")) { String resolvableElString = elContaining.substring(2, elContaining.length() - 1) + "?: null"; Object elResult = spelResolver.get().getBySpel(resolvableElString); return elResult; } else { // otherwise, do string value substitution to build a value. String elResolvable = elContaining; Matcher matcher = Pattern.compile("\\$\\{(.(?!\\$\\{))+\\}").matcher(elResolvable); while (matcher.find()) { String elString = matcher.group(); String resolvableElString = elString.substring(2, elString.length() - 1) + "?: null"; Object elResult = spelResolver.get().getBySpel(resolvableElString); String resolvedElString = elResult != null ? String.valueOf(elResult) : ""; elResolvable = elResolvable.replace(elString, resolvedElString); matcher.reset(elResolvable); } return elResolvable; } } /** * Appends two subpath segments together and handles * period replacement appropriately. * @param path A string path. * @param suffix A string path to add to the prior path. * @return A combined path. */ protected String appendPath(String path, String suffix) { String newPath = path + (path.endsWith(".") ? "" : ".") + suffix; if (newPath.startsWith(".")) { newPath = newPath.substring(1); } if (newPath.endsWith(".")) { newPath = newPath.substring(0, newPath.length() - 1); } return newPath; } public String getErrorMessagePrefix() { return errorMessagePrefix; } public void setErrorMessagePrefix(String errorMessagePrefix) { this.errorMessagePrefix = errorMessagePrefix == null ? "" : errorMessagePrefix; } public String getFieldLabelPrefix() { return fieldLabelPrefix; } public void setFieldLabelPrefix(String fieldLabelPrefix) { this.fieldLabelPrefix = fieldLabelPrefix == null ? "" : fieldLabelPrefix; } }