View Javadoc

1   /**
2    * Copyright 2005-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.datadictionary.validation.processor;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.kuali.rice.core.api.CoreApiServiceLocator;
21  import org.kuali.rice.core.api.search.SearchOperator;
22  import org.kuali.rice.core.api.util.ClassLoaderUtils;
23  import org.kuali.rice.core.api.util.RiceKeyConstants;
24  import org.kuali.rice.core.web.format.DateFormatter;
25  import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException;
26  import org.kuali.rice.krad.datadictionary.validation.AttributeValueReader;
27  import org.kuali.rice.krad.datadictionary.validation.ValidationUtils;
28  import org.kuali.rice.krad.datadictionary.validation.capability.Constrainable;
29  import org.kuali.rice.krad.datadictionary.validation.capability.Formatable;
30  import org.kuali.rice.krad.datadictionary.validation.constraint.Constraint;
31  import org.kuali.rice.krad.datadictionary.validation.constraint.ValidCharactersConstraint;
32  import org.kuali.rice.krad.datadictionary.validation.result.ConstraintValidationResult;
33  import org.kuali.rice.krad.datadictionary.validation.result.DictionaryValidationResult;
34  import org.kuali.rice.krad.datadictionary.validation.result.ProcessorResult;
35  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
36  import org.kuali.rice.krad.util.KRADConstants;
37  
38  import java.lang.reflect.Method;
39  import java.util.List;
40  
41  /**
42   * This class defines a constraint processor to ensure that attribute values are constrained to valid characters, as
43   * defined by some regular expression. Of the
44   * constraint processors written for this version, this one is potentially the most difficult to understand because it
45   * holds on to a lot of legacy processing.
46   *
47   * @author Kuali Rice Team (rice.collab@kuali.org)
48   */
49  public class ValidCharactersConstraintProcessor extends MandatoryElementConstraintProcessor<ValidCharactersConstraint> {
50  
51      public static final String VALIDATE_METHOD = "validate";
52  
53      private static final Logger LOG = Logger.getLogger(ValidCharactersConstraintProcessor.class);
54      private static final String[] DATE_RANGE_ERROR_PREFIXES = {KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX,
55              KRADConstants.LOOKUP_RANGE_UPPER_BOUND_PROPERTY_PREFIX};
56  
57      private static final String CONSTRAINT_NAME = "valid characters constraint";
58  
59      /**
60       * @see org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor#process(DictionaryValidationResult,
61       *      Object, org.kuali.rice.krad.datadictionary.validation.capability.Validatable,
62       *      org.kuali.rice.krad.datadictionary.validation.AttributeValueReader)
63       */
64      @Override
65      public ProcessorResult process(DictionaryValidationResult result, Object value,
66              ValidCharactersConstraint constraint,
67              AttributeValueReader attributeValueReader) throws AttributeValidationException {
68  
69          return new ProcessorResult(processSingleValidCharacterConstraint(result, value, constraint,
70                  attributeValueReader));
71      }
72  
73      @Override
74      public String getName() {
75          return CONSTRAINT_NAME;
76      }
77  
78      /**
79       * @see org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor#getConstraintType()
80       */
81      @Override
82      public Class<? extends Constraint> getConstraintType() {
83          return ValidCharactersConstraint.class;
84      }
85  
86      protected ConstraintValidationResult processSingleValidCharacterConstraint(DictionaryValidationResult result,
87              Object value, ValidCharactersConstraint constraint,
88              AttributeValueReader attributeValueReader) throws AttributeValidationException {
89  
90          if (constraint == null) {
91              return result.addNoConstraint(attributeValueReader, CONSTRAINT_NAME);
92          }
93  
94          if (ValidationUtils.isNullOrEmpty(value)) {
95              return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
96          }
97  
98          // This mix-in interface is here to allow some definitions to avoid the extra processing that goes on in KNS
99          // to decipher and validate things like date range strings -- something that looks like "02/02/2002..03/03/2003"
100         Constrainable definition = attributeValueReader.getDefinition(attributeValueReader.getAttributeName());
101         if (definition instanceof Formatable) {
102             return doProcessFormattableValidCharConstraint(result, constraint, (Formatable) definition, value,
103                     attributeValueReader);
104         }
105 
106         ConstraintValidationResult constraintValidationResult = doProcessValidCharConstraint(constraint, value);
107         if (constraintValidationResult == null) {
108             return result.addSuccess(attributeValueReader, CONSTRAINT_NAME);
109         }
110 
111         result.addConstraintValidationResult(attributeValueReader, constraintValidationResult);
112         constraintValidationResult.setConstraintLabelKey(constraint.getMessageKey());
113         constraintValidationResult.setErrorParameters(constraint.getValidationMessageParamsArray());
114         return constraintValidationResult;
115     }
116 
117     protected ConstraintValidationResult doProcessFormattableValidCharConstraint(DictionaryValidationResult result,
118             ValidCharactersConstraint validCharsConstraint, Formatable definition, Object value,
119             AttributeValueReader attributeValueReader) throws AttributeValidationException {
120         String entryName = attributeValueReader.getEntryName();
121         String attributeName = attributeValueReader.getAttributeName();
122 
123         // 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
124         List<String> parsedAttributeValues = attributeValueReader.getCleanSearchableValues(attributeName);
125 
126         if (parsedAttributeValues != null) {
127 
128             Class<?> formatterClass = null;
129             Boolean doValidateDateRangeOrder = null;
130 
131             // 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
132             if (parsedAttributeValues.size() != 2) {
133                 doValidateDateRangeOrder = Boolean.FALSE;
134             }
135 
136             // Use integer to iterate since we need to track which field we're looking at
137             for (int i = 0; i < parsedAttributeValues.size(); i++) {
138                 String parsedAttributeValue = parsedAttributeValues.get(i);
139 
140                 ConstraintValidationResult constraintValidationResult = doProcessValidCharConstraint(
141                         validCharsConstraint, parsedAttributeValue);
142 
143                 // If this is an error then some non-null validation result will be returned
144                 if (constraintValidationResult != null) {
145                     constraintValidationResult.setConstraintLabelKey(validCharsConstraint.getMessageKey());
146                     constraintValidationResult.setErrorParameters(
147                             validCharsConstraint.getValidationMessageParamsArray());
148                     // 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
149                     if (formatterClass == null) {
150                         String formatterClassName = definition.getFormatterClass();
151                         if (formatterClassName != null) {
152                             formatterClass = ClassLoaderUtils.getClass(formatterClassName);
153                         }
154                     }
155 
156                     if (formatterClass != null) {
157                         // Use the Boolean value being null to ensure we only do this once
158                         if (doValidateDateRangeOrder == null) {
159                             // 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)
160                             doValidateDateRangeOrder = Boolean.valueOf(DateFormatter.class.isAssignableFrom(
161                                     formatterClass) && StringUtils.contains(ValidationUtils.getString(value),
162                                     SearchOperator.BETWEEN.toString()));
163                         }
164 
165                         constraintValidationResult = processFormatterValidation(result, formatterClass, entryName,
166                                 attributeName, parsedAttributeValue, DATE_RANGE_ERROR_PREFIXES[i]);
167 
168                         if (constraintValidationResult != null) {
169                             result.addConstraintValidationResult(attributeValueReader, constraintValidationResult);
170                             return constraintValidationResult;
171                         }
172                     } else {
173                         // Otherwise, just report the validation result (apparently the formatter can't provide any fall-through validation because it doesn't exist)
174                         result.addConstraintValidationResult(attributeValueReader, constraintValidationResult);
175                         return constraintValidationResult;
176                     }
177                 }
178             }
179 
180             if (doValidateDateRangeOrder != null && doValidateDateRangeOrder.booleanValue()) {
181                 ConstraintValidationResult dateOrderValidationResult = validateDateOrder(parsedAttributeValues.get(0),
182                         parsedAttributeValues.get(1), entryName, attributeName);
183 
184                 if (dateOrderValidationResult != null) {
185                     result.addConstraintValidationResult(attributeValueReader, dateOrderValidationResult);
186                     return dateOrderValidationResult;
187                 }
188             }
189 
190             return result.addSuccess(attributeValueReader, CONSTRAINT_NAME);
191         }
192         return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
193     }
194 
195     protected ConstraintValidationResult doProcessValidCharConstraint(ValidCharactersConstraint validCharsConstraint,
196             Object value) {
197 
198         StringBuilder fieldValue = new StringBuilder();
199         String validChars = validCharsConstraint.getValue();
200 
201         if (value instanceof java.sql.Date) {
202             fieldValue.append(getDateTimeService().toDateString((java.sql.Date) value));
203         } else {
204             fieldValue.append(ValidationUtils.getString(value));
205         }
206 
207         //        int typIdx = validChars.indexOf(":");
208         //        String processorType = "regex";
209         //        if (-1 == typIdx) {
210         //            validChars = "[" + validChars + "]*";
211         //        } else {
212         //            processorType = validChars.substring(0, typIdx);
213         //            validChars = validChars.substring(typIdx + 1);
214         //        }
215 
216         //        if ("regex".equalsIgnoreCase(processorType) && !validChars.equals(".*")) {
217         if (!fieldValue.toString().matches(validChars)) {
218             ConstraintValidationResult constraintValidationResult = new ConstraintValidationResult(CONSTRAINT_NAME);
219             constraintValidationResult.setError(RiceKeyConstants.ERROR_INVALID_FORMAT, fieldValue.toString());
220             constraintValidationResult.setConstraintLabelKey(validCharsConstraint.getMessageKey());
221             constraintValidationResult.setErrorParameters(validCharsConstraint.getValidationMessageParamsArray());
222             return constraintValidationResult;
223         }
224         //        }
225 
226         return null;
227     }
228 
229     protected ConstraintValidationResult processFormatterValidation(DictionaryValidationResult result,
230             Class<?> formatterClass, String entryName, String attributeName, String parsedAttributeValue,
231             String errorKeyPrefix) {
232 
233         boolean isError = false;
234 
235         try {
236             Method validatorMethod = formatterClass.getDeclaredMethod(VALIDATE_METHOD, new Class<?>[]{String.class});
237             Object o = validatorMethod.invoke(formatterClass.newInstance(), parsedAttributeValue);
238             if (o instanceof Boolean) {
239                 isError = !((Boolean) o).booleanValue();
240             }
241         } catch (Exception e) {
242             if (LOG.isDebugEnabled()) {
243                 LOG.debug(e.getMessage(), e);
244             }
245 
246             isError = true;
247         }
248 
249         if (isError) {
250             String errorMessageKey = getDataDictionaryService().getAttributeValidatingErrorMessageKey(entryName,
251                     attributeName);
252             String[] errorMessageParameters = getDataDictionaryService().getAttributeValidatingErrorMessageParameters(
253                     entryName, attributeName);
254 
255             ConstraintValidationResult constraintValidationResult = new ConstraintValidationResult(CONSTRAINT_NAME);
256             constraintValidationResult.setEntryName(entryName);
257             constraintValidationResult.setAttributeName(errorKeyPrefix + attributeName);
258             constraintValidationResult.setError(errorMessageKey, errorMessageParameters);
259 
260             return constraintValidationResult;
261         }
262 
263         return null;
264     }
265 
266     protected ConstraintValidationResult validateDateOrder(String firstDateTime, String secondDateTime,
267             String entryName, String attributeName) {
268         // this means that we only have 2 values and it's a date range.
269         java.sql.Timestamp lVal = null;
270         java.sql.Timestamp uVal = null;
271         try {
272             lVal = CoreApiServiceLocator.getDateTimeService().convertToSqlTimestamp(firstDateTime);
273             uVal = CoreApiServiceLocator.getDateTimeService().convertToSqlTimestamp(secondDateTime);
274         } catch (Exception ex) {
275             // this shouldn't happen because the tests passed above.
276             String errorMessageKey =
277                     KRADServiceLocatorWeb.getDataDictionaryService().getAttributeValidatingErrorMessageKey(entryName,
278                             attributeName);
279             String[] errorMessageParameters =
280                     KRADServiceLocatorWeb.getDataDictionaryService().getAttributeValidatingErrorMessageParameters(
281                             entryName, attributeName);
282             ConstraintValidationResult constraintValidationResult = new ConstraintValidationResult(CONSTRAINT_NAME);
283             constraintValidationResult.setEntryName(entryName);
284             constraintValidationResult.setAttributeName(
285                     KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + attributeName);
286             constraintValidationResult.setError(errorMessageKey, errorMessageParameters);
287             return constraintValidationResult;
288         }
289 
290         if (lVal != null && lVal.compareTo(uVal) > 0) { // check the bounds
291             String errorMessageKey =
292                     KRADServiceLocatorWeb.getDataDictionaryService().getAttributeValidatingErrorMessageKey(entryName,
293                             attributeName);
294             String[] errorMessageParameters =
295                     KRADServiceLocatorWeb.getDataDictionaryService().getAttributeValidatingErrorMessageParameters(
296                             entryName, attributeName);
297             ConstraintValidationResult constraintValidationResult = new ConstraintValidationResult(CONSTRAINT_NAME);
298             constraintValidationResult.setEntryName(entryName);
299             constraintValidationResult.setAttributeName(
300                     KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + attributeName);
301             constraintValidationResult.setError(errorMessageKey + ".range", errorMessageParameters);
302             return constraintValidationResult;
303         }
304 
305         return null;
306     }
307 
308 }