View Javadoc

1   /**
2    * Copyright 2005-2013 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.CollectionDefinition;
26  import org.kuali.rice.krad.datadictionary.ComplexAttributeDefinition;
27  import org.kuali.rice.krad.datadictionary.DataDictionaryEntry;
28  import org.kuali.rice.krad.datadictionary.DataDictionaryEntryBase;
29  import org.kuali.rice.krad.datadictionary.DataObjectEntry;
30  import org.kuali.rice.krad.datadictionary.ReferenceDefinition;
31  import org.kuali.rice.krad.datadictionary.exception.AttributeValidationException;
32  import org.kuali.rice.krad.datadictionary.state.StateMapping;
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.capability.Constrainable;
37  import org.kuali.rice.krad.datadictionary.validation.constraint.Constraint;
38  import org.kuali.rice.krad.datadictionary.validation.constraint.provider.ConstraintProvider;
39  import org.kuali.rice.krad.datadictionary.validation.processor.CollectionConstraintProcessor;
40  import org.kuali.rice.krad.datadictionary.validation.processor.ConstraintProcessor;
41  import org.kuali.rice.krad.datadictionary.validation.result.ConstraintValidationResult;
42  import org.kuali.rice.krad.datadictionary.validation.result.DictionaryValidationResult;
43  import org.kuali.rice.krad.datadictionary.validation.result.ProcessorResult;
44  import org.kuali.rice.krad.document.Document;
45  import org.kuali.rice.krad.document.TransactionalDocument;
46  import org.kuali.rice.krad.exception.ObjectNotABusinessObjectRuntimeException;
47  import org.kuali.rice.krad.service.BusinessObjectService;
48  import org.kuali.rice.krad.service.DataDictionaryService;
49  import org.kuali.rice.krad.service.DictionaryValidationService;
50  import org.kuali.rice.krad.service.DocumentDictionaryService;
51  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
52  import org.kuali.rice.krad.service.PersistenceService;
53  import org.kuali.rice.krad.service.PersistenceStructureService;
54  import org.kuali.rice.krad.uif.UifConstants;
55  import org.kuali.rice.krad.uif.util.ConstraintStateUtils;
56  import org.kuali.rice.krad.util.ErrorMessage;
57  import org.kuali.rice.krad.util.GlobalVariables;
58  import org.kuali.rice.krad.util.MessageMap;
59  import org.kuali.rice.krad.util.ObjectUtils;
60  
61  import java.beans.PropertyDescriptor;
62  import java.lang.reflect.InvocationTargetException;
63  import java.util.Collection;
64  import java.util.IdentityHashMap;
65  import java.util.Iterator;
66  import java.util.LinkedList;
67  import java.util.List;
68  import java.util.Map;
69  import java.util.Queue;
70  import java.util.Set;
71  
72  /**
73   * Validates Documents, Business Objects, and Attributes against the data dictionary. Including min, max lengths, and
74   * validating expressions. This is the default, Kuali delivered implementation.
75   *
76   * KULRICE - 3355 Modified to prevent infinite looping (to maxDepth) scenario when a parent references a child which
77   * references a parent
78   *
79   * @author Kuali Rice Team (rice.collab@kuali.org)
80   */
81  public class DictionaryValidationServiceImpl implements DictionaryValidationService {
82      private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(
83              DictionaryValidationServiceImpl.class);
84  
85      /**
86       * Constant defines a validation method for an attribute value.
87       * <p>Value is "validate"
88       */
89      public static final String VALIDATE_METHOD = "validate";
90  
91      protected DataDictionaryService dataDictionaryService;
92      protected BusinessObjectService businessObjectService;
93      protected PersistenceService persistenceService;
94      protected DocumentDictionaryService documentDictionaryService;
95      protected PersistenceStructureService persistenceStructureService;
96  
97      @SuppressWarnings("unchecked")
98      private List<CollectionConstraintProcessor> collectionConstraintProcessors;
99      @SuppressWarnings("unchecked")
100     private List<ConstraintProvider> constraintProviders;
101     @SuppressWarnings("unchecked")
102     private List<ConstraintProcessor> elementConstraintProcessors;
103 
104     /**
105      * creates a new IdentitySet.
106      *
107      * @return a new Set
108      */
109     protected final Set<BusinessObject> newIdentitySet() {
110         return java.util.Collections.newSetFromMap(new IdentityHashMap<BusinessObject, Boolean>());
111     }
112 
113     /**
114      * @see org.kuali.rice.krad.service.DictionaryValidationService#validate(java.lang.Object)
115      */
116     public DictionaryValidationResult validate(Object object) {
117         return validate(object, object.getClass().getName(), (String) null, true);
118     }
119 
120     /**
121      * @see org.kuali.rice.krad.service.DictionaryValidationService#validate(java.lang.Object, java.lang.String,
122      *      java.lang.String, boolean)
123      */
124     public DictionaryValidationResult validate(Object object, String entryName, String attributeName,
125             boolean doOptionalProcessing) {
126         StateMapping stateMapping = null;
127         String validationState = null;
128         DataDictionaryEntry entry = getDataDictionaryService().getDataDictionary().getDictionaryObjectEntry(entryName);
129         if (entry != null) {
130             stateMapping = entry.getStateMapping();
131             if (stateMapping != null) {
132                 validationState = stateMapping.getCurrentState(object);
133             }
134         }
135 
136         AttributeValueReader attributeValueReader = new DictionaryObjectAttributeValueReader(object, entryName, entry);
137         attributeValueReader.setAttributeName(attributeName);
138         return validate(attributeValueReader, doOptionalProcessing, validationState, stateMapping);
139     }
140 
141     /**
142      * @see DictionaryValidationService#validateAgainstNextState(Object)
143      */
144     @Override
145     public DictionaryValidationResult validateAgainstNextState(Object object) {
146         String entryName = object.getClass().getName();
147         StateMapping stateMapping = null;
148         String validationState = null;
149         DataDictionaryEntry entry = getDataDictionaryService().getDataDictionary().getDictionaryObjectEntry(entryName);
150         if (entry != null) {
151             stateMapping = entry.getStateMapping();
152             if (stateMapping != null) {
153                 validationState = stateMapping.getNextState(object);
154             }
155         }
156         AttributeValueReader attributeValueReader = new DictionaryObjectAttributeValueReader(object, entryName, entry);
157         return validate(attributeValueReader, true, validationState, stateMapping);
158     }
159 
160     /**
161      * @see DictionaryValidationService#validateAgainstState(Object, String)
162      */
163     @Override
164     public DictionaryValidationResult validateAgainstState(Object object, String validationState) {
165         String entryName = object.getClass().getName();
166         StateMapping stateMapping = null;
167         DataDictionaryEntry entry = getDataDictionaryService().getDataDictionary().getDictionaryObjectEntry(entryName);
168         if (entry != null) {
169             stateMapping = entry.getStateMapping();
170             if (stateMapping != null && StringUtils.isBlank(validationState)) {
171                 validationState = stateMapping.getCurrentState(object);
172             }
173         }
174 
175         AttributeValueReader attributeValueReader = new DictionaryObjectAttributeValueReader(object, entryName, entry);
176         return validate(attributeValueReader, true, validationState, stateMapping);
177     }
178 
179     /**
180      * @see DictionaryValidationService#validate(Object, String, DataDictionaryEntry, boolean)
181      */
182     @Override
183     public DictionaryValidationResult validate(Object object, String entryName, DataDictionaryEntry entry,
184             boolean doOptionalProcessing) {
185         StateMapping stateMapping = null;
186         String validationState = null;
187         if (entry != null) {
188             stateMapping = entry.getStateMapping();
189             if (stateMapping != null) {
190                 validationState = stateMapping.getCurrentState(object);
191             }
192         }
193         AttributeValueReader attributeValueReader = new DictionaryObjectAttributeValueReader(object, entryName, entry);
194         return validate(attributeValueReader, doOptionalProcessing, validationState, stateMapping);
195     }
196 
197     /**
198      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDocument(org.kuali.rice.krad.document.Document)
199      */
200     @Override
201     public void validateDocument(Document document) {
202         String documentEntryName = document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName();
203 
204         validate(document, documentEntryName, (String) null, true);
205     }
206 
207     /**
208      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDocumentAttribute(org.kuali.rice.krad.document.Document,
209      *      java.lang.String, java.lang.String)
210      */
211     @Override
212     public void validateDocumentAttribute(Document document, String attributeName, String errorPrefix) {
213         String documentEntryName = document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName();
214 
215         validate(document, documentEntryName, attributeName, true);
216     }
217 
218     /**
219      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDocumentAndUpdatableReferencesRecursively(org.kuali.rice.krad.document.Document,
220      *      int, boolean)
221      */
222     @Override
223     public void validateDocumentAndUpdatableReferencesRecursively(Document document, int maxDepth,
224             boolean validateRequired) {
225         validateDocumentAndUpdatableReferencesRecursively(document, maxDepth, validateRequired, false);
226     }
227 
228     /**
229      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDocumentAndUpdatableReferencesRecursively(org.kuali.rice.krad.document.Document,
230      *      int, boolean, boolean)
231      */
232     @Override
233     public void validateDocumentAndUpdatableReferencesRecursively(Document document, int maxDepth,
234             boolean validateRequired, boolean chompLastLetterSFromCollectionName) {
235         String documentEntryName = document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName();
236         validate(document, documentEntryName, (String) null, true);
237 
238         if (maxDepth > 0) {
239             validateUpdatabableReferencesRecursively(document, maxDepth - 1, validateRequired,
240                     chompLastLetterSFromCollectionName, newIdentitySet());
241         }
242     }
243 
244     protected void validateUpdatabableReferencesRecursively(BusinessObject businessObject, int maxDepth,
245             boolean validateRequired, boolean chompLastLetterSFromCollectionName, Set<BusinessObject> processedBOs) {
246         // if null or already processed, return
247         if (ObjectUtils.isNull(businessObject) || processedBOs.contains(businessObject)) {
248             return;
249         }
250         processedBOs.add(businessObject);  // add bo to list to prevent excessive looping
251         Map<String, Class> references = persistenceStructureService.listReferenceObjectFields(
252                 businessObject.getClass());
253         for (String referenceName : references.keySet()) {
254             if (persistenceStructureService.isReferenceUpdatable(businessObject.getClass(), referenceName)) {
255                 Object referenceObj = ObjectUtils.getPropertyValue(businessObject, referenceName);
256 
257                 if (ObjectUtils.isNull(referenceObj) || !(referenceObj instanceof PersistableBusinessObject)) {
258                     continue;
259                 }
260 
261                 BusinessObject referenceBusinessObject = (BusinessObject) referenceObj;
262                 GlobalVariables.getMessageMap().addToErrorPath(referenceName);
263                 validateBusinessObject(referenceBusinessObject, validateRequired);
264                 if (maxDepth > 0) {
265                     validateUpdatabableReferencesRecursively(referenceBusinessObject, maxDepth - 1, validateRequired,
266                             chompLastLetterSFromCollectionName, processedBOs);
267                 }
268                 GlobalVariables.getMessageMap().removeFromErrorPath(referenceName);
269             }
270         }
271         Map<String, Class> collections = persistenceStructureService.listCollectionObjectTypes(
272                 businessObject.getClass());
273         for (String collectionName : collections.keySet()) {
274             if (persistenceStructureService.isCollectionUpdatable(businessObject.getClass(), collectionName)) {
275                 Object listObj = ObjectUtils.getPropertyValue(businessObject, collectionName);
276 
277                 if (ObjectUtils.isNull(listObj)) {
278                     continue;
279                 }
280 
281                 if (!(listObj instanceof List)) {
282                     if (LOG.isInfoEnabled()) {
283                         LOG.info("The reference named " + collectionName + " of BO class " +
284                                 businessObject.getClass().getName() +
285                                 " should be of type java.util.List to be validated properly.");
286                     }
287                     continue;
288                 }
289 
290                 List list = (List) listObj;
291 
292                 //should we materialize the proxied collection or just skip validation here assuming an unmaterialized objects are valid?
293                 ObjectUtils.materializeObjects(list);
294 
295                 for (int i = 0; i < list.size(); i++) {
296                     final Object o = list.get(i);
297                     if (ObjectUtils.isNotNull(o) && o instanceof PersistableBusinessObject) {
298                         final BusinessObject element = (BusinessObject) o;
299 
300                         final String errorPathAddition;
301                         if (chompLastLetterSFromCollectionName) {
302                             errorPathAddition = StringUtils.chomp(collectionName, "s")
303                                     + "["
304                                     + Integer.toString(i)
305                                     + "]";
306                         } else {
307                             errorPathAddition = collectionName + "[" + Integer.toString(i) + "]";
308                         }
309 
310                         GlobalVariables.getMessageMap().addToErrorPath(errorPathAddition);
311                         validateBusinessObject(element, validateRequired);
312                         if (maxDepth > 0) {
313                             validateUpdatabableReferencesRecursively(element, maxDepth - 1, validateRequired,
314                                     chompLastLetterSFromCollectionName, processedBOs);
315                         }
316                         GlobalVariables.getMessageMap().removeFromErrorPath(errorPathAddition);
317                     }
318                 }
319             }
320         }
321     }
322 
323     /**
324      * @see org.kuali.rice.krad.service.DictionaryValidationService#isBusinessObjectValid(org.kuali.rice.krad.bo.BusinessObject)
325      */
326     public boolean isBusinessObjectValid(BusinessObject businessObject) {
327         return isBusinessObjectValid(businessObject, null);
328     }
329 
330     /**
331      * @see org.kuali.rice.krad.service.DictionaryValidationService#isBusinessObjectValid(org.kuali.rice.krad.bo.BusinessObject,
332      *      String)
333      */
334     public boolean isBusinessObjectValid(BusinessObject businessObject, String prefix) {
335         final MessageMap errorMap = GlobalVariables.getMessageMap();
336         int originalErrorCount = errorMap.getErrorCount();
337 
338         errorMap.addToErrorPath(prefix);
339         validateBusinessObject(businessObject);
340         errorMap.removeFromErrorPath(prefix);
341 
342         return errorMap.getErrorCount() == originalErrorCount;
343     }
344 
345     /**
346      * @param businessObject - business object to validate
347      */
348     public void validateBusinessObjectsRecursively(BusinessObject businessObject, int depth) {
349         if (ObjectUtils.isNull(businessObject)) {
350             return;
351         }
352 
353         // validate primitives and any specific bo validation
354         validateBusinessObject(businessObject);
355 
356         // call method to recursively find business objects and validate
357         validateBusinessObjectsFromDescriptors(businessObject, PropertyUtils.getPropertyDescriptors(
358                 businessObject.getClass()), depth);
359     }
360 
361     /**
362      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateBusinessObject(org.kuali.rice.krad.bo.BusinessObject)
363      */
364     @Override
365     public void validateBusinessObject(BusinessObject businessObject) {
366         validateBusinessObject(businessObject, true);
367     }
368 
369     /**
370      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateBusinessObject(org.kuali.rice.krad.bo.BusinessObject,
371      *      boolean)
372      */
373     @Override
374     public void validateBusinessObject(BusinessObject businessObject, boolean validateRequired) {
375         if (ObjectUtils.isNull(businessObject)) {
376             return;
377         }
378 
379         validate(businessObject, businessObject.getClass().getName(), (String) null, validateRequired);
380     }
381 
382     /**
383      * iterates through the property descriptors looking for business objects or lists of business objects. calls
384      * validate method
385      * for each bo found
386      *
387      * @param object
388      * @param propertyDescriptors
389      */
390     protected void validateBusinessObjectsFromDescriptors(Object object, PropertyDescriptor[] propertyDescriptors,
391             int depth) {
392         for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
393             // validate the properties that are descended from BusinessObject
394             if (propertyDescriptor.getPropertyType() != null &&
395                     PersistableBusinessObject.class.isAssignableFrom(propertyDescriptor.getPropertyType()) &&
396                     ObjectUtils.getPropertyValue(object, propertyDescriptor.getName()) != null) {
397                 BusinessObject bo = (BusinessObject) ObjectUtils.getPropertyValue(object, propertyDescriptor.getName());
398                 if (depth == 0) {
399                     GlobalVariables.getMessageMap().addToErrorPath(propertyDescriptor.getName());
400                     validateBusinessObject(bo);
401                     GlobalVariables.getMessageMap().removeFromErrorPath(propertyDescriptor.getName());
402                 } else {
403                     validateBusinessObjectsRecursively(bo, depth - 1);
404                 }
405             }
406 
407             /*
408              * if property is a List, then walk the list and do the validation on each contained object that is a descendent of
409              * BusinessObject
410              */
411             else if (propertyDescriptor.getPropertyType() != null &&
412                     (List.class).isAssignableFrom(propertyDescriptor.getPropertyType()) &&
413                     ObjectUtils.getPropertyValue(object, propertyDescriptor.getName()) != null) {
414                 List propertyList = (List) ObjectUtils.getPropertyValue(object, propertyDescriptor.getName());
415                 for (int j = 0; j < propertyList.size(); j++) {
416                     if (propertyList.get(j) != null && propertyList.get(j) instanceof PersistableBusinessObject) {
417                         if (depth == 0) {
418                             GlobalVariables.getMessageMap().addToErrorPath(StringUtils.chomp(
419                                     propertyDescriptor.getName(), "s") + "[" +
420                                     (new Integer(j)).toString() + "]");
421                             validateBusinessObject((BusinessObject) propertyList.get(j));
422                             GlobalVariables.getMessageMap().removeFromErrorPath(StringUtils.chomp(
423                                     propertyDescriptor.getName(), "s") + "[" +
424                                     (new Integer(j)).toString() + "]");
425                         } else {
426                             validateBusinessObjectsRecursively((BusinessObject) propertyList.get(j), depth - 1);
427                         }
428                     }
429                 }
430             }
431         }
432     }
433 
434     /**
435      * calls validate format and required check for the given propertyDescriptor
436      *
437      * @param entryName
438      * @param object
439      * @param propertyDescriptor
440      * @param errorPrefix
441      * @deprecated since 1.1
442      */
443     @Deprecated
444     public void validatePrimitiveFromDescriptor(String entryName, Object object, PropertyDescriptor propertyDescriptor,
445             String errorPrefix, boolean validateRequired) {
446 
447         // validate the primitive attributes if defined in the dictionary
448         if (null != propertyDescriptor) {
449             validate(object, entryName, propertyDescriptor.getName(), validateRequired);
450         }
451     }
452 
453     /**
454      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateReferenceExists(org.kuali.rice.krad.bo.BusinessObject,
455      *      org.kuali.rice.krad.datadictionary.ReferenceDefinition)
456      */
457     public boolean validateReferenceExists(BusinessObject bo, ReferenceDefinition reference) {
458         return validateReferenceExists(bo, reference.getAttributeName());
459     }
460 
461     /**
462      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateReferenceExists(org.kuali.rice.krad.bo.BusinessObject,
463      *      java.lang.String)
464      */
465     public boolean validateReferenceExists(BusinessObject bo, String referenceName) {
466 
467         // attempt to retrieve the specified object from the db
468         BusinessObject referenceBo = businessObjectService.getReferenceIfExists(bo, referenceName);
469 
470         // if it isn't there, then it doesn't exist, return false
471         if (ObjectUtils.isNotNull(referenceBo)) {
472             return true;
473         }
474 
475         // otherwise, it is there, return true
476         return false;
477     }
478 
479     /**
480      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateReferenceIsActive(org.kuali.rice.krad.bo.BusinessObject,
481      *      org.kuali.rice.krad.datadictionary.ReferenceDefinition)
482      */
483     public boolean validateReferenceIsActive(BusinessObject bo, ReferenceDefinition reference) {
484         return validateReferenceIsActive(bo, reference.getAttributeName());
485     }
486 
487     /**
488      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateReferenceIsActive(org.kuali.rice.krad.bo.BusinessObject,
489      *      String)
490      */
491     public boolean validateReferenceIsActive(BusinessObject bo, String referenceName) {
492 
493         // attempt to retrieve the specified object from the db
494         BusinessObject referenceBo = businessObjectService.getReferenceIfExists(bo, referenceName);
495         if (referenceBo == null) {
496             return false;
497         }
498         if (!(referenceBo instanceof MutableInactivatable) || ((MutableInactivatable) referenceBo).isActive()) {
499             return true;
500         }
501 
502         return false;
503     }
504 
505     /**
506      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateReferenceExistsAndIsActive(org.kuali.rice.krad.bo.BusinessObject,
507      *      org.kuali.rice.krad.datadictionary.ReferenceDefinition)
508      */
509     public boolean validateReferenceExistsAndIsActive(BusinessObject bo, ReferenceDefinition reference) {
510         boolean success = true;
511         // intelligently use the fieldname from the reference, or get it out
512         // of the dataDictionaryService
513         String displayFieldName;
514         if (reference.isDisplayFieldNameSet()) {
515             displayFieldName = reference.getDisplayFieldName();
516         } else {
517             Class<?> boClass =
518                     reference.isCollectionReference() ? reference.getCollectionBusinessObjectClass() : bo.getClass();
519             displayFieldName = dataDictionaryService.getAttributeLabel(boClass,
520                     reference.getAttributeToHighlightOnFail());
521         }
522 
523         if (reference.isCollectionReference()) {
524             success = validateCollectionReferenceExistsAndIsActive(bo, reference, displayFieldName, StringUtils.split(
525                     reference.getCollection(), "."), null);
526         } else {
527             success = validateReferenceExistsAndIsActive(bo, reference.getAttributeName(),
528                     reference.getAttributeToHighlightOnFail(), displayFieldName);
529         }
530         return success;
531     }
532 
533     /**
534      * @param bo the object to get the collection from
535      * @param reference the <code>ReferenceDefinition</code> of the collection to validate
536      * @param displayFieldName the name of the field
537      * @param intermediateCollections array containing the path to the collection as tokens
538      * @param pathToAttributeI the rebuilt path to the ReferenceDefinition.attributeToHighlightOnFail which includes
539      * the
540      * index of
541      * each subcollection
542      * @return
543      */
544     private boolean validateCollectionReferenceExistsAndIsActive(BusinessObject bo, ReferenceDefinition reference,
545             String displayFieldName, String[] intermediateCollections, String pathToAttributeI) {
546         boolean success = true;
547         Collection<PersistableBusinessObject> referenceCollection;
548         String collectionName = intermediateCollections[0];
549         // remove current collection from intermediates
550         intermediateCollections = (String[]) ArrayUtils.removeElement(intermediateCollections, collectionName);
551         try {
552             referenceCollection = (Collection) PropertyUtils.getProperty(bo, collectionName);
553         } catch (Exception e) {
554             throw new RuntimeException(e);
555         }
556         int pos = 0;
557         Iterator<PersistableBusinessObject> iterator = referenceCollection.iterator();
558         while (iterator.hasNext()) {
559             String pathToAttribute = StringUtils.defaultString(pathToAttributeI)
560                     + collectionName
561                     + "["
562                     + (pos++)
563                     + "].";
564             // keep drilling down until we reach the nested collection we want
565             if (intermediateCollections.length > 0) {
566                 success &= validateCollectionReferenceExistsAndIsActive(iterator.next(), reference, displayFieldName,
567                         intermediateCollections, pathToAttribute);
568             } else {
569                 String attributeToHighlightOnFail = pathToAttribute + reference.getAttributeToHighlightOnFail();
570                 success &= validateReferenceExistsAndIsActive(iterator.next(), reference.getAttributeName(),
571                         attributeToHighlightOnFail, displayFieldName);
572             }
573         }
574 
575         return success;
576     }
577 
578     /**
579      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateReferenceExistsAndIsActive(org.kuali.rice.krad.bo.BusinessObject,
580      *      String, String, String)
581      */
582     public boolean validateReferenceExistsAndIsActive(BusinessObject bo, String referenceName,
583             String attributeToHighlightOnFail, String displayFieldName) {
584 
585         // if we're dealing with a nested attribute, we need to resolve down to the BO where the primitive attribute is located
586         // this is primarily to deal with the case of a defaultExistenceCheck that uses an "extension", i.e referenceName
587         // would be extension.attributeName
588         if (ObjectUtils.isNestedAttribute(referenceName)) {
589             String nestedAttributePrefix = ObjectUtils.getNestedAttributePrefix(referenceName);
590             String nestedAttributePrimitive = ObjectUtils.getNestedAttributePrimitive(referenceName);
591             Object nestedObject = ObjectUtils.getPropertyValue(bo, nestedAttributePrefix);
592             if (!(nestedObject instanceof BusinessObject)) {
593                 throw new ObjectNotABusinessObjectRuntimeException(
594                         "Attribute requested (" + nestedAttributePrefix + ") is of class: " + "'" +
595                                 nestedObject.getClass().getName() + "' and is not a " +
596                                 "descendent of BusinessObject.");
597             }
598             return validateReferenceExistsAndIsActive((BusinessObject) nestedObject, nestedAttributePrimitive,
599                     attributeToHighlightOnFail, displayFieldName);
600         }
601 
602         boolean success = true;
603         boolean exists;
604         boolean active;
605 
606         boolean fkFieldsPopulated = true;
607         // need to check for DD relationship FKs
608         List<String> fkFields = getDataDictionaryService().getRelationshipSourceAttributes(bo.getClass().getName(),
609                 referenceName);
610         if (fkFields != null) {
611             for (String fkFieldName : fkFields) {
612                 Object fkFieldValue = null;
613                 try {
614                     fkFieldValue = PropertyUtils.getProperty(bo, fkFieldName);
615                 }
616                 // if we cant retrieve the field value, then
617                 // it doesnt have a value
618                 catch (IllegalAccessException e) {
619                     fkFieldsPopulated = false;
620                 } catch (InvocationTargetException e) {
621                     fkFieldsPopulated = false;
622                 } catch (NoSuchMethodException e) {
623                     fkFieldsPopulated = false;
624                 }
625 
626                 // test the value
627                 if (fkFieldValue == null) {
628                     fkFieldsPopulated = false;
629                 } else if (String.class.isAssignableFrom(fkFieldValue.getClass())) {
630                     if (StringUtils.isBlank((String) fkFieldValue)) {
631                         fkFieldsPopulated = false;
632                     }
633                 }
634             }
635         } else if (bo instanceof PersistableBusinessObject) { // if no DD relationship exists, check the persistence service
636             fkFieldsPopulated = persistenceService.allForeignKeyValuesPopulatedForReference(
637                     (PersistableBusinessObject) bo, referenceName);
638         }
639 
640         // only bother if all the fk fields have values
641         if (fkFieldsPopulated) {
642 
643             // do the existence test
644             exists = validateReferenceExists(bo, referenceName);
645             if (exists) {
646 
647                 // do the active test, if appropriate
648                 if (!(bo instanceof MutableInactivatable) || ((MutableInactivatable) bo).isActive()) {
649                     active = validateReferenceIsActive(bo, referenceName);
650                     if (!active) {
651                         GlobalVariables.getMessageMap().putError(attributeToHighlightOnFail,
652                                 RiceKeyConstants.ERROR_INACTIVE, displayFieldName);
653                         success &= false;
654                     }
655                 }
656             } else {
657                 GlobalVariables.getMessageMap().putError(attributeToHighlightOnFail, RiceKeyConstants.ERROR_EXISTENCE,
658                         displayFieldName);
659                 success &= false;
660             }
661         }
662         return success;
663     }
664 
665     /**
666      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDefaultExistenceChecks(org.kuali.rice.krad.bo.BusinessObject)
667      */
668     public boolean validateDefaultExistenceChecks(BusinessObject bo) {
669         boolean success = true;
670 
671         // get a collection of all the referenceDefinitions setup for this object
672         Collection references = getDocumentDictionaryService().getDefaultExistenceChecks(bo.getClass());
673 
674         // walk through the references, doing the tests on each
675         for (Iterator iter = references.iterator(); iter.hasNext(); ) {
676             ReferenceDefinition reference = (ReferenceDefinition) iter.next();
677 
678             // do the existence and validation testing
679             success &= validateReferenceExistsAndIsActive(bo, reference);
680         }
681         return success;
682     }
683 
684     /**
685      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDefaultExistenceChecksForNewCollectionItem(org.kuali.rice.krad.bo.BusinessObject,
686      *      org.kuali.rice.krad.bo.BusinessObject, java.lang.String)
687      */
688     public boolean validateDefaultExistenceChecksForNewCollectionItem(BusinessObject bo,
689             BusinessObject newCollectionItem, String collectionName) {
690         boolean success = true;
691 
692         if (StringUtils.isNotBlank(collectionName)) {
693             // get a collection of all the referenceDefinitions setup for this object
694             Collection references = getDocumentDictionaryService().getDefaultExistenceChecks(bo.getClass());
695 
696             // walk through the references, doing the tests on each
697             for (Iterator iter = references.iterator(); iter.hasNext(); ) {
698                 ReferenceDefinition reference = (ReferenceDefinition) iter.next();
699                 if (collectionName != null && collectionName.equals(reference.getCollection())) {
700                     String displayFieldName;
701                     if (reference.isDisplayFieldNameSet()) {
702                         displayFieldName = reference.getDisplayFieldName();
703                     } else {
704                         Class boClass =
705                                 reference.isCollectionReference() ? reference.getCollectionBusinessObjectClass() :
706                                         bo.getClass();
707                         displayFieldName = dataDictionaryService.getAttributeLabel(boClass,
708                                 reference.getAttributeToHighlightOnFail());
709                     }
710 
711                     success &= validateReferenceExistsAndIsActive(newCollectionItem, reference.getAttributeName(),
712                             reference.getAttributeToHighlightOnFail(), displayFieldName);
713                 }
714             }
715         }
716 
717         return success;
718     }
719 
720     /**
721      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDefaultExistenceChecksForTransDoc(org.kuali.rice.krad.document.TransactionalDocument)
722      */
723     public boolean validateDefaultExistenceChecksForTransDoc(TransactionalDocument document) {
724         boolean success = true;
725 
726         // get a collection of all the referenceDefinitions setup for this object
727         Collection references = getDocumentDictionaryService().getDefaultExistenceChecks(document);
728 
729         // walk through the references, doing the tests on each
730         for (Iterator iter = references.iterator(); iter.hasNext(); ) {
731             ReferenceDefinition reference = (ReferenceDefinition) iter.next();
732 
733             // do the existence and validation testing
734             success &= validateReferenceExistsAndIsActive(document, reference);
735         }
736         return success;
737     }
738 
739     /**
740      * @see org.kuali.rice.krad.service.DictionaryValidationService#validateDefaultExistenceChecksForNewCollectionItem(org.kuali.rice.krad.document.TransactionalDocument,
741      *      org.kuali.rice.krad.bo.BusinessObject, String)
742      */
743     public boolean validateDefaultExistenceChecksForNewCollectionItem(TransactionalDocument document,
744             BusinessObject newCollectionItem, String collectionName) {
745         boolean success = true;
746         if (StringUtils.isNotBlank(collectionName)) {
747             // get a collection of all the referenceDefinitions setup for this object
748             Collection references = getDocumentDictionaryService().getDefaultExistenceChecks(document);
749 
750             // walk through the references, doing the tests on each
751             for (Iterator iter = references.iterator(); iter.hasNext(); ) {
752                 ReferenceDefinition reference = (ReferenceDefinition) iter.next();
753                 if (collectionName != null && collectionName.equals(reference.getCollection())) {
754                     String displayFieldName;
755                     if (reference.isDisplayFieldNameSet()) {
756                         displayFieldName = reference.getDisplayFieldName();
757                     } else {
758                         Class boClass =
759                                 reference.isCollectionReference() ? reference.getCollectionBusinessObjectClass() :
760                                         document.getClass();
761                         displayFieldName = dataDictionaryService.getAttributeLabel(boClass,
762                                 reference.getAttributeToHighlightOnFail());
763                     }
764 
765                     success &= validateReferenceExistsAndIsActive(newCollectionItem, reference.getAttributeName(),
766                             reference.getAttributeToHighlightOnFail(), displayFieldName);
767                 }
768             }
769         }
770         return success;
771     }
772 
773     /*
774     * 1.1 validation methods
775     */
776 
777     /**
778      * Validates using the defined AttributeValueReader (which allows access the object being validated) against
779      * the validationState and stateMapping (if specified).
780      *
781      * <p>If state information is null, validates the constraints as stateless (ie all constraints apply regardless of
782      * their states attribute).</p>
783      *
784      * @param valueReader - an object to validate
785      * @param doOptionalProcessing true if the validation should do optional validation (e.g. to check if empty values
786      * are required or not), false otherwise
787      * @param validationState
788      * @param stateMapping
789      * @return
790      */
791     public DictionaryValidationResult validate(AttributeValueReader valueReader, boolean doOptionalProcessing,
792             String validationState, StateMapping stateMapping) {
793         DictionaryValidationResult result = new DictionaryValidationResult();
794 
795         if (valueReader.getAttributeName() == null) {
796             validateObject(result, valueReader, doOptionalProcessing, true, validationState, stateMapping);
797         } else {
798             validateAttribute(result, valueReader, doOptionalProcessing, validationState, stateMapping);
799         }
800 
801         if (result.getNumberOfErrors() > 0) {
802 
803             String[] prefixParams = new String[1];
804             String prefixMessageKey = UifConstants.Messages.STATE_PREFIX;
805             if (stateMapping != null) {
806                 prefixParams[0] = stateMapping.getStateNameMessage(validationState);
807             }
808 
809             if (StringUtils.isBlank(prefixParams[0])) {
810                 prefixMessageKey = null;
811             }
812 
813             for (Iterator<ConstraintValidationResult> iterator = result.iterator(); iterator.hasNext(); ) {
814                 ConstraintValidationResult constraintValidationResult = iterator.next();
815                 if (constraintValidationResult.getStatus().getLevel() >= ErrorLevel.WARN.getLevel()) {
816                     String attributePath = constraintValidationResult.getAttributePath();
817                     if (attributePath == null || attributePath.isEmpty()) {
818                         attributePath = constraintValidationResult.getAttributeName();
819                     }
820 
821                     if (constraintValidationResult.getConstraintLabelKey() != null) {
822                         ErrorMessage errorMessage = new ErrorMessage(constraintValidationResult.getConstraintLabelKey(),
823                                 constraintValidationResult.getErrorParameters());
824                         errorMessage.setMessagePrefixKey(prefixMessageKey);
825                         errorMessage.setMessagePrefixParameters(prefixParams);
826                         GlobalVariables.getMessageMap().putError(attributePath, errorMessage);
827                     } else {
828                         ErrorMessage errorMessage = new ErrorMessage(constraintValidationResult.getErrorKey(),
829                                 constraintValidationResult.getErrorParameters());
830                         errorMessage.setMessagePrefixKey(prefixMessageKey);
831                         errorMessage.setMessagePrefixParameters(prefixParams);
832                         GlobalVariables.getMessageMap().putError(attributePath, errorMessage);
833                     }
834                 }
835             }
836         }
837 
838         return result;
839     }
840 
841     /**
842      * process constraints for the provided value using the element constraint processors
843      *
844      * @param result - used to store the validation results
845      * @param value - the object on which constraints are to be processed - the value of a complex attribute
846      * @param definition - a Data Dictionary definition e.g. {@code ComplexAttributeDefinition}
847      * @param attributeValueReader - a class that encapsulate access to both dictionary metadata and object field
848      * values
849      * @param doOptionalProcessing - true if the validation should do optional validation, false otherwise
850      */
851     protected void processElementConstraints(DictionaryValidationResult result, Object value, Constrainable definition,
852             AttributeValueReader attributeValueReader, boolean doOptionalProcessing, String validationState,
853             StateMapping stateMapping) {
854         processConstraints(result, elementConstraintProcessors, value, definition, attributeValueReader,
855                 doOptionalProcessing, validationState, stateMapping);
856     }
857 
858     /**
859      * process constraints for the provided collection using the collection constraint processors
860      *
861      * @param result - used to store the validation results
862      * @param collection - the object on which constraints are to be processed - a collection
863      * @param definition - a Data Dictionary definition e.g. {@code CollectionDefinition}
864      * @param attributeValueReader - a class that encapsulate access to both dictionary metadata and object field
865      * values
866      * @param doOptionalProcessing - true if the validation should do optional validation, false otherwise
867      */
868     protected void processCollectionConstraints(DictionaryValidationResult result, Collection<?> collection,
869             Constrainable definition, AttributeValueReader attributeValueReader, boolean doOptionalProcessing,
870             String validationState, StateMapping stateMapping) {
871         processConstraints(result, collectionConstraintProcessors, collection, definition, attributeValueReader,
872                 doOptionalProcessing, validationState, stateMapping);
873     }
874 
875     /**
876      * process constraints for the provided value using the provided constraint processors
877      *
878      * @param result - used to store the validation results
879      * @param value - the object on which constraints are to be processed - a collection or the value of an attribute
880      * @param definition - a Data Dictionary definition e.g. {@code ComplexAttributeDefinition} or {@code
881      * CollectionDefinition}
882      * @param attributeValueReader - a class that encapsulate access to both dictionary metadata and object field
883      * values
884      * @param doOptionalProcessing - true if the validation should do optional validation, false otherwise
885      */
886     @SuppressWarnings("unchecked")
887     private void processConstraints(DictionaryValidationResult result,
888             List<? extends ConstraintProcessor> constraintProcessors, Object value, Constrainable definition,
889             AttributeValueReader attributeValueReader, boolean doOptionalProcessing, String validationState,
890             StateMapping stateMapping) {
891         //TODO: Implement custom validators
892 
893         if (constraintProcessors != null) {
894             Constrainable selectedDefinition = definition;
895             AttributeValueReader selectedAttributeValueReader = attributeValueReader;
896 
897             // First - take the constrainable definition and get its constraints
898 
899             Queue<Constraint> constraintQueue = new LinkedList<Constraint>();
900 
901             // Using a for loop to iterate through constraint processors because ordering is important
902             for (ConstraintProcessor<Object, Constraint> processor : constraintProcessors) {
903 
904                 // Let the calling method opt out of any optional processing
905                 if (!doOptionalProcessing && processor.isOptional()) {
906                     result.addSkipped(attributeValueReader, processor.getName());
907                     continue;
908                 }
909 
910                 Class<? extends Constraint> constraintType = processor.getConstraintType();
911 
912                 // Add all of the constraints for this constraint type for all providers to the queue
913                 for (ConstraintProvider constraintProvider : constraintProviders) {
914                     if (constraintProvider.isSupported(selectedDefinition)) {
915                         Collection<Constraint> constraintList = constraintProvider.getConstraints(selectedDefinition,
916                                 constraintType);
917                         if (constraintList != null) {
918                             constraintQueue.addAll(constraintList);
919                         }
920                     }
921                 }
922 
923                 // If there are no constraints provided for this definition, then just skip it
924                 if (constraintQueue.isEmpty()) {
925                     result.addSkipped(attributeValueReader, processor.getName());
926                     continue;
927                 }
928 
929                 Collection<Constraint> additionalConstraints = new LinkedList<Constraint>();
930 
931                 // This loop is functionally identical to a for loop, but it has the advantage of letting us keep the queue around
932                 // and populate it with any new constraints contributed by the processor
933                 while (!constraintQueue.isEmpty()) {
934 
935                     Constraint constraint = constraintQueue.poll();
936 
937                     // If this constraint is not one that this process handles, then skip and add to the queue for the next processor;
938                     // obviously this would be redundant (we're only looking at constraints that this processor can process) except that
939                     // the previous processor might have stuck a new constraint (or constraints) on the queue
940                     if (!constraintType.isInstance(constraint)) {
941                         result.addSkipped(attributeValueReader, processor.getName());
942                         additionalConstraints.add(constraint);
943                         continue;
944                     }
945 
946                     constraint = ConstraintStateUtils.getApplicableConstraint(constraint, validationState,
947                             stateMapping);
948 
949                     if (constraint != null) {
950                         ProcessorResult processorResult = processor.process(result, value, constraint,
951                                 selectedAttributeValueReader);
952 
953                         Collection<Constraint> processorResultContraints = processorResult.getConstraints();
954                         if (processorResultContraints != null && processorResultContraints.size() > 0) {
955                             constraintQueue.addAll(processorResultContraints);
956                         }
957 
958                         // Change the selected definition to whatever was returned from the processor
959                         if (processorResult.isDefinitionProvided()) {
960                             selectedDefinition = processorResult.getDefinition();
961                         }
962                         // Change the selected attribute value reader to whatever was returned from the processor
963                         if (processorResult.isAttributeValueReaderProvided()) {
964                             selectedAttributeValueReader = processorResult.getAttributeValueReader();
965                         }
966                     }
967                 }
968 
969                 // After iterating through all the constraints for this processor, add the ones that werent consumed by this processor to the queue
970                 constraintQueue.addAll(additionalConstraints);
971             }
972         }
973     }
974 
975     /**
976      * validates an attribute
977      *
978      * @param result - used to store the validation results
979      * @param attributeValueReader - a class that encapsulate access to both dictionary metadata and object field
980      * values
981      * @param checkIfRequired - check if empty values are required or not
982      * @throws AttributeValidationException
983      */
984     protected void validateAttribute(DictionaryValidationResult result, AttributeValueReader attributeValueReader,
985             boolean checkIfRequired, String validationState,
986             StateMapping stateMapping) throws AttributeValidationException {
987         Constrainable definition = attributeValueReader.getDefinition(attributeValueReader.getAttributeName());
988         validateAttribute(result, definition, attributeValueReader, checkIfRequired, validationState, stateMapping);
989     }
990 
991     /**
992      * Validates the attribute specified by definition
993      *
994      * @param definition -   the constrainable attribute definition of a specific attribute name
995      * @throws AttributeValidationException
996      * @see #validateAttribute(DictionaryValidationResult, AttributeValueReader, boolean) for the other parameters
997      */
998     protected void validateAttribute(DictionaryValidationResult result, Constrainable definition,
999             AttributeValueReader attributeValueReader, boolean checkIfRequired, String validationState,
1000             StateMapping stateMapping) throws AttributeValidationException {
1001 
1002         if (definition == null) {
1003             throw new AttributeValidationException(
1004                     "Unable to validate constraints for attribute \"" + attributeValueReader.getAttributeName() +
1005                             "\" on entry \"" + attributeValueReader.getEntryName() +
1006                             "\" because no attribute definition can be found.");
1007         }
1008 
1009         Object value = attributeValueReader.getValue();
1010 
1011         processElementConstraints(result, value, definition, attributeValueReader, checkIfRequired, validationState,
1012                 stateMapping);
1013     }
1014 
1015     /**
1016      * validates an object and its attributes recursively
1017      *
1018      * @param result - used to store the validation results
1019      * @param attributeValueReader - a class that encapsulate access to both dictionary metadata and object field
1020      * values
1021      * @param doOptionalProcessing - true if the validation should do optional validation, false otherwise
1022      * @param processAttributes - if true process all attribute definitions, skip if false
1023      * @throws AttributeValidationException
1024      */
1025     protected void validateObject(DictionaryValidationResult result, AttributeValueReader attributeValueReader,
1026             boolean doOptionalProcessing, boolean processAttributes, String validationState,
1027             StateMapping stateMapping) throws AttributeValidationException {
1028 
1029         // 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
1030         Constrainable objectEntry = attributeValueReader.getEntry();
1031         processElementConstraints(result, attributeValueReader.getObject(), objectEntry, attributeValueReader,
1032                 doOptionalProcessing, validationState, stateMapping);
1033 
1034         List<Constrainable> definitions = attributeValueReader.getDefinitions();
1035 
1036         // Exit if the attribute value reader has no child definitions
1037         if (null == definitions) {
1038             return;
1039         }
1040 
1041         //Process all attribute definitions (unless being skipped)
1042         if (processAttributes) {
1043             for (Constrainable definition : definitions) {
1044                 String attributeName = definition.getName();
1045                 attributeValueReader.setAttributeName(attributeName);
1046 
1047                 if (attributeValueReader.isReadable()) {
1048                     Object value = attributeValueReader.getValue(attributeName);
1049 
1050                     processElementConstraints(result, value, definition, attributeValueReader, doOptionalProcessing,
1051                             validationState, stateMapping);
1052                 }
1053             }
1054         }
1055 
1056         //Process any constraints that may be defined on complex attributes
1057         if (objectEntry instanceof DataDictionaryEntryBase) {
1058             List<ComplexAttributeDefinition> complexAttrDefinitions =
1059                     ((DataDictionaryEntryBase) objectEntry).getComplexAttributes();
1060 
1061             if (complexAttrDefinitions != null) {
1062                 for (ComplexAttributeDefinition complexAttrDefinition : complexAttrDefinitions) {
1063                     String attributeName = complexAttrDefinition.getName();
1064                     attributeValueReader.setAttributeName(attributeName);
1065 
1066                     if (attributeValueReader.isReadable()) {
1067                         Object value = attributeValueReader.getValue();
1068 
1069                         DataDictionaryEntry childEntry = complexAttrDefinition.getDataObjectEntry();
1070                         if (value != null) {
1071                             AttributeValueReader nestedAttributeValueReader = new DictionaryObjectAttributeValueReader(
1072                                     value, childEntry.getFullClassName(), childEntry, attributeValueReader.getPath());
1073                             nestedAttributeValueReader.setAttributeName(attributeValueReader.getAttributeName());
1074                             //Validate nested object, however skip attribute definition porcessing on
1075                             //nested object entry, since they have already been processed above.
1076                             validateObject(result, nestedAttributeValueReader, doOptionalProcessing, false,
1077                                     validationState, stateMapping);
1078                         }
1079 
1080                         processElementConstraints(result, value, complexAttrDefinition, attributeValueReader,
1081                                 doOptionalProcessing, validationState, stateMapping);
1082                     }
1083                 }
1084             }
1085         }
1086 
1087         //FIXME: I think we may want to use a new CollectionConstrainable interface instead to obtain from
1088         //DictionaryObjectAttributeValueReader
1089         DataObjectEntry entry = (DataObjectEntry) attributeValueReader.getEntry();
1090         if (entry != null) {
1091             for (CollectionDefinition collectionDefinition : entry.getCollections()) {
1092                 //TODO: Do we need to be able to handle simple collections (ie. String, etc)
1093 
1094                 String childEntryName = collectionDefinition.getDataObjectClass();
1095                 String attributeName = collectionDefinition.getName();
1096                 attributeValueReader.setAttributeName(attributeName);
1097 
1098                 if (attributeValueReader.isReadable()) {
1099                     Collection<?> collectionObject = attributeValueReader.getValue();
1100                     DataDictionaryEntry childEntry = childEntryName != null ?
1101                             getDataDictionaryService().getDataDictionary().getDictionaryObjectEntry(childEntryName) :
1102                             null;
1103                     if (collectionObject != null) {
1104                         int index = 0;
1105                         for (Object value : collectionObject) {
1106                             //NOTE: This path is only correct for collections that guarantee order
1107                             String objectAttributePath = attributeValueReader.getPath() + "[" + index + "]";
1108 
1109                             //FIXME: It's inefficient to be creating new attribute reader for each item in collection
1110                             AttributeValueReader nestedAttributeValueReader = new DictionaryObjectAttributeValueReader(
1111                                     value, childEntryName, childEntry, objectAttributePath);
1112                             validateObject(result, nestedAttributeValueReader, doOptionalProcessing, true,
1113                                     validationState, stateMapping);
1114                             index++;
1115                         }
1116                     }
1117 
1118                     processCollectionConstraints(result, collectionObject, collectionDefinition, attributeValueReader,
1119                             doOptionalProcessing, validationState, stateMapping);
1120                 }
1121             }
1122         }
1123     }
1124 
1125     /**
1126      * gets the {@link DataDictionaryService}
1127      *
1128      * @return Returns the dataDictionaryService
1129      */
1130     public DataDictionaryService getDataDictionaryService() {
1131         return dataDictionaryService;
1132     }
1133 
1134     /**
1135      * sets the {@link DataDictionaryService}
1136      *
1137      * @param dataDictionaryService The dataDictionaryService to set
1138      */
1139     public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
1140         this.dataDictionaryService = dataDictionaryService;
1141     }
1142 
1143     /**
1144      * Sets the {@link BusinessObjectService} attribute value
1145      *
1146      * @param businessObjectService - the businessObjectService to set
1147      */
1148     public void setBusinessObjectService(BusinessObjectService businessObjectService) {
1149         this.businessObjectService = businessObjectService;
1150     }
1151 
1152     /**
1153      * Sets the {@link PersistenceService} attribute value
1154      *
1155      * @param persistenceService The persistenceService to set
1156      */
1157     public void setPersistenceService(PersistenceService persistenceService) {
1158         this.persistenceService = persistenceService;
1159     }
1160 
1161     /**
1162      * sets the @{PersistenceStructureService}
1163      *
1164      * @param persistenceStructureService - the {@code PersistenceStructureService} to set
1165      */
1166     public void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) {
1167         this.persistenceStructureService = persistenceStructureService;
1168     }
1169 
1170     /**
1171      * gets the list of {@link CollectionConstraintProcessor}
1172      *
1173      * <p>Collection constraint processors are classes that determine if a feature of a collection of objects
1174      * satisfies some constraint</p>
1175      *
1176      * @return the collectionConstraintProcessors
1177      */
1178     @SuppressWarnings("unchecked")
1179     public List<CollectionConstraintProcessor> getCollectionConstraintProcessors() {
1180         return this.collectionConstraintProcessors;
1181     }
1182 
1183     /**
1184      * sets the list of {@link CollectionConstraintProcessor}
1185      *
1186      * @param collectionConstraintProcessors the collectionConstraintProcessors to set
1187      */
1188     @SuppressWarnings("unchecked")
1189     public void setCollectionConstraintProcessors(List<CollectionConstraintProcessor> collectionConstraintProcessors) {
1190         this.collectionConstraintProcessors = collectionConstraintProcessors;
1191     }
1192 
1193     /**
1194      * gets the list of {@link ConstraintProvider}s
1195      *
1196      * <p>Constraint providers are classes that map specific constraint types to a constraint resolver,
1197      * which takes a constrainable definition</p>
1198      *
1199      * @return the constraintProviders
1200      */
1201     @SuppressWarnings("unchecked")
1202     public List<ConstraintProvider> getConstraintProviders() {
1203         return this.constraintProviders;
1204     }
1205 
1206     /**
1207      * sets a list of {@link ConstraintProvider}
1208      *
1209      * @param constraintProviders the constraintProviders to set
1210      */
1211     @SuppressWarnings("unchecked")
1212     public void setConstraintProviders(List<ConstraintProvider> constraintProviders) {
1213         this.constraintProviders = constraintProviders;
1214     }
1215 
1216     /**
1217      * gets the list of element {@link ConstraintProcessor}
1218      *
1219      * <p>Element constraint processors are classes that determine if a passed value is valid
1220      * for a specific constraint at the individual object or object attribute level</p>
1221      *
1222      * @return the elementConstraintProcessors
1223      */
1224     @SuppressWarnings("unchecked")
1225     public List<ConstraintProcessor> getElementConstraintProcessors() {
1226         return this.elementConstraintProcessors;
1227     }
1228 
1229     /**
1230      * sets the list of {@link ConstraintProcessor}
1231      *
1232      * @param elementConstraintProcessors the elementConstraintProcessors to set
1233      */
1234     @SuppressWarnings("unchecked")
1235     public void setElementConstraintProcessors(List<ConstraintProcessor> elementConstraintProcessors) {
1236         this.elementConstraintProcessors = elementConstraintProcessors;
1237     }
1238 
1239     /**
1240      * gets the locally saved instance of @{link DocumentDictionaryService}
1241      *
1242      * <p>If the instance in this class has not be set, retrieve it using
1243      * {@link KRADServiceLocatorWeb#getDocumentDictionaryService()} and save locally</p>
1244      *
1245      * @return the locally saved instance of {@code DocumentDictionaryService}
1246      */
1247     public DocumentDictionaryService getDocumentDictionaryService() {
1248         if (documentDictionaryService == null) {
1249             this.documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
1250         }
1251         return documentDictionaryService;
1252     }
1253 
1254     /**
1255      * sets the {@link DocumentDictionaryService}
1256      *
1257      * @param documentDictionaryService - the {@code DocumentDictionaryService} to set
1258      */
1259     public void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
1260         this.documentDictionaryService = documentDictionaryService;
1261     }
1262 }