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.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
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
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
103
104
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
120
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
139
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
168 protected List<ActionRequestValue> findApplicableActionRequests(List<ActionRequestValue> actionRequests) {
169 return actionRequests;
170 }
171
172 @Override
173 public boolean isActionCompatibleRequest(List<ActionRequestValue> requests) {
174 return true;
175 }
176
177 @Override
178 protected ActionRequestType getReturnToInitiatorActionRequestType() {
179 return ActionRequestType.COMPLETE;
180 }
181
182
183
184
185 @Override
186 protected PrincipalContract determineInitialNodePrincipal(DocumentRouteHeaderValue routeHeader) {
187 return getPrincipal();
188 }
189
190 @Override
191 protected void sendAdditionalNotifications() {
192 super.sendAdditionalNotifications();
193
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
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
207
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
218 String errorMessage = validateActionRules(getActionRequestService().findAllPendingRequests(getDocumentId()));
219 if (!org.apache.commons.lang.StringUtils.isEmpty(errorMessage)) {
220 throw new InvalidActionTakenException(errorMessage);
221 }
222
223
224
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
238 String oldStatus = getRouteHeader().getDocRouteStatus();
239 getRouteHeader().markDocumentSaved();
240 String newStatus = getRouteHeader().getDocRouteStatus();
241 notifyStatusChange(newStatus, oldStatus);
242 KEWServiceLocator.getRouteHeaderService().saveRouteHeader(routeHeader);
243 }
244
245
246 ActionTakenValue last = getLastActionTaken(getDocumentId());
247 if (last != null) {
248 notifyAfterActionTaken(last);
249 }
250 }
251
252
253
254
255
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 }