001    /**
002     * Copyright 2005-2012 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     */
016    package org.kuali.rice.kns.service.impl;
017    
018    import org.apache.commons.beanutils.PropertyUtils;
019    import org.apache.commons.lang.StringUtils;
020    import org.kuali.rice.core.api.CoreApiServiceLocator;
021    import org.kuali.rice.core.api.util.RiceKeyConstants;
022    import org.kuali.rice.core.api.util.type.TypeUtils;
023    import org.kuali.rice.core.framework.persistence.jdbc.sql.SQLUtils;
024    import org.kuali.rice.core.web.format.DateFormatter;
025    import org.kuali.rice.kns.datadictionary.MaintainableFieldDefinition;
026    import org.kuali.rice.kns.datadictionary.MaintainableItemDefinition;
027    import org.kuali.rice.kns.datadictionary.MaintainableSectionDefinition;
028    import org.kuali.rice.kns.datadictionary.MaintenanceDocumentEntry;
029    import org.kuali.rice.kns.service.DictionaryValidationService;
030    import org.kuali.rice.kns.service.KNSServiceLocator;
031    import org.kuali.rice.krad.bo.BusinessObject;
032    import org.kuali.rice.krad.datadictionary.control.ControlDefinition;
033    import org.kuali.rice.krad.document.Document;
034    import org.kuali.rice.krad.util.GlobalVariables;
035    import org.kuali.rice.krad.util.KRADConstants;
036    import org.kuali.rice.krad.util.ObjectUtils;
037    
038    import java.beans.PropertyDescriptor;
039    import java.lang.reflect.Method;
040    import java.math.BigDecimal;
041    import java.util.List;
042    import java.util.regex.Pattern;
043    
044    /**
045     * @author Kuali Rice Team (rice.collab@kuali.org)
046     */
047    @Deprecated
048    public class DictionaryValidationServiceImpl extends org.kuali.rice.krad.service.impl.DictionaryValidationServiceImpl implements DictionaryValidationService {
049        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(
050                DictionaryValidationServiceImpl.class);
051    
052        /**
053         * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDocumentAndUpdatableReferencesRecursively(org.kuali.rice.krad.document.Document, int, boolean, boolean)
054         * @deprecated since 2.1
055         */
056        @Override
057        @Deprecated
058        public void validateDocumentAndUpdatableReferencesRecursively(Document document, int maxDepth,
059                boolean validateRequired, boolean chompLastLetterSFromCollectionName) {
060            // Use the KNS validation code here -- this overrides the behavior in the krad version which calls validate(...)
061            validateBusinessObject(document, validateRequired);
062    
063            if (maxDepth > 0) {
064                validateUpdatabableReferencesRecursively(document, maxDepth - 1, validateRequired,
065                        chompLastLetterSFromCollectionName, newIdentitySet());
066            }
067        }
068    
069        /**
070         * @see org.kuali.rice.kns.service.DictionaryValidationService#validateDocumentRecursively(org.kuali.rice.krad.document.Document, int)
071         * @deprecated since 2.0
072         */
073        @Deprecated
074        @Override
075        public void validateDocumentRecursively(Document document, int depth) {
076            // validate primitives of document
077            validateDocument(document);
078    
079            // call method to recursively find business objects and validate
080            validateBusinessObjectsFromDescriptors(document, PropertyUtils.getPropertyDescriptors(document.getClass()),
081                    depth);
082        }
083    
084        /**
085         * @see org.kuali.rice.kns.service.DictionaryValidationService#validateDocument(org.kuali.rice.krad.document.Document)
086         * @param document - document to validate
087         * @deprecated since 2.1.2
088         */
089        @Deprecated
090        @Override
091        public void validateDocument(Document document) {
092            String documentEntryName = document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName();
093    
094            validatePrimitivesFromDescriptors(documentEntryName, document, PropertyUtils.getPropertyDescriptors(document.getClass()), "", true);
095        }
096    
097        @Override
098        @Deprecated
099        public void validateBusinessObject(BusinessObject businessObject) {
100            validateBusinessObject(businessObject, true);
101        }
102    
103        @Override
104        @Deprecated
105        public void validateBusinessObject(BusinessObject businessObject, boolean validateRequired) {
106            if (ObjectUtils.isNull(businessObject)) {
107                return;
108            }
109            try {
110                // validate the primitive attributes of the bo
111                validatePrimitivesFromDescriptors(businessObject.getClass().getName(), businessObject,
112                        PropertyUtils.getPropertyDescriptors(businessObject.getClass()), "", validateRequired);
113            } catch (RuntimeException e) {
114                LOG.error(String.format("Exception while validating %s", businessObject.getClass().getName()), e);
115                throw e;
116            }
117        }
118    
119        /**
120         * @deprecated since 1.1
121         */
122        @Deprecated
123        @Override
124        public void validateBusinessObjectOnMaintenanceDocument(BusinessObject businessObject, String docTypeName) {
125            MaintenanceDocumentEntry entry =
126                    KNSServiceLocator.getMaintenanceDocumentDictionaryService().getMaintenanceDocumentEntry(docTypeName);
127            for (MaintainableSectionDefinition sectionDefinition : entry.getMaintainableSections()) {
128                validateBusinessObjectOnMaintenanceDocumentHelper(businessObject, sectionDefinition.getMaintainableItems(),
129                        "");
130            }
131        }
132    
133        protected void validateBusinessObjectOnMaintenanceDocumentHelper(BusinessObject businessObject,
134                List<? extends MaintainableItemDefinition> itemDefinitions, String errorPrefix) {
135            for (MaintainableItemDefinition itemDefinition : itemDefinitions) {
136                if (itemDefinition instanceof MaintainableFieldDefinition) {
137                    if (getDataDictionaryService().isAttributeDefined(businessObject.getClass(),
138                            itemDefinition.getName())) {
139                        Object value = ObjectUtils.getPropertyValue(businessObject, itemDefinition.getName());
140                        if (value != null && StringUtils.isNotBlank(value.toString())) {
141                            Class propertyType = ObjectUtils.getPropertyType(businessObject, itemDefinition.getName(),
142                                    persistenceStructureService);
143                            if (TypeUtils.isStringClass(propertyType) ||
144                                    TypeUtils.isIntegralClass(propertyType) ||
145                                    TypeUtils.isDecimalClass(propertyType) ||
146                                    TypeUtils.isTemporalClass(propertyType)) {
147                                // check value format against dictionary
148                                if (!TypeUtils.isTemporalClass(propertyType)) {
149                                    validateAttributeFormat(businessObject.getClass().getName(), itemDefinition.getName(),
150                                            value.toString(), errorPrefix + itemDefinition.getName());
151                                }
152                            }
153                        }
154                    }
155                }
156            }
157        }
158    
159        /**
160         * iterates through property descriptors looking for primitives types, calls validate format and required check
161         *
162         * @param entryName
163         * @param object
164         * @param propertyDescriptors
165         * @param errorPrefix
166         */
167        @Deprecated
168        protected void validatePrimitivesFromDescriptors(String entryName, Object object,
169                PropertyDescriptor[] propertyDescriptors, String errorPrefix, boolean validateRequired) {
170            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
171                validatePrimitiveFromDescriptor(entryName, object, propertyDescriptor, errorPrefix, validateRequired);
172            }
173        }
174    
175        /**
176         * calls validate format and required check for the given propertyDescriptor
177         *
178         * @param entryName
179         * @param object
180         * @param propertyDescriptor
181         * @param errorPrefix
182         */
183        @Override
184        @Deprecated
185        public void validatePrimitiveFromDescriptor(String entryName, Object object, PropertyDescriptor propertyDescriptor,
186                String errorPrefix, boolean validateRequired) {
187            // validate the primitive attributes if defined in the dictionary
188            if (null != propertyDescriptor && getDataDictionaryService().isAttributeDefined(entryName,
189                    propertyDescriptor.getName())) {
190                Object value = ObjectUtils.getPropertyValue(object, propertyDescriptor.getName());
191                Class propertyType = propertyDescriptor.getPropertyType();
192    
193                if (TypeUtils.isStringClass(propertyType) ||
194                        TypeUtils.isIntegralClass(propertyType) ||
195                        TypeUtils.isDecimalClass(propertyType) ||
196                        TypeUtils.isTemporalClass(propertyType)) {
197    
198                    // check value format against dictionary
199                    if (value != null && StringUtils.isNotBlank(value.toString())) {
200                        if (!TypeUtils.isTemporalClass(propertyType)) {
201                            validateAttributeFormat(entryName, propertyDescriptor.getName(), value.toString(),
202                                    errorPrefix + propertyDescriptor.getName());
203                        }
204                    } else if (validateRequired) {
205                        validateAttributeRequired(entryName, propertyDescriptor.getName(), value, Boolean.FALSE,
206                                errorPrefix + propertyDescriptor.getName());
207                    }
208                }
209            }
210        }
211    
212        /**
213         * @see org.kuali.rice.kns.service.DictionaryValidationService#validateAttributeFormat(String, String, String, String)
214         *      objectClassName is the docTypeName
215         * @deprecated since 1.1
216         */
217        @Override
218        @Deprecated
219        public void validateAttributeFormat(String objectClassName, String attributeName, String attributeInValue,
220                String errorKey) {
221            // Retrieve the field's data type, or set to the string data type if an exception occurs when retrieving the class or the DD entry.
222            String attributeDataType = null;
223            try {
224                attributeDataType = getWorkflowAttributePropertyResolutionService().determineFieldDataType(
225                        (Class<? extends BusinessObject>) Class.forName(
226                                getDataDictionaryService().getDataDictionary().getDictionaryObjectEntry(objectClassName)
227                                        .getFullClassName()), attributeName);
228            } catch (ClassNotFoundException e) {
229                attributeDataType = KRADConstants.DATA_TYPE_STRING;
230            } catch (NullPointerException e) {
231                attributeDataType = KRADConstants.DATA_TYPE_STRING;
232            }
233    
234            validateAttributeFormat(objectClassName, attributeName, attributeInValue, attributeDataType, errorKey);
235        }
236    
237        /**
238         * The attributeDataType parameter should be one of the data types specified by the SearchableAttribute
239         * interface; will default to DATA_TYPE_STRING if a data type other than the ones from SearchableAttribute
240         * is specified.
241         *
242         * @deprecated since 1.1
243         */
244        @Override
245        @Deprecated
246        public void validateAttributeFormat(String objectClassName, String attributeName, String attributeInValue,
247                String attributeDataType, String errorKey) {
248            boolean checkDateBounds = false; // this is used so we can check date bounds
249            Class<?> formatterClass = null;
250    
251            if (LOG.isDebugEnabled()) {
252                LOG.debug("(bo, attributeName, attributeValue) = (" + objectClassName + "," + attributeName + "," +
253                        attributeInValue + ")");
254            }
255    
256            /*
257            *  This will return a list of searchable attributes. so if the value is
258            *  12/07/09 .. 12/08/09 it will return [12/07/09,12/08/09]
259            */
260    
261            final List<String> attributeValues = SQLUtils.getCleanedSearchableValues(attributeInValue, attributeDataType);
262    
263            if (attributeValues == null || attributeValues.isEmpty()) {
264                return;
265            }
266    
267            for (String attributeValue : attributeValues) {
268    
269                // FIXME: JLR : Replacing this logic with KS-style validation is trickier, since KS validation requires a DataProvider object that can
270                // look back and find other attribute values aside from the one we're working on.
271                // Also - the date stuff below is implemented very differently.
272                //validator.validateAttributeField(businessObject, fieldName);
273    
274                if (StringUtils.isNotBlank(attributeValue)) {
275                    Integer minLength = getDataDictionaryService().getAttributeMinLength(objectClassName, attributeName);
276                    if ((minLength != null) && (minLength.intValue() > attributeValue.length())) {
277                        String errorLabel = getDataDictionaryService().getAttributeErrorLabel(objectClassName,
278                                attributeName);
279                        GlobalVariables.getMessageMap().putError(errorKey, RiceKeyConstants.ERROR_MIN_LENGTH,
280                                new String[]{errorLabel, minLength.toString()});
281                        return;
282                    }
283                    Integer maxLength = getDataDictionaryService().getAttributeMaxLength(objectClassName, attributeName);
284                    if ((maxLength != null) && (maxLength.intValue() < attributeValue.length())) {
285                        String errorLabel = getDataDictionaryService().getAttributeErrorLabel(objectClassName,
286                                attributeName);
287                        GlobalVariables.getMessageMap().putError(errorKey, RiceKeyConstants.ERROR_MAX_LENGTH,
288                                new String[]{errorLabel, maxLength.toString()});
289                        return;
290                    }
291                    Pattern validationExpression = getDataDictionaryService().getAttributeValidatingExpression(
292                            objectClassName, attributeName);
293                    if (validationExpression != null && !validationExpression.pattern().equals(".*")) {
294                        if (LOG.isDebugEnabled()) {
295                            LOG.debug("(bo, attributeName, validationExpression) = (" + objectClassName + "," +
296                                    attributeName + "," + validationExpression + ")");
297                        }
298    
299                        if (!validationExpression.matcher(attributeValue).matches()) {
300                            // Retrieving formatter class
301                            if (formatterClass == null) {
302                                // this is just a cache check... all dates ranges get called twice
303                                formatterClass = getDataDictionaryService().getAttributeFormatter(objectClassName,
304                                        attributeName);
305                            }
306    
307                            if (formatterClass != null) {
308                                boolean valuesAreValid = true;
309                                boolean isError = true;
310                                String errorKeyPrefix = "";
311                                try {
312    
313                                    // this is a special case for date ranges in order to set the proper error message
314                                    if (DateFormatter.class.isAssignableFrom(formatterClass)) {
315                                        String[] values = attributeInValue.split("\\.\\."); // is it a range
316                                        if (values.length == 2 &&
317                                                attributeValues.size() == 2) { // make sure it's not like a .. b | c
318                                            checkDateBounds = true; // now we need to check that a <= b
319                                            if (attributeValues.indexOf(attributeValue) ==
320                                                    0) { // only care about lower bound
321                                                errorKeyPrefix = KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX;
322                                            }
323                                        }
324                                    }
325    
326                                    Method validatorMethod = formatterClass.getDeclaredMethod(VALIDATE_METHOD,
327                                            new Class<?>[]{String.class});
328                                    Object o = validatorMethod.invoke(formatterClass.newInstance(), attributeValue);
329                                    if (o instanceof Boolean) {
330                                        isError = !((Boolean) o).booleanValue();
331                                    }
332                                    valuesAreValid &= !isError;
333                                } catch (Exception e) {
334                                    if (LOG.isDebugEnabled()) {
335                                        LOG.debug(e.getMessage(), e);
336                                    }
337                                    isError = true;
338                                    valuesAreValid = false;
339                                }
340                                if (isError) {
341                                    checkDateBounds = false; // it's already invalid, no need to check date bounds
342                                    String errorMessageKey =
343                                            getDataDictionaryService().getAttributeValidatingErrorMessageKey(
344                                                    objectClassName, attributeName);
345                                    String[] errorMessageParameters =
346                                            getDataDictionaryService().getAttributeValidatingErrorMessageParameters(
347                                                    objectClassName, attributeName);
348                                    GlobalVariables.getMessageMap().putError(errorKeyPrefix + errorKey, errorMessageKey,
349                                            errorMessageParameters);
350                                }
351                            } else {
352                                // if it fails the default validation and has no formatter class then it's still a std failure.
353                                String errorMessageKey = getDataDictionaryService().getAttributeValidatingErrorMessageKey(
354                                        objectClassName, attributeName);
355                                String[] errorMessageParameters =
356                                        getDataDictionaryService().getAttributeValidatingErrorMessageParameters(
357                                                objectClassName, attributeName);
358                                GlobalVariables.getMessageMap().putError(errorKey, errorMessageKey, errorMessageParameters);
359                            }
360                        }
361                    }
362                    /*BigDecimal*/
363                    String exclusiveMin = getDataDictionaryService().getAttributeExclusiveMin(objectClassName,
364                            attributeName);
365                    if (exclusiveMin != null) {
366                        try {
367                            BigDecimal exclusiveMinBigDecimal = new BigDecimal(exclusiveMin);
368                            if (exclusiveMinBigDecimal.compareTo(new BigDecimal(attributeValue)) >= 0) {
369                                String errorLabel = getDataDictionaryService().getAttributeErrorLabel(objectClassName,
370                                        attributeName);
371                                GlobalVariables.getMessageMap().putError(errorKey, RiceKeyConstants.ERROR_EXCLUSIVE_MIN,
372                                        // todo: Formatter for currency?
373                                        new String[]{errorLabel, exclusiveMin.toString()});
374                                return;
375                            }
376                        } catch (NumberFormatException e) {
377                            // quash; this indicates that the DD contained a min for a non-numeric attribute
378                        }
379                    }
380                    /*BigDecimal*/
381                    String inclusiveMax = getDataDictionaryService().getAttributeInclusiveMax(objectClassName,
382                            attributeName);
383                    if (inclusiveMax != null) {
384                        try {
385                            BigDecimal inclusiveMaxBigDecimal = new BigDecimal(inclusiveMax);
386                            if (inclusiveMaxBigDecimal.compareTo(new BigDecimal(attributeValue)) < 0) {
387                                String errorLabel = getDataDictionaryService().getAttributeErrorLabel(objectClassName,
388                                        attributeName);
389                                GlobalVariables.getMessageMap().putError(errorKey, RiceKeyConstants.ERROR_INCLUSIVE_MAX,
390                                        // todo: Formatter for currency?
391                                        new String[]{errorLabel, inclusiveMax.toString()});
392                                return;
393                            }
394                        } catch (NumberFormatException e) {
395                            // quash; this indicates that the DD contained a max for a non-numeric attribute
396                        }
397                    }
398                }
399            }
400    
401            if (checkDateBounds) {
402                // this means that we only have 2 values and it's a date range.
403                java.sql.Timestamp lVal = null;
404                java.sql.Timestamp uVal = null;
405                try {
406                    lVal = CoreApiServiceLocator.getDateTimeService().convertToSqlTimestamp(attributeValues.get(0));
407                    uVal = CoreApiServiceLocator.getDateTimeService().convertToSqlTimestamp(attributeValues.get(1));
408                } catch (Exception ex) {
409                    // this shouldn't happen because the tests passed above.
410                    String errorMessageKey = getDataDictionaryService().getAttributeValidatingErrorMessageKey(
411                            objectClassName, attributeName);
412                    String[] errorMessageParameters =
413                            getDataDictionaryService().getAttributeValidatingErrorMessageParameters(objectClassName,
414                                    attributeName);
415                    GlobalVariables.getMessageMap().putError(
416                            KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + errorKey, errorMessageKey,
417                            errorMessageParameters);
418                }
419    
420                if (lVal != null && lVal.compareTo(uVal) > 0) { // check the bounds
421                    String errorMessageKey = getDataDictionaryService().getAttributeValidatingErrorMessageKey(
422                            objectClassName, attributeName);
423                    String[] errorMessageParameters =
424                            getDataDictionaryService().getAttributeValidatingErrorMessageParameters(objectClassName,
425                                    attributeName);
426                    GlobalVariables.getMessageMap().putError(
427                            KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + errorKey, errorMessageKey + ".range",
428                            errorMessageParameters);
429                }
430            }
431        }
432    
433        // FIXME: JLR - this is now redundant and should be using the same code as the required processing elsewhere, but the control definition stuff doesn't really fit
434        // it doesn't seem to be used anywhere
435        @Override
436        @Deprecated
437        public void validateAttributeRequired(String objectClassName, String attributeName, Object attributeValue,
438                Boolean forMaintenance, String errorKey) {
439            // check if field is a required field for the business object
440            if (attributeValue == null || (attributeValue instanceof String && StringUtils.isBlank(
441                    (String) attributeValue))) {
442                Boolean required = getDataDictionaryService().isAttributeRequired(objectClassName, attributeName);
443                ControlDefinition controlDef = getDataDictionaryService().getAttributeControlDefinition(objectClassName,
444                        attributeName);
445    
446                if (required != null && required.booleanValue() && !(controlDef != null && controlDef.isHidden())) {
447    
448                    // get label of attribute for message
449                    String errorLabel = getDataDictionaryService().getAttributeErrorLabel(objectClassName, attributeName);
450                    GlobalVariables.getMessageMap().putError(errorKey, RiceKeyConstants.ERROR_REQUIRED, errorLabel);
451                }
452            }
453        }
454    }