View Javadoc
1   /**
2    * Copyright 2005-2015 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.kew.actionrequest.service.impl;
17  
18  import java.util.ArrayList;
19  import java.util.Collections;
20  import java.util.LinkedList;
21  import java.util.List;
22  
23  import org.apache.commons.collections.CollectionUtils;
24  import org.apache.commons.collections.Predicate;
25  import org.kuali.rice.kew.actionitem.ActionItem;
26  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
27  import org.kuali.rice.kew.api.action.ActionRequest;
28  import org.kuali.rice.kew.api.action.RecipientType;
29  import org.kuali.rice.kew.engine.node.NodeState;
30  import org.kuali.rice.kew.engine.node.RouteNodeInstance;
31  import org.kuali.rice.kew.service.KEWServiceLocator;
32  
33  /**
34   * This utility class encapsulates functions used to provide notification suppression
35   * 
36   * @author Kuali Rice Team (rice.collab@kuali.org)
37   *
38   */
39  public class NotificationSuppression {
40  
41      public static final String SUPPRESS_NOTIFY_KEY_START = "SuppressNotify";
42      
43  	/**
44  	 * add metadata (a NodeState) to the route node so that if this action request is regenerated 
45  	 * verbatim,  the notification email will suppressed (since it is a duplicate!).
46  	 * @param nodeInstance where additional NodeState will be added
47  	 * @param actionRequestValue 
48  	 */
49      public void addNotificationSuppression(
50      		RouteNodeInstance nodeInstance, ActionRequestValue actionRequestValue) {
51  
52      	// iterative depth first traversal of the action request tree
53      	LinkedList<ActionRequestValue> stack = new LinkedList<ActionRequestValue>();
54      	// push
55      	stack.add(actionRequestValue);
56  
57      	while (stack.size() > 0) {
58      		// pop our next action request 
59      		ActionRequestValue childActionRequest = stack.removeLast(); 
60  
61      		// process this action request only if it is a leaf
62      		if (childActionRequest.getChildrenRequests() == null || 
63      				childActionRequest.getChildrenRequests().size() == 0) {
64      			List<String> requestKeys = getSuppressNotifyNodeStateKeys(childActionRequest);
65      			if (requestKeys != null) for (String requestKey : requestKeys) { 
66      				if (nodeInstance.getNodeState(requestKey) == null) { // only add once
67      					NodeState ns = new NodeState();
68      					ns.setKey(requestKey);
69      					ns.setValue("notification suppression");
70      					nodeInstance.addNodeState(ns);
71      				}
72      			}
73      		}
74  
75      		// put child action requests on the stack
76      		if (childActionRequest.getChildrenRequests() != null) {
77      			// equivalent to 'push' all
78      			stack.addAll(childActionRequest.getChildrenRequests());
79      		}
80      	}
81      }
82  	
83  	/**
84  	 * This method filters any ActionItems whose related ActionRequestValueS have been flagged for notification
85  	 * suppression.
86  	 * 
87  	 * @param actionItems the ActionItemS to filter
88  	 * @param routeNodeInstance the RouteNodeInstance that the actionItems are associated with
89  	 */
90  	protected void filterNotificationSuppressedActionItems(List<ActionItem> actionItems, 
91  			final RouteNodeInstance routeNodeInstance) {
92  		
93  		// remove all actionItems from the collection whose request has a suppress notification node state element
94  		CollectionUtils.filter(actionItems, new Predicate() {
95  			public boolean evaluate(Object object) {
96  				boolean result = true;
97  				ActionItem actionItem = (ActionItem)object;
98  				ActionRequestValue actionRequest = 
99  					KEWServiceLocator.getActionRequestService().findByActionRequestId(actionItem.getActionRequestId());
100 				
101 				List<String> suppressNotificationKeys = getSuppressNotifyNodeStateKeys(actionRequest);
102 				if (suppressNotificationKeys != null && suppressNotificationKeys.size() > 0) {
103 					// if any of the keys are not present, we need to notify
104 					boolean containsAll = true;
105 					for (String key : suppressNotificationKeys) {
106 						if (routeNodeInstance.getNodeState(key) == null) {
107 							containsAll = false;
108 							break;
109 						}
110 					}
111 					// actionItem will be filtered if this Predicate returns false
112 					result = !containsAll; // only filters if all keys are present
113 				}
114 				return result;
115 			}
116 		});
117 	}
118 	
119 	/**
120 	 * 
121 	 * <p>This method takes care of notification for ActionItemS.  It has logic for suppressing notifications 
122      * when the RouteNodeInstance has NodeState specifically hinting for notification suppression for a given 
123      * ActionItem.
124 	 * 
125 	 * <p>A side effect is that any notification suppression NodeStateS will be removed
126 	 * from the RouteNodeInstance after notifications are sent.
127 	 * 
128 	 * @param actionItems a list of ActionItemS related to the given routeNodeInstance
129 	 * @param routeNodeInstance the RouteNodeInstance related to the given actionItems
130 	 */
131 	public void notify(List<ActionItem> actionItems, RouteNodeInstance routeNodeInstance) {
132 		
133 		if (actionItems != null && actionItems.size() > 0) {
134 			actionItems = new ArrayList<ActionItem>(actionItems); // defensive copy since we will filter
135 			filterNotificationSuppressedActionItems(actionItems, routeNodeInstance);
136 			// notify for any actionItems that were not filtered
137 			if (actionItems.size() > 0) { 
138 			    KEWServiceLocator.getNotificationService().notify(ActionItem.to(actionItems)); 
139 			}
140 			deleteNotificationSuppression(routeNodeInstance);
141 		}
142 	}
143 
144 	/**
145 	 * This method removes all NodeStates related to notification suppression, saving the RouteNodeInstance if there
146 	 * were any removed.
147 	 * 
148 	 * @param routeNodeInstance
149 	 */
150 	@SuppressWarnings("unchecked")
151 	private void deleteNotificationSuppression(
152 			final RouteNodeInstance routeNodeInstance) {
153 		// remove all suppress notification node states
154 		List<NodeState> nodeStates = routeNodeInstance.getState();
155 		if (nodeStates != null && nodeStates.size() > 0) {
156 			List<String> nodeStateKeysToRemove = new ArrayList<String>(nodeStates.size());
157 
158 			for (NodeState nodeState : nodeStates) {
159 				if (nodeState.getKey().startsWith(NotificationSuppression.SUPPRESS_NOTIFY_KEY_START)) {
160 					nodeStateKeysToRemove.add(nodeState.getKey());
161 				}
162 			}
163 			if (nodeStateKeysToRemove.size() > 0) {
164 				for (String nodeStateKeyToRemove : nodeStateKeysToRemove) {
165 					routeNodeInstance.removeNodeState(nodeStateKeyToRemove);
166 				}
167 				KEWServiceLocator.getRouteNodeService().save(routeNodeInstance);
168 			}
169 		}
170 	}
171 
172 	
173     /**
174      * Builds keys for action requests used for notification suppression.
175      * <p>NOTE: This method needs to stay in sync with {@link #getSuppressNotifyNodeStateKeys(org.kuali.rice.kew.dto.ActionRequestDTO)}
176      * Any changes here must be made there as well!
177      * @param a
178      * @return List
179      */
180 	protected List<String> getSuppressNotifyNodeStateKeys(ActionRequest a) {
181 		List<String> results = Collections.emptyList(); 
182 		if (a != null) {
183 			results = new ArrayList<String>(3);
184 			addSuppressNotifyNodeStateKey(results, RecipientType.PRINCIPAL.getCode(), a.getPrincipalId());
185 			addSuppressNotifyNodeStateKey(results, RecipientType.GROUP.getCode(), a.getGroupId());
186 			addSuppressNotifyNodeStateKey(results, RecipientType.ROLE.getCode(), a.getQualifiedRoleName());
187 		}
188 		return results;
189     }
190 
191     /**
192      * Builds keys for action requests used for notification suppression.
193      * <p>NOTE: This method needs to stay in sync with {@link #getSuppressNotifyNodeStateKeys(org.kuali.rice.kew.actionrequest.ActionRequestValue)}
194      * Any changes here must be made there as well!
195      * @param a
196      * @return List
197      */
198 	protected List<String> getSuppressNotifyNodeStateKeys(ActionRequestValue a) {
199 		List<String> results = Collections.emptyList(); 
200 		if (a != null) {
201 			results = new ArrayList<String>(3);
202 			addSuppressNotifyNodeStateKey(results, RecipientType.PRINCIPAL.getCode(), a.getPrincipalId());
203 			addSuppressNotifyNodeStateKey(results, RecipientType.GROUP.getCode(), a.getGroupId());
204 			addSuppressNotifyNodeStateKey(results, RecipientType.ROLE.getCode(), a.getQualifiedRoleName());
205 		}
206 		return results;
207 	}
208 
209 	
210 	/**
211 	 * This method adds a suppress notify key to the passed in list
212 	 * 
213 	 * @param results the list that the key will be added to
214 	 * @param responsiblePartyType
215 	 * @param responsiblePartyId
216 	 */
217 	private void addSuppressNotifyNodeStateKey(List<String> results, String responsiblePartyType,
218 			String responsiblePartyId) {
219 		if (responsiblePartyId != null && responsiblePartyType != null) {
220 			StringBuilder sb = new StringBuilder(SUPPRESS_NOTIFY_KEY_START);
221 			sb.append("(");
222 			sb.append(responsiblePartyType);
223 			sb.append(",");
224 			sb.append(responsiblePartyId);
225 			sb.append(")");
226 			results.add(sb.toString());
227 		}
228 	}
229 	
230 }