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.rules;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.core.api.config.property.ConfigurationService;
020    import org.kuali.rice.core.api.util.RiceKeyConstants;
021    import org.kuali.rice.coreservice.framework.parameter.ParameterConstants;
022    import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
023    import org.kuali.rice.kim.api.KimConstants;
024    import org.kuali.rice.kim.api.group.Group;
025    import org.kuali.rice.kim.api.group.GroupService;
026    import org.kuali.rice.kim.api.identity.Person;
027    import org.kuali.rice.kim.api.identity.PersonService;
028    import org.kuali.rice.kim.api.permission.PermissionService;
029    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
030    import org.kuali.rice.krad.bo.AdHocRoutePerson;
031    import org.kuali.rice.krad.bo.AdHocRouteWorkgroup;
032    import org.kuali.rice.krad.bo.DocumentHeader;
033    import org.kuali.rice.krad.bo.Note;
034    import org.kuali.rice.krad.document.Document;
035    import org.kuali.rice.krad.maintenance.MaintenanceDocument;
036    import org.kuali.rice.krad.document.TransactionalDocument;
037    import org.kuali.rice.krad.rules.rule.AddAdHocRoutePersonRule;
038    import org.kuali.rice.krad.rules.rule.AddAdHocRouteWorkgroupRule;
039    import org.kuali.rice.krad.rules.rule.AddNoteRule;
040    import org.kuali.rice.krad.rules.rule.ApproveDocumentRule;
041    import org.kuali.rice.krad.rules.rule.RouteDocumentRule;
042    import org.kuali.rice.krad.rules.rule.SaveDocumentRule;
043    import org.kuali.rice.krad.rules.rule.SendAdHocRequestsRule;
044    import org.kuali.rice.krad.rules.rule.event.ApproveDocumentEvent;
045    import org.kuali.rice.krad.service.DataDictionaryService;
046    import org.kuali.rice.krad.service.DictionaryValidationService;
047    import org.kuali.rice.krad.service.DocumentDictionaryService;
048    import org.kuali.rice.krad.service.KRADServiceLocator;
049    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
050    import org.kuali.rice.krad.util.GlobalVariables;
051    import org.kuali.rice.krad.util.KRADConstants;
052    import org.kuali.rice.krad.util.KRADPropertyConstants;
053    import org.kuali.rice.krad.util.KRADUtils;
054    import org.kuali.rice.krad.util.MessageMap;
055    import org.kuali.rice.krad.util.RouteToCompletionUtil;
056    
057    /**
058     * Contains all of the business rules that are common to all documents
059     *
060     * @author Kuali Rice Team (rice.collab@kuali.org)
061     */
062    public abstract class DocumentRuleBase implements SaveDocumentRule, RouteDocumentRule, ApproveDocumentRule, AddNoteRule,
063            AddAdHocRoutePersonRule, AddAdHocRouteWorkgroupRule, SendAdHocRequestsRule {
064        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DocumentRuleBase.class);
065    
066        private static PersonService personService;
067        private static DictionaryValidationService dictionaryValidationService;
068        private static DocumentDictionaryService documentDictionaryService;
069        private static ConfigurationService kualiConfigurationService;
070        private static GroupService groupService;
071        private static PermissionService permissionService;
072        private static DataDictionaryService dataDictionaryService;
073    
074        // just some arbitrarily high max depth that's unlikely to occur in real life to prevent recursion problems
075        private int maxDictionaryValidationDepth = 100;
076    
077        /**
078         * Verifies that the document's overview fields are valid - it does required and format checks.
079         *
080         * @param document
081         * @return boolean True if the document description is valid, false otherwise.
082         */
083        public boolean isDocumentOverviewValid(Document document) {
084            // add in the documentHeader path
085            GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
086            GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_HEADER_PROPERTY_NAME);
087    
088            // check the document header for fields like the description
089            getDictionaryValidationService().validateBusinessObject(document.getDocumentHeader());
090            validateSensitiveDataValue(KRADPropertyConstants.EXPLANATION, document.getDocumentHeader().getExplanation(),
091                    getDataDictionaryService().getAttributeLabel(DocumentHeader.class, KRADPropertyConstants.EXPLANATION));
092    
093            // drop the error path keys off now
094            GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_HEADER_PROPERTY_NAME);
095            GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
096    
097            return GlobalVariables.getMessageMap().hasNoErrors();
098        }
099    
100        /**
101         * Validates the document attributes against the data dictionary.
102         *
103         * @param document
104         * @param validateRequired if true, then an error will be retruned if a DD required field is empty. if false, no
105         * required
106         * checking is done
107         * @return True if the document attributes are valid, false otherwise.
108         */
109        public boolean isDocumentAttributesValid(Document document, boolean validateRequired) {
110            // start updating the error path name
111            GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
112    
113            // check the document for fields like explanation and org doc #
114            getDictionaryValidationService().validateDocumentAndUpdatableReferencesRecursively(document,
115                    getMaxDictionaryValidationDepth(), validateRequired);
116    
117            // drop the error path keys off now
118            GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
119    
120            return GlobalVariables.getMessageMap().hasNoErrors();
121        }
122    
123        /**
124         * Runs all business rules needed prior to saving. This includes both common rules for all documents, plus
125         * class-specific
126         * business rules. This method will only return false if it fails the isValidForSave() test. Otherwise, it will
127         * always return
128         * positive regardless of the outcome of the business rules. However, any error messages resulting from the business
129         * rules will
130         * still be populated, for display to the consumer of this service.
131         *
132         * @see org.kuali.rice.krad.rules.rule.SaveDocumentRule#processSaveDocument(org.kuali.rice.krad.document.Document)
133         */
134        public boolean processSaveDocument(Document document) {
135            boolean isValid = true;
136    
137            isValid = isDocumentOverviewValid(document);
138    
139            GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
140    
141            getDictionaryValidationService().validateDocumentAndUpdatableReferencesRecursively(document,
142                    getMaxDictionaryValidationDepth(), false);
143            getDictionaryValidationService().validateDefaultExistenceChecksForTransDoc((TransactionalDocument) document);
144    
145            GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
146    
147            isValid &= GlobalVariables.getMessageMap().hasNoErrors();
148            isValid &= processCustomSaveDocumentBusinessRules(document);
149    
150            return isValid;
151        }
152    
153        /**
154         * This method should be overridden by children rule classes as a hook to implement document specific business rule
155         * checks for
156         * the "save document" event.
157         *
158         * @param document
159         * @return boolean True if the rules checks passed, false otherwise.
160         */
161        protected boolean processCustomSaveDocumentBusinessRules(Document document) {
162            return true;
163        }
164    
165        /**
166         * Runs all business rules needed prior to routing. This includes both common rules for all maintenance documents,
167         * plus
168         * class-specific business rules. This method will return false if any business rule fails, or if the document is in
169         * an invalid
170         * state, and not routable (see isDocumentValidForRouting()).
171         *
172         * @see org.kuali.rice.krad.rules.rule.RouteDocumentRule#processRouteDocument(org.kuali.rice.krad.document.Document)
173         */
174        public boolean processRouteDocument(Document document) {
175            boolean isValid = true;
176    
177            isValid = isDocumentAttributesValid(document, true);
178    
179            boolean completeRequestPending = RouteToCompletionUtil.checkIfAtleastOneAdHocCompleteRequestExist(document);
180    
181            // Validate the document if the header is valid and no pending completion requests
182            if (isValid && !completeRequestPending) {
183                isValid &= processCustomRouteDocumentBusinessRules(document);
184            }
185    
186            return isValid;
187        }
188    
189        /**
190         * This method should be overridden by children rule classes as a hook to implement document specific business rule
191         * checks for
192         * the "route document" event.
193         *
194         * @param document
195         * @return boolean True if the rules checks passed, false otherwise.
196         */
197        protected boolean processCustomRouteDocumentBusinessRules(Document document) {
198            return true;
199        }
200    
201        /**
202         * Runs all business rules needed prior to approving. This includes both common rules for all documents, plus
203         * class-specific
204         * business rules. This method will return false if any business rule fails, or if the document is in an invalid
205         * state, and not
206         * approveble.
207         *
208         * @see org.kuali.rice.krad.rules.rule.ApproveDocumentRule#processApproveDocument(org.kuali.rice.krad.rules.rule.event.ApproveDocumentEvent)
209         */
210        public boolean processApproveDocument(ApproveDocumentEvent approveEvent) {
211            boolean isValid = true;
212    
213            isValid = processCustomApproveDocumentBusinessRules(approveEvent);
214    
215            return isValid;
216        }
217    
218        /**
219         * This method should be overridden by children rule classes as a hook to implement document specific business rule
220         * checks for
221         * the "approve document" event.
222         *
223         * @param approveEvent
224         * @return boolean True if the rules checks passed, false otherwise.
225         */
226        protected boolean processCustomApproveDocumentBusinessRules(ApproveDocumentEvent approveEvent) {
227            return true;
228        }
229    
230        /**
231         * Runs all business rules needed prior to adding a document note. This method will return false if any business
232         * rule fails
233         */
234        public boolean processAddNote(Document document, Note note) {
235            boolean isValid = true;
236    
237            isValid &= isNoteValid(note);
238            isValid &= processCustomAddNoteBusinessRules(document, note);
239    
240            return isValid;
241        }
242    
243        /**
244         * Verifies that the note's fields are valid - it does required and format checks.
245         *
246         * @param note
247         * @return boolean True if the document description is valid, false otherwise.
248         */
249        public boolean isNoteValid(Note note) {
250            // add the error path keys on the stack
251            GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.NEW_DOCUMENT_NOTE_PROPERTY_NAME);
252    
253            // check the document header for fields like the description
254            getDictionaryValidationService().validateBusinessObject(note);
255    
256            validateSensitiveDataValue(KRADConstants.NOTE_TEXT_PROPERTY_NAME, note.getNoteText(),
257                    getDataDictionaryService().getAttributeLabel(Note.class, KRADConstants.NOTE_TEXT_PROPERTY_NAME));
258    
259            // drop the error path keys off now
260            GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.NEW_DOCUMENT_NOTE_PROPERTY_NAME);
261    
262            return GlobalVariables.getMessageMap().hasNoErrors();
263        }
264    
265        /**
266         * This method should be overridden by children rule classes as a hook to implement document specific business rule
267         * checks for
268         * the "add document note" event.
269         *
270         * @param document
271         * @param note
272         * @return boolean True if the rules checks passed, false otherwise.
273         */
274        protected boolean processCustomAddNoteBusinessRules(Document document, Note note) {
275            return true;
276        }
277    
278        /**
279         * @see org.kuali.rice.krad.rules.rule.AddAdHocRoutePersonRule#processAddAdHocRoutePerson(org.kuali.rice.krad.document.Document,
280         *      org.kuali.rice.krad.bo.AdHocRoutePerson)
281         */
282        public boolean processAddAdHocRoutePerson(Document document, AdHocRoutePerson adHocRoutePerson) {
283            boolean isValid = true;
284    
285            isValid &= isAddHocRoutePersonValid(document, adHocRoutePerson);
286    
287            isValid &= processCustomAddAdHocRoutePersonBusinessRules(document, adHocRoutePerson);
288            return isValid;
289        }
290    
291        /**
292         * @see org.kuali.rice.krad.rules.rule.SendAdHocRequestsRule#processSendAdHocRequests(org.kuali.rice.krad.document.Document)
293         */
294        public boolean processSendAdHocRequests(Document document) {
295            boolean isValid = true;
296    
297            isValid &= isAdHocRouteRecipientsValid(document);
298            isValid &= processCustomSendAdHocRequests(document);
299    
300            return isValid;
301        }
302    
303        protected boolean processCustomSendAdHocRequests(Document document) {
304            return true;
305        }
306    
307        /**
308         * Checks the adhoc route recipient list to ensure there are recipients or
309         * else throws an error that at least one recipient is required.
310         *
311         * @param document
312         * @return
313         */
314        protected boolean isAdHocRouteRecipientsValid(Document document) {
315            boolean isValid = true;
316            MessageMap errorMap = GlobalVariables.getMessageMap();
317    
318            if (errorMap.getErrorPath().size() == 0) {
319                // add the error path keys on the stack
320                errorMap.addToErrorPath(KRADConstants.NEW_AD_HOC_ROUTE_PERSON_PROPERTY_NAME);
321            }
322    
323            if ((document.getAdHocRoutePersons() == null || document.getAdHocRoutePersons().isEmpty()) && (document
324                    .getAdHocRouteWorkgroups() == null || document.getAdHocRouteWorkgroups().isEmpty())) {
325    
326                GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID, "error.adhoc.missing.recipients");
327                isValid = false;
328            }
329    
330            // drop the error path keys off now
331            errorMap.removeFromErrorPath(KRADConstants.NEW_AD_HOC_ROUTE_PERSON_PROPERTY_NAME);
332    
333            return isValid;
334        }
335    
336        /**
337         * Verifies that the adHocRoutePerson's fields are valid - it does required and format checks.
338         *
339         * @param person
340         * @return boolean True if valid, false otherwise.
341         */
342        public boolean isAddHocRoutePersonValid(Document document, AdHocRoutePerson person) {
343            MessageMap errorMap = GlobalVariables.getMessageMap();
344    
345            // new recipients are not embedded in the error path; existing lines should be
346            if (errorMap.getErrorPath().size() == 0) {
347                // add the error path keys on the stack
348                errorMap.addToErrorPath(KRADConstants.NEW_AD_HOC_ROUTE_PERSON_PROPERTY_NAME);
349            }
350    
351            if (StringUtils.isNotBlank(person.getId())) {
352                Person user = getPersonService().getPersonByPrincipalName(person.getId());
353    
354                if (user == null) {
355                    GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
356                            RiceKeyConstants.ERROR_INVALID_ADHOC_PERSON_ID);
357                } else if (!getPermissionService().hasPermission(user.getPrincipalId(),
358                        KimConstants.KIM_TYPE_DEFAULT_NAMESPACE, KimConstants.PermissionNames.LOG_IN)) {
359                    GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
360                            RiceKeyConstants.ERROR_INACTIVE_ADHOC_PERSON_ID);
361                } else {
362                    Class docOrBoClass = null;
363                    if (document instanceof MaintenanceDocument) {
364                        docOrBoClass = ((MaintenanceDocument) document).getNewMaintainableObject().getDataObjectClass();
365                    } else {
366                        docOrBoClass = document.getClass();
367                    }
368    
369                    if (!getDocumentDictionaryService().getDocumentAuthorizer(document).canReceiveAdHoc(document, user,
370                            person.getActionRequested())) {
371                        GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
372                                RiceKeyConstants.ERROR_UNAUTHORIZED_ADHOC_PERSON_ID);
373                    }
374                }
375            } else {
376                GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
377                        RiceKeyConstants.ERROR_MISSING_ADHOC_PERSON_ID);
378            }
379    
380            // drop the error path keys off now
381            errorMap.removeFromErrorPath(KRADConstants.NEW_AD_HOC_ROUTE_PERSON_PROPERTY_NAME);
382    
383            return GlobalVariables.getMessageMap().hasNoErrors();
384        }
385    
386        /**
387         * This method should be overridden by children rule classes as a hook to implement document specific business rule
388         * checks for
389         * the "add ad hoc route person" event.
390         *
391         * @param document
392         * @param person
393         * @return boolean True if the rules checks passed, false otherwise.
394         */
395        protected boolean processCustomAddAdHocRoutePersonBusinessRules(Document document, AdHocRoutePerson person) {
396            return true;
397        }
398    
399        /**
400         * @see org.kuali.rice.krad.rules.rule.AddAdHocRouteWorkgroupRule#processAddAdHocRouteWorkgroup(org.kuali.rice.krad.document.Document,
401         *      org.kuali.rice.krad.bo.AdHocRouteWorkgroup)
402         */
403        public boolean processAddAdHocRouteWorkgroup(Document document, AdHocRouteWorkgroup adHocRouteWorkgroup) {
404            boolean isValid = true;
405    
406            isValid &= isAddHocRouteWorkgroupValid(adHocRouteWorkgroup);
407    
408            isValid &= processCustomAddAdHocRouteWorkgroupBusinessRules(document, adHocRouteWorkgroup);
409            return isValid;
410        }
411    
412        /**
413         * Verifies that the adHocRouteWorkgroup's fields are valid - it does required and format checks.
414         *
415         * @param workgroup
416         * @return boolean True if valid, false otherwise.
417         */
418        public boolean isAddHocRouteWorkgroupValid(AdHocRouteWorkgroup workgroup) {
419            MessageMap errorMap = GlobalVariables.getMessageMap();
420    
421            // new recipients are not embedded in the error path; existing lines should be
422            if (errorMap.getErrorPath().size() == 0) {
423                // add the error path keys on the stack
424                GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.NEW_AD_HOC_ROUTE_WORKGROUP_PROPERTY_NAME);
425            }
426    
427            if (workgroup.getRecipientName() != null && workgroup.getRecipientNamespaceCode() != null) {
428                // validate that they are a workgroup from the workgroup service by looking them up
429                try {
430                    Group group = getGroupService().getGroupByNamespaceCodeAndName(workgroup.getRecipientNamespaceCode(),
431                            workgroup.getRecipientName());
432                    if (group == null || !group.isActive()) {
433                        GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
434                                RiceKeyConstants.ERROR_INVALID_ADHOC_WORKGROUP_ID);
435                    }
436                } catch (Exception e) {
437                    LOG.error("isAddHocRouteWorkgroupValid(AdHocRouteWorkgroup)", e);
438    
439                    GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
440                            RiceKeyConstants.ERROR_INVALID_ADHOC_WORKGROUP_ID);
441                }
442            } else {
443                GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
444                        RiceKeyConstants.ERROR_MISSING_ADHOC_WORKGROUP_ID);
445            }
446    
447            // drop the error path keys off now
448            GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.NEW_AD_HOC_ROUTE_WORKGROUP_PROPERTY_NAME);
449    
450            return GlobalVariables.getMessageMap().hasNoErrors();
451        }
452    
453        /**
454         * This method should be overridden by children rule classes as a hook to implement document specific business rule
455         * checks for
456         * the "add ad hoc route workgroup" event.
457         *
458         * @param document
459         * @param workgroup
460         * @return boolean True if the rules checks passed, false otherwise.
461         */
462        protected boolean processCustomAddAdHocRouteWorkgroupBusinessRules(Document document,
463                AdHocRouteWorkgroup workgroup) {
464            return true;
465        }
466    
467        /**
468         * Gets the maximum number of levels the data-dictionary based validation will recurse for the document
469         */
470        public int getMaxDictionaryValidationDepth() {
471            return this.maxDictionaryValidationDepth;
472        }
473    
474        /**
475         * Gets the maximum number of levels the data-dictionary based validation will recurse for the document
476         */
477        public void setMaxDictionaryValidationDepth(int maxDictionaryValidationDepth) {
478            if (maxDictionaryValidationDepth < 0) {
479                LOG.error("Dictionary validation depth should be greater than or equal to 0.  Value received was: "
480                        + maxDictionaryValidationDepth);
481                throw new RuntimeException(
482                        "Dictionary validation depth should be greater than or equal to 0.  Value received was: "
483                                + maxDictionaryValidationDepth);
484            }
485            this.maxDictionaryValidationDepth = maxDictionaryValidationDepth;
486        }
487    
488        protected boolean validateSensitiveDataValue(String fieldName, String fieldValue, String fieldLabel) {
489            boolean dataValid = true;
490    
491            if (fieldValue == null) {
492                return dataValid;
493            }
494    
495            boolean patternFound = KRADUtils.containsSensitiveDataPatternMatch(fieldValue);
496            boolean warnForSensitiveData = CoreFrameworkServiceLocator.getParameterService().getParameterValueAsBoolean(
497                    KRADConstants.KNS_NAMESPACE, ParameterConstants.ALL_COMPONENT,
498                    KRADConstants.SystemGroupParameterNames.SENSITIVE_DATA_PATTERNS_WARNING_IND);
499            if (patternFound && !warnForSensitiveData) {
500                dataValid = false;
501                GlobalVariables.getMessageMap().putError(fieldName,
502                        RiceKeyConstants.ERROR_DOCUMENT_FIELD_CONTAINS_POSSIBLE_SENSITIVE_DATA, fieldLabel);
503            }
504    
505            return dataValid;
506        }
507    
508        /**
509         * Business rules check will include all save action rules and any custom rules required by the document specific rule implementation
510         *
511         * @param document Document
512         * @return true if all validations are passed
513         */
514        public boolean processCompleteDocument(Document document) {
515            boolean isValid = true;
516            isValid &= processSaveDocument(document);
517            isValid &= processCustomCompleteDocumentBusinessRules(document);
518            return isValid;
519        }
520    
521        /**
522         * Hook method for deriving business rule classes to provide custom validations required during completion action
523         *
524         * @param document
525         * @return default is true
526         */
527        protected boolean processCustomCompleteDocumentBusinessRules(Document document) {
528            return true;
529        }
530    
531        protected DataDictionaryService getDataDictionaryService() {
532            if (dataDictionaryService == null) {
533                dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
534            }
535            return dataDictionaryService;
536        }
537    
538        protected PersonService getPersonService() {
539            if (personService == null) {
540                personService = KimApiServiceLocator.getPersonService();
541            }
542            return personService;
543        }
544    
545        public static GroupService getGroupService() {
546            if (groupService == null) {
547                groupService = KimApiServiceLocator.getGroupService();
548            }
549            return groupService;
550        }
551    
552        public static PermissionService getPermissionService() {
553            if (permissionService == null) {
554                permissionService = KimApiServiceLocator.getPermissionService();
555            }
556            return permissionService;
557        }
558    
559        protected DictionaryValidationService getDictionaryValidationService() {
560            if (dictionaryValidationService == null) {
561                dictionaryValidationService = KRADServiceLocatorWeb.getDictionaryValidationService();
562            }
563            return dictionaryValidationService;
564        }
565    
566        protected ConfigurationService getKualiConfigurationService() {
567            if (kualiConfigurationService == null) {
568                kualiConfigurationService = KRADServiceLocator.getKualiConfigurationService();
569            }
570            return kualiConfigurationService;
571        }
572    
573        protected static DocumentDictionaryService getDocumentDictionaryService() {
574            if (documentDictionaryService == null) {
575                documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
576            }
577            return documentDictionaryService;
578        }
579    
580        public static void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
581            DocumentRuleBase.documentDictionaryService = documentDictionaryService;
582        }
583    }