View Javadoc

1   /**
2    * Copyright 2005-2014 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
36   * specific range
37   *
38   * <p> An attribute
39   * that is {@link RangeConstrainable} will expose a minimum and maximum value, and these will be validated against the
40   * passed
41   * value in the code below.</p>
42   *
43   * @author Kuali Rice Team (rice.collab@kuali.org)
44   */
45  public class RangeConstraintProcessor extends MandatoryElementConstraintProcessor<RangeConstraint> {
46  
47      private static final String CONSTRAINT_NAME = "range constraint";
48      private static final String MIN_EXCLUSIVE_KEY = "validation.minExclusive";
49      private static final String MAX_INCLUSIVE_KEY = "validation.maxInclusive";
50      private static final String RANGE_KEY = "validation.range";
51  
52      /**
53       * @see org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor#process(org.kuali.rice.krad.datadictionary.validation.result.DictionaryValidationResult,
54       *      Object, org.kuali.rice.krad.datadictionary.validation.constraint.Constraint,
55       *      org.kuali.rice.krad.datadictionary.validation.AttributeValueReader)
56       */
57      @Override
58      public ProcessorResult process(DictionaryValidationResult result, Object value, RangeConstraint constraint,
59              AttributeValueReader attributeValueReader) throws AttributeValidationException {
60  
61          // 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
62          return new ProcessorResult(processSingleRangeConstraint(result, value, constraint, attributeValueReader));
63      }
64  
65      @Override
66      public String getName() {
67          return CONSTRAINT_NAME;
68      }
69  
70      /**
71       * @see org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor#getConstraintType()
72       */
73      @Override
74      public Class<? extends Constraint> getConstraintType() {
75          return RangeConstraint.class;
76      }
77  
78      /**
79       * validates the value provided using {@code RangeConstraint}
80       *
81       * @param result - a holder for any already run validation results
82       * @param value - the value to validate
83       * @param constraint - the range constraint to use
84       * @param attributeValueReader - provides access to the attribute being validated
85       * @return the passed in result, updated with the results of the processing
86       */
87      protected ConstraintValidationResult processSingleRangeConstraint(DictionaryValidationResult result, Object value,
88              RangeConstraint constraint, AttributeValueReader attributeValueReader) throws AttributeValidationException {
89          // Can't process any range constraints on null values
90          if (ValidationUtils.isNullOrEmpty(value) || (constraint.getExclusiveMin() == null
91                  && constraint.getInclusiveMax() == null)) {
92              return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
93          }
94  
95          // This is necessary because sometimes we'll be getting a string, for example, that represents a date.
96          DataType dataType = constraint.getDataType();
97          Object typedValue = value;
98  
99          if (dataType != null) {
100             typedValue = ValidationUtils.convertToDataType(value, dataType, dateTimeService);
101         } else if (value instanceof String) {
102             //assume string is a number of type double
103             try {
104                 Double d = Double.parseDouble((String) value);
105                 typedValue = d;
106             } catch (NumberFormatException n) {
107                 //do nothing, typedValue is never reset
108             }
109         }
110 
111         // 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
112         if (typedValue instanceof Date) {
113             return validateRange(result, (Date) typedValue, constraint, attributeValueReader);
114         } else if (typedValue instanceof Number) {
115             return validateRange(result, (Number) typedValue, constraint, attributeValueReader);
116         }
117 
118         return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
119     }
120 
121     /**
122      * validates the date value using the range constraint provided
123      *
124      * @param result - a holder for any already run validation results
125      * @param value - the value to validate
126      * @param constraint - the range constraint to use
127      * @param attributeValueReader - provides access to the attribute being validated
128      * @return the passed in result, updated with the results of the processing
129      * @throws IllegalArgumentException
130      */
131     protected ConstraintValidationResult validateRange(DictionaryValidationResult result, Date value,
132             RangeConstraint constraint, AttributeValueReader attributeValueReader) throws IllegalArgumentException {
133 
134         Date date = value != null ? ValidationUtils.getDate(value, dateTimeService) : null;
135 
136         String inclusiveMaxText = constraint.getInclusiveMax();
137         String exclusiveMinText = constraint.getExclusiveMin();
138 
139         Date inclusiveMax = inclusiveMaxText != null ? ValidationUtils.getDate(inclusiveMaxText, dateTimeService) :
140                 null;
141         Date exclusiveMin = exclusiveMinText != null ? ValidationUtils.getDate(exclusiveMinText, dateTimeService) :
142                 null;
143 
144         return isInRange(result, date, inclusiveMax, inclusiveMaxText, exclusiveMin, exclusiveMinText,
145                 attributeValueReader);
146     }
147 
148     /**
149      * validates the number value using the range constraint provided
150      *
151      * @param result - a holder for any already run validation results
152      * @param value - the value to validate
153      * @param constraint - the range constraint to use
154      * @param attributeValueReader - provides access to the attribute being validated
155      * @return the passed in result, updated with the results of the processing
156      * @throws IllegalArgumentException
157      */
158     protected ConstraintValidationResult validateRange(DictionaryValidationResult result, Number value,
159             RangeConstraint constraint, AttributeValueReader attributeValueReader) throws IllegalArgumentException {
160 
161         // TODO: JLR - need a code review of the conversions below to make sure this is the best way to ensure accuracy across all numerics
162         // 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
163         BigDecimal number = value != null ? new BigDecimal(value.toString()) : null;
164 
165         String inclusiveMaxText = constraint.getInclusiveMax();
166         String exclusiveMinText = constraint.getExclusiveMin();
167 
168         BigDecimal inclusiveMax = inclusiveMaxText != null ? new BigDecimal(inclusiveMaxText) : null;
169         BigDecimal exclusiveMin = exclusiveMinText != null ? new BigDecimal(exclusiveMinText) : null;
170 
171         return isInRange(result, number, inclusiveMax, inclusiveMaxText, exclusiveMin, exclusiveMinText,
172                 attributeValueReader);
173     }
174 
175     /**
176      * checks whether the value provided is in the range specified by inclusiveMax and exclusiveMin
177      *
178      * @param - a holder for any already run validation results
179      * @param value - the value to check
180      * @param inclusiveMax - the maximum value of the attribute
181      * @param inclusiveMaxText - the string representation of inclusiveMax
182      * @param exclusiveMin -  the minimum value of the attribute
183      * @param exclusiveMinText - the string representation of exclusiveMin
184      * @param attributeValueReader - provides access to the attribute being validated
185      * @return the passed in result, updated with the results of the range check
186      */
187     private <T> ConstraintValidationResult isInRange(DictionaryValidationResult result, T value,
188             Comparable<T> inclusiveMax, String inclusiveMaxText, Comparable<T> exclusiveMin, String exclusiveMinText,
189             AttributeValueReader attributeValueReader) {
190         // 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')
191         Result lessThanMax = ValidationUtils.isLessThanOrEqual(value, inclusiveMax);
192         // 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')
193         Result greaterThanMin = ValidationUtils.isGreaterThan(value, exclusiveMin);
194 
195         // 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. 
196         if (lessThanMax != Result.INVALID && greaterThanMin != Result.INVALID) {
197             // Of course, if they're both undefined then we didn't actually have a real constraint
198             if (lessThanMax == Result.UNDEFINED && greaterThanMin == Result.UNDEFINED) {
199                 return result.addNoConstraint(attributeValueReader, CONSTRAINT_NAME);
200             }
201 
202             // In this case, we've succeeded
203             return result.addSuccess(attributeValueReader, CONSTRAINT_NAME);
204         }
205 
206         // If both comparisons happened then if either comparison failed we can show the end user the expected range on both sides.
207         if (lessThanMax != Result.UNDEFINED && greaterThanMin != Result.UNDEFINED) {
208             return result.addError(RANGE_KEY, attributeValueReader, CONSTRAINT_NAME,
209                     RiceKeyConstants.ERROR_OUT_OF_RANGE, exclusiveMinText, inclusiveMaxText);
210         }
211         // If it's the max comparison that fails, then just tell the end user what the max can be
212         else if (lessThanMax == Result.INVALID) {
213             return result.addError(MAX_INCLUSIVE_KEY, attributeValueReader, CONSTRAINT_NAME,
214                     RiceKeyConstants.ERROR_INCLUSIVE_MAX, inclusiveMaxText);
215         }
216         // Otherwise, just tell them what the min can be
217         else {
218             return result.addError(MIN_EXCLUSIVE_KEY, attributeValueReader, CONSTRAINT_NAME,
219                     RiceKeyConstants.ERROR_EXCLUSIVE_MIN, exclusiveMinText);
220         }
221     }
222 
223 }