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 org.apache.commons.lang.StringUtils;
019import org.apache.log4j.Logger;
020import org.kuali.rice.core.api.CoreApiServiceLocator;
021import org.kuali.rice.core.api.search.SearchOperator;
022import org.kuali.rice.core.api.util.ClassLoaderUtils;
023import org.kuali.rice.core.api.util.RiceKeyConstants;
024import org.kuali.rice.core.web.format.DateFormatter;
025import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException;
026import org.kuali.rice.krad.datadictionary.validation.AttributeValueReader;
027import org.kuali.rice.krad.datadictionary.validation.ValidationUtils;
028import org.kuali.rice.krad.datadictionary.validation.capability.Constrainable;
029import org.kuali.rice.krad.datadictionary.validation.capability.Formatable;
030import org.kuali.rice.krad.datadictionary.validation.constraint.Constraint;
031import org.kuali.rice.krad.datadictionary.validation.constraint.ValidCharactersConstraint;
032import org.kuali.rice.krad.datadictionary.validation.result.ConstraintValidationResult;
033import org.kuali.rice.krad.datadictionary.validation.result.DictionaryValidationResult;
034import org.kuali.rice.krad.datadictionary.validation.result.ProcessorResult;
035import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
036import org.kuali.rice.krad.util.KRADConstants;
037
038import java.lang.reflect.Method;
039import java.util.List;
040
041/**
042 * This class defines a constraint processor to ensure that attribute values are constrained to valid characters, as
043 * defined by some regular expression. Of the
044 * constraint processors written for this version, this one is potentially the most difficult to understand because it
045 * holds on to a lot of legacy processing.
046 *
047 * @author Kuali Rice Team (rice.collab@kuali.org)
048 */
049public class ValidCharactersConstraintProcessor extends MandatoryElementConstraintProcessor<ValidCharactersConstraint> {
050
051    public static final String VALIDATE_METHOD = "validate";
052
053    private static final Logger LOG = Logger.getLogger(ValidCharactersConstraintProcessor.class);
054    private static final String[] DATE_RANGE_ERROR_PREFIXES = {KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX,
055            KRADConstants.LOOKUP_RANGE_UPPER_BOUND_PROPERTY_PREFIX};
056
057    private static final String CONSTRAINT_NAME = "valid characters constraint";
058
059    /**
060     * {@inheritDoc}
061     */
062    @Override
063    public ProcessorResult process(DictionaryValidationResult result, Object value,
064            ValidCharactersConstraint constraint,
065            AttributeValueReader attributeValueReader) throws AttributeValidationException {
066
067        return new ProcessorResult(processSingleValidCharacterConstraint(result, value, constraint,
068                attributeValueReader));
069    }
070
071    @Override
072    public String getName() {
073        return CONSTRAINT_NAME;
074    }
075
076    /**
077     * @see org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor#getConstraintType()
078     */
079    @Override
080    public Class<? extends Constraint> getConstraintType() {
081        return ValidCharactersConstraint.class;
082    }
083
084    protected ConstraintValidationResult processSingleValidCharacterConstraint(DictionaryValidationResult result,
085            Object value, ValidCharactersConstraint constraint,
086            AttributeValueReader attributeValueReader) throws AttributeValidationException {
087
088        if (constraint == null) {
089            return result.addNoConstraint(attributeValueReader, CONSTRAINT_NAME);
090        }
091
092        if (ValidationUtils.isNullOrEmpty(value)) {
093            return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
094        }
095
096        // This mix-in interface is here to allow some definitions to avoid the extra processing that goes on in KNS
097        // to decipher and validate things like date range strings -- something that looks like "02/02/2002..03/03/2003"
098        Constrainable definition = attributeValueReader.getDefinition(attributeValueReader.getAttributeName());
099        if (definition instanceof Formatable) {
100            return doProcessFormattableValidCharConstraint(result, constraint, (Formatable) definition, value,
101                    attributeValueReader);
102        }
103
104        ConstraintValidationResult constraintValidationResult = doProcessValidCharConstraint(constraint, value);
105        if (constraintValidationResult == null) {
106            return result.addSuccess(attributeValueReader, CONSTRAINT_NAME);
107        }
108
109        result.addConstraintValidationResult(attributeValueReader, constraintValidationResult);
110        constraintValidationResult.setConstraintLabelKey(constraint.getMessageKey());
111        constraintValidationResult.setErrorParameters(constraint.getValidationMessageParamsArray());
112        return constraintValidationResult;
113    }
114
115    protected ConstraintValidationResult doProcessFormattableValidCharConstraint(DictionaryValidationResult result,
116            ValidCharactersConstraint validCharsConstraint, Formatable definition, Object value,
117            AttributeValueReader attributeValueReader) throws AttributeValidationException {
118        String entryName = attributeValueReader.getEntryName();
119        String attributeName = attributeValueReader.getAttributeName();
120
121        // This is a strange KNS thing for validating searchable fields -- they sometimes come in a date range format, for example 2/12/2010..2/14/2010, and need to be split up
122        List<String> parsedAttributeValues = attributeValueReader.getCleanSearchableValues(attributeName);
123
124        if (parsedAttributeValues != null) {
125
126            Class<?> formatterClass = null;
127            Boolean doValidateDateRangeOrder = null;
128
129            // It can't be a date range if it's more than two fields, for example "a .. b | c" is not a date range -- this saves us a tiny bit of processing later
130            if (parsedAttributeValues.size() != 2) {
131                doValidateDateRangeOrder = Boolean.FALSE;
132            }
133
134            // Use integer to iterate since we need to track which field we're looking at
135            for (int i = 0; i < parsedAttributeValues.size(); i++) {
136                String parsedAttributeValue = parsedAttributeValues.get(i);
137
138                ConstraintValidationResult constraintValidationResult = doProcessValidCharConstraint(
139                        validCharsConstraint, parsedAttributeValue);
140
141                // If this is an error then some non-null validation result will be returned
142                if (constraintValidationResult != null) {
143                    constraintValidationResult.setConstraintLabelKey(validCharsConstraint.getMessageKey());
144                    constraintValidationResult.setErrorParameters(
145                            validCharsConstraint.getValidationMessageParamsArray());
146                    // Another strange KNS thing -- if the validation fails (not sure why only in that case) then some further error checking is done using the formatter, if one exists
147                    if (formatterClass == null) {
148                        String formatterClassName = definition.getFormatterClass();
149                        if (formatterClassName != null) {
150                            formatterClass = ClassLoaderUtils.getClass(formatterClassName);
151                        }
152                    }
153
154                    if (formatterClass != null) {
155                        // Use the Boolean value being null to ensure we only do this once
156                        if (doValidateDateRangeOrder == null) {
157                            // We only want to validate a date range if we're dealing with something that has a date formatter on it and that looks like an actual range (is made up of 2 values with a between operator between them)
158                            doValidateDateRangeOrder = Boolean.valueOf(DateFormatter.class.isAssignableFrom(
159                                    formatterClass) && StringUtils.contains(ValidationUtils.getString(value),
160                                    SearchOperator.BETWEEN.toString()));
161                        }
162
163                        constraintValidationResult = processFormatterValidation(result, formatterClass, entryName,
164                                attributeName, parsedAttributeValue, DATE_RANGE_ERROR_PREFIXES[i]);
165
166                        if (constraintValidationResult != null) {
167                            result.addConstraintValidationResult(attributeValueReader, constraintValidationResult);
168                            return constraintValidationResult;
169                        }
170                    } else {
171                        // Otherwise, just report the validation result (apparently the formatter can't provide any fall-through validation because it doesn't exist)
172                        result.addConstraintValidationResult(attributeValueReader, constraintValidationResult);
173                        return constraintValidationResult;
174                    }
175                }
176            }
177
178            if (doValidateDateRangeOrder != null && doValidateDateRangeOrder.booleanValue()) {
179                ConstraintValidationResult dateOrderValidationResult = validateDateOrder(parsedAttributeValues.get(0),
180                        parsedAttributeValues.get(1), entryName, attributeName);
181
182                if (dateOrderValidationResult != null) {
183                    result.addConstraintValidationResult(attributeValueReader, dateOrderValidationResult);
184                    return dateOrderValidationResult;
185                }
186            }
187
188            return result.addSuccess(attributeValueReader, CONSTRAINT_NAME);
189        }
190        return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
191    }
192
193    protected ConstraintValidationResult doProcessValidCharConstraint(ValidCharactersConstraint validCharsConstraint,
194            Object value) {
195
196        StringBuilder fieldValue = new StringBuilder();
197        String validChars = validCharsConstraint.getValue();
198
199        if (value instanceof java.sql.Date) {
200            fieldValue.append(getDateTimeService().toDateString((java.sql.Date) value));
201        } else {
202            fieldValue.append(ValidationUtils.getString(value));
203        }
204
205        //        int typIdx = validChars.indexOf(":");
206        //        String processorType = "regex";
207        //        if (-1 == typIdx) {
208        //            validChars = "[" + validChars + "]*";
209        //        } else {
210        //            processorType = validChars.substring(0, typIdx);
211        //            validChars = validChars.substring(typIdx + 1);
212        //        }
213
214        //        if ("regex".equalsIgnoreCase(processorType) && !validChars.equals(".*")) {
215        if (!fieldValue.toString().matches(validChars)) {
216            ConstraintValidationResult constraintValidationResult = new ConstraintValidationResult(CONSTRAINT_NAME);
217            constraintValidationResult.setError(RiceKeyConstants.ERROR_INVALID_FORMAT, fieldValue.toString());
218            constraintValidationResult.setConstraintLabelKey(validCharsConstraint.getMessageKey());
219            constraintValidationResult.setErrorParameters(validCharsConstraint.getValidationMessageParamsArray());
220            return constraintValidationResult;
221        }
222        //        }
223
224        return null;
225    }
226
227    protected ConstraintValidationResult processFormatterValidation(DictionaryValidationResult result,
228            Class<?> formatterClass, String entryName, String attributeName, String parsedAttributeValue,
229            String errorKeyPrefix) {
230
231        boolean isError = false;
232
233        try {
234            Method validatorMethod = formatterClass.getDeclaredMethod(VALIDATE_METHOD, new Class<?>[]{String.class});
235            Object o = validatorMethod.invoke(formatterClass.newInstance(), parsedAttributeValue);
236            if (o instanceof Boolean) {
237                isError = !((Boolean) o).booleanValue();
238            }
239        } catch (Exception e) {
240            if (LOG.isDebugEnabled()) {
241                LOG.debug(e.getMessage(), e);
242            }
243
244            isError = true;
245        }
246
247        if (isError) {
248            String errorMessageKey = getDataDictionaryService().getAttributeValidatingErrorMessageKey(entryName,
249                    attributeName);
250            String[] errorMessageParameters = getDataDictionaryService().getAttributeValidatingErrorMessageParameters(
251                    entryName, attributeName);
252
253            ConstraintValidationResult constraintValidationResult = new ConstraintValidationResult(CONSTRAINT_NAME);
254            constraintValidationResult.setEntryName(entryName);
255            constraintValidationResult.setAttributeName(errorKeyPrefix + attributeName);
256            constraintValidationResult.setError(errorMessageKey, errorMessageParameters);
257
258            return constraintValidationResult;
259        }
260
261        return null;
262    }
263
264    protected ConstraintValidationResult validateDateOrder(String firstDateTime, String secondDateTime,
265            String entryName, String attributeName) {
266        // this means that we only have 2 values and it's a date range.
267        java.sql.Timestamp lVal = null;
268        java.sql.Timestamp uVal = null;
269        try {
270            lVal = CoreApiServiceLocator.getDateTimeService().convertToSqlTimestamp(firstDateTime);
271            uVal = CoreApiServiceLocator.getDateTimeService().convertToSqlTimestamp(secondDateTime);
272        } catch (Exception ex) {
273            // this shouldn't happen because the tests passed above.
274            String errorMessageKey =
275                    KRADServiceLocatorWeb.getDataDictionaryService().getAttributeValidatingErrorMessageKey(entryName,
276                            attributeName);
277            String[] errorMessageParameters =
278                    KRADServiceLocatorWeb.getDataDictionaryService().getAttributeValidatingErrorMessageParameters(
279                            entryName, attributeName);
280            ConstraintValidationResult constraintValidationResult = new ConstraintValidationResult(CONSTRAINT_NAME);
281            constraintValidationResult.setEntryName(entryName);
282            constraintValidationResult.setAttributeName(
283                    KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + attributeName);
284            constraintValidationResult.setError(errorMessageKey, errorMessageParameters);
285            return constraintValidationResult;
286        }
287
288        if (lVal != null && lVal.compareTo(uVal) > 0) { // check the bounds
289            String errorMessageKey =
290                    KRADServiceLocatorWeb.getDataDictionaryService().getAttributeValidatingErrorMessageKey(entryName,
291                            attributeName);
292            String[] errorMessageParameters =
293                    KRADServiceLocatorWeb.getDataDictionaryService().getAttributeValidatingErrorMessageParameters(
294                            entryName, attributeName);
295            ConstraintValidationResult constraintValidationResult = new ConstraintValidationResult(CONSTRAINT_NAME);
296            constraintValidationResult.setEntryName(entryName);
297            constraintValidationResult.setAttributeName(
298                    KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + attributeName);
299            constraintValidationResult.setError(errorMessageKey + ".range", errorMessageParameters);
300            return constraintValidationResult;
301        }
302
303        return null;
304    }
305
306}