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