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