View Javadoc

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