All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springjutsu.validation.ValidationEvaluationContext Maven / Gradle / Ivy

/*
 * Copyright 2010-2013 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.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Pattern;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.TypeConverter;
import org.springframework.validation.Errors;
import org.springjutsu.validation.exceptions.CircularValidationTemplateReferenceException;
import org.springjutsu.validation.exceptions.IllegalTemplateReferenceException;
import org.springjutsu.validation.rules.ValidationRule;
import org.springjutsu.validation.rules.ValidationTemplate;
import org.springjutsu.validation.rules.ValidationTemplateReference;
import org.springjutsu.validation.spel.CurrentModelPropertyAccessor;
import org.springjutsu.validation.spel.SPELResolver;
import org.springjutsu.validation.util.PathUtils;

/**
 * The ValidationEvaluationContext is responsible for tracking
 * contextual information about "where" the validation process is
 * in terms of the rules being evaluated, and nested paths during
 * model bean traversal.
 * It provides access to model objects relative to the current
 * paths being validated, and also is supplied with the current 
 * SPEL resolver initialized for any current validation context(s).
 * Also provides details of the current paths being evaluated, 
 * for purposes of logging and error message resolution. 
 * @author Clark Duplichien
 *
 */
public class ValidationEvaluationContext {
	
	/**
	 * The errors object on which validation errors will be recorded.
	 */
	private Errors errors;
	
	/**
	 * A bean wrapper wrapping the base model object under validation.
	 */
	private BeanWrapper modelWrapper;
	
	/**
	 * The SPEL resolver, with named contexts initialized by 
	 * any active ValidationContextHandler instances. 
	 */
	private SPELResolver spelResolver;
	
	/**
	 * The deep nested path to the current model being evaluated,
	 * rooted from the base model object under evaluation
	 */
	private Stack nestedPath;
	
	/**
	 * A stack of active validation templates in the order they
	 * were activated. Also used to detect infinite recursion.
	 */
	private Stack templateNames;
	
	/**
	 * A nested path from the base model object including all
	 * path segments provided by validation template base paths.
	 */
	private Stack templateBasePaths;
	
	/**
	 * Maps path segments indicating collections to 
	 * path segment replacements indicating the current collection
	 * index being evaluated. In other words, keeps track of the current
	 * position within nested collections. 
	 */
	private Map collectionPathReplacements;
	
	/**
	 * The active JSR-303 validation groups. 
	 * Spring calls them validation hints.
	 * Sounds more mysterious.
	 * It's like we're looking for clues, man.
	 */
	private String[] validationHints;
	
	/**
	 * Checked model hashes prevent infinite recursion.
	 * As the recursion stack grows, each list of checked model hashes
	 * is inherited from the prior state in the stack.
	 * This allows the same bean to be validated on different nested 
	 * path structures, but will prevent infinite recursive validation
	 * of the same bean on the same nested path structure in the event
	 * of a cyclic datamodel, as bi-direction relationships are not 
	 * uncommon within JPA bean models. 
	 * This collection must still handle hashes when not recursed, and
	 * as such will always be one push deeper than the nested path stack.
	 */
	private Stack> checkedModelHashes;
	
	/**
	 * Constructs a new ValidationEvaluationContext.
	 * There will be a single ValidationEvalautionContext created
	 * for each model validation performed.  
	 * @param model The object to validate
	 * @param errors The errors object on which to record errors
	 * @param validationHints Any JSR-303 validation groups to activate
	 */
	public ValidationEvaluationContext(Object model, Errors errors, TypeConverter typeConverter, Object... validationHints) {
		this.modelWrapper = model == null ? null : new BeanWrapperImpl(model);
		this.errors = errors;
		this.validationHints = new String[validationHints.length];
		for (int i = 0; i < validationHints.length; i++) {
			this.validationHints[i] = 
				(validationHints[i] instanceof Class) ? 
					((Class) validationHints[i]).getCanonicalName() :
						String.valueOf(validationHints[i]);
		}
		this.spelResolver = new SPELResolver(model, typeConverter);
		this.spelResolver.getScopedContext().addPropertyAccessor(new CurrentModelPropertyAccessor());
		this.spelResolver.getScopedContext().addContext("currentModel", this.new CurrentModelAccessor());
		this.nestedPath = new Stack();
		this.checkedModelHashes = new Stack>();
		this.checkedModelHashes.push(new ArrayList());
		this.templateNames = new Stack();
		this.templateBasePaths = new Stack();
		this.collectionPathReplacements = new LinkedHashMap();
	}
	
	/**
	 * @return the object described by the current nested path,
	 * which is built during recursive model validation.
	 * This does not include any active template base paths.
	 */
	public Object getBeanAtNestedPath() {
		String joinedPath = PathUtils.joinPathSegments(nestedPath);
		return joinedPath.isEmpty() ? getRootModel() : modelWrapper.getPropertyValue(joinedPath);
	}
	
	/**
	 * @param bean The bean to check for repeated validation
	 * @return true if the given bean has already been validated.
	 */
	protected boolean previouslyValidated(Object bean) {
		return checkedModelHashes.peek().contains(bean.hashCode());
	}
	
	/**
	 * Marks the given bean as having already been validated,
	 * to avoid infinite recursion during recursive sub bean validation.
	 * @param bean the bean to mark validated
	 */
	protected void markValidated(Object bean) {
		checkedModelHashes.peek().add(bean.hashCode());
	}
	
	/**
	 * @return the base bean being validated
	 */
	public Object getRootModel() {
		return modelWrapper == null ? null : modelWrapper.getWrappedInstance();
	}
	
	/**
	 * @return the object currently under evaluation based
	 * on any nested and/or template paths. 
	 */
	public Object getCurrentModel() {
		String currentPath = getCurrentNestedPath();
		return currentPath.isEmpty() ? getRootModel() : modelWrapper.getPropertyValue(currentPath);
	}
	
	/**
	 * 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))
	 * to resolve that EL.
	 * @param rule The rule for which to resolve the model
	 * @return the resolved rule model
	 */
	protected Object resolveRuleModel(ValidationRule rule) {
		Object result = null;
		if (rule.getPath() == null || rule.getPath().isEmpty()) {
			return getRootModel();
		}
		// TODO / Note to self: the expression is actually the rule path,
		// which at this point has already been localized by the nested path
		// via rule cloning, so long as the rule path didn't contain EL
		if (PathUtils.containsEL(rule.getPath())) {
			result = spelResolver.resolveSPELString(rule.getPath());
		} else {
			BeanWrapperImpl beanWrapper = new BeanWrapperImpl(getRootModel());
			String localizedRulePath = localizePath(rule.getPath());
			// TODO: Why is this check here?
			// Under what circumstances did we want this to return null
			// instead of throwing an exception?
			if (beanWrapper.isReadableProperty(localizedRulePath)) {
				result = beanWrapper.getPropertyValue(localizedRulePath);
			}
		}
		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 rule the rule for which to resolve argument
	 * @param expression The string path expression for the model.
	 * @return the Object to serve as a rule argument
	 */
	protected Object resolveRuleArgument(ValidationRule rule) {
		Object result = null;
		if (rule.getValue() == null || rule.getValue().isEmpty()) {
			return null;
		}
		if (PathUtils.containsEL(rule.getValue())) {
			result = spelResolver.resolveSPELString(rule.getValue());
		} else {
			result = rule.getValue();
		}
		return result;
	}
	
	/**
	 * Used during recursive sub-bean validation to indicate
	 * that the validation process is moving to a sub bean path.
	 * @param subPath the field name of a sub bean which will be validated next
	 * @return the object at the pushed nested path.
	 */
	protected Object pushNestedPath(String subPath) {
		nestedPath.push(subPath);
		checkedModelHashes.push(new ArrayList(checkedModelHashes.peek()));
		return getBeanAtNestedPath();
	}
	
	/**
	 * Called after validating all fields on a sub-bean, this method
	 * removes the sub bean's field and returns the validation context
	 * to its owning object.
	 */
	protected void popNestedPath() {
		nestedPath.pop();
		checkedModelHashes.pop();
	}
	
	/**
	 * Pushes a validation template onto the validation template stack,
	 * after ensuring that the given validation template is not already active
	 * in order to prevent infinite recursion of nested validation templates
	 * @param templateReference the validation template reference object which indicates
	 * what nested path the validation template applies to
	 * @param actualTemplate the validation template referenced by the validation template reference
	 */
	protected void pushTemplate(ValidationTemplateReference templateReference, ValidationTemplate actualTemplate) {
		if (templateNames.contains(templateReference.getTemplateName())) {
			throw new CircularValidationTemplateReferenceException(
				"Circular use of validation template named " + templateReference.getTemplateName());
		}
		String localizedTemplatePath = localizePath(templateReference.getBasePath());
		Class templateTargetClass = PathUtils.getClassForPath(modelWrapper.getWrappedClass(), localizedTemplatePath, true);
		if (!actualTemplate.getApplicableEntityClass().isAssignableFrom(templateTargetClass)) {
			throw new IllegalTemplateReferenceException(
				"Template named " + actualTemplate.getName() + 
				" expects class " + actualTemplate.getApplicableEntityClass() +
				" but got instance of " + templateTargetClass);
		}
		
				
		templateNames.push(templateReference.getTemplateName());
		templateBasePaths.push(templateReference.getBasePath());
	}
	
	/**
	 * Removes the last validation template reference from 
	 * the validation template and template reference stacks.
	 */
	protected void popTemplate() {
		templateNames.pop();
		templateBasePaths.pop();
	}
	
	/**
	 * Performs the following operations to localize a sub path
	 * (e.g. rule path) to the current context:
	 * 1) prepends with template base paths
	 * 2) prepends resultant path with nestedPath
	 * 3) applies collection replacements 
	 * @param subPath the path to localize
	 * @return currently localizedPath
	 */
	protected String localizePath(String subPath) {
		if (PathUtils.containsEL(subPath)) {
			return subPath;
		}
		String localizedPath = PathUtils.appendPath(
				PathUtils.joinPathSegments(nestedPath),
				PathUtils.joinPathSegments(templateBasePaths), 
				subPath);
		// Apply collection path replacements.
		// Multiple collection paths may build off of one another,
		// so it is important to run all possible path replacements.
		// Path replacement order is maintained by the use of a LinkedHashMap
		for (Map.Entry collectionPathReplacement : collectionPathReplacements.entrySet()) {
			if (localizedPath.startsWith(collectionPathReplacement.getKey())) {
				localizedPath = localizedPath.replaceAll(
					"^" + Pattern.quote(collectionPathReplacement.getKey()),
					collectionPathReplacement.getValue());
			}
		}
		return localizedPath;
	}
	
	/**
	 * @return the current nested path including any 
	 * pushed nested paths from recursive sub bean validation,
	 * any active validation template reference base paths,
	 * and any collection path replacements from collection iteration.
	 */
	public String getCurrentNestedPath() {
		return localizePath("");
	}
	
	/**
	 * @return the string representations of the qualifiers
	 * for any active JSR-303 validation groups
	 */
	public String[] getValidationHints() {
		return validationHints;
	}

	/**
	 * @return the Errors object on which validation errors
	 * will be recorded
	 */
	public Errors getErrors() {
		return errors;
	}

	/**
	 * @return the bean wrapper wrapping the base bean
	 * under validation
	 */
	public BeanWrapper getModelWrapper() {
		return modelWrapper;
	}

	/**
	 * @return the current SPELResolver initialized
	 * by any active ValidationContextHandler instances
	 */
	public SPELResolver getSpelResolver() {
		return spelResolver;
	}

	/**
	 * @return the current stack of nested paths pushed
	 * by recursive sub bean validation.
	 */
	protected Stack getNestedPath() {
		return nestedPath;
	}	

	/**
	 * @return the hash codes of model beans already validated
	 */
	protected Stack> getCheckedModelHashes() {
		return checkedModelHashes;
	}
	
	/**
	 * @return the names of active validation templates
	 */
	protected Stack getTemplateNames() {
		return templateNames;
	}

	/**
	 * @return the nested stack of active validation template base paths
	 */
	protected Stack getTemplateBasePaths() {
		return templateBasePaths;
	}
	
	/**
	 * @return the collection path replacements indicating for each 
	 * nested collection path the indexed collection path for the current
	 * iteration of the validated collection.
	 */
	protected Map getCollectionPathReplacements() {
		return collectionPathReplacements;
	}
	
	/**
	 * Used by @see CurrentModelPropertyAccessor to gain access
	 * to the current model under validation, without exposing
	 * the entirety of the current ValidationEvaluationContext
	 * to SPEL expressions. 
	 * @author Clark Duplichien
	 *
	 */
	public class CurrentModelAccessor {
		
		public Object accessCurrentModel() {
			return getCurrentModel();
		}
		
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy