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}