001/** 002 * Copyright 2005-2016 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 org.kuali.rice.core.api.uif.DataType; 019import org.kuali.rice.core.api.util.RiceKeyConstants; 020import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException; 021import org.kuali.rice.krad.datadictionary.validation.AttributeValueReader; 022import org.kuali.rice.krad.datadictionary.validation.ValidationUtils; 023import org.kuali.rice.krad.datadictionary.validation.ValidationUtils.Result; 024import org.kuali.rice.krad.datadictionary.validation.capability.RangeConstrainable; 025import org.kuali.rice.krad.datadictionary.validation.constraint.Constraint; 026import org.kuali.rice.krad.datadictionary.validation.constraint.RangeConstraint; 027import org.kuali.rice.krad.datadictionary.validation.result.ConstraintValidationResult; 028import org.kuali.rice.krad.datadictionary.validation.result.DictionaryValidationResult; 029import org.kuali.rice.krad.datadictionary.validation.result.ProcessorResult; 030 031import java.math.BigDecimal; 032import java.util.Date; 033 034/** 035 * This class enforces range constraints - that is, constraints that keep a number or a date within a specific range. An attribute 036 * that is {@link RangeConstrainable} will expose a minimum and maximum value, and these will be validated against the passed 037 * value in the code below. 038 * 039 * @author Kuali Rice Team (rice.collab@kuali.org) 040 */ 041public class RangeConstraintProcessor extends MandatoryElementConstraintProcessor<RangeConstraint> { 042 043 private static final String CONSTRAINT_NAME = "range constraint"; 044 private static final String MIN_EXCLUSIVE_KEY = "validation.minExclusive"; 045 private static final String MAX_INCLUSIVE_KEY = "validation.maxInclusive"; 046 private static final String RANGE_KEY = "validation.range"; 047 048 /** 049 * @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) 050 */ 051 @Override 052 public ProcessorResult process(DictionaryValidationResult result, Object value, RangeConstraint constraint, AttributeValueReader attributeValueReader) throws AttributeValidationException { 053 054 // 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 055 return new ProcessorResult(processSingleRangeConstraint(result, value, constraint, attributeValueReader)); 056 } 057 058 @Override 059 public String getName() { 060 return CONSTRAINT_NAME; 061 } 062 063 /** 064 * @see org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor#getConstraintType() 065 */ 066 @Override 067 public Class<? extends Constraint> getConstraintType() { 068 return RangeConstraint.class; 069 } 070 071 protected ConstraintValidationResult processSingleRangeConstraint(DictionaryValidationResult result, Object value, RangeConstraint constraint, AttributeValueReader attributeValueReader) throws AttributeValidationException { 072 // Can't process any range constraints on null values 073 if (ValidationUtils.isNullOrEmpty(value) || 074 (constraint.getExclusiveMin() == null && constraint.getInclusiveMax() == null)){ 075 return result.addSkipped(attributeValueReader, CONSTRAINT_NAME); 076 } 077 078 079 // This is necessary because sometimes we'll be getting a string, for example, that represents a date. 080 DataType dataType = constraint.getDataType(); 081 Object typedValue = value; 082 083 if (dataType != null) { 084 typedValue = ValidationUtils.convertToDataType(value, dataType, dateTimeService); 085 } 086 else if(value instanceof String){ 087 //assume string is a number of type double 088 try{ 089 Double d = Double.parseDouble((String)value); 090 typedValue = d; 091 } 092 catch(NumberFormatException n){ 093 //do nothing, typedValue is never reset 094 } 095 } 096 097 // 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 098 if (typedValue instanceof Date) 099 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}