001    /**
002     * Copyright 2005-2014 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.kew.actionrequest.service.impl;
017    
018    import java.util.ArrayList;
019    import java.util.Collections;
020    import java.util.LinkedList;
021    import java.util.List;
022    
023    import org.apache.commons.collections.CollectionUtils;
024    import org.apache.commons.collections.Predicate;
025    import org.kuali.rice.kew.actionitem.ActionItem;
026    import org.kuali.rice.kew.actionrequest.ActionRequestValue;
027    import org.kuali.rice.kew.api.action.ActionRequest;
028    import org.kuali.rice.kew.api.action.RecipientType;
029    import org.kuali.rice.kew.engine.node.NodeState;
030    import org.kuali.rice.kew.engine.node.RouteNodeInstance;
031    import org.kuali.rice.kew.service.KEWServiceLocator;
032    
033    /**
034     * This utility class encapsulates functions used to provide notification suppression
035     * 
036     * @author Kuali Rice Team (rice.collab@kuali.org)
037     *
038     */
039    public class NotificationSuppression {
040    
041        public static final String SUPPRESS_NOTIFY_KEY_START = "SuppressNotify";
042        
043            /**
044             * add metadata (a NodeState) to the route node so that if this action request is regenerated 
045             * verbatim,  the notification email will suppressed (since it is a duplicate!).
046             * @param nodeInstance where additional NodeState will be added
047             * @param actionRequestValue 
048             */
049        public void addNotificationSuppression(
050                    RouteNodeInstance nodeInstance, ActionRequestValue actionRequestValue) {
051    
052            // iterative depth first traversal of the action request tree
053            LinkedList<ActionRequestValue> stack = new LinkedList<ActionRequestValue>();
054            // push
055            stack.add(actionRequestValue);
056    
057            while (stack.size() > 0) {
058                    // pop our next action request 
059                    ActionRequestValue childActionRequest = stack.removeLast(); 
060    
061                    // process this action request only if it is a leaf
062                    if (childActionRequest.getChildrenRequests() == null || 
063                                    childActionRequest.getChildrenRequests().size() == 0) {
064                            List<String> requestKeys = getSuppressNotifyNodeStateKeys(childActionRequest);
065                            if (requestKeys != null) for (String requestKey : requestKeys) { 
066                                    if (nodeInstance.getNodeState(requestKey) == null) { // only add once
067                                            NodeState ns = new NodeState();
068                                            ns.setKey(requestKey);
069                                            ns.setValue("notification suppression");
070                                            nodeInstance.addNodeState(ns);
071                                    }
072                            }
073                    }
074    
075                    // put child action requests on the stack
076                    if (childActionRequest.getChildrenRequests() != null) {
077                            // equivalent to 'push' all
078                            stack.addAll(childActionRequest.getChildrenRequests());
079                    }
080            }
081        }
082            
083            /**
084             * This method filters any ActionItems whose related ActionRequestValueS have been flagged for notification
085             * suppression.
086             * 
087             * @param actionItems the ActionItemS to filter
088             * @param routeNodeInstance the RouteNodeInstance that the actionItems are associated with
089             */
090            protected void filterNotificationSuppressedActionItems(List<ActionItem> actionItems, 
091                            final RouteNodeInstance routeNodeInstance) {
092                    
093                    // remove all actionItems from the collection whose request has a suppress notification node state element
094                    CollectionUtils.filter(actionItems, new Predicate() {
095                            public boolean evaluate(Object object) {
096                                    boolean result = true;
097                                    ActionItem actionItem = (ActionItem)object;
098                                    ActionRequestValue actionRequest = 
099                                            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    }