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}