View Javadoc

1   /**
2    * Copyright 2005-2012 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.rules;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.core.api.config.property.ConfigurationService;
20  import org.kuali.rice.core.api.util.RiceKeyConstants;
21  import org.kuali.rice.coreservice.framework.parameter.ParameterConstants;
22  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
23  import org.kuali.rice.kim.api.KimConstants;
24  import org.kuali.rice.kim.api.group.Group;
25  import org.kuali.rice.kim.api.group.GroupService;
26  import org.kuali.rice.kim.api.identity.Person;
27  import org.kuali.rice.kim.api.identity.PersonService;
28  import org.kuali.rice.kim.api.permission.PermissionService;
29  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
30  import org.kuali.rice.krad.bo.AdHocRoutePerson;
31  import org.kuali.rice.krad.bo.AdHocRouteWorkgroup;
32  import org.kuali.rice.krad.bo.DocumentHeader;
33  import org.kuali.rice.krad.bo.Note;
34  import org.kuali.rice.krad.document.Document;
35  import org.kuali.rice.krad.maintenance.MaintenanceDocument;
36  import org.kuali.rice.krad.document.TransactionalDocument;
37  import org.kuali.rice.krad.rules.rule.AddAdHocRoutePersonRule;
38  import org.kuali.rice.krad.rules.rule.AddAdHocRouteWorkgroupRule;
39  import org.kuali.rice.krad.rules.rule.AddNoteRule;
40  import org.kuali.rice.krad.rules.rule.ApproveDocumentRule;
41  import org.kuali.rice.krad.rules.rule.RouteDocumentRule;
42  import org.kuali.rice.krad.rules.rule.SaveDocumentRule;
43  import org.kuali.rice.krad.rules.rule.SendAdHocRequestsRule;
44  import org.kuali.rice.krad.rules.rule.event.ApproveDocumentEvent;
45  import org.kuali.rice.krad.service.DataDictionaryService;
46  import org.kuali.rice.krad.service.DictionaryValidationService;
47  import org.kuali.rice.krad.service.DocumentDictionaryService;
48  import org.kuali.rice.krad.service.KRADServiceLocator;
49  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
50  import org.kuali.rice.krad.util.GlobalVariables;
51  import org.kuali.rice.krad.util.KRADConstants;
52  import org.kuali.rice.krad.util.KRADPropertyConstants;
53  import org.kuali.rice.krad.util.KRADUtils;
54  import org.kuali.rice.krad.util.MessageMap;
55  
56  /**
57   * Contains all of the business rules that are common to all documents
58   *
59   * @author Kuali Rice Team (rice.collab@kuali.org)
60   */
61  public abstract class DocumentRuleBase implements SaveDocumentRule, RouteDocumentRule, ApproveDocumentRule, AddNoteRule,
62          AddAdHocRoutePersonRule, AddAdHocRouteWorkgroupRule, SendAdHocRequestsRule {
63      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DocumentRuleBase.class);
64  
65      private static PersonService personService;
66      private static DictionaryValidationService dictionaryValidationService;
67      private static DocumentDictionaryService documentDictionaryService;
68      private static ConfigurationService kualiConfigurationService;
69      private static GroupService groupService;
70      private static PermissionService permissionService;
71      private static DataDictionaryService dataDictionaryService;
72  
73      // just some arbitrarily high max depth that's unlikely to occur in real life to prevent recursion problems
74      private int maxDictionaryValidationDepth = 100;
75  
76      /**
77       * Verifies that the document's overview fields are valid - it does required and format checks.
78       *
79       * @param document
80       * @return boolean True if the document description is valid, false otherwise.
81       */
82      public boolean isDocumentOverviewValid(Document document) {
83          // add in the documentHeader path
84          GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
85          GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_HEADER_PROPERTY_NAME);
86  
87          // check the document header for fields like the description
88          getDictionaryValidationService().validateBusinessObject(document.getDocumentHeader());
89          validateSensitiveDataValue(KRADPropertyConstants.EXPLANATION, document.getDocumentHeader().getExplanation(),
90                  getDataDictionaryService().getAttributeLabel(DocumentHeader.class, KRADPropertyConstants.EXPLANATION));
91  
92          // drop the error path keys off now
93          GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_HEADER_PROPERTY_NAME);
94          GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
95  
96          return GlobalVariables.getMessageMap().hasNoErrors();
97      }
98  
99      /**
100      * Validates the document attributes against the data dictionary.
101      *
102      * @param document
103      * @param validateRequired if true, then an error will be retruned if a DD required field is empty. if false, no
104      * required
105      * checking is done
106      * @return True if the document attributes are valid, false otherwise.
107      */
108     public boolean isDocumentAttributesValid(Document document, boolean validateRequired) {
109         // start updating the error path name
110         GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
111 
112         // check the document for fields like explanation and org doc #
113         getDictionaryValidationService().validateDocumentAndUpdatableReferencesRecursively(document,
114                 getMaxDictionaryValidationDepth(), validateRequired);
115 
116         // drop the error path keys off now
117         GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
118 
119         return GlobalVariables.getMessageMap().hasNoErrors();
120     }
121 
122     /**
123      * Runs all business rules needed prior to saving. This includes both common rules for all documents, plus
124      * class-specific
125      * business rules. This method will only return false if it fails the isValidForSave() test. Otherwise, it will
126      * always return
127      * positive regardless of the outcome of the business rules. However, any error messages resulting from the business
128      * rules will
129      * still be populated, for display to the consumer of this service.
130      *
131      * @see org.kuali.rice.krad.rules.rule.SaveDocumentRule#processSaveDocument(org.kuali.rice.krad.document.Document)
132      */
133     public boolean processSaveDocument(Document document) {
134         boolean isValid = true;
135         isValid = isDocumentOverviewValid(document);
136         GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
137         getDictionaryValidationService().validateDocumentAndUpdatableReferencesRecursively(document,
138                 getMaxDictionaryValidationDepth(), false);
139         getDictionaryValidationService().validateDefaultExistenceChecksForTransDoc((TransactionalDocument) document);
140         GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
141         isValid &= GlobalVariables.getMessageMap().hasNoErrors();
142         isValid &= processCustomSaveDocumentBusinessRules(document);
143 
144         return isValid;
145     }
146 
147     /**
148      * This method should be overridden by children rule classes as a hook to implement document specific business rule
149      * checks for
150      * the "save document" event.
151      *
152      * @param document
153      * @return boolean True if the rules checks passed, false otherwise.
154      */
155     protected boolean processCustomSaveDocumentBusinessRules(Document document) {
156         return true;
157     }
158 
159     /**
160      * Runs all business rules needed prior to routing. This includes both common rules for all maintenance documents,
161      * plus
162      * class-specific business rules. This method will return false if any business rule fails, or if the document is in
163      * an invalid
164      * state, and not routable (see isDocumentValidForRouting()).
165      *
166      * @see org.kuali.rice.krad.rules.rule.RouteDocumentRule#processRouteDocument(org.kuali.rice.krad.document.Document)
167      */
168     public boolean processRouteDocument(Document document) {
169         boolean isValid = true;
170 
171         isValid = isDocumentAttributesValid(document, true);
172 
173         // don't validate the document if the header is invalid
174         if (isValid) {
175             isValid &= processCustomRouteDocumentBusinessRules(document);
176         }
177         return isValid;
178     }
179 
180     /**
181      * This method should be overridden by children rule classes as a hook to implement document specific business rule
182      * checks for
183      * the "route document" event.
184      *
185      * @param document
186      * @return boolean True if the rules checks passed, false otherwise.
187      */
188     protected boolean processCustomRouteDocumentBusinessRules(Document document) {
189         return true;
190     }
191 
192     /**
193      * Runs all business rules needed prior to approving. This includes both common rules for all documents, plus
194      * class-specific
195      * business rules. This method will return false if any business rule fails, or if the document is in an invalid
196      * state, and not
197      * approveble.
198      *
199      * @see org.kuali.rice.krad.rules.rule.ApproveDocumentRule#processApproveDocument(org.kuali.rice.krad.rules.rule.event.ApproveDocumentEvent)
200      */
201     public boolean processApproveDocument(ApproveDocumentEvent approveEvent) {
202         boolean isValid = true;
203 
204         isValid = processCustomApproveDocumentBusinessRules(approveEvent);
205 
206         return isValid;
207     }
208 
209     /**
210      * This method should be overridden by children rule classes as a hook to implement document specific business rule
211      * checks for
212      * the "approve document" event.
213      *
214      * @param approveEvent
215      * @return boolean True if the rules checks passed, false otherwise.
216      */
217     protected boolean processCustomApproveDocumentBusinessRules(ApproveDocumentEvent approveEvent) {
218         return true;
219     }
220 
221     /**
222      * Runs all business rules needed prior to adding a document note. This method will return false if any business
223      * rule fails
224      */
225     public boolean processAddNote(Document document, Note note) {
226         boolean isValid = true;
227 
228         isValid &= isNoteValid(note);
229         isValid &= processCustomAddNoteBusinessRules(document, note);
230 
231         return isValid;
232     }
233 
234     /**
235      * Verifies that the note's fields are valid - it does required and format checks.
236      *
237      * @param note
238      * @return boolean True if the document description is valid, false otherwise.
239      */
240     public boolean isNoteValid(Note note) {
241         // add the error path keys on the stack
242         GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.NEW_DOCUMENT_NOTE_PROPERTY_NAME);
243 
244         // check the document header for fields like the description
245         getDictionaryValidationService().validateBusinessObject(note);
246 
247         validateSensitiveDataValue(KRADConstants.NOTE_TEXT_PROPERTY_NAME, note.getNoteText(),
248                 getDataDictionaryService().getAttributeLabel(Note.class, KRADConstants.NOTE_TEXT_PROPERTY_NAME));
249 
250         // drop the error path keys off now
251         GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.NEW_DOCUMENT_NOTE_PROPERTY_NAME);
252 
253         return GlobalVariables.getMessageMap().hasNoErrors();
254     }
255 
256     /**
257      * This method should be overridden by children rule classes as a hook to implement document specific business rule
258      * checks for
259      * the "add document note" event.
260      *
261      * @param document
262      * @param note
263      * @return boolean True if the rules checks passed, false otherwise.
264      */
265     protected boolean processCustomAddNoteBusinessRules(Document document, Note note) {
266         return true;
267     }
268 
269     /**
270      * @see org.kuali.rice.krad.rules.rule.AddAdHocRoutePersonRule#processAddAdHocRoutePerson(org.kuali.rice.krad.document.Document,
271      *      org.kuali.rice.krad.bo.AdHocRoutePerson)
272      */
273     public boolean processAddAdHocRoutePerson(Document document, AdHocRoutePerson adHocRoutePerson) {
274         boolean isValid = true;
275 
276         isValid &= isAddHocRoutePersonValid(document, adHocRoutePerson);
277 
278         isValid &= processCustomAddAdHocRoutePersonBusinessRules(document, adHocRoutePerson);
279         return isValid;
280     }
281 
282     /**
283      * @see org.kuali.rice.krad.rules.rule.SendAdHocRequestsRule#processSendAdHocRequests(org.kuali.rice.krad.document.Document)
284      */
285     public boolean processSendAdHocRequests(Document document) {
286         boolean isValid = true;
287 
288         isValid &= isAdHocRouteRecipientsValid(document);
289         isValid &= processCustomSendAdHocRequests(document);
290 
291         return isValid;
292     }
293 
294     protected boolean processCustomSendAdHocRequests(Document document) {
295         return true;
296     }
297 
298     /**
299      * Checks the adhoc route recipient list to ensure there are recipients or
300      * else throws an error that at least one recipient is required.
301      *
302      * @param document
303      * @return
304      */
305     protected boolean isAdHocRouteRecipientsValid(Document document) {
306         boolean isValid = true;
307         MessageMap errorMap = GlobalVariables.getMessageMap();
308 
309         if (errorMap.getErrorPath().size() == 0) {
310             // add the error path keys on the stack
311             errorMap.addToErrorPath(KRADConstants.NEW_AD_HOC_ROUTE_PERSON_PROPERTY_NAME);
312         }
313 
314         if ((document.getAdHocRoutePersons() == null || document.getAdHocRoutePersons().isEmpty()) && (document
315                 .getAdHocRouteWorkgroups() == null || document.getAdHocRouteWorkgroups().isEmpty())) {
316 
317             GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID, "error.adhoc.missing.recipients");
318             isValid = false;
319         }
320 
321         // drop the error path keys off now
322         errorMap.removeFromErrorPath(KRADConstants.NEW_AD_HOC_ROUTE_PERSON_PROPERTY_NAME);
323 
324         return isValid;
325     }
326 
327     /**
328      * Verifies that the adHocRoutePerson's fields are valid - it does required and format checks.
329      *
330      * @param person
331      * @return boolean True if valid, false otherwise.
332      */
333     public boolean isAddHocRoutePersonValid(Document document, AdHocRoutePerson person) {
334         MessageMap errorMap = GlobalVariables.getMessageMap();
335 
336         // new recipients are not embedded in the error path; existing lines should be
337         if (errorMap.getErrorPath().size() == 0) {
338             // add the error path keys on the stack
339             errorMap.addToErrorPath(KRADConstants.NEW_AD_HOC_ROUTE_PERSON_PROPERTY_NAME);
340         }
341 
342         if (StringUtils.isNotBlank(person.getId())) {
343             Person user = getPersonService().getPersonByPrincipalName(person.getId());
344 
345             if (user == null) {
346                 GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
347                         RiceKeyConstants.ERROR_INVALID_ADHOC_PERSON_ID);
348             } else if (!getPermissionService().hasPermission(user.getPrincipalId(),
349                     KimConstants.KIM_TYPE_DEFAULT_NAMESPACE, KimConstants.PermissionNames.LOG_IN)) {
350                 GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
351                         RiceKeyConstants.ERROR_INACTIVE_ADHOC_PERSON_ID);
352             } else {
353                 Class docOrBoClass = null;
354                 if (document instanceof MaintenanceDocument) {
355                     docOrBoClass = ((MaintenanceDocument) document).getNewMaintainableObject().getDataObjectClass();
356                 } else {
357                     docOrBoClass = document.getClass();
358                 }
359 
360                 if (!getDocumentDictionaryService().getDocumentAuthorizer(document).canReceiveAdHoc(document, user,
361                         person.getActionRequested())) {
362                     GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
363                             RiceKeyConstants.ERROR_UNAUTHORIZED_ADHOC_PERSON_ID);
364                 }
365             }
366         } else {
367             GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
368                     RiceKeyConstants.ERROR_MISSING_ADHOC_PERSON_ID);
369         }
370 
371         // drop the error path keys off now
372         errorMap.removeFromErrorPath(KRADConstants.NEW_AD_HOC_ROUTE_PERSON_PROPERTY_NAME);
373 
374         return GlobalVariables.getMessageMap().hasNoErrors();
375     }
376 
377     /**
378      * This method should be overridden by children rule classes as a hook to implement document specific business rule
379      * checks for
380      * the "add ad hoc route person" event.
381      *
382      * @param document
383      * @param person
384      * @return boolean True if the rules checks passed, false otherwise.
385      */
386     protected boolean processCustomAddAdHocRoutePersonBusinessRules(Document document, AdHocRoutePerson person) {
387         return true;
388     }
389 
390     /**
391      * @see org.kuali.rice.krad.rules.rule.AddAdHocRouteWorkgroupRule#processAddAdHocRouteWorkgroup(org.kuali.rice.krad.document.Document,
392      *      org.kuali.rice.krad.bo.AdHocRouteWorkgroup)
393      */
394     public boolean processAddAdHocRouteWorkgroup(Document document, AdHocRouteWorkgroup adHocRouteWorkgroup) {
395         boolean isValid = true;
396 
397         isValid &= isAddHocRouteWorkgroupValid(adHocRouteWorkgroup);
398 
399         isValid &= processCustomAddAdHocRouteWorkgroupBusinessRules(document, adHocRouteWorkgroup);
400         return isValid;
401     }
402 
403     /**
404      * Verifies that the adHocRouteWorkgroup's fields are valid - it does required and format checks.
405      *
406      * @param workgroup
407      * @return boolean True if valid, false otherwise.
408      */
409     public boolean isAddHocRouteWorkgroupValid(AdHocRouteWorkgroup workgroup) {
410         MessageMap errorMap = GlobalVariables.getMessageMap();
411 
412         // new recipients are not embedded in the error path; existing lines should be
413         if (errorMap.getErrorPath().size() == 0) {
414             // add the error path keys on the stack
415             GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.NEW_AD_HOC_ROUTE_WORKGROUP_PROPERTY_NAME);
416         }
417 
418         if (workgroup.getRecipientName() != null && workgroup.getRecipientNamespaceCode() != null) {
419             // validate that they are a workgroup from the workgroup service by looking them up
420             try {
421                 Group group = getGroupService().getGroupByNamespaceCodeAndName(workgroup.getRecipientNamespaceCode(),
422                         workgroup.getRecipientName());
423                 if (group == null || !group.isActive()) {
424                     GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
425                             RiceKeyConstants.ERROR_INVALID_ADHOC_WORKGROUP_ID);
426                 }
427             } catch (Exception e) {
428                 LOG.error("isAddHocRouteWorkgroupValid(AdHocRouteWorkgroup)", e);
429 
430                 GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
431                         RiceKeyConstants.ERROR_INVALID_ADHOC_WORKGROUP_ID);
432             }
433         } else {
434             GlobalVariables.getMessageMap().putError(KRADPropertyConstants.ID,
435                     RiceKeyConstants.ERROR_MISSING_ADHOC_WORKGROUP_ID);
436         }
437 
438         // drop the error path keys off now
439         GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.NEW_AD_HOC_ROUTE_WORKGROUP_PROPERTY_NAME);
440 
441         return GlobalVariables.getMessageMap().hasNoErrors();
442     }
443 
444     /**
445      * This method should be overridden by children rule classes as a hook to implement document specific business rule
446      * checks for
447      * the "add ad hoc route workgroup" event.
448      *
449      * @param document
450      * @param workgroup
451      * @return boolean True if the rules checks passed, false otherwise.
452      */
453     protected boolean processCustomAddAdHocRouteWorkgroupBusinessRules(Document document,
454             AdHocRouteWorkgroup workgroup) {
455         return true;
456     }
457 
458     /**
459      * Gets the maximum number of levels the data-dictionary based validation will recurse for the document
460      */
461     public int getMaxDictionaryValidationDepth() {
462         return this.maxDictionaryValidationDepth;
463     }
464 
465     /**
466      * Gets the maximum number of levels the data-dictionary based validation will recurse for the document
467      */
468     public void setMaxDictionaryValidationDepth(int maxDictionaryValidationDepth) {
469         if (maxDictionaryValidationDepth < 0) {
470             LOG.error("Dictionary validation depth should be greater than or equal to 0.  Value received was: "
471                     + maxDictionaryValidationDepth);
472             throw new RuntimeException(
473                     "Dictionary validation depth should be greater than or equal to 0.  Value received was: "
474                             + maxDictionaryValidationDepth);
475         }
476         this.maxDictionaryValidationDepth = maxDictionaryValidationDepth;
477     }
478 
479     protected boolean validateSensitiveDataValue(String fieldName, String fieldValue, String fieldLabel) {
480         boolean dataValid = true;
481 
482         if (fieldValue == null) {
483             return dataValid;
484         }
485 
486         boolean patternFound = KRADUtils.containsSensitiveDataPatternMatch(fieldValue);
487         boolean warnForSensitiveData = CoreFrameworkServiceLocator.getParameterService().getParameterValueAsBoolean(
488                 KRADConstants.KNS_NAMESPACE, ParameterConstants.ALL_COMPONENT,
489                 KRADConstants.SystemGroupParameterNames.SENSITIVE_DATA_PATTERNS_WARNING_IND);
490         if (patternFound && !warnForSensitiveData) {
491             dataValid = false;
492             GlobalVariables.getMessageMap().putError(fieldName,
493                     RiceKeyConstants.ERROR_DOCUMENT_FIELD_CONTAINS_POSSIBLE_SENSITIVE_DATA, fieldLabel);
494         }
495 
496         return dataValid;
497     }
498 
499     protected DataDictionaryService getDataDictionaryService() {
500         if (dataDictionaryService == null) {
501             dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
502         }
503         return dataDictionaryService;
504     }
505 
506     protected PersonService getPersonService() {
507         if (personService == null) {
508             personService = KimApiServiceLocator.getPersonService();
509         }
510         return personService;
511     }
512 
513     public static GroupService getGroupService() {
514         if (groupService == null) {
515             groupService = KimApiServiceLocator.getGroupService();
516         }
517         return groupService;
518     }
519 
520     public static PermissionService getPermissionService() {
521         if (permissionService == null) {
522             permissionService = KimApiServiceLocator.getPermissionService();
523         }
524         return permissionService;
525     }
526 
527     protected DictionaryValidationService getDictionaryValidationService() {
528         if (dictionaryValidationService == null) {
529             dictionaryValidationService = KRADServiceLocatorWeb.getDictionaryValidationService();
530         }
531         return dictionaryValidationService;
532     }
533 
534     protected ConfigurationService getKualiConfigurationService() {
535         if (kualiConfigurationService == null) {
536             kualiConfigurationService = KRADServiceLocator.getKualiConfigurationService();
537         }
538         return kualiConfigurationService;
539     }
540 
541     protected static DocumentDictionaryService getDocumentDictionaryService() {
542         if (documentDictionaryService == null) {
543             documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
544         }
545         return documentDictionaryService;
546     }
547 
548     public static void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
549         DocumentRuleBase.documentDictionaryService = documentDictionaryService;
550     }
551 }