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.kns.service.impl;
017
018import org.apache.commons.beanutils.PropertyUtils;
019import org.apache.commons.lang.StringUtils;
020import org.kuali.rice.core.api.CoreApiServiceLocator;
021import org.kuali.rice.core.api.util.RiceKeyConstants;
022import org.kuali.rice.core.api.util.type.TypeUtils;
023import org.kuali.rice.core.framework.persistence.jdbc.sql.SQLUtils;
024import org.kuali.rice.core.web.format.DateFormatter;
025import org.kuali.rice.kns.datadictionary.MaintainableFieldDefinition;
026import org.kuali.rice.kns.datadictionary.MaintainableItemDefinition;
027import org.kuali.rice.kns.datadictionary.MaintainableSectionDefinition;
028import org.kuali.rice.kns.datadictionary.MaintenanceDocumentEntry;
029import org.kuali.rice.kns.service.DictionaryValidationService;
030import org.kuali.rice.kns.service.KNSServiceLocator;
031import org.kuali.rice.krad.bo.BusinessObject;
032import org.kuali.rice.krad.datadictionary.control.ControlDefinition;
033import org.kuali.rice.krad.document.Document;
034import org.kuali.rice.krad.util.GlobalVariables;
035import org.kuali.rice.krad.util.KRADConstants;
036import org.kuali.rice.krad.util.ObjectUtils;
037
038import java.beans.PropertyDescriptor;
039import java.lang.reflect.Method;
040import java.math.BigDecimal;
041import java.util.List;
042import java.util.regex.Pattern;
043
044/**
045 * @author Kuali Rice Team (rice.collab@kuali.org)
046 */
047@Deprecated
048public 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}