View Javadoc

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