001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.service.impl;
017
018import org.apache.commons.beanutils.PropertyUtils;
019import org.apache.commons.lang.ArrayUtils;
020import org.apache.commons.lang.StringUtils;
021import org.kuali.rice.core.api.mo.common.active.MutableInactivatable;
022import org.kuali.rice.core.api.util.RiceKeyConstants;
023import org.kuali.rice.krad.bo.BusinessObject;
024import org.kuali.rice.krad.bo.PersistableBusinessObject;
025import org.kuali.rice.krad.datadictionary.AttributeDefinition;
026import org.kuali.rice.krad.datadictionary.CollectionDefinition;
027import org.kuali.rice.krad.datadictionary.ComplexAttributeDefinition;
028import org.kuali.rice.krad.datadictionary.DataDictionaryEntry;
029import org.kuali.rice.krad.datadictionary.DataDictionaryEntryBase;
030import org.kuali.rice.krad.datadictionary.DataObjectEntry;
031import org.kuali.rice.krad.datadictionary.ReferenceDefinition;
032import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException;
033import org.kuali.rice.krad.datadictionary.validation.AttributeValueReader;
034import org.kuali.rice.krad.datadictionary.validation.DictionaryObjectAttributeValueReader;
035import org.kuali.rice.krad.datadictionary.validation.ErrorLevel;
036import org.kuali.rice.krad.datadictionary.validation.SingleAttributeValueReader;
037import org.kuali.rice.krad.datadictionary.validation.capability.Constrainable;
038import org.kuali.rice.krad.datadictionary.validation.constraint.Constraint;
039import org.kuali.rice.krad.datadictionary.validation.constraint.provider.ConstraintProvider;
040import org.kuali.rice.krad.datadictionary.validation.processor.CollectionConstraintProcessor;
041import org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor;
042import org.kuali.rice.krad.datadictionary.validation.result.ConstraintValidationResult;
043import org.kuali.rice.krad.datadictionary.validation.result.DictionaryValidationResult;
044import org.kuali.rice.krad.datadictionary.validation.result.ProcessorResult;
045import org.kuali.rice.krad.document.Document;
046import org.kuali.rice.krad.document.TransactionalDocument;
047import org.kuali.rice.krad.exception.ObjectNotABusinessObjectRuntimeException;
048import org.kuali.rice.krad.service.BusinessObjectService;
049import org.kuali.rice.krad.service.DataDictionaryService;
050import org.kuali.rice.krad.service.DictionaryValidationService;
051import org.kuali.rice.krad.service.DocumentDictionaryService;
052import org.kuali.rice.krad.service.KRADServiceLocatorInternal;
053import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
054import org.kuali.rice.krad.service.PersistenceService;
055import org.kuali.rice.krad.service.PersistenceStructureService;
056import org.kuali.rice.krad.util.GlobalVariables;
057import org.kuali.rice.krad.util.MessageMap;
058import org.kuali.rice.krad.util.ObjectUtils;
059import org.kuali.rice.krad.workflow.service.WorkflowAttributePropertyResolutionService;
060
061import java.beans.PropertyDescriptor;
062import java.lang.reflect.InvocationTargetException;
063import java.util.Arrays;
064import java.util.Collection;
065import java.util.IdentityHashMap;
066import java.util.Iterator;
067import java.util.LinkedList;
068import java.util.List;
069import java.util.Map;
070import java.util.Queue;
071import java.util.Set;
072
073/**
074 * Validates Documents, Business Objects, and Attributes against the data dictionary. Including min, max lengths, and
075 * validating expressions. This is the default, Kuali delivered implementation.
076 *
077 * KULRICE - 3355 Modified to prevent infinite looping (to maxDepth) scenario when a parent references a child which
078 * references a parent
079 *
080 * @author Kuali Rice Team (rice.collab@kuali.org)
081 */
082public class DictionaryValidationServiceImpl implements DictionaryValidationService {
083    private static org.apache.log4j.Logger LOG =
084            org.apache.log4j.Logger.getLogger(DictionaryValidationServiceImpl.class);
085
086    /**
087     * Constant defines a validation method for an attribute value.
088     * <p>Value is "validate"
089     */
090    public static final String VALIDATE_METHOD = "validate";
091
092    protected DataDictionaryService dataDictionaryService;
093    protected BusinessObjectService businessObjectService;
094    protected PersistenceService persistenceService;
095    protected DocumentDictionaryService documentDictionaryService;
096    protected WorkflowAttributePropertyResolutionService workflowAttributePropertyResolutionService;
097    protected PersistenceStructureService persistenceStructureService;
098
099    @SuppressWarnings("unchecked")
100    private List<CollectionConstraintProcessor> collectionConstraintProcessors;
101    @SuppressWarnings("unchecked")
102    private List<ConstraintProvider> constraintProviders;
103    @SuppressWarnings("unchecked")
104    private List<ConstraintProcessor> elementConstraintProcessors;
105
106    /**
107     * creates a new IdentitySet.
108     *
109     * @return a new Set
110     */
111    protected final Set<BusinessObject> newIdentitySet() {
112        return java.util.Collections.newSetFromMap(new IdentityHashMap<BusinessObject, Boolean>());
113    }
114
115    /**
116     * @see org.kuali.rice.krad.service.DictionaryValidationService#validate(java.lang.Object)
117     */
118    public DictionaryValidationResult validate(Object object) {
119        return validate(object, object.getClass().getName(), true);
120    }
121
122    /**
123     * @see org.kuali.rice.krad.service.DictionaryValidationService#validate(java.lang.Object, boolean)
124     */
125    public DictionaryValidationResult validate(Object object, boolean doOptionalProcessing) {
126        return validate(object, object.getClass().getName(), doOptionalProcessing);
127    }
128
129    /**
130     * @see org.kuali.rice.krad.service.DictionaryValidationService#validate(java.lang.Object, java.lang.String)
131     */
132    public DictionaryValidationResult validate(Object object, String entryName) {
133        return validate(object, entryName, true);
134    }
135
136    /**
137     * @see org.kuali.rice.krad.service.DictionaryValidationService#validate(java.lang.Object, java.lang.String,
138     *      boolean)
139     */
140    public DictionaryValidationResult validate(Object object, String entryName, boolean doOptionalProcessing) {
141        return validate(object, entryName, (String) null, doOptionalProcessing);
142    }
143
144    /**
145     * @see org.kuali.rice.krad.service.DictionaryValidationService#validate(java.lang.Object, java.lang.String,
146     *      java.lang.String)
147     */
148    public DictionaryValidationResult validate(Object object, String entryName, String attributeName) {
149        return validate(object, entryName, attributeName, true);
150    }
151
152    /**
153     * @see org.kuali.rice.krad.service.DictionaryValidationService#validate(java.lang.Object, java.lang.String,
154     *      java.lang.String, boolean)
155     */
156    public DictionaryValidationResult validate(Object object, String entryName, String attributeName,
157            boolean doOptionalProcessing) {
158        DataDictionaryEntry entry = getDataDictionaryService().getDataDictionary().getDictionaryObjectEntry(entryName);
159        AttributeValueReader attributeValueReader = new DictionaryObjectAttributeValueReader(object, entryName, entry);
160        attributeValueReader.setAttributeName(attributeName);
161        return validate(attributeValueReader, doOptionalProcessing);
162    }
163
164    public DictionaryValidationResult validate(Object object, String entryName, DataDictionaryEntry entry,
165            boolean doOptionalProcessing) {
166        AttributeValueReader attributeValueReader = new DictionaryObjectAttributeValueReader(object, entryName, entry);
167        return validate(attributeValueReader, doOptionalProcessing);
168    }
169
170    public void validate(String entryName, String attributeName, Object attributeValue) {
171        validate(entryName, attributeName, attributeValue, true);
172    }
173
174    public void validate(String entryName, String attributeName, Object attributeValue, boolean doOptionalProcessing) {
175        AttributeDefinition attributeDefinition =
176                getDataDictionaryService().getAttributeDefinition(entryName, attributeName);
177
178        if (attributeDefinition == null) {
179            // FIXME: JLR - this is what the code was doing effectively already, but seems weird not to throw an exception here if you try to validate
180            // something that doesn't have an attribute definition
181            return;
182        }
183
184        SingleAttributeValueReader attributeValueReader =
185                new SingleAttributeValueReader(attributeValue, entryName, attributeName, attributeDefinition);
186        validate(attributeValueReader, doOptionalProcessing);
187    }
188
189    /**
190     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDocument(org.kuali.rice.krad.document.Document)
191     */
192    @Override
193        public void validateDocument(Document document) {
194        String documentEntryName = document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName();
195
196        validate(document, documentEntryName);
197    }
198
199    /**
200     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDocumentAttribute(org.kuali.rice.krad.document.Document,
201     *      java.lang.String, java.lang.String)
202     */
203    @Override
204        public void validateDocumentAttribute(Document document, String attributeName, String errorPrefix) {
205        String documentEntryName = document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName();
206
207        validate(document, documentEntryName, attributeName, true);
208    }
209
210    /**
211     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDocumentAndUpdatableReferencesRecursively(org.kuali.rice.krad.document.Document, int, boolean)
212     */
213    @Override
214    public void validateDocumentAndUpdatableReferencesRecursively(Document document, int maxDepth,
215            boolean validateRequired) {
216        validateDocumentAndUpdatableReferencesRecursively(document, maxDepth, validateRequired, false);
217    }
218    /**
219     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDocumentAndUpdatableReferencesRecursively(org.kuali.rice.krad.document.Document, int, boolean, boolean)
220     */
221    @Override
222    public void validateDocumentAndUpdatableReferencesRecursively(Document document, int maxDepth, 
223            boolean validateRequired, boolean chompLastLetterSFromCollectionName) {
224        String documentEntryName = document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName();
225        validate(document, documentEntryName, validateRequired);
226
227        if (maxDepth > 0) {
228            validateUpdatabableReferencesRecursively(document, maxDepth - 1, validateRequired,
229                    chompLastLetterSFromCollectionName, newIdentitySet());
230        }
231    }
232
233    protected void validateUpdatabableReferencesRecursively(BusinessObject businessObject, int maxDepth,
234            boolean validateRequired, boolean chompLastLetterSFromCollectionName, Set<BusinessObject> processedBOs) {
235        // if null or already processed, return
236        if (ObjectUtils.isNull(businessObject) || processedBOs.contains(businessObject)) {
237            return;
238        }
239        processedBOs.add(businessObject);  // add bo to list to prevent excessive looping
240        Map<String, Class> references =
241                persistenceStructureService.listReferenceObjectFields(businessObject.getClass());
242        for (String referenceName : references.keySet()) {
243            if (persistenceStructureService.isReferenceUpdatable(businessObject.getClass(), referenceName)) {
244                Object referenceObj = ObjectUtils.getPropertyValue(businessObject, referenceName);
245
246                if (ObjectUtils.isNull(referenceObj) || !(referenceObj instanceof PersistableBusinessObject)) {
247                    continue;
248                }
249
250                BusinessObject referenceBusinessObject = (BusinessObject) referenceObj;
251                GlobalVariables.getMessageMap().addToErrorPath(referenceName);
252                validateBusinessObject(referenceBusinessObject, validateRequired);
253                if (maxDepth > 0) {
254                    validateUpdatabableReferencesRecursively(referenceBusinessObject, maxDepth - 1, validateRequired,
255                            chompLastLetterSFromCollectionName, processedBOs);
256                }
257                GlobalVariables.getMessageMap().removeFromErrorPath(referenceName);
258            }
259        }
260        Map<String, Class> collections =
261                persistenceStructureService.listCollectionObjectTypes(businessObject.getClass());
262        for (String collectionName : collections.keySet()) {
263            if (persistenceStructureService.isCollectionUpdatable(businessObject.getClass(), collectionName)) {
264                Object listObj = ObjectUtils.getPropertyValue(businessObject, collectionName);
265
266                if (ObjectUtils.isNull(listObj)) {
267                    continue;
268                }
269
270                if (!(listObj instanceof List)) {
271                    if (LOG.isInfoEnabled()) {
272                        LOG.info("The reference named " + collectionName + " of BO class " +
273                                businessObject.getClass().getName() +
274                                " should be of type java.util.List to be validated properly.");
275                    }
276                    continue;
277                }
278
279                List list = (List) listObj;
280
281                //should we materialize the proxied collection or just skip validation here assuming an unmaterialized objects are valid?
282                ObjectUtils.materializeObjects(list);
283
284                for (int i = 0; i < list.size(); i++) {
285                    final Object o = list.get(i);
286                    if (ObjectUtils.isNotNull(o) && o instanceof PersistableBusinessObject) {
287                        final BusinessObject element = (BusinessObject) o;
288
289                        final String errorPathAddition;
290                        if (chompLastLetterSFromCollectionName) {
291                            errorPathAddition =
292                                    StringUtils.chomp(collectionName, "s") + "[" + Integer.toString(i) + "]";
293                        } else {
294                            errorPathAddition = collectionName + "[" + Integer.toString(i) + "]";
295                        }
296
297                        GlobalVariables.getMessageMap().addToErrorPath(errorPathAddition);
298                        validateBusinessObject(element, validateRequired);
299                        if (maxDepth > 0) {
300                            validateUpdatabableReferencesRecursively(element, maxDepth - 1, validateRequired,
301                                    chompLastLetterSFromCollectionName, processedBOs);
302                        }
303                        GlobalVariables.getMessageMap().removeFromErrorPath(errorPathAddition);
304                    }
305                }
306            }
307        }
308    }
309
310    /**
311     * @see org.kuali.rice.krad.service.DictionaryValidationService#isBusinessObjectValid(org.kuali.rice.krad.bo.BusinessObject)
312     */
313    public boolean isBusinessObjectValid(BusinessObject businessObject) {
314        return isBusinessObjectValid(businessObject, null);
315    }
316
317    /**
318     * @see org.kuali.rice.krad.service.DictionaryValidationService#isBusinessObjectValid(org.kuali.rice.krad.bo.BusinessObject,
319     *      String)
320     */
321    public boolean isBusinessObjectValid(BusinessObject businessObject, String prefix) {
322        final MessageMap errorMap = GlobalVariables.getMessageMap();
323        int originalErrorCount = errorMap.getErrorCount();
324
325        errorMap.addToErrorPath(prefix);
326        validateBusinessObject(businessObject);
327        errorMap.removeFromErrorPath(prefix);
328
329        return errorMap.getErrorCount() == originalErrorCount;
330    }
331
332    /**
333     * @param businessObject - business object to validate
334     */
335    public void validateBusinessObjectsRecursively(BusinessObject businessObject, int depth) {
336        if (ObjectUtils.isNull(businessObject)) {
337            return;
338        }
339
340        // validate primitives and any specific bo validation
341        validateBusinessObject(businessObject);
342
343        // call method to recursively find business objects and validate
344        validateBusinessObjectsFromDescriptors(businessObject,
345                PropertyUtils.getPropertyDescriptors(businessObject.getClass()), depth);
346    }
347
348    /**
349     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateBusinessObject(org.kuali.rice.krad.bo.BusinessObject)
350     */
351    @Override
352    public void validateBusinessObject(BusinessObject businessObject) {
353        validateBusinessObject(businessObject, true);
354    }
355
356    /**
357     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateBusinessObject(org.kuali.rice.krad.bo.BusinessObject,
358     *      boolean)
359     */
360    @Override
361    public void validateBusinessObject(BusinessObject businessObject, boolean validateRequired) {
362        if (ObjectUtils.isNull(businessObject)) {
363            return;
364        }
365
366        validate(businessObject, businessObject.getClass().getName(), validateRequired);
367    }
368
369    /**
370     * iterates through the property descriptors looking for business objects or lists of business objects. calls
371     * validate method
372     * for each bo found
373     *
374     * @param object
375     * @param propertyDescriptors
376     */
377    protected void validateBusinessObjectsFromDescriptors(Object object, PropertyDescriptor[] propertyDescriptors,
378            int depth) {
379        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
380            // validate the properties that are descended from BusinessObject
381            if (propertyDescriptor.getPropertyType() != null &&
382                    PersistableBusinessObject.class.isAssignableFrom(propertyDescriptor.getPropertyType()) &&
383                    ObjectUtils.getPropertyValue(object, propertyDescriptor.getName()) != null) {
384                BusinessObject bo = (BusinessObject) ObjectUtils.getPropertyValue(object, propertyDescriptor.getName());
385                if (depth == 0) {
386                    GlobalVariables.getMessageMap().addToErrorPath(propertyDescriptor.getName());
387                    validateBusinessObject(bo);
388                    GlobalVariables.getMessageMap().removeFromErrorPath(propertyDescriptor.getName());
389                } else {
390                    validateBusinessObjectsRecursively(bo, depth - 1);
391                }
392            }
393
394            /*
395             * if property is a List, then walk the list and do the validation on each contained object that is a descendent of
396             * BusinessObject
397             */
398            else if (propertyDescriptor.getPropertyType() != null &&
399                    (List.class).isAssignableFrom(propertyDescriptor.getPropertyType()) &&
400                    ObjectUtils.getPropertyValue(object, propertyDescriptor.getName()) != null) {
401                List propertyList = (List) ObjectUtils.getPropertyValue(object, propertyDescriptor.getName());
402                for (int j = 0; j < propertyList.size(); j++) {
403                    if (propertyList.get(j) != null && propertyList.get(j) instanceof PersistableBusinessObject) {
404                        if (depth == 0) {
405                            GlobalVariables.getMessageMap().addToErrorPath(
406                                    StringUtils.chomp(propertyDescriptor.getName(), "s") + "[" +
407                                            (new Integer(j)).toString() + "]");
408                            validateBusinessObject((BusinessObject) propertyList.get(j));
409                            GlobalVariables.getMessageMap().removeFromErrorPath(
410                                    StringUtils.chomp(propertyDescriptor.getName(), "s") + "[" +
411                                            (new Integer(j)).toString() + "]");
412                        } else {
413                            validateBusinessObjectsRecursively((BusinessObject) propertyList.get(j), depth - 1);
414                        }
415                    }
416                }
417            }
418        }
419    }
420
421    /**
422     * calls validate format and required check for the given propertyDescriptor
423     *
424     * @param entryName
425     * @param object
426     * @param propertyDescriptor
427     * @param errorPrefix
428     * @deprecated since 1.1
429     */
430    @Deprecated
431    public void validatePrimitiveFromDescriptor(String entryName, Object object, PropertyDescriptor propertyDescriptor,
432            String errorPrefix, boolean validateRequired) {
433
434        // validate the primitive attributes if defined in the dictionary
435        if (null != propertyDescriptor) {
436            validate(object, entryName, propertyDescriptor.getName(), validateRequired);
437        }
438    }
439
440    /**
441     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateReferenceExists(org.kuali.rice.krad.bo.BusinessObject,
442     *      org.kuali.rice.krad.datadictionary.ReferenceDefinition)
443     */
444    public boolean validateReferenceExists(BusinessObject bo, ReferenceDefinition reference) {
445        return validateReferenceExists(bo, reference.getAttributeName());
446    }
447
448    /**
449     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateReferenceExists(org.kuali.rice.krad.bo.BusinessObject,
450     *      java.lang.String)
451     */
452    public boolean validateReferenceExists(BusinessObject bo, String referenceName) {
453
454        // attempt to retrieve the specified object from the db
455        BusinessObject referenceBo = businessObjectService.getReferenceIfExists(bo, referenceName);
456
457        // if it isn't there, then it doesn't exist, return false
458        if (ObjectUtils.isNotNull(referenceBo)) {
459            return true;
460        }
461
462        // otherwise, it is there, return true
463        return false;
464    }
465
466    /**
467     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateReferenceIsActive(org.kuali.rice.krad.bo.BusinessObject,
468     *      org.kuali.rice.krad.datadictionary.ReferenceDefinition)
469     */
470    public boolean validateReferenceIsActive(BusinessObject bo, ReferenceDefinition reference) {
471        return validateReferenceIsActive(bo, reference.getAttributeName());
472    }
473
474    /**
475     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateReferenceIsActive(org.kuali.rice.krad.bo.BusinessObject, String)
476     */
477    public boolean validateReferenceIsActive(BusinessObject bo, String referenceName) {
478
479        // attempt to retrieve the specified object from the db
480        BusinessObject referenceBo = businessObjectService.getReferenceIfExists(bo, referenceName);
481        if (referenceBo == null) {
482            return false;
483        }
484        if (!(referenceBo instanceof MutableInactivatable) || ((MutableInactivatable) referenceBo).isActive()) {
485            return true;
486        }
487
488        return false;
489    }
490
491    /**
492     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateReferenceExistsAndIsActive(org.kuali.rice.krad.bo.BusinessObject,
493     *      org.kuali.rice.krad.datadictionary.ReferenceDefinition)
494     */
495    public boolean validateReferenceExistsAndIsActive(BusinessObject bo, ReferenceDefinition reference) {
496        boolean success = true;
497        // intelligently use the fieldname from the reference, or get it out
498        // of the dataDictionaryService
499        String displayFieldName;
500        if (reference.isDisplayFieldNameSet()) {
501            displayFieldName = reference.getDisplayFieldName();
502        } else {
503            Class<?> boClass =
504                    reference.isCollectionReference() ? reference.getCollectionBusinessObjectClass() : bo.getClass();
505            displayFieldName =
506                    dataDictionaryService.getAttributeLabel(boClass, reference.getAttributeToHighlightOnFail());
507        }
508
509        if (reference.isCollectionReference()) {
510            success = validateCollectionReferenceExistsAndIsActive(bo, reference, displayFieldName,
511                    StringUtils.split(reference.getCollection(), "."), null);
512        } else {
513            success = validateReferenceExistsAndIsActive(bo, reference.getAttributeName(),
514                    reference.getAttributeToHighlightOnFail(), displayFieldName);
515        }
516        return success;
517    }
518
519    /**
520     * @param bo the object to get the collection from
521     * @param reference the <code>ReferenceDefinition</code> of the collection to validate
522     * @param displayFieldName the name of the field
523     * @param intermediateCollections array containing the path to the collection as tokens
524     * @param pathToAttributeI the rebuilt path to the ReferenceDefinition.attributeToHighlightOnFail which includes the
525     * index of
526     * each subcollection
527     * @return
528     */
529    private boolean validateCollectionReferenceExistsAndIsActive(BusinessObject bo, ReferenceDefinition reference,
530            String displayFieldName, String[] intermediateCollections, String pathToAttributeI) {
531        boolean success = true;
532        Collection<PersistableBusinessObject> referenceCollection;
533        String collectionName = intermediateCollections[0];
534        // remove current collection from intermediates
535        intermediateCollections = (String[]) ArrayUtils.removeElement(intermediateCollections, collectionName);
536        try {
537            referenceCollection = (Collection) PropertyUtils.getProperty(bo, collectionName);
538        } catch (Exception e) {
539            throw new RuntimeException(e);
540        }
541        int pos = 0;
542        Iterator<PersistableBusinessObject> iterator = referenceCollection.iterator();
543        while (iterator.hasNext()) {
544            String pathToAttribute =
545                    StringUtils.defaultString(pathToAttributeI) + collectionName + "[" + (pos++) + "].";
546            // keep drilling down until we reach the nested collection we want
547            if (intermediateCollections.length > 0) {
548                success &= validateCollectionReferenceExistsAndIsActive(iterator.next(), reference, displayFieldName,
549                        intermediateCollections, pathToAttribute);
550            } else {
551                String attributeToHighlightOnFail = pathToAttribute + reference.getAttributeToHighlightOnFail();
552                success &= validateReferenceExistsAndIsActive(iterator.next(), reference.getAttributeName(),
553                        attributeToHighlightOnFail, displayFieldName);
554            }
555        }
556
557        return success;
558    }
559
560    /**
561     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateReferenceExistsAndIsActive(org.kuali.rice.krad.bo.BusinessObject, String, String, String)
562     */
563
564    public boolean validateReferenceExistsAndIsActive(BusinessObject bo, String referenceName,
565            String attributeToHighlightOnFail, String displayFieldName) {
566
567        // if we're dealing with a nested attribute, we need to resolve down to the BO where the primitive attribute is located
568        // this is primarily to deal with the case of a defaultExistenceCheck that uses an "extension", i.e referenceName
569        // would be extension.attributeName
570        if (ObjectUtils.isNestedAttribute(referenceName)) {
571            String nestedAttributePrefix = ObjectUtils.getNestedAttributePrefix(referenceName);
572            String nestedAttributePrimitive = ObjectUtils.getNestedAttributePrimitive(referenceName);
573            Object nestedObject = ObjectUtils.getPropertyValue(bo, nestedAttributePrefix);
574            if (!(nestedObject instanceof BusinessObject)) {
575                throw new ObjectNotABusinessObjectRuntimeException(
576                        "Attribute requested (" + nestedAttributePrefix + ") is of class: " + "'" +
577                                nestedObject.getClass().getName() + "' and is not a " +
578                                "descendent of BusinessObject.");
579            }
580            return validateReferenceExistsAndIsActive((BusinessObject) nestedObject, nestedAttributePrimitive,
581                    attributeToHighlightOnFail, displayFieldName);
582        }
583
584        boolean success = true;
585        boolean exists;
586        boolean active;
587
588        boolean fkFieldsPopulated = true;
589        // need to check for DD relationship FKs
590        List<String> fkFields =
591                getDataDictionaryService().getRelationshipSourceAttributes(bo.getClass().getName(), referenceName);
592        if (fkFields != null) {
593            for (String fkFieldName : fkFields) {
594                Object fkFieldValue = null;
595                try {
596                    fkFieldValue = PropertyUtils.getProperty(bo, fkFieldName);
597                }
598                // if we cant retrieve the field value, then
599                // it doesnt have a value
600                catch (IllegalAccessException e) {
601                    fkFieldsPopulated = false;
602                } catch (InvocationTargetException e) {
603                    fkFieldsPopulated = false;
604                } catch (NoSuchMethodException e) {
605                    fkFieldsPopulated = false;
606                }
607
608                // test the value
609                if (fkFieldValue == null) {
610                    fkFieldsPopulated = false;
611                } else if (String.class.isAssignableFrom(fkFieldValue.getClass())) {
612                    if (StringUtils.isBlank((String) fkFieldValue)) {
613                        fkFieldsPopulated = false;
614                    }
615                }
616            }
617        } else if (bo instanceof PersistableBusinessObject) { // if no DD relationship exists, check the persistence service
618            fkFieldsPopulated = persistenceService
619                    .allForeignKeyValuesPopulatedForReference((PersistableBusinessObject) bo, referenceName);
620        }
621
622        // only bother if all the fk fields have values
623        if (fkFieldsPopulated) {
624
625            // do the existence test
626            exists = validateReferenceExists(bo, referenceName);
627            if (exists) {
628
629                // do the active test, if appropriate
630                if (!(bo instanceof MutableInactivatable) || ((MutableInactivatable) bo).isActive()) {
631                    active = validateReferenceIsActive(bo, referenceName);
632                    if (!active) {
633                        GlobalVariables.getMessageMap()
634                                .putError(attributeToHighlightOnFail, RiceKeyConstants.ERROR_INACTIVE,
635                                        displayFieldName);
636                        success &= false;
637                    }
638                }
639            } else {
640                GlobalVariables.getMessageMap()
641                        .putError(attributeToHighlightOnFail, RiceKeyConstants.ERROR_EXISTENCE, displayFieldName);
642                success &= false;
643            }
644        }
645        return success;
646    }
647
648    /**
649     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDefaultExistenceChecks(org.kuali.rice.krad.bo.BusinessObject)
650     */
651    public boolean validateDefaultExistenceChecks(BusinessObject bo) {
652        boolean success = true;
653
654        // get a collection of all the referenceDefinitions setup for this object
655        Collection references = getDocumentDictionaryService().getDefaultExistenceChecks(bo.getClass());
656
657        // walk through the references, doing the tests on each
658        for (Iterator iter = references.iterator(); iter.hasNext(); ) {
659            ReferenceDefinition reference = (ReferenceDefinition) iter.next();
660
661            // do the existence and validation testing
662            success &= validateReferenceExistsAndIsActive(bo, reference);
663        }
664        return success;
665    }
666
667    /**
668     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDefaultExistenceChecksForNewCollectionItem(org.kuali.rice.krad.bo.BusinessObject,
669     *      org.kuali.rice.krad.bo.BusinessObject, java.lang.String)
670     */
671    public boolean validateDefaultExistenceChecksForNewCollectionItem(BusinessObject bo,
672            BusinessObject newCollectionItem, String collectionName) {
673        boolean success = true;
674
675        if (StringUtils.isNotBlank(collectionName)) {
676            // get a collection of all the referenceDefinitions setup for this object
677            Collection references = getDocumentDictionaryService().getDefaultExistenceChecks(bo.getClass());
678
679            // walk through the references, doing the tests on each
680            for (Iterator iter = references.iterator(); iter.hasNext(); ) {
681                ReferenceDefinition reference = (ReferenceDefinition) iter.next();
682                if (collectionName != null && collectionName.equals(reference.getCollection())) {
683                    String displayFieldName;
684                    if (reference.isDisplayFieldNameSet()) {
685                        displayFieldName = reference.getDisplayFieldName();
686                    } else {
687                        Class boClass =
688                                reference.isCollectionReference() ? reference.getCollectionBusinessObjectClass() :
689                                        bo.getClass();
690                        displayFieldName = dataDictionaryService
691                                .getAttributeLabel(boClass, reference.getAttributeToHighlightOnFail());
692                    }
693
694                    success &= validateReferenceExistsAndIsActive(newCollectionItem, reference.getAttributeName(),
695                            reference.getAttributeToHighlightOnFail(), displayFieldName);
696                }
697            }
698        }
699
700        return success;
701    }
702
703    /**
704     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDefaultExistenceChecksForTransDoc(org.kuali.rice.krad.document.TransactionalDocument)
705     */
706    public boolean validateDefaultExistenceChecksForTransDoc(TransactionalDocument document) {
707        boolean success = true;
708
709        // get a collection of all the referenceDefinitions setup for this object
710        Collection references = getDocumentDictionaryService().getDefaultExistenceChecks(document);
711
712        // walk through the references, doing the tests on each
713        for (Iterator iter = references.iterator(); iter.hasNext(); ) {
714            ReferenceDefinition reference = (ReferenceDefinition) iter.next();
715
716            // do the existence and validation testing
717            success &= validateReferenceExistsAndIsActive(document, reference);
718        }
719        return success;
720    }
721
722    /**
723     * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDefaultExistenceChecksForNewCollectionItem(org.kuali.rice.krad.document.TransactionalDocument, org.kuali.rice.krad.bo.BusinessObject, String)
724     */
725    public boolean validateDefaultExistenceChecksForNewCollectionItem(TransactionalDocument document,
726            BusinessObject newCollectionItem, String collectionName) {
727        boolean success = true;
728        if (StringUtils.isNotBlank(collectionName)) {
729            // get a collection of all the referenceDefinitions setup for this object
730            Collection references = getDocumentDictionaryService().getDefaultExistenceChecks(document);
731
732            // walk through the references, doing the tests on each
733            for (Iterator iter = references.iterator(); iter.hasNext(); ) {
734                ReferenceDefinition reference = (ReferenceDefinition) iter.next();
735                if (collectionName != null && collectionName.equals(reference.getCollection())) {
736                    String displayFieldName;
737                    if (reference.isDisplayFieldNameSet()) {
738                        displayFieldName = reference.getDisplayFieldName();
739                    } else {
740                        Class boClass =
741                                reference.isCollectionReference() ? reference.getCollectionBusinessObjectClass() :
742                                        document.getClass();
743                        displayFieldName = dataDictionaryService
744                                .getAttributeLabel(boClass, reference.getAttributeToHighlightOnFail());
745                    }
746
747                    success &= validateReferenceExistsAndIsActive(newCollectionItem, reference.getAttributeName(),
748                            reference.getAttributeToHighlightOnFail(), displayFieldName);
749                }
750            }
751        }
752        return success;
753    }
754
755    /*
756    * 1.1 validation methods
757    */
758
759    /*
760      * This is the top-level validation method for all attribute value readers
761      */
762    public DictionaryValidationResult validate(AttributeValueReader valueReader, boolean doOptionalProcessing) {
763        DictionaryValidationResult result = new DictionaryValidationResult();
764
765        if (valueReader.getAttributeName() == null) {
766            validateObject(result, valueReader, doOptionalProcessing, true);
767        } else {
768            validateAttribute(result, valueReader, doOptionalProcessing);
769        }
770
771        if (result.getNumberOfErrors() > 0) {
772            for (Iterator<ConstraintValidationResult> iterator = result.iterator(); iterator.hasNext(); ) {
773                ConstraintValidationResult constraintValidationResult = iterator.next();
774                if (constraintValidationResult.getStatus().getLevel() >= ErrorLevel.WARN.getLevel()){                    
775                    String attributePath = constraintValidationResult.getAttributePath();
776                    if (attributePath == null || attributePath.isEmpty()){
777                        attributePath = constraintValidationResult.getAttributeName();
778                    }
779                    if(constraintValidationResult.getConstraintLabelKey() != null){
780                        GlobalVariables.getMessageMap().putError(attributePath,
781                                constraintValidationResult.getConstraintLabelKey(),
782                                constraintValidationResult.getErrorParameters());
783                    }
784                    else{
785                        GlobalVariables.getMessageMap().putError(attributePath,
786                                constraintValidationResult.getErrorKey(),
787                                constraintValidationResult.getErrorParameters());
788                    }
789                }
790            }
791        }
792
793        return result;
794    }
795
796    private void processElementConstraints(DictionaryValidationResult result, Object value, Constrainable definition,
797            AttributeValueReader attributeValueReader, boolean doOptionalProcessing) {
798        processConstraints(result, elementConstraintProcessors, value, definition, attributeValueReader,
799                doOptionalProcessing);
800    }
801
802    private void processCollectionConstraints(DictionaryValidationResult result, Collection<?> collection,
803            Constrainable definition, AttributeValueReader attributeValueReader, boolean doOptionalProcessing) {
804        processConstraints(result, collectionConstraintProcessors, collection, definition, attributeValueReader,
805                doOptionalProcessing);
806    }
807
808    @SuppressWarnings("unchecked")
809    private void processConstraints(DictionaryValidationResult result,
810            List<? extends ConstraintProcessor> constraintProcessors, Object value, Constrainable definition,
811            AttributeValueReader attributeValueReader, boolean doOptionalProcessing) {
812        //TODO: Implement custom validators
813
814        if (constraintProcessors != null) {
815            Constrainable selectedDefinition = definition;
816            AttributeValueReader selectedAttributeValueReader = attributeValueReader;
817
818            // First - take the constrainable definition and get its constraints
819
820            Queue<Constraint> constraintQueue = new LinkedList<Constraint>();
821
822            // Using a for loop to iterate through constraint processors because ordering is important
823            for (ConstraintProcessor<Object, Constraint> processor : constraintProcessors) {
824
825                // Let the calling method opt out of any optional processing
826                if (!doOptionalProcessing && processor.isOptional()) {
827                    result.addSkipped(attributeValueReader, processor.getName());
828                    continue;
829                }
830
831                Class<? extends Constraint> constraintType = processor.getConstraintType();
832
833                // Add all of the constraints for this constraint type for all providers to the queue
834                for (ConstraintProvider constraintProvider : constraintProviders) {
835                    if (constraintProvider.isSupported(selectedDefinition)) {
836                        Collection<Constraint> constraintList =
837                                constraintProvider.getConstraints(selectedDefinition, constraintType);
838                        if (constraintList != null)
839                            constraintQueue.addAll(constraintList);
840                    }
841                }
842
843                // If there are no constraints provided for this definition, then just skip it
844                if (constraintQueue.isEmpty()) {
845                    result.addSkipped(attributeValueReader, processor.getName());
846                    continue;
847                }
848
849                Collection<Constraint> additionalConstraints = new LinkedList<Constraint>();
850
851                // This loop is functionally identical to a for loop, but it has the advantage of letting us keep the queue around
852                // and populate it with any new constraints contributed by the processor
853                while (!constraintQueue.isEmpty()) {
854
855                    Constraint constraint = constraintQueue.poll();
856
857                    // If this constraint is not one that this process handles, then skip and add to the queue for the next processor;
858                    // obviously this would be redundant (we're only looking at constraints that this processor can process) except that
859                    // the previous processor might have stuck a new constraint (or constraints) on the queue
860                    if (!constraintType.isInstance(constraint)) {
861                        result.addSkipped(attributeValueReader, processor.getName());
862                        additionalConstraints.add(constraint);
863                        continue;
864                    }
865
866                    ProcessorResult processorResult =
867                            processor.process(result, value, constraint, selectedAttributeValueReader);
868
869                    Collection<Constraint> processorResultContraints = processorResult.getConstraints();
870                    if (processorResultContraints != null && processorResultContraints.size() > 0)
871                        constraintQueue.addAll(processorResultContraints);
872
873                    // Change the selected definition to whatever was returned from the processor
874                    if (processorResult.isDefinitionProvided())
875                        selectedDefinition = processorResult.getDefinition();
876                    // Change the selected attribute value reader to whatever was returned from the processor
877                    if (processorResult.isAttributeValueReaderProvided())
878                        selectedAttributeValueReader = processorResult.getAttributeValueReader();
879
880                }
881
882                // After iterating through all the constraints for this processor, add the ones that werent consumed by this processor to the queue
883                constraintQueue.addAll(additionalConstraints);
884            }
885        }
886    }
887
888    private void setFieldError(String entryName, String attributeName, String key, String... args) {
889        if (getDataDictionaryService() == null)
890            return;
891
892        String errorLabel = getDataDictionaryService().getAttributeErrorLabel(entryName, attributeName);
893        // FIXME: There's got to be a cleaner way of doing this.
894        List<String> list = new LinkedList<String>();
895        list.add(errorLabel);
896        list.addAll(Arrays.asList(args));
897        String[] array = new String[list.size()];
898        array = list.toArray(array);
899        GlobalVariables.getMessageMap().putError(attributeName, key, array);
900    }
901
902    private void validateAttribute(DictionaryValidationResult result, AttributeValueReader attributeValueReader,
903            boolean checkIfRequired) throws AttributeValidationException {
904        Constrainable definition = attributeValueReader.getDefinition(attributeValueReader.getAttributeName());
905        validateAttribute(result, definition, attributeValueReader, checkIfRequired);
906    }
907
908    private void validateAttribute(DictionaryValidationResult result, Constrainable definition,
909            AttributeValueReader attributeValueReader, boolean checkIfRequired) throws AttributeValidationException {
910
911        if (definition == null)
912            throw new AttributeValidationException(
913                    "Unable to validate constraints for attribute \"" + attributeValueReader.getAttributeName() +
914                            "\" on entry \"" + attributeValueReader.getEntryName() +
915                            "\" because no attribute definition can be found.");
916        
917        Object value = attributeValueReader.getValue();
918
919        processElementConstraints(result, value, definition, attributeValueReader, checkIfRequired);
920    }
921
922    private void validateObject(DictionaryValidationResult result, AttributeValueReader attributeValueReader, 
923            boolean doOptionalProcessing, boolean processAttributes) throws AttributeValidationException {
924
925        // If the entry itself is constrainable then the attribute value reader will return it here and we'll need to check if it has any constraints
926        Constrainable objectEntry = attributeValueReader.getEntry();
927        processElementConstraints(result, attributeValueReader.getObject(), objectEntry, attributeValueReader,
928                doOptionalProcessing);
929
930        List<Constrainable> definitions = attributeValueReader.getDefinitions();
931
932        // Exit if the attribute value reader has no child definitions
933        if (null == definitions)
934            return;
935
936        //Process all attribute definitions (unless being skipped)
937        if (processAttributes){
938            for (Constrainable definition : definitions) {
939                String attributeName = definition.getName();
940                attributeValueReader.setAttributeName(attributeName);
941
942                if (attributeValueReader.isReadable()) {
943                    Object value = attributeValueReader.getValue(attributeName);
944
945                    processElementConstraints(result, value, definition, attributeValueReader, doOptionalProcessing);
946                }
947            }
948        }
949
950        //Process any constraints that may be defined on complex attributes
951        if (objectEntry instanceof DataDictionaryEntryBase) {
952            List<ComplexAttributeDefinition> complexAttrDefinitions =
953                    ((DataDictionaryEntryBase) objectEntry).getComplexAttributes();
954
955            if (complexAttrDefinitions != null) {
956                for (ComplexAttributeDefinition complexAttrDefinition : complexAttrDefinitions) {
957                    String attributeName = complexAttrDefinition.getName();
958                    attributeValueReader.setAttributeName(attributeName);
959
960                    if (attributeValueReader.isReadable()) {
961                        Object value = attributeValueReader.getValue();
962
963                        DataDictionaryEntry childEntry = complexAttrDefinition.getDataObjectEntry();
964                        if (value != null) {
965                            AttributeValueReader nestedAttributeValueReader = new DictionaryObjectAttributeValueReader(
966                                    value, childEntry.getFullClassName(), childEntry, attributeValueReader.getPath());
967                            nestedAttributeValueReader.setAttributeName(attributeValueReader.getAttributeName());
968                            //Validate nested object, however skip attribute definition porcessing on
969                            //nested object entry, since they have already been processed above.
970                            validateObject(result, nestedAttributeValueReader, doOptionalProcessing, false);
971                        }
972
973                        processElementConstraints(result, value, complexAttrDefinition, attributeValueReader,
974                                doOptionalProcessing);
975                    }
976                }
977            }
978        }
979
980        //FIXME: I think we may want to use a new CollectionConstrainable interface instead to obtain from
981        //DictionaryObjectAttributeValueReader
982        DataObjectEntry entry = (DataObjectEntry) attributeValueReader.getEntry();
983        if (entry != null) {
984            for (CollectionDefinition collectionDefinition : entry.getCollections()) {
985                //TODO: Do we need to be able to handle simple collections (ie. String, etc)
986
987                String childEntryName = collectionDefinition.getDataObjectClass();
988                String attributeName = collectionDefinition.getName();
989                attributeValueReader.setAttributeName(attributeName);
990
991                if (attributeValueReader.isReadable()) {
992                    Collection<?> collectionObject = attributeValueReader.getValue();
993                    DataDictionaryEntry childEntry = childEntryName != null ?
994                            getDataDictionaryService().getDataDictionary().getDictionaryObjectEntry(childEntryName) :
995                            null;
996                    if (collectionObject != null) {
997                        int index = 0;
998                        for (Object value : collectionObject) {
999                            //NOTE: This path is only correct for collections that guarantee order
1000                            String objectAttributePath = attributeValueReader.getPath() + "[" + index + "]";
1001
1002                            //FIXME: It's inefficient to be creating new attribute reader for each item in collection
1003                            AttributeValueReader nestedAttributeValueReader = new DictionaryObjectAttributeValueReader(
1004                                    value, childEntryName, childEntry, objectAttributePath);
1005                            validateObject(result, nestedAttributeValueReader, doOptionalProcessing, true);
1006                            index++;
1007                        }
1008                    }
1009
1010                    processCollectionConstraints(result, collectionObject, collectionDefinition, attributeValueReader,
1011                            doOptionalProcessing);
1012                }
1013            }
1014        }
1015    }
1016
1017    /**
1018     * @return Returns the dataDictionaryService.
1019     */
1020    public DataDictionaryService getDataDictionaryService() {
1021        return dataDictionaryService;
1022    }
1023
1024    /**
1025     * @param dataDictionaryService The dataDictionaryService to set.
1026     */
1027    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
1028        this.dataDictionaryService = dataDictionaryService;
1029    }
1030
1031    /**
1032     * Sets the businessObjectService attribute value.
1033     *
1034     * @param businessObjectService The businessObjectService to set.
1035     */
1036    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
1037        this.businessObjectService = businessObjectService;
1038    }
1039
1040    /**
1041     * Sets the persistenceService attribute value.
1042     *
1043     * @param persistenceService The persistenceService to set.
1044     */
1045    public void setPersistenceService(PersistenceService persistenceService) {
1046        this.persistenceService = persistenceService;
1047    }
1048
1049    public void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) {
1050        this.persistenceStructureService = persistenceStructureService;
1051    }
1052
1053    protected WorkflowAttributePropertyResolutionService getWorkflowAttributePropertyResolutionService() {
1054        if (workflowAttributePropertyResolutionService == null) {
1055            workflowAttributePropertyResolutionService =
1056                    KRADServiceLocatorInternal.getWorkflowAttributePropertyResolutionService();
1057        }
1058        return workflowAttributePropertyResolutionService;
1059    }
1060
1061    /**
1062     * @return the collectionConstraintProcessors
1063     */
1064    @SuppressWarnings("unchecked")
1065    public List<CollectionConstraintProcessor> getCollectionConstraintProcessors() {
1066        return this.collectionConstraintProcessors;
1067    }
1068
1069    /**
1070     * @param collectionConstraintProcessors the collectionConstraintProcessors to set
1071     */
1072    @SuppressWarnings("unchecked")
1073    public void setCollectionConstraintProcessors(List<CollectionConstraintProcessor> collectionConstraintProcessors) {
1074        this.collectionConstraintProcessors = collectionConstraintProcessors;
1075    }
1076
1077    /**
1078     * @return the constraintProviders
1079     */
1080    @SuppressWarnings("unchecked")
1081    public List<ConstraintProvider> getConstraintProviders() {
1082        return this.constraintProviders;
1083    }
1084
1085    /**
1086     * @param constraintProviders the constraintProviders to set
1087     */
1088    @SuppressWarnings("unchecked")
1089    public void setConstraintProviders(List<ConstraintProvider> constraintProviders) {
1090        this.constraintProviders = constraintProviders;
1091    }
1092
1093    /**
1094     * @return the elementConstraintProcessors
1095     */
1096    @SuppressWarnings("unchecked")
1097    public List<ConstraintProcessor> getElementConstraintProcessors() {
1098        return this.elementConstraintProcessors;
1099    }
1100
1101    /**
1102     * @param elementConstraintProcessors the elementConstraintProcessors to set
1103     */
1104    @SuppressWarnings("unchecked")
1105    public void setElementConstraintProcessors(List<ConstraintProcessor> elementConstraintProcessors) {
1106        this.elementConstraintProcessors = elementConstraintProcessors;
1107    }
1108
1109    public DocumentDictionaryService getDocumentDictionaryService() {
1110        if (documentDictionaryService == null) {
1111            this.documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
1112        }
1113        return documentDictionaryService;
1114    }
1115
1116    public void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
1117        this.documentDictionaryService = documentDictionaryService;
1118    }
1119}