001/** 002 * Copyright 2005-2018 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.datadictionary.validation.processor; 017 018import java.math.BigDecimal; 019import java.util.Date; 020 021import org.kuali.rice.core.api.data.DataType; 022import org.kuali.rice.core.api.util.RiceKeyConstants; 023import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException; 024import org.kuali.rice.krad.datadictionary.validation.AttributeValueReader; 025import org.kuali.rice.krad.datadictionary.validation.ValidationUtils; 026import org.kuali.rice.krad.datadictionary.validation.ValidationUtils.Result; 027import org.kuali.rice.krad.datadictionary.validation.capability.RangeConstrainable; 028import org.kuali.rice.krad.datadictionary.validation.constraint.Constraint; 029import org.kuali.rice.krad.datadictionary.validation.constraint.RangeConstraint; 030import org.kuali.rice.krad.datadictionary.validation.result.ConstraintValidationResult; 031import org.kuali.rice.krad.datadictionary.validation.result.DictionaryValidationResult; 032import org.kuali.rice.krad.datadictionary.validation.result.ProcessorResult; 033 034/** 035 * RangeConstraintProcessor enforces range constraints - that is, constraints that keep a number or a date within a 036 * specific range 037 * 038 * <p> An attribute 039 * that is {@link RangeConstrainable} will expose a minimum and maximum value, and these will be validated against the 040 * passed 041 * value in the code below.</p> 042 * 043 * @author Kuali Rice Team (rice.collab@kuali.org) 044 */ 045public class RangeConstraintProcessor extends MandatoryElementConstraintProcessor<RangeConstraint> { 046 047 private static final String CONSTRAINT_NAME = "range constraint"; 048 private static final String MIN_EXCLUSIVE_KEY = "validation.minExclusive"; 049 private static final String MAX_INCLUSIVE_KEY = "validation.maxInclusive"; 050 private static final String RANGE_KEY = "validation.range"; 051 052 /** 053 * @see org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor#process(org.kuali.rice.krad.datadictionary.validation.result.DictionaryValidationResult, 054 * Object, org.kuali.rice.krad.datadictionary.validation.constraint.Constraint, 055 * org.kuali.rice.krad.datadictionary.validation.AttributeValueReader) 056 */ 057 @Override 058 public ProcessorResult process(DictionaryValidationResult result, Object value, RangeConstraint constraint, 059 AttributeValueReader attributeValueReader) throws AttributeValidationException { 060 061 // 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 062 return new ProcessorResult(processSingleRangeConstraint(result, value, constraint, attributeValueReader)); 063 } 064 065 @Override 066 public String getName() { 067 return CONSTRAINT_NAME; 068 } 069 070 /** 071 * @see org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor#getConstraintType() 072 */ 073 @Override 074 public Class<? extends Constraint> getConstraintType() { 075 return RangeConstraint.class; 076 } 077 078 /** 079 * validates the value provided using {@code RangeConstraint} 080 * 081 * @param result - a holder for any already run validation results 082 * @param value - the value to validate 083 * @param constraint - the range constraint to use 084 * @param attributeValueReader - provides access to the attribute being validated 085 * @return the passed in result, updated with the results of the processing 086 * @throws AttributeValidationException if validation fails 087 */ 088 protected ConstraintValidationResult processSingleRangeConstraint(DictionaryValidationResult result, Object value, 089 RangeConstraint constraint, AttributeValueReader attributeValueReader) throws AttributeValidationException { 090 // Can't process any range constraints on null values 091 if (ValidationUtils.isNullOrEmpty(value) || (constraint.getExclusiveMin() == null 092 && constraint.getInclusiveMax() == null)) { 093 return result.addSkipped(attributeValueReader, CONSTRAINT_NAME); 094 } 095 096 // This is necessary because sometimes we'll be getting a string, for example, that represents a date. 097 DataType dataType = constraint.getDataType(); 098 Object typedValue = value; 099 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}