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