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}