View Javadoc
1   /**
2    * Copyright 2005-2015 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 java.math.BigDecimal;
19  import java.util.Date;
20  
21  import org.kuali.rice.core.api.data.DataType;
22  import org.kuali.rice.core.api.util.RiceKeyConstants;
23  import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException;
24  import org.kuali.rice.krad.datadictionary.validation.AttributeValueReader;
25  import org.kuali.rice.krad.datadictionary.validation.ValidationUtils;
26  import org.kuali.rice.krad.datadictionary.validation.ValidationUtils.Result;
27  import org.kuali.rice.krad.datadictionary.validation.capability.RangeConstrainable;
28  import org.kuali.rice.krad.datadictionary.validation.constraint.Constraint;
29  import org.kuali.rice.krad.datadictionary.validation.constraint.RangeConstraint;
30  import org.kuali.rice.krad.datadictionary.validation.result.ConstraintValidationResult;
31  import org.kuali.rice.krad.datadictionary.validation.result.DictionaryValidationResult;
32  import org.kuali.rice.krad.datadictionary.validation.result.ProcessorResult;
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       * @throws AttributeValidationException if validation fails
87       */
88      protected ConstraintValidationResult processSingleRangeConstraint(DictionaryValidationResult result, Object value,
89              RangeConstraint constraint, AttributeValueReader attributeValueReader) throws AttributeValidationException {
90          // Can't process any range constraints on null values
91          if (ValidationUtils.isNullOrEmpty(value) || (constraint.getExclusiveMin() == null
92                  && constraint.getInclusiveMax() == null)) {
93              return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
94          }
95  
96          // This is necessary because sometimes we'll be getting a string, for example, that represents a date.
97          DataType dataType = constraint.getDataType();
98          Object typedValue = value;
99  
100         if (dataType != null) {
101             typedValue = ValidationUtils.convertToDataType(value, dataType, dateTimeService);
102         } else if (value instanceof String) {
103             //assume string is a number of type double
104             try {
105                 Double d = Double.parseDouble((String) value);
106                 typedValue = d;
107             } catch (NumberFormatException n) {
108                 //do nothing, typedValue is never reset
109             }
110         }
111 
112         // 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
113         if (typedValue instanceof Date) {
114             return validateRange(result, (Date) typedValue, constraint, attributeValueReader);
115         } else if (typedValue instanceof Number) {
116             return validateRange(result, (Number) typedValue, constraint, attributeValueReader);
117         }
118 
119         return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
120     }
121 
122     /**
123      * validates the date value using the range constraint provided
124      *
125      * @param result - a holder for any already run validation results
126      * @param value - the value to validate
127      * @param constraint - the range constraint to use
128      * @param attributeValueReader - provides access to the attribute being validated
129      * @return the passed in result, updated with the results of the processing
130      * @throws IllegalArgumentException
131      */
132     protected ConstraintValidationResult validateRange(DictionaryValidationResult result, Date value,
133             RangeConstraint constraint, AttributeValueReader attributeValueReader) throws IllegalArgumentException {
134 
135         Date date = value != null ? ValidationUtils.getDate(value, dateTimeService) : null;
136 
137         String inclusiveMaxText = constraint.getInclusiveMax();
138         String exclusiveMinText = constraint.getExclusiveMin();
139 
140         Date inclusiveMax = inclusiveMaxText != null ? ValidationUtils.getDate(inclusiveMaxText, dateTimeService) :
141                 null;
142         Date exclusiveMin = exclusiveMinText != null ? ValidationUtils.getDate(exclusiveMinText, dateTimeService) :
143                 null;
144 
145         return isInRange(result, date, inclusiveMax, inclusiveMaxText, exclusiveMin, exclusiveMinText,
146                 attributeValueReader);
147     }
148 
149     /**
150      * validates the number value using the range constraint provided
151      *
152      * @param result - a holder for any already run validation results
153      * @param value - the value to validate
154      * @param constraint - the range constraint to use
155      * @param attributeValueReader - provides access to the attribute being validated
156      * @return the passed in result, updated with the results of the processing
157      * @throws IllegalArgumentException
158      */
159     protected ConstraintValidationResult validateRange(DictionaryValidationResult result, Number value,
160             RangeConstraint constraint, AttributeValueReader attributeValueReader) throws IllegalArgumentException {
161 
162         // TODO: JLR - need a code review of the conversions below to make sure this is the best way to ensure accuracy across all numerics
163         // 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
164         BigDecimal number = value != null ? new BigDecimal(value.toString()) : null;
165 
166         String inclusiveMaxText = constraint.getInclusiveMax();
167         String exclusiveMinText = constraint.getExclusiveMin();
168 
169         BigDecimal inclusiveMax = inclusiveMaxText != null ? new BigDecimal(inclusiveMaxText) : null;
170         BigDecimal exclusiveMin = exclusiveMinText != null ? new BigDecimal(exclusiveMinText) : null;
171 
172         return isInRange(result, number, inclusiveMax, inclusiveMaxText, exclusiveMin, exclusiveMinText,
173                 attributeValueReader);
174     }
175 
176     /**
177      * checks whether the value provided is in the range specified by inclusiveMax and exclusiveMin
178      *
179      * @param result a holder for any already run validation results
180      * @param value the value to check
181      * @param inclusiveMax the maximum value of the attribute
182      * @param inclusiveMaxText the string representation of inclusiveMax
183      * @param exclusiveMin the minimum value of the attribute
184      * @param exclusiveMinText the string representation of exclusiveMin
185      * @param attributeValueReader provides access to the attribute being validated
186      * @return the passed in result, updated with the results of the range check
187      */
188     private <T> ConstraintValidationResult isInRange(DictionaryValidationResult result, T value,
189             Comparable<T> inclusiveMax, String inclusiveMaxText, Comparable<T> exclusiveMin, String exclusiveMinText,
190             AttributeValueReader attributeValueReader) {
191         // 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')
192         Result lessThanMax = ValidationUtils.isLessThanOrEqual(value, inclusiveMax);
193         // 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')
194         Result greaterThanMin = ValidationUtils.isGreaterThan(value, exclusiveMin);
195 
196         // 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. 
197         if (lessThanMax != Result.INVALID && greaterThanMin != Result.INVALID) {
198             // Of course, if they're both undefined then we didn't actually have a real constraint
199             if (lessThanMax == Result.UNDEFINED && greaterThanMin == Result.UNDEFINED) {
200                 return result.addNoConstraint(attributeValueReader, CONSTRAINT_NAME);
201             }
202 
203             // In this case, we've succeeded
204             return result.addSuccess(attributeValueReader, CONSTRAINT_NAME);
205         }
206 
207         // If both comparisons happened then if either comparison failed we can show the end user the expected range on both sides.
208         if (lessThanMax != Result.UNDEFINED && greaterThanMin != Result.UNDEFINED) {
209             return result.addError(RANGE_KEY, attributeValueReader, CONSTRAINT_NAME,
210                     RiceKeyConstants.ERROR_OUT_OF_RANGE, exclusiveMinText, inclusiveMaxText);
211         }
212         // If it's the max comparison that fails, then just tell the end user what the max can be
213         else if (lessThanMax == Result.INVALID) {
214             return result.addError(MAX_INCLUSIVE_KEY, attributeValueReader, CONSTRAINT_NAME,
215                     RiceKeyConstants.ERROR_INCLUSIVE_MAX, inclusiveMaxText);
216         }
217         // Otherwise, just tell them what the min can be
218         else {
219             return result.addError(MIN_EXCLUSIVE_KEY, attributeValueReader, CONSTRAINT_NAME,
220                     RiceKeyConstants.ERROR_EXCLUSIVE_MIN, exclusiveMinText);
221         }
222     }
223 
224 }