View Javadoc

1   /**
2    * Copyright 2005-2012 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.actions;
17  
18  import org.apache.commons.lang.BooleanUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.apache.log4j.Logger;
21  import org.jdom.input.DOMBuilder;
22  import org.kuali.rice.core.api.exception.RiceRuntimeException;
23  import org.kuali.rice.core.api.util.xml.XmlHelper;
24  import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
25  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
26  import org.kuali.rice.kew.actionrequest.KimGroupRecipient;
27  import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient;
28  import org.kuali.rice.kew.actionrequest.Recipient;
29  import org.kuali.rice.kew.actiontaken.ActionTakenValue;
30  import org.kuali.rice.kew.api.KewApiConstants;
31  import org.kuali.rice.kew.api.KewApiServiceLocator;
32  import org.kuali.rice.kew.api.WorkflowRuntimeException;
33  import org.kuali.rice.kew.api.action.ActionRequestPolicy;
34  import org.kuali.rice.kew.api.action.ActionRequestType;
35  import org.kuali.rice.kew.api.action.ActionType;
36  import org.kuali.rice.kew.api.doctype.DocumentTypePolicy;
37  import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
38  import org.kuali.rice.kew.doctype.bo.DocumentType;
39  import org.kuali.rice.kew.role.KimRoleRecipient;
40  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
41  import org.kuali.rice.kew.rule.RuleResponsibilityBo;
42  import org.kuali.rice.kew.service.KEWServiceLocator;
43  import org.kuali.rice.kew.xml.CommonXmlParser;
44  import org.kuali.rice.kim.api.identity.principal.PrincipalContract;
45  import org.kuali.rice.kim.api.role.Role;
46  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
47  import org.w3c.dom.Document;
48  import org.w3c.dom.Element;
49  import org.w3c.dom.Node;
50  import org.w3c.dom.NodeList;
51  import org.xml.sax.InputSource;
52  import org.xml.sax.SAXException;
53  
54  import javax.xml.parsers.DocumentBuilderFactory;
55  import javax.xml.parsers.ParserConfigurationException;
56  import java.io.IOException;
57  import java.io.StringReader;
58  import java.util.ArrayList;
59  import java.util.Collection;
60  import java.util.Collections;
61  import java.util.Comparator;
62  import java.util.HashMap;
63  import java.util.HashSet;
64  import java.util.List;
65  import java.util.Map;
66  import java.util.Set;
67  
68  /**
69   * @since 2.1
70   */
71  public class RecallAction extends ReturnToPreviousNodeAction {
72      private static final Logger LOG = Logger.getLogger(RecallAction.class);
73  
74      protected final boolean cancel;
75      protected final Collection<Recipient> notificationRecipients;
76  
77      /**
78       * Constructor required for ActionRegistry validation
79       */
80      public RecallAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal) {
81          this(routeHeader, principal, null, true, true, true);
82      }
83  
84      public RecallAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, boolean cancel) {
85          this(routeHeader, principal, annotation, cancel, true, true);
86      }
87  
88      public RecallAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, boolean cancel, boolean sendNotifications) {
89          this(routeHeader, principal, annotation, cancel, sendNotifications, true);
90      }
91  
92      public RecallAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, boolean cancel, boolean sendNotifications, boolean runPostProcessorLogic) {
93          super(ActionType.RECALL.getCode(), routeHeader, principal,
94                principal.getPrincipalName() + " recalled document" + (StringUtils.isBlank(annotation) ? "" : ": " + annotation),
95                INITIAL_NODE_NAME,
96                sendNotifications, runPostProcessorLogic);
97          this.cancel = cancel;
98          this.notificationRecipients = Collections.unmodifiableCollection(parseNotificationRecipients(routeHeader));
99      }
100 
101     /**
102      * Parses notification recipients from the RECALL_NOTIFICATION document type policy, if present
103      * @param routeHeader this document
104      * @return notification recipient RuleResponsibilityBos
105      */
106     protected static Collection<Recipient> parseNotificationRecipients(DocumentRouteHeaderValue routeHeader) {
107         Collection<Recipient> toNotify = new ArrayList<Recipient>();
108         org.kuali.rice.kew.doctype.DocumentTypePolicy recallNotification = routeHeader.getDocumentType().getRecallNotification();
109         if (recallNotification != null) {
110             String config = recallNotification.getPolicyStringValue();
111             if (!StringUtils.isBlank(config)) {
112                 Document notificationConfig;
113                 try {
114                     notificationConfig = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(config)));
115                 } catch (Exception e) {
116                     throw new RuntimeException(e);
117                 }
118                 DOMBuilder jdom = new DOMBuilder();
119                 // ok, so using getElementsByTagName is less strict than it could be because it inspects all descendants
120                 // not just immediate children.  but the w3c dom API stinks and this is less painful than manually searching immediate children
121                 NodeList recipients = notificationConfig.getDocumentElement().getElementsByTagName("recipients");
122                 for (int i = 0; i < recipients.getLength(); i++) {
123                     NodeList children = recipients.item(i).getChildNodes();
124                     for (int j = 0; j < children.getLength(); j++) {
125                         Node n = children.item(j);
126                         if (n instanceof Element) {
127                             Element e = (Element) n;
128                             if ("role".equals(e.getNodeName())) {
129                                 String ns = e.getAttribute("namespace");
130                                 String name = e.getAttribute("name");
131                                 Role role = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName(ns, name);
132                                 if (role == null) {
133                                     LOG.error("No role round: " + ns + ":" + name);
134                                 } else {
135                                     toNotify.add(new KimRoleRecipient(role));
136                                 }
137                             } else {
138                                 // parseResponsibilityNameAndType parses a single responsibility choice from a parent element
139                                 // wrap the element with a parent so we can use this method
140                                 org.jdom.Element parent = new org.jdom.Element("parent");
141                                 parent.addContent(jdom.build((Element) e.cloneNode(true)).detach());
142                                 toNotify.add(CommonXmlParser.parseResponsibilityNameAndType(parent).getRecipient());
143                             }
144                         }
145                     }
146                 }
147             }
148         }
149         return toNotify;
150     }
151 
152     @Override
153     public String validateActionRules(List<ActionRequestValue> actionRequests) {
154         if (!getRouteHeader().isValidActionToTake(getActionPerformedCode())) {
155             return "Document of status '" + getRouteHeader().getDocRouteStatus() + "' cannot taken action '" + ActionType.fromCode(this.getActionTakenCode()).getLabel() + "' to node name " + this.nodeName;
156         }
157 
158         if (!KEWServiceLocator.getDocumentTypePermissionService().canRecall(getPrincipal().getPrincipalId(), getRouteHeader().getDocumentId(),
159                                                                             getRouteHeader().getDocumentType(), getRouteHeader().getCurrentNodeNames(),
160                                                                             getRouteHeader().getDocRouteStatus(), getRouteHeader().getApplicationDocumentStatus(),
161                                                                             getRouteHeader().getInitiatorPrincipalId())) {
162             return "User is not authorized to Recall document";
163         }
164         return "";
165     }
166 
167     @Override // overridden to simply pass through all actionrequests
168     protected List<ActionRequestValue> findApplicableActionRequests(List<ActionRequestValue> actionRequests) {
169         return actionRequests;
170     }
171 
172     @Override // overridden to implement Recall action validation
173     public boolean isActionCompatibleRequest(List<ActionRequestValue> requests) {
174         return true;
175     }
176 
177     @Override // When invoked, RECALL TO ACTION LIST action will return the document back to their action list with the route status of SAVED and requested action of COMPLETE.
178     protected ActionRequestType getReturnToInitiatorActionRequestType() {
179         return ActionRequestType.COMPLETE;
180     }
181 
182     /**
183      * Override the default return to previous behavior so that the document is returned to the recaller, not initiator
184      */
185     @Override
186     protected PrincipalContract determineInitialNodePrincipal(DocumentRouteHeaderValue routeHeader) {
187         return getPrincipal();
188     }
189 
190     @Override
191     protected void sendAdditionalNotifications() {
192         super.sendAdditionalNotifications();
193         // NOTE: do we need construct w/ nodeInstance here?
194         ActionRequestFactory arFactory = new ActionRequestFactory(routeHeader);
195         for (Recipient recipient: this.notificationRecipients) {
196             if (!(recipient instanceof KimRoleRecipient)) {
197                 arFactory.addRootActionRequest(ActionRequestType.FYI.getCode(), 0, recipient, "Document was recalled", KewApiConstants.MACHINE_GENERATED_RESPONSIBILITY_ID, null, null, null);
198             } else {
199                 KimRoleRecipient kimRoleRecipient = (KimRoleRecipient) recipient;
200                 // no qualifications
201                 Map<String, String> qualifications = Collections.emptyMap();
202                 LOG.info("Adding KIM Role Request for " + kimRoleRecipient.getRole());
203                 arFactory.addKimRoleRequest(ActionRequestType.FYI.getCode(), 0, kimRoleRecipient.getRole(),
204                                             KimApiServiceLocator.getRoleService().getRoleMembers(Collections.singletonList(kimRoleRecipient.getRole().getId()), qualifications),
205                                             "Document was recalled", KewApiConstants.MACHINE_GENERATED_RESPONSIBILITY_ID,
206                                             // force action, so the member is notified even if they have already participated in the workflow
207                                             // since this is a notification about an exceptional case they should receive
208                                             true, ActionRequestPolicy.FIRST.getCode(), "Document was recalled");
209             }
210         }
211         getActionRequestService().activateRequests(arFactory.getRequestGraphs());
212     }
213 
214     @Override
215     public void recordAction() throws InvalidActionTakenException {
216         if (this.cancel) {
217             // perform action validity check here as well as WorkflowDocumentServiceImpl calls recordAction immediate after action construction
218             String errorMessage = validateActionRules(getActionRequestService().findAllPendingRequests(getDocumentId()));
219             if (!org.apache.commons.lang.StringUtils.isEmpty(errorMessage)) {
220                 throw new InvalidActionTakenException(errorMessage);
221             }
222             // we are in cancel mode, execute a cancel action with RECALL code
223             // NOTE: this performs CancelAction validation, including getDocumentTypePermissionService().canCancel
224             // stub out isActionCompatibleRequest since we are programmatically forcing this action
225             new CancelAction(ActionType.RECALL, this.routeHeader, this.getPrincipal(), this.annotation) {
226                 @Override
227                 public boolean isActionCompatibleRequest(List<ActionRequestValue> requests) {
228                     return true;
229                 }
230                 @Override
231                 protected void markDocumentStatus() throws InvalidActionTakenException {
232                     getRouteHeader().markDocumentRecalled();
233                 }
234             }.recordAction();
235         } else {
236             super.recordAction();
237             // When invoked, RECALL TO ACTION LIST action will return the document back to their action list with the route status of SAVED and requested action of COMPLETE.
238             String oldStatus = getRouteHeader().getDocRouteStatus();
239             getRouteHeader().markDocumentSaved();
240             String newStatus = getRouteHeader().getDocRouteStatus();
241             notifyStatusChange(newStatus, oldStatus);
242             KEWServiceLocator.getRouteHeaderService().saveRouteHeader(routeHeader);
243         }
244         // we don't have an established mechanism for exposing the action taken that is saved as the result of a (nested) action
245         // so use the last action take saved
246         ActionTakenValue last = getLastActionTaken(getDocumentId());
247         if (last != null) {
248             notifyAfterActionTaken(last);
249         }
250     }
251 
252     /**
253      * Returns the last action taken on a document
254      * @param docId the doc id
255      * @return last action taken on a document
256      */
257     protected static ActionTakenValue getLastActionTaken(String docId) {
258         ActionTakenValue last = null;
259         Collection<ActionTakenValue> actionsTaken = (Collection<ActionTakenValue>) KEWServiceLocator.getActionTakenService().getActionsTaken(docId);
260         for (ActionTakenValue at: actionsTaken) {
261             if (last == null || at.getActionDate().after(last.getActionDate())) {
262                 last = at;
263             }
264         }
265         return last;
266     }
267 }