1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.kuali.rice.kew.actions;
17
18 import org.apache.commons.lang.ArrayUtils;
19 import org.apache.commons.lang.BooleanUtils;
20 import org.apache.commons.lang.StringUtils;
21 import org.apache.log4j.Logger;
22 import org.jdom.input.DOMBuilder;
23 import org.kuali.rice.core.api.CoreApiServiceLocator;
24 import org.kuali.rice.core.api.exception.RiceRuntimeException;
25 import org.kuali.rice.core.api.util.xml.XmlHelper;
26 import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
27 import org.kuali.rice.coreservice.impl.CoreServiceImplServiceLocator;
28 import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
29 import org.kuali.rice.kew.actionrequest.ActionRequestValue;
30 import org.kuali.rice.kew.actionrequest.KimGroupRecipient;
31 import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient;
32 import org.kuali.rice.kew.actionrequest.Recipient;
33 import org.kuali.rice.kew.actiontaken.ActionTakenValue;
34 import org.kuali.rice.kew.api.KewApiConstants;
35 import org.kuali.rice.kew.api.KewApiServiceLocator;
36 import org.kuali.rice.kew.api.WorkflowRuntimeException;
37 import org.kuali.rice.kew.api.action.ActionRequestPolicy;
38 import org.kuali.rice.kew.api.action.ActionRequestType;
39 import org.kuali.rice.kew.api.action.ActionTaken;
40 import org.kuali.rice.kew.api.action.ActionType;
41 import org.kuali.rice.kew.api.doctype.DocumentTypePolicy;
42 import org.kuali.rice.kew.api.doctype.ProcessDefinition;
43 import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
44 import org.kuali.rice.kew.doctype.bo.DocumentType;
45 import org.kuali.rice.kew.engine.node.ProcessDefinitionBo;
46 import org.kuali.rice.kew.engine.node.RouteNode;
47 import org.kuali.rice.kew.role.KimRoleRecipient;
48 import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
49 import org.kuali.rice.kew.rule.RuleResponsibilityBo;
50 import org.kuali.rice.kew.service.KEWServiceLocator;
51 import org.kuali.rice.kew.xml.CommonXmlParser;
52 import org.kuali.rice.kim.api.identity.principal.PrincipalContract;
53 import org.kuali.rice.kim.api.role.Role;
54 import org.kuali.rice.kim.api.services.KimApiServiceLocator;
55 import org.kuali.rice.krad.service.KRADServiceLocator;
56 import org.w3c.dom.Document;
57 import org.w3c.dom.Element;
58 import org.w3c.dom.Node;
59 import org.w3c.dom.NodeList;
60 import org.xml.sax.InputSource;
61 import org.xml.sax.SAXException;
62
63 import javax.xml.parsers.DocumentBuilderFactory;
64 import javax.xml.parsers.ParserConfigurationException;
65 import java.io.IOException;
66 import java.io.StringReader;
67 import java.util.ArrayList;
68 import java.util.Arrays;
69 import java.util.Collection;
70 import java.util.Collections;
71 import java.util.Comparator;
72 import java.util.HashMap;
73 import java.util.HashSet;
74 import java.util.List;
75 import java.util.Map;
76 import java.util.Set;
77
78
79
80
81 public class RecallAction extends ReturnToPreviousNodeAction {
82 private static final Logger LOG = Logger.getLogger(RecallAction.class);
83
84 protected final boolean cancel;
85 protected final Collection<Recipient> notificationRecipients;
86
87
88
89
90 public RecallAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal) {
91 this(routeHeader, principal, null, true, true, true);
92 }
93
94 public RecallAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, boolean cancel) {
95 this(routeHeader, principal, annotation, cancel, true, true);
96 }
97
98 public RecallAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, boolean cancel, boolean sendNotifications) {
99 this(routeHeader, principal, annotation, cancel, sendNotifications, true);
100 }
101
102 public RecallAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, boolean cancel, boolean sendNotifications, boolean runPostProcessorLogic) {
103 super(ActionType.RECALL.getCode(), routeHeader, principal,
104 principal.getPrincipalName() + " recalled document" + (StringUtils.isBlank(annotation) ? "" : ": " + annotation),
105 INITIAL_NODE_NAME,
106 sendNotifications, runPostProcessorLogic);
107 this.cancel = cancel;
108 this.notificationRecipients = Collections.unmodifiableCollection(parseNotificationRecipients(routeHeader));
109 }
110
111
112
113
114
115
116 protected static Collection<Recipient> parseNotificationRecipients(DocumentRouteHeaderValue routeHeader) {
117 Collection<Recipient> toNotify = new ArrayList<Recipient>();
118 org.kuali.rice.kew.doctype.DocumentTypePolicy recallNotification = routeHeader.getDocumentType().getRecallNotification();
119 if (recallNotification != null) {
120 String config = recallNotification.getPolicyStringValue();
121 if (!StringUtils.isBlank(config)) {
122 Document notificationConfig;
123 try {
124 notificationConfig = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(config)));
125 } catch (Exception e) {
126 throw new RuntimeException(e);
127 }
128 DOMBuilder jdom = new DOMBuilder();
129
130
131 NodeList recipients = notificationConfig.getDocumentElement().getElementsByTagName("recipients");
132 for (int i = 0; i < recipients.getLength(); i++) {
133 NodeList children = recipients.item(i).getChildNodes();
134 for (int j = 0; j < children.getLength(); j++) {
135 Node n = children.item(j);
136 if (n instanceof Element) {
137 Element e = (Element) n;
138 if ("role".equals(e.getNodeName())) {
139 String ns = e.getAttribute("namespace");
140 String name = e.getAttribute("name");
141 Role role = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName(ns, name);
142 if (role == null) {
143 LOG.error("No role round: " + ns + ":" + name);
144 } else {
145 toNotify.add(new KimRoleRecipient(role));
146 }
147 } else {
148
149
150 org.jdom.Element parent = new org.jdom.Element("parent");
151 parent.addContent(jdom.build((Element) e.cloneNode(true)).detach());
152 toNotify.add(CommonXmlParser.parseResponsibilityNameAndType(parent).getRecipient());
153 }
154 }
155 }
156 }
157 }
158 }
159 return toNotify;
160 }
161
162 @Override
163 public String validateActionRules(List<ActionRequestValue> actionRequests) {
164 if (!getRouteHeader().isValidActionToTake(getActionPerformedCode())) {
165 return "Document of status '" + getRouteHeader().getDocRouteStatus() + "' cannot taken action '" + ActionType.fromCode(this.getActionTakenCode()).getLabel() + "' to node name " + this.nodeName;
166 }
167
168
169 String errMsg = validateActionsTaken(getRouteHeader());
170 if (errMsg != null) {
171 return errMsg;
172 }
173
174
175 errMsg = validateRouting(getRouteHeader());
176 if (errMsg != null) {
177 return errMsg;
178 }
179
180
181 if (!KEWServiceLocator.getDocumentTypePermissionService().canRecall(getPrincipal().getPrincipalId(), getRouteHeader())) {
182 return "User is not authorized to Recall document";
183 }
184 return "";
185 }
186
187
188
189
190
191
192
193 protected String validateActionsTaken(DocumentRouteHeaderValue rh) {
194 String validActionsTaken = rh.getDocumentType().getPolicyByName(DocumentTypePolicy.RECALL_VALID_ACTIONSTAKEN.getCode(), (String) null).getPolicyStringValue();
195 if (StringUtils.isNotBlank(validActionsTaken)) {
196
197 String[] validActionsTakenStrings = validActionsTaken.split("\\s*,\\s*");
198 Set<ActionType> validActionTypes = new HashSet<ActionType>(validActionsTakenStrings.length);
199 for (String s: validActionsTakenStrings) {
200 ActionType at = ActionType.fromCodeOrLabel(s);
201 if (at == null) {
202 throw new IllegalArgumentException("Failed to locate the ActionType with the given code or label: " + s);
203 }
204 validActionTypes.add(at);
205 }
206
207 Collection<ActionTakenValue> actionsTaken = KEWServiceLocator.getActionTakenService().findByDocumentId(getRouteHeader().getDocumentId());
208
209 for (ActionTakenValue actionTaken: actionsTaken) {
210 ActionType at = ActionType.fromCode(actionTaken.getActionTaken());
211 if (!validActionTypes.contains(at)) {
212 return "Invalid prior action taken: '" + at.getLabel() + "'. Cannot Recall.";
213 }
214 }
215 }
216 return null;
217 }
218
219
220
221
222
223
224
225
226 protected String validateRouting(DocumentRouteHeaderValue rh) {
227 List<ProcessDefinitionBo> processes = rh.getDocumentType().getProcesses();
228
229 String errMsg = null;
230 if (processes.size() == 0) {
231
232 errMsg = "No processes are defined for document type. Recall is not applicable.";
233 } else {
234
235 RouteNode initialRouteNode = rh.getDocumentType().getPrimaryProcess().getInitialRouteNode();
236 if (initialRouteNode.getName().equalsIgnoreCase("ADHOC") && initialRouteNode.getNextNodeIds().isEmpty()) {
237 errMsg = "No non-adhoc route nodes defined for document type. Recall is not applicable";
238 }
239 }
240 return errMsg;
241 }
242
243 @Override
244 protected List<ActionRequestValue> findApplicableActionRequests(List<ActionRequestValue> actionRequests) {
245 return actionRequests;
246 }
247
248 @Override
249 public boolean isActionCompatibleRequest(List<ActionRequestValue> requests) {
250 return true;
251 }
252
253 @Override
254 protected ActionRequestType getReturnToInitiatorActionRequestType() {
255 return ActionRequestType.COMPLETE;
256 }
257
258
259
260
261 @Override
262 protected PrincipalContract determineInitialNodePrincipal(DocumentRouteHeaderValue routeHeader) {
263 return getPrincipal();
264 }
265
266 @Override
267 protected void sendAdditionalNotifications() {
268 super.sendAdditionalNotifications();
269
270 ActionRequestFactory arFactory = new ActionRequestFactory(routeHeader);
271 for (Recipient recipient: this.notificationRecipients) {
272 if (!(recipient instanceof KimRoleRecipient)) {
273 arFactory.addRootActionRequest(ActionRequestType.FYI.getCode(), 0, recipient, "Document was recalled", KewApiConstants.MACHINE_GENERATED_RESPONSIBILITY_ID, null, null, null);
274 } else {
275 KimRoleRecipient kimRoleRecipient = (KimRoleRecipient) recipient;
276
277 Map<String, String> qualifications = Collections.emptyMap();
278 LOG.info("Adding KIM Role Request for " + kimRoleRecipient.getRole());
279 arFactory.addKimRoleRequest(ActionRequestType.FYI.getCode(), 0, kimRoleRecipient.getRole(),
280 KimApiServiceLocator.getRoleService().getRoleMembers(Collections.singletonList(kimRoleRecipient.getRole().getId()), qualifications),
281 "Document was recalled", KewApiConstants.MACHINE_GENERATED_RESPONSIBILITY_ID,
282
283
284 true, ActionRequestPolicy.FIRST.getCode(), "Document was recalled");
285 }
286 }
287 getActionRequestService().activateRequests(arFactory.getRequestGraphs());
288 }
289
290 @Override
291 public void recordAction() throws InvalidActionTakenException {
292 if (this.cancel) {
293
294 String errorMessage = validateActionRules(getActionRequestService().findAllPendingRequests(getDocumentId()));
295 if (!org.apache.commons.lang.StringUtils.isEmpty(errorMessage)) {
296 throw new InvalidActionTakenException(errorMessage);
297 }
298
299
300
301 new CancelAction(ActionType.RECALL, this.routeHeader, this.getPrincipal(), this.annotation) {
302 @Override
303 public boolean isActionCompatibleRequest(List<ActionRequestValue> requests) {
304 return true;
305 }
306 @Override
307 protected void markDocumentStatus() throws InvalidActionTakenException {
308 getRouteHeader().markDocumentRecalled();
309 }
310 }.recordAction();
311 } else {
312 super.recordAction();
313
314 String oldStatus = getRouteHeader().getDocRouteStatus();
315 getRouteHeader().markDocumentSaved();
316 String newStatus = getRouteHeader().getDocRouteStatus();
317 notifyStatusChange(newStatus, oldStatus);
318 DocumentRouteHeaderValue routeHeaderValue = KEWServiceLocator.getRouteHeaderService().
319 saveRouteHeader(routeHeader);
320 setRouteHeader(routeHeaderValue);
321 }
322
323
324 ActionTakenValue last = getLastActionTaken(getDocumentId());
325 if (last != null) {
326 notifyAfterActionTaken(last);
327 }
328 }
329
330
331
332
333
334
335 protected static ActionTakenValue getLastActionTaken(String docId) {
336 ActionTakenValue last = null;
337 Collection<ActionTakenValue> actionsTaken = KEWServiceLocator.getActionTakenService().findByDocumentId(docId);
338 for (ActionTakenValue at: actionsTaken) {
339 if (last == null || at.getActionDate().after(last.getActionDate())) {
340 last = at;
341 }
342 }
343 return last;
344 }
345 }