View Javadoc

1   /**
2    * Copyright 2005-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.datadictionary.validation.processor;
17  
18  import org.kuali.rice.core.api.uif.DataType;
19  import org.kuali.rice.core.api.util.RiceKeyConstants;
20  import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException;
21  import org.kuali.rice.krad.datadictionary.validation.AttributeValueReader;
22  import org.kuali.rice.krad.datadictionary.validation.ValidationUtils;
23  import org.kuali.rice.krad.datadictionary.validation.ValidationUtils.Result;
24  import org.kuali.rice.krad.datadictionary.validation.capability.RangeConstrainable;
25  import org.kuali.rice.krad.datadictionary.validation.constraint.Constraint;
26  import org.kuali.rice.krad.datadictionary.validation.constraint.RangeConstraint;
27  import org.kuali.rice.krad.datadictionary.validation.result.ConstraintValidationResult;
28  import org.kuali.rice.krad.datadictionary.validation.result.DictionaryValidationResult;
29  import org.kuali.rice.krad.datadictionary.validation.result.ProcessorResult;
30  
31  import java.math.BigDecimal;
32  import java.util.Date;
33  
34  /**
35   * RangeConstraintProcessor enforces range constraints - that is, constraints that keep a number or a date within a specific range
36   *
37   * <p> An attribute
38   * that is {@link RangeConstrainable} will expose a minimum and maximum value, and these will be validated against the passed
39   * value in the code below.</p>
40   * 
41   * @author Kuali Rice Team (rice.collab@kuali.org) 
42   */
43  public class RangeConstraintProcessor extends MandatoryElementConstraintProcessor<RangeConstraint> {
44  
45  	private static final String CONSTRAINT_NAME = "range constraint";
46      private static final String MIN_EXCLUSIVE_KEY = "validation.minExclusive";
47      private static final String MAX_INCLUSIVE_KEY = "validation.maxInclusive";
48      private static final String RANGE_KEY = "validation.range";
49  
50  	/**
51  	 * @see org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor#process(org.kuali.rice.krad.datadictionary.validation.result.DictionaryValidationResult, Object, org.kuali.rice.krad.datadictionary.validation.constraint.Constraint, org.kuali.rice.krad.datadictionary.validation.AttributeValueReader)
52  	 */
53  	@Override
54  	public ProcessorResult process(DictionaryValidationResult result, Object value, RangeConstraint constraint, AttributeValueReader attributeValueReader) throws AttributeValidationException {
55  		
56  		// Since any given definition that is range constrained only expressed a single min and max, it means that there is only a single constraint to impose
57  		return new ProcessorResult(processSingleRangeConstraint(result, value, constraint, attributeValueReader));
58  	}
59  	
60  	@Override 
61  	public String getName() {
62  		return CONSTRAINT_NAME;
63  	}
64  
65  	/**
66  	 * @see org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor#getConstraintType()
67  	 */
68  	@Override
69  	public Class<? extends Constraint> getConstraintType() {
70  		return RangeConstraint.class;
71  	}
72  
73      /**
74       *  validates the value provided using {@code RangeConstraint}
75       *
76       * @param result - a holder for any already run validation results
77       * @param value - the value to validate
78       * @param constraint - the range constraint to use
79       * @param attributeValueReader - provides access to the attribute being validated
80       * @return  the passed in result, updated with the results of the processing
81       */
82  	protected ConstraintValidationResult processSingleRangeConstraint(DictionaryValidationResult result, Object value, RangeConstraint constraint, AttributeValueReader attributeValueReader) throws AttributeValidationException {
83  		// Can't process any range constraints on null values
84  		if (ValidationUtils.isNullOrEmpty(value) ||
85                  (constraint.getExclusiveMin() == null && constraint.getInclusiveMax() ==  null)){
86  			return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
87          }
88  		
89  	
90  		// This is necessary because sometimes we'll be getting a string, for example, that represents a date. 
91  		DataType dataType = constraint.getDataType();
92  		Object typedValue = value;
93  
94  		if (dataType != null) {
95  			typedValue = ValidationUtils.convertToDataType(value, dataType, dateTimeService);
96  		}
97          else if(value instanceof String){
98              //assume string is a number of type double
99              try{
100                 Double d = Double.parseDouble((String)value);
101                 typedValue = d;
102             }
103             catch(NumberFormatException n){
104                 //do nothing, typedValue is never reset
105             }
106         }
107 
108 		// TODO: decide if there is any reason why the following would be insufficient - i.e. if something numeric could still be cast to String at this point
109 		if (typedValue instanceof Date)
110 			return validateRange(result, (Date)typedValue, constraint, attributeValueReader);
111 		else if (typedValue instanceof Number)
112 			return validateRange(result, (Number)typedValue, constraint, attributeValueReader);
113 		
114 		return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
115 	}
116 
117     /**
118      * validates the date value using the range constraint provided
119      *
120      * @param result - a holder for any already run validation results
121      * @param value - the value to validate
122      * @param constraint - the range constraint to use
123      * @param attributeValueReader - provides access to the attribute being validated
124      * @return the passed in result, updated with the results of the processing
125      * @throws IllegalArgumentException
126      */
127 	protected ConstraintValidationResult validateRange(DictionaryValidationResult result, Date value, RangeConstraint constraint, AttributeValueReader attributeValueReader) throws IllegalArgumentException {	
128 
129 		Date date = value != null ? ValidationUtils.getDate(value, dateTimeService) : null;
130 
131         String inclusiveMaxText = constraint.getInclusiveMax();
132         String exclusiveMinText = constraint.getExclusiveMin();
133 
134         Date inclusiveMax = inclusiveMaxText != null ? ValidationUtils.getDate(inclusiveMaxText, dateTimeService) : null;
135         Date exclusiveMin = exclusiveMinText != null ? ValidationUtils.getDate(exclusiveMinText, dateTimeService) : null;
136         
137 		return isInRange(result, date, inclusiveMax, inclusiveMaxText, exclusiveMin, exclusiveMinText, attributeValueReader);
138 	}
139 
140     /**
141      * validates the number value using the range constraint provided
142      *
143      * @param result - a holder for any already run validation results
144      * @param value - the value to validate
145      * @param constraint - the range constraint to use
146      * @param attributeValueReader - provides access to the attribute being validated
147      * @return the passed in result, updated with the results of the processing
148      * @throws IllegalArgumentException
149      */
150 	protected ConstraintValidationResult validateRange(DictionaryValidationResult result, Number value, RangeConstraint constraint, AttributeValueReader attributeValueReader) throws IllegalArgumentException {
151 
152 
153 		// TODO: JLR - need a code review of the conversions below to make sure this is the best way to ensure accuracy across all numerics
154         // This will throw NumberFormatException if the value is 'NaN' or infinity... probably shouldn't be a NFE but something more intelligible at a higher level
155         BigDecimal number = value != null ? new BigDecimal(value.toString()) : null;
156 
157         String inclusiveMaxText = constraint.getInclusiveMax();
158         String exclusiveMinText = constraint.getExclusiveMin();
159         
160         BigDecimal inclusiveMax = inclusiveMaxText != null ? new BigDecimal(inclusiveMaxText) : null;
161         BigDecimal exclusiveMin = exclusiveMinText != null ? new BigDecimal(exclusiveMinText) : null;
162         
163 		return isInRange(result, number, inclusiveMax, inclusiveMaxText, exclusiveMin, exclusiveMinText, attributeValueReader);
164 	}
165 
166     /**
167      * checks whether the value provided is in the range specified by inclusiveMax and exclusiveMin
168      *
169      * @param - a holder for any already run validation results
170      * @param value - the value to check
171      * @param inclusiveMax - the maximum value of the attribute
172      * @param inclusiveMaxText - the string representation of inclusiveMax
173      * @param exclusiveMin -  the minimum value of the attribute
174      * @param exclusiveMinText - the string representation of exclusiveMin
175      * @param attributeValueReader - provides access to the attribute being validated
176      * @return the passed in result, updated with the results of the range check
177      */
178 	private <T> ConstraintValidationResult isInRange(DictionaryValidationResult result, T value, Comparable<T> inclusiveMax, String inclusiveMaxText, Comparable<T> exclusiveMin, String exclusiveMinText, AttributeValueReader attributeValueReader) {
179         // What we want to know is that the maximum value is greater than or equal to the number passed (the number can be equal to the max, i.e. it's 'inclusive')
180         Result lessThanMax = ValidationUtils.isLessThanOrEqual(value, inclusiveMax); 
181         // On the other hand, since the minimum is exclusive, we just want to make sure it's less than the number (the number can't be equal to the min, i.e. it's 'exclusive')
182         Result greaterThanMin = ValidationUtils.isGreaterThan(value, exclusiveMin); 
183           
184         // It's okay for one end of the range to be undefined - that's not an error. It's only an error if one of them is actually invalid. 
185         if (lessThanMax != Result.INVALID && greaterThanMin != Result.INVALID) { 
186         	// Of course, if they're both undefined then we didn't actually have a real constraint
187         	if (lessThanMax == Result.UNDEFINED && greaterThanMin == Result.UNDEFINED)
188         		return result.addNoConstraint(attributeValueReader, CONSTRAINT_NAME);
189         	
190         	// In this case, we've succeeded
191         	return result.addSuccess(attributeValueReader, CONSTRAINT_NAME);
192         }
193         
194         // If both comparisons happened then if either comparison failed we can show the end user the expected range on both sides.
195         if (lessThanMax != Result.UNDEFINED && greaterThanMin != Result.UNDEFINED) 
196         	return result.addError(RANGE_KEY, attributeValueReader, CONSTRAINT_NAME, RiceKeyConstants.ERROR_OUT_OF_RANGE, exclusiveMinText, inclusiveMaxText);
197         // If it's the max comparison that fails, then just tell the end user what the max can be
198         else if (lessThanMax == Result.INVALID)
199         	return result.addError(MAX_INCLUSIVE_KEY, attributeValueReader, CONSTRAINT_NAME, RiceKeyConstants.ERROR_INCLUSIVE_MAX, inclusiveMaxText);
200         // Otherwise, just tell them what the min can be
201         else 
202         	return result.addError(MIN_EXCLUSIVE_KEY, attributeValueReader, CONSTRAINT_NAME, RiceKeyConstants.ERROR_EXCLUSIVE_MIN, exclusiveMinText);
203 	}
204 	
205 }