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   * This class enforces range constraints - that is, constraints that keep a number or a date within a specific range. An attribute 
36   * that is {@link RangeConstrainable} will expose a minimum and maximum value, and these will be validated against the passed
37   * value in the code below. 
38   * 
39   * @author Kuali Rice Team (rice.collab@kuali.org) 
40   */
41  public class RangeConstraintProcessor extends MandatoryElementConstraintProcessor<RangeConstraint> {
42  
43  	private static final String CONSTRAINT_NAME = "range constraint";
44      private static final String MIN_EXCLUSIVE_KEY = "validation.minExclusive";
45      private static final String MAX_INCLUSIVE_KEY = "validation.maxInclusive";
46      private static final String RANGE_KEY = "validation.range";
47  
48  	/**
49  	 * @see org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor#process(DictionaryValidationResult, Object, org.kuali.rice.krad.datadictionary.validation.capability.Validatable, org.kuali.rice.krad.datadictionary.validation.AttributeValueReader)
50  	 */
51  	@Override
52  	public ProcessorResult process(DictionaryValidationResult result, Object value, RangeConstraint constraint, AttributeValueReader attributeValueReader) throws AttributeValidationException {
53  		
54  		// 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
55  		return new ProcessorResult(processSingleRangeConstraint(result, value, constraint, attributeValueReader));
56  	}
57  	
58  	@Override 
59  	public String getName() {
60  		return CONSTRAINT_NAME;
61  	}
62  
63  	/**
64  	 * @see org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor#getConstraintType()
65  	 */
66  	@Override
67  	public Class<? extends Constraint> getConstraintType() {
68  		return RangeConstraint.class;
69  	}
70  	
71  	protected ConstraintValidationResult processSingleRangeConstraint(DictionaryValidationResult result, Object value, RangeConstraint constraint, AttributeValueReader attributeValueReader) throws AttributeValidationException {
72  		// Can't process any range constraints on null values
73  		if (ValidationUtils.isNullOrEmpty(value) ||
74                  (constraint.getExclusiveMin() == null && constraint.getInclusiveMax() ==  null)){
75  			return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
76          }
77  		
78  	
79  		// This is necessary because sometimes we'll be getting a string, for example, that represents a date. 
80  		DataType dataType = constraint.getDataType();
81  		Object typedValue = value;
82  
83  		if (dataType != null) {
84  			typedValue = ValidationUtils.convertToDataType(value, dataType, dateTimeService);
85  		}
86          else if(value instanceof String){
87              //assume string is a number of type double
88              try{
89                  Double d = Double.parseDouble((String)value);
90                  typedValue = d;
91              }
92              catch(NumberFormatException n){
93                  //do nothing, typedValue is never reset
94              }
95          }
96  
97  		// 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
98  		if (typedValue instanceof Date)
99  			return validateRange(result, (Date)typedValue, constraint, attributeValueReader);
100 		else if (typedValue instanceof Number)
101 			return validateRange(result, (Number)typedValue, constraint, attributeValueReader);
102 		
103 		return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
104 	}
105 	
106 	protected ConstraintValidationResult validateRange(DictionaryValidationResult result, Date value, RangeConstraint constraint, AttributeValueReader attributeValueReader) throws IllegalArgumentException {	
107 
108 		Date date = value != null ? ValidationUtils.getDate(value, dateTimeService) : null;
109 
110         String inclusiveMaxText = constraint.getInclusiveMax();
111         String exclusiveMinText = constraint.getExclusiveMin();
112 
113         Date inclusiveMax = inclusiveMaxText != null ? ValidationUtils.getDate(inclusiveMaxText, dateTimeService) : null;
114         Date exclusiveMin = exclusiveMinText != null ? ValidationUtils.getDate(exclusiveMinText, dateTimeService) : null;
115         
116 		return isInRange(result, date, inclusiveMax, inclusiveMaxText, exclusiveMin, exclusiveMinText, attributeValueReader);
117 	}
118 	
119 	protected ConstraintValidationResult validateRange(DictionaryValidationResult result, Number value, RangeConstraint constraint, AttributeValueReader attributeValueReader) throws IllegalArgumentException {
120 
121 		// TODO: JLR - need a code review of the conversions below to make sure this is the best way to ensure accuracy across all numerics
122         // 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
123         BigDecimal number = value != null ? new BigDecimal(value.toString()) : null;
124 
125         String inclusiveMaxText = constraint.getInclusiveMax();
126         String exclusiveMinText = constraint.getExclusiveMin();
127         
128         BigDecimal inclusiveMax = inclusiveMaxText != null ? new BigDecimal(inclusiveMaxText) : null;
129         BigDecimal exclusiveMin = exclusiveMinText != null ? new BigDecimal(exclusiveMinText) : null;
130         
131 		return isInRange(result, number, inclusiveMax, inclusiveMaxText, exclusiveMin, exclusiveMinText, attributeValueReader);
132 	}
133 
134 	private <T> ConstraintValidationResult isInRange(DictionaryValidationResult result, T value, Comparable<T> inclusiveMax, String inclusiveMaxText, Comparable<T> exclusiveMin, String exclusiveMinText, AttributeValueReader attributeValueReader) {
135         // 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')
136         Result lessThanMax = ValidationUtils.isLessThanOrEqual(value, inclusiveMax); 
137         // 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')
138         Result greaterThanMin = ValidationUtils.isGreaterThan(value, exclusiveMin); 
139           
140         // 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. 
141         if (lessThanMax != Result.INVALID && greaterThanMin != Result.INVALID) { 
142         	// Of course, if they're both undefined then we didn't actually have a real constraint
143         	if (lessThanMax == Result.UNDEFINED && greaterThanMin == Result.UNDEFINED)
144         		return result.addNoConstraint(attributeValueReader, CONSTRAINT_NAME);
145         	
146         	// In this case, we've succeeded
147         	return result.addSuccess(attributeValueReader, CONSTRAINT_NAME);
148         }
149         
150         // If both comparisons happened then if either comparison failed we can show the end user the expected range on both sides.
151         if (lessThanMax != Result.UNDEFINED && greaterThanMin != Result.UNDEFINED) 
152         	return result.addError(RANGE_KEY, attributeValueReader, CONSTRAINT_NAME, RiceKeyConstants.ERROR_OUT_OF_RANGE, exclusiveMinText, inclusiveMaxText);
153         // If it's the max comparison that fails, then just tell the end user what the max can be
154         else if (lessThanMax == Result.INVALID)
155         	return result.addError(MAX_INCLUSIVE_KEY, attributeValueReader, CONSTRAINT_NAME, RiceKeyConstants.ERROR_INCLUSIVE_MAX, inclusiveMaxText);
156         // Otherwise, just tell them what the min can be
157         else 
158         	return result.addError(MIN_EXCLUSIVE_KEY, attributeValueReader, CONSTRAINT_NAME, RiceKeyConstants.ERROR_EXCLUSIVE_MIN, exclusiveMinText);
159 	}
160 	
161 }