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