001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.kew.actionrequest.service.impl;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.LinkedList;
021import java.util.List;
022
023import org.apache.commons.collections.CollectionUtils;
024import org.apache.commons.collections.Predicate;
025import org.kuali.rice.kew.actionitem.ActionItem;
026import org.kuali.rice.kew.actionrequest.ActionRequestValue;
027import org.kuali.rice.kew.api.action.ActionRequest;
028import org.kuali.rice.kew.api.action.RecipientType;
029import org.kuali.rice.kew.engine.node.NodeState;
030import org.kuali.rice.kew.engine.node.RouteNodeInstance;
031import 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 */
039public 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}