View Javadoc

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