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.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 defined by some regular expression. Of the 043 * constraint processors written for this version, this one is potentially the most difficult to understand because it holds on to a lot of legacy processing. 044 * 045 * @author Kuali Rice Team (rice.collab@kuali.org) 046 */ 047public class ValidCharactersConstraintProcessor extends MandatoryElementConstraintProcessor<ValidCharactersConstraint> { 048 049 public static final String VALIDATE_METHOD = "validate"; 050 051 private static final Logger LOG = Logger.getLogger(ValidCharactersConstraintProcessor.class); 052 private static final String[] DATE_RANGE_ERROR_PREFIXES = { KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX, KRADConstants.LOOKUP_RANGE_UPPER_BOUND_PROPERTY_PREFIX }; 053 054 private static final String CONSTRAINT_NAME = "valid characters constraint"; 055 056 /** 057 * @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) 058 */ 059 @Override 060 public ProcessorResult process(DictionaryValidationResult result, Object value, ValidCharactersConstraint constraint, AttributeValueReader attributeValueReader) throws AttributeValidationException { 061 062 return new ProcessorResult(processSingleValidCharacterConstraint(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 ValidCharactersConstraint.class; 076 } 077 078 079 protected ConstraintValidationResult processSingleValidCharacterConstraint(DictionaryValidationResult result, Object value, ValidCharactersConstraint constraint, AttributeValueReader attributeValueReader) throws AttributeValidationException { 080 081 if (constraint == null) 082 return result.addNoConstraint(attributeValueReader, CONSTRAINT_NAME); 083 084 if (ValidationUtils.isNullOrEmpty(value)) 085 return result.addSkipped(attributeValueReader, CONSTRAINT_NAME); 086 087 // This mix-in interface is here to allow some definitions to avoid the extra processing that goes on in KNS 088 // to decipher and validate things like date range strings -- something that looks like "02/02/2002..03/03/2003" 089 Constrainable definition = attributeValueReader.getDefinition(attributeValueReader.getAttributeName()); 090 if (definition instanceof Formatable) { 091 return doProcessFormattableValidCharConstraint(result, constraint, (Formatable)definition, value, attributeValueReader); 092 } 093 094 ConstraintValidationResult constraintValidationResult = doProcessValidCharConstraint(constraint, value); 095 if (constraintValidationResult == null) 096 return result.addSuccess(attributeValueReader, CONSTRAINT_NAME); 097 098 result.addConstraintValidationResult(attributeValueReader, constraintValidationResult); 099 constraintValidationResult.setConstraintLabelKey(constraint.getLabelKey()); 100 constraintValidationResult.setErrorParameters(constraint.getValidationMessageParamsArray()); 101 return constraintValidationResult; 102 } 103 104 protected ConstraintValidationResult doProcessFormattableValidCharConstraint(DictionaryValidationResult result, ValidCharactersConstraint validCharsConstraint, Formatable definition, Object value, AttributeValueReader attributeValueReader) throws AttributeValidationException { 105 String entryName = attributeValueReader.getEntryName(); 106 String attributeName = attributeValueReader.getAttributeName(); 107 108 // 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 109 List<String> parsedAttributeValues = attributeValueReader.getCleanSearchableValues(attributeName); 110 111 if (parsedAttributeValues != null) { 112 113 Class<?> formatterClass = null; 114 Boolean doValidateDateRangeOrder = null; 115 116 // 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 117 if (parsedAttributeValues.size() != 2) 118 doValidateDateRangeOrder = Boolean.FALSE; 119 120 // Use integer to iterate since we need to track which field we're looking at 121 for (int i=0;i<parsedAttributeValues.size();i++) { 122 String parsedAttributeValue = parsedAttributeValues.get(i); 123 124 ConstraintValidationResult constraintValidationResult = doProcessValidCharConstraint(validCharsConstraint, parsedAttributeValue); 125 126 // If this is an error then some non-null validation result will be returned 127 if (constraintValidationResult != null) { 128 constraintValidationResult.setConstraintLabelKey(validCharsConstraint.getLabelKey()); 129 constraintValidationResult.setErrorParameters(validCharsConstraint.getValidationMessageParamsArray()); 130 // 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 131 if (formatterClass == null) { 132 String formatterClassName = definition.getFormatterClass(); 133 if (formatterClassName != null) 134 formatterClass = ClassLoaderUtils.getClass(formatterClassName); 135 } 136 137 if (formatterClass != null) { 138 // Use the Boolean value being null to ensure we only do this once 139 if (doValidateDateRangeOrder == null) { 140 // 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) 141 doValidateDateRangeOrder = Boolean.valueOf(DateFormatter.class.isAssignableFrom(formatterClass) && StringUtils.contains(ValidationUtils.getString(value), SearchOperator 142 .BETWEEN.toString())); 143 } 144 145 constraintValidationResult = processFormatterValidation(result, formatterClass, entryName, attributeName, parsedAttributeValue, DATE_RANGE_ERROR_PREFIXES[i]); 146 147 if (constraintValidationResult != null) { 148 result.addConstraintValidationResult(attributeValueReader, constraintValidationResult); 149 return constraintValidationResult; 150 } 151 } else { 152 // Otherwise, just report the validation result (apparently the formatter can't provide any fall-through validation because it doesn't exist) 153 result.addConstraintValidationResult(attributeValueReader, constraintValidationResult); 154 return constraintValidationResult; 155 } 156 } 157 } 158 159 if (doValidateDateRangeOrder != null && doValidateDateRangeOrder.booleanValue()) { 160 ConstraintValidationResult dateOrderValidationResult = validateDateOrder(parsedAttributeValues.get(0), parsedAttributeValues.get(1), entryName, attributeName); 161 162 if (dateOrderValidationResult != null) { 163 result.addConstraintValidationResult(attributeValueReader, dateOrderValidationResult); 164 return dateOrderValidationResult; 165 } 166 } 167 168 return result.addSuccess(attributeValueReader, CONSTRAINT_NAME); 169 } 170 return result.addSkipped(attributeValueReader, CONSTRAINT_NAME); 171 } 172 173 protected ConstraintValidationResult doProcessValidCharConstraint(ValidCharactersConstraint validCharsConstraint, Object value) { 174 175 StringBuilder fieldValue = new StringBuilder(); 176 String validChars = validCharsConstraint.getValue(); 177 178 fieldValue.append(ValidationUtils.getString(value)); 179 180// int typIdx = validChars.indexOf(":"); 181// String processorType = "regex"; 182// if (-1 == typIdx) { 183// validChars = "[" + validChars + "]*"; 184// } else { 185// processorType = validChars.substring(0, typIdx); 186// validChars = validChars.substring(typIdx + 1); 187// } 188 189// if ("regex".equalsIgnoreCase(processorType) && !validChars.equals(".*")) { 190 if (!fieldValue.toString().matches(validChars)) { 191 ConstraintValidationResult constraintValidationResult = new ConstraintValidationResult(CONSTRAINT_NAME); 192 if (validCharsConstraint.getLabelKey() != null) { 193 // FIXME: This shouldn't surface label key itself to the user - it should look up the label key, but this needs to be implemented in Rice 194 constraintValidationResult.setError(RiceKeyConstants.ERROR_CUSTOM, validCharsConstraint.getLabelKey()); 195 return constraintValidationResult; 196 } 197 198 constraintValidationResult.setError(RiceKeyConstants.ERROR_INVALID_FORMAT, fieldValue.toString()); 199 constraintValidationResult.setConstraintLabelKey(validCharsConstraint.getLabelKey()); 200 constraintValidationResult.setErrorParameters(validCharsConstraint.getValidationMessageParamsArray()); 201 return constraintValidationResult; 202 } 203// } 204 205 return null; 206 } 207 208 protected ConstraintValidationResult processFormatterValidation(DictionaryValidationResult result, Class<?> formatterClass, String entryName, String attributeName, String parsedAttributeValue, String errorKeyPrefix) { 209 210 boolean isError = false; 211 212 try { 213 Method validatorMethod = formatterClass.getDeclaredMethod(VALIDATE_METHOD, new Class<?>[] {String.class}); 214 Object o = validatorMethod.invoke(formatterClass.newInstance(), parsedAttributeValue); 215 if (o instanceof Boolean) { 216 isError = !((Boolean)o).booleanValue(); 217 } 218 } catch (Exception e) { 219 if ( LOG.isDebugEnabled() ) 220 LOG.debug(e.getMessage(), e); 221 222 isError = true; 223 } 224 225 if (isError) { 226 String errorMessageKey = getDataDictionaryService().getAttributeValidatingErrorMessageKey(entryName, attributeName); 227 String[] errorMessageParameters = getDataDictionaryService().getAttributeValidatingErrorMessageParameters(entryName, attributeName); 228 229 ConstraintValidationResult constraintValidationResult = new ConstraintValidationResult(CONSTRAINT_NAME); 230 constraintValidationResult.setEntryName(entryName); 231 constraintValidationResult.setAttributeName(errorKeyPrefix + attributeName); 232 constraintValidationResult.setError(errorMessageKey, errorMessageParameters); 233 234 return constraintValidationResult; 235 } 236 237 return null; 238 } 239 240 protected ConstraintValidationResult validateDateOrder(String firstDateTime, String secondDateTime, String entryName, String attributeName) { 241 // this means that we only have 2 values and it's a date range. 242 java.sql.Timestamp lVal = null; 243 java.sql.Timestamp uVal = null; 244 try { 245 lVal = CoreApiServiceLocator.getDateTimeService().convertToSqlTimestamp(firstDateTime); 246 uVal = CoreApiServiceLocator.getDateTimeService().convertToSqlTimestamp(secondDateTime); 247 } catch (Exception ex){ 248 // this shouldn't happen because the tests passed above. 249 String errorMessageKey = KRADServiceLocatorWeb.getDataDictionaryService().getAttributeValidatingErrorMessageKey(entryName, attributeName); 250 String[] errorMessageParameters = KRADServiceLocatorWeb.getDataDictionaryService().getAttributeValidatingErrorMessageParameters(entryName, attributeName); 251 ConstraintValidationResult constraintValidationResult = new ConstraintValidationResult(CONSTRAINT_NAME); 252 constraintValidationResult.setEntryName(entryName); 253 constraintValidationResult.setAttributeName( 254 KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + attributeName); 255 constraintValidationResult.setError(errorMessageKey, errorMessageParameters); 256 return constraintValidationResult; 257 } 258 259 if (lVal != null && lVal.compareTo(uVal) > 0){ // check the bounds 260 String errorMessageKey = KRADServiceLocatorWeb.getDataDictionaryService().getAttributeValidatingErrorMessageKey(entryName, attributeName); 261 String[] errorMessageParameters = KRADServiceLocatorWeb.getDataDictionaryService().getAttributeValidatingErrorMessageParameters(entryName, attributeName); 262 ConstraintValidationResult constraintValidationResult = new ConstraintValidationResult(CONSTRAINT_NAME); 263 constraintValidationResult.setEntryName(entryName); 264 constraintValidationResult.setAttributeName( 265 KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + attributeName); 266 constraintValidationResult.setError(errorMessageKey + ".range", errorMessageParameters); 267 return constraintValidationResult; 268 } 269 270 return null; 271 } 272 273}