001: /*
002: * Copyright 2005-2006 The Kuali Foundation.
003: *
004: *
005: * Licensed under the Educational Community License, Version 1.0 (the "License");
006: * you may not use this file except in compliance with the License.
007: * You may obtain a copy of the License at
008: *
009: * http://www.opensource.org/licenses/ecl1.php
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package edu.iu.uis.eden.actions;
018:
019: import java.util.ArrayList;
020: import java.util.Collection;
021: import java.util.Iterator;
022: import java.util.List;
023:
024: import org.apache.log4j.MDC;
025:
026: import edu.iu.uis.eden.DocumentRouteLevelChange;
027: import edu.iu.uis.eden.EdenConstants;
028: import edu.iu.uis.eden.KEWServiceLocator;
029: import edu.iu.uis.eden.actionrequests.ActionRequestFactory;
030: import edu.iu.uis.eden.actionrequests.ActionRequestValue;
031: import edu.iu.uis.eden.actiontaken.ActionTakenValue;
032: import edu.iu.uis.eden.engine.CompatUtils;
033: import edu.iu.uis.eden.engine.RouteHelper;
034: import edu.iu.uis.eden.engine.node.NodeGraphSearchCriteria;
035: import edu.iu.uis.eden.engine.node.NodeGraphSearchResult;
036: import edu.iu.uis.eden.engine.node.RouteNode;
037: import edu.iu.uis.eden.engine.node.RouteNodeInstance;
038: import edu.iu.uis.eden.engine.node.RouteNodeService;
039: import edu.iu.uis.eden.exception.EdenUserNotFoundException;
040: import edu.iu.uis.eden.exception.InvalidActionTakenException;
041: import edu.iu.uis.eden.exception.WorkflowRuntimeException;
042: import edu.iu.uis.eden.postprocessor.PostProcessor;
043: import edu.iu.uis.eden.postprocessor.ProcessDocReport;
044: import edu.iu.uis.eden.routeheader.DocumentRouteHeaderValue;
045: import edu.iu.uis.eden.user.Recipient;
046: import edu.iu.uis.eden.user.WorkflowUser;
047: import edu.iu.uis.eden.util.Utilities;
048:
049: /**
050: * Returns a document to a previous node in the route.
051: *
052: * Current implementation only supports returning to a node on the main branch of the
053: * document.
054: *
055: * @author ewestfal
056: */
057: public class ReturnToPreviousNodeAction extends ActionTakenEvent {
058:
059: protected final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
060: .getLogger(getClass());
061:
062: private RouteHelper helper = new RouteHelper();
063: private String nodeName;
064: private boolean super UserUsage;
065: private boolean sendNotifications = true;
066:
067: public ReturnToPreviousNodeAction(
068: DocumentRouteHeaderValue routeHeader, WorkflowUser user) {
069: super (routeHeader, user);
070: setActionTakenCode(EdenConstants.ACTION_TAKEN_RETURNED_TO_PREVIOUS_CD);
071: }
072:
073: public ReturnToPreviousNodeAction(
074: DocumentRouteHeaderValue routeHeader, WorkflowUser user,
075: String annotation, String nodeName,
076: boolean sendNotifications) {
077: super (routeHeader, user, annotation);
078: setActionTakenCode(EdenConstants.ACTION_TAKEN_RETURNED_TO_PREVIOUS_CD);
079: this .nodeName = nodeName;
080: this .sendNotifications = sendNotifications;
081: }
082:
083: /**
084: * TODO will this work properly in the case of an ALL APPROVE role requests with some of the requests already completed?
085: */
086: private void revokePendingRequests(List pendingRequests,
087: ActionTakenValue actionTaken, Recipient delegator)
088: throws EdenUserNotFoundException {
089: revokeRequests(pendingRequests);
090: getActionRequestService().deactivateRequests(actionTaken,
091: pendingRequests);
092: if (sendNotifications) {
093: ActionRequestFactory arFactory = new ActionRequestFactory(
094: getRouteHeader());
095: List notificationRequests = arFactory
096: .generateNotifications(pendingRequests, getUser(),
097: delegator,
098: EdenConstants.ACTION_REQUEST_FYI_REQ,
099: getActionTakenCode());
100: getActionRequestService().activateRequests(
101: notificationRequests);
102: }
103: }
104:
105: /**
106: * Takes a list of root action requests and marks them and all of their children as "non-current".
107: */
108: private void revokeRequests(List actionRequests)
109: throws EdenUserNotFoundException {
110: for (Iterator iterator = actionRequests.iterator(); iterator
111: .hasNext();) {
112: ActionRequestValue actionRequest = (ActionRequestValue) iterator
113: .next();
114: actionRequest.setCurrentIndicator(Boolean.FALSE);
115: if (actionRequest.getActionTaken() != null) {
116: actionRequest.getActionTaken().setCurrentIndicator(
117: Boolean.FALSE);
118: KEWServiceLocator
119: .getActionTakenService()
120: .saveActionTaken(actionRequest.getActionTaken());
121: }
122: revokeRequests(actionRequest.getChildrenRequests());
123: KEWServiceLocator.getActionRequestService()
124: .saveActionRequest(actionRequest);
125: }
126: }
127:
128: private void processReturnToInitiator(
129: RouteNodeInstance newNodeInstance)
130: throws EdenUserNotFoundException {
131: RouteNode initialNode = getRouteHeader().getDocumentType()
132: .getPrimaryProcess().getInitialRouteNode();
133: if (newNodeInstance.getRouteNode().getRouteNodeId().equals(
134: initialNode.getRouteNodeId())) {
135: LOG.debug("Document was returned to initiator");
136: ActionRequestFactory arFactory = new ActionRequestFactory(
137: getRouteHeader(), newNodeInstance);
138: ActionRequestValue notificationRequest = arFactory
139: .createNotificationRequest(
140: EdenConstants.ACTION_REQUEST_APPROVE_REQ,
141: getRouteHeader().getInitiatorUser(),
142: getActionTakenCode(), getUser(),
143: "Document initiator");
144: getActionRequestService().activateRequest(
145: notificationRequest);
146: }
147: }
148:
149: /* (non-Javadoc)
150: * @see edu.iu.uis.eden.actions.ActionTakenEvent#isActionCompatibleRequest(java.util.List)
151: */
152: @Override
153: public String validateActionRules()
154: throws EdenUserNotFoundException {
155: return validateActionRules(getActionRequestService()
156: .findAllValidRequests(getUser(),
157: routeHeader.getRouteHeaderId(),
158: EdenConstants.ACTION_REQUEST_COMPLETE_REQ));
159: }
160:
161: private String validateActionRules(List actionRequests)
162: throws EdenUserNotFoundException {
163: String super Error = super .validateActionTakenRules();
164: if (!Utilities.isEmpty(super Error)) {
165: return super Error;
166: }
167: if (!getRouteHeader().isValidActionToTake(
168: getActionPerformedCode())) {
169: String docStatus = getRouteHeader().getDocRouteStatus();
170: return "Document of status '" + docStatus
171: + "' cannot taken action '"
172: + EdenConstants.ACTION_TAKEN_RETURNED_TO_PREVIOUS
173: + "' to node name " + nodeName;
174: }
175: if (!isActionCompatibleRequest(actionRequests)
176: && !isSuperUserUsage()) {
177: return "No request for the user is compatible with the RETURN TO PREVIOUS NODE action";
178: }
179: return "";
180: }
181:
182: /* (non-Javadoc)
183: * @see edu.iu.uis.eden.actions.ActionTakenEvent#isActionCompatibleRequest(java.util.List)
184: */
185: @Override
186: public boolean isActionCompatibleRequest(List requests)
187: throws EdenUserNotFoundException {
188: String actionTakenCode = getActionPerformedCode();
189:
190: // Move is always correct because the client application has authorized it
191: if (EdenConstants.ACTION_TAKEN_MOVE_CD.equals(actionTakenCode)) {
192: return true;
193: }
194:
195: // can always cancel saved or initiated document
196: if (routeHeader.isStateInitiated()
197: || routeHeader.isStateSaved()) {
198: return true;
199: }
200:
201: boolean actionCompatible = false;
202: Iterator ars = requests.iterator();
203: ActionRequestValue actionRequest = null;
204:
205: while (ars.hasNext()) {
206: actionRequest = (ActionRequestValue) ars.next();
207:
208: //if (actionRequest.isWorkgroupRequest() && !actionRequest.getWorkgroup().hasMember(this.delegator)) {
209: // TODO might not need this, if so, do role check
210: /*if (actionRequest.isWorkgroupRequest() && !actionRequest.getWorkgroup().hasMember(this.user)) {
211: continue;
212: }*/
213:
214: String request = actionRequest.getActionRequested();
215:
216: if ((EdenConstants.ACTION_REQUEST_FYI_REQ.equals(request))
217: || (EdenConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ
218: .equals(request))
219: || (EdenConstants.ACTION_REQUEST_APPROVE_REQ
220: .equals(request))
221: || (EdenConstants.ACTION_REQUEST_COMPLETE_REQ
222: .equals(request))) {
223: actionCompatible = true;
224: break;
225: }
226:
227: // RETURN_TO_PREVIOUS_ROUTE_LEVEL action available only if you've been routed a complete or approve request
228: if (EdenConstants.ACTION_TAKEN_RETURNED_TO_PREVIOUS_CD
229: .equals(actionTakenCode)
230: && (EdenConstants.ACTION_REQUEST_COMPLETE_REQ
231: .equals(request) || EdenConstants.ACTION_REQUEST_APPROVE_REQ
232: .equals(request))) {
233: actionCompatible = true;
234: }
235: }
236:
237: return actionCompatible;
238: }
239:
240: public void recordAction() throws InvalidActionTakenException,
241: EdenUserNotFoundException {
242: MDC.put("docId", getRouteHeader().getRouteHeaderId());
243: checkLocking();
244: updateSearchableAttributesIfPossible();
245: LOG.debug("Returning document "
246: + getRouteHeader().getRouteHeaderId()
247: + " to previous node: " + nodeName + ", annotation: "
248: + annotation);
249:
250: List actionRequests = getActionRequestService()
251: .findAllValidRequests(getUser(), getRouteHeaderId(),
252: EdenConstants.ACTION_REQUEST_COMPLETE_REQ);
253: String errorMessage = validateActionRules(actionRequests);
254: if (!Utilities.isEmpty(errorMessage)) {
255: throw new InvalidActionTakenException(errorMessage);
256: }
257:
258: // if (getRouteHeader().isValidActionToTake(getActionTakenCode())) {
259: //
260: // List actionRequests = getActionRequestService().findAllValidRequests(getUser(), getRouteHeaderId(), EdenConstants.ACTION_REQUEST_COMPLETE_REQ);
261: // if (! isActionCompatibleRequest(actionRequests, getActionTakenCode()) && ! isSuperUserUsage()) {
262: // throw new InvalidActionTakenException("No request for the user is compatible with the RETURN TO PREVIOUS NODE action");
263: // }
264:
265: Collection activeNodeInstances = KEWServiceLocator
266: .getRouteNodeService().getActiveNodeInstances(
267: getRouteHeader().getRouteHeaderId());
268: NodeGraphSearchCriteria criteria = new NodeGraphSearchCriteria(
269: NodeGraphSearchCriteria.SEARCH_DIRECTION_BACKWARD,
270: activeNodeInstances, nodeName);
271: NodeGraphSearchResult result = KEWServiceLocator
272: .getRouteNodeService().searchNodeGraph(criteria);
273: validateReturnPoint(nodeName, activeNodeInstances, result);
274:
275: LOG.debug("Record the returnToPreviousNode action");
276: super .currentInd = Boolean.FALSE;
277: Recipient delegator = findDelegatorForActionRequests(actionRequests);
278: saveActionTaken(delegator);
279:
280: //getActionRequestService().deactivateRequests(actionTaken, actionRequests);
281: //notifyActionTaken(this.actionTaken);
282:
283: LOG
284: .debug("Finding requests in return path and setting current indicator to FALSE");
285: List doneRequests = new ArrayList();
286: List pendingRequests = new ArrayList();
287: for (Iterator iterator = result.getPath().iterator(); iterator
288: .hasNext();) {
289: RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator
290: .next();
291: // mark the node instance as having been revoked
292: KEWServiceLocator.getRouteNodeService().revokeNodeInstance(
293: getRouteHeader(), nodeInstance);
294: Long nodeInstanceId = nodeInstance.getRouteNodeInstanceId();
295: List nodeRequests = getActionRequestService()
296: .findRootRequestsByDocIdAtRouteNode(
297: getRouteHeader().getRouteHeaderId(),
298: nodeInstanceId);
299: for (Iterator requestIt = nodeRequests.iterator(); requestIt
300: .hasNext();) {
301: ActionRequestValue request = (ActionRequestValue) requestIt
302: .next();
303: if (request.isDone()) {
304: doneRequests.add(request);
305: } else {
306: pendingRequests.add(request);
307: }
308: }
309: }
310: revokeRequests(doneRequests);
311: LOG
312: .debug("Change pending requests to FYI and activate for docId "
313: + getRouteHeader().getRouteHeaderId());
314: revokePendingRequests(pendingRequests, actionTaken, delegator);
315: notifyActionTaken(this .actionTaken);
316: executeNodeChange(activeNodeInstances, result);
317: // } else {
318: // String docStatus = getRouteHeader().getDocRouteStatus();
319: // throw new InvalidActionTakenException("Document of status '" + docStatus + "' cannot taken action '" + EdenConstants.ACTION_TAKEN_RETURNED_TO_PREVIOUS + "' to node name "+nodeName);
320: // }
321: }
322:
323: /**
324: * This method runs various validation checks on the nodes we ended up at so as to make sure we don't
325: * invoke strange return scenarios.
326: */
327: private void validateReturnPoint(String nodeName,
328: Collection activeNodeInstances, NodeGraphSearchResult result)
329: throws InvalidActionTakenException {
330: RouteNodeInstance resultNodeInstance = result
331: .getResultNodeInstance();
332: if (result.getResultNodeInstance() == null) {
333: throw new InvalidActionTakenException(
334: "Could not locate return point for node name '"
335: + nodeName + "'.");
336: }
337: assertValidNodeType(resultNodeInstance);
338: assertValidBranch(resultNodeInstance, activeNodeInstances);
339: assertValidProcess(resultNodeInstance, activeNodeInstances);
340: assertFinalApprovalNodeNotInPath(result.getPath());
341: }
342:
343: private void assertValidNodeType(
344: RouteNodeInstance resultNodeInstance)
345: throws InvalidActionTakenException {
346: // the return point can only be a simple or a split node
347: if (!helper.isSimpleNode(resultNodeInstance.getRouteNode())
348: && !helper.isSplitNode(resultNodeInstance
349: .getRouteNode())) {
350: throw new InvalidActionTakenException(
351: "Can only return to a simple or a split node, attempting to return to "
352: + resultNodeInstance.getRouteNode()
353: .getNodeType());
354: }
355: }
356:
357: private void assertValidBranch(
358: RouteNodeInstance resultNodeInstance,
359: Collection activeNodeInstances)
360: throws InvalidActionTakenException {
361: // the branch of the return point needs to be the same as one of the branches of the active nodes or the same as the root branch
362: boolean inValidBranch = false;
363: if (resultNodeInstance.getBranch().getParentBranch() == null) {
364: inValidBranch = true;
365: } else {
366: for (Iterator iterator = activeNodeInstances.iterator(); iterator
367: .hasNext();) {
368: RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator
369: .next();
370: if (nodeInstance.getBranch().getBranchId().equals(
371: resultNodeInstance.getBranch().getBranchId())) {
372: inValidBranch = true;
373: break;
374: }
375: }
376: }
377: if (!inValidBranch) {
378: throw new InvalidActionTakenException(
379: "Returning to an illegal branch, can only return to node within the same branch as an active node or to the primary branch.");
380: }
381: }
382:
383: private void assertValidProcess(
384: RouteNodeInstance resultNodeInstance,
385: Collection activeNodeInstances)
386: throws InvalidActionTakenException {
387: // if we are in a process, we need to return within the same process
388: if (resultNodeInstance.isInProcess()) {
389: boolean inValidProcess = false;
390: for (Iterator iterator = activeNodeInstances.iterator(); iterator
391: .hasNext();) {
392: RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator
393: .next();
394: if (nodeInstance.isInProcess()
395: && nodeInstance
396: .getProcess()
397: .getRouteNodeInstanceId()
398: .equals(
399: nodeInstance
400: .getProcess()
401: .getRouteNodeInstanceId())) {
402: inValidProcess = true;
403: break;
404: }
405: }
406: if (!inValidProcess) {
407: throw new InvalidActionTakenException(
408: "Returning into an illegal process, cannot return to node within a previously executing process.");
409: }
410: }
411: }
412:
413: /**
414: * Cannot return past a COMPLETE final approval node. This means that you can return from an active and incomplete final approval node.
415: * @param path
416: * @throws InvalidActionTakenException
417: */
418: private void assertFinalApprovalNodeNotInPath(List path)
419: throws InvalidActionTakenException {
420: for (Iterator iterator = path.iterator(); iterator.hasNext();) {
421: RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator
422: .next();
423: // if we have a complete final approval node in our path, we cannot return past it
424: if (nodeInstance.isComplete()
425: && Boolean.TRUE.equals(nodeInstance.getRouteNode()
426: .getFinalApprovalInd())) {
427: throw new InvalidActionTakenException(
428: "Cannot return past or through the final approval node '"
429: + nodeInstance.getName() + "'.");
430: }
431: }
432: }
433:
434: private void executeNodeChange(Collection activeNodes,
435: NodeGraphSearchResult result)
436: throws InvalidActionTakenException,
437: EdenUserNotFoundException {
438: Integer oldRouteLevel = null;
439: Integer newRouteLevel = null;
440: if (CompatUtils.isRouteLevelCompatible(getRouteHeader())) {
441: int returnPathLength = result.getPath().size() - 1;
442: oldRouteLevel = getRouteHeader().getDocRouteLevel();
443: newRouteLevel = new Integer(oldRouteLevel.intValue()
444: - returnPathLength);
445: LOG.debug("Changing route header "
446: + getRouteHeader().getRouteHeaderId()
447: + " route level for backward compatibility to "
448: + newRouteLevel);
449: getRouteHeader().setDocRouteLevel(newRouteLevel);
450: getRouteHeaderService().saveRouteHeader(routeHeader);
451: }
452: List startingNodes = determineStartingNodes(result.getPath(),
453: activeNodes);
454: RouteNodeInstance newNodeInstance = materializeReturnPoint(
455: startingNodes, result);
456: for (Iterator iterator = startingNodes.iterator(); iterator
457: .hasNext();) {
458: RouteNodeInstance activeNode = (RouteNodeInstance) iterator
459: .next();
460: notifyNodeChange(oldRouteLevel, newRouteLevel, activeNode,
461: newNodeInstance);
462: }
463: processReturnToInitiator(newNodeInstance);
464: }
465:
466: private void notifyNodeChange(Integer oldRouteLevel,
467: Integer newRouteLevel, RouteNodeInstance oldNodeInstance,
468: RouteNodeInstance newNodeInstance)
469: throws InvalidActionTakenException {
470: try {
471: LOG.debug("Notifying post processor of route node change '"
472: + oldNodeInstance.getName() + "'->'"
473: + newNodeInstance.getName());
474: PostProcessor postProcessor = routeHeader.getDocumentType()
475: .getPostProcessor();
476: getRouteHeaderService().saveRouteHeader(getRouteHeader());
477: DocumentRouteLevelChange routeNodeChange = new DocumentRouteLevelChange(
478: routeHeader.getRouteHeaderId(), routeHeader
479: .getAppDocId(), oldRouteLevel,
480: newRouteLevel, oldNodeInstance.getName(),
481: newNodeInstance.getName(), oldNodeInstance
482: .getRouteNodeInstanceId(), newNodeInstance
483: .getRouteNodeInstanceId());
484: ProcessDocReport report = postProcessor
485: .doRouteLevelChange(routeNodeChange);
486: setRouteHeader(getRouteHeaderService().getRouteHeader(
487: getRouteHeaderId()));
488: if (!report.isSuccess()) {
489: LOG.warn(report.getMessage(), report
490: .getProcessException());
491: throw new InvalidActionTakenException(report
492: .getMessage());
493: }
494: } catch (Exception ex) {
495: throw new WorkflowRuntimeException(ex.getMessage());
496: }
497: }
498:
499: private List determineStartingNodes(List path,
500: Collection activeNodes) {
501: List startingNodes = new ArrayList();
502: for (Iterator iterator = activeNodes.iterator(); iterator
503: .hasNext();) {
504: RouteNodeInstance activeNodeInstance = (RouteNodeInstance) iterator
505: .next();
506: if (isInPath(activeNodeInstance, path)) {
507: startingNodes.add(activeNodeInstance);
508: }
509: }
510: return startingNodes;
511: }
512:
513: private boolean isInPath(RouteNodeInstance nodeInstance, List path) {
514: for (Iterator iterator = path.iterator(); iterator.hasNext();) {
515: RouteNodeInstance pathNodeInstance = (RouteNodeInstance) iterator
516: .next();
517: if (pathNodeInstance.getRouteNodeInstanceId().equals(
518: nodeInstance.getRouteNodeInstanceId())) {
519: return true;
520: }
521: }
522: return false;
523: }
524:
525: private RouteNodeInstance materializeReturnPoint(
526: Collection startingNodes, NodeGraphSearchResult result) {
527: RouteNodeService nodeService = KEWServiceLocator
528: .getRouteNodeService();
529: RouteNodeInstance returnInstance = result
530: .getResultNodeInstance();
531: RouteNodeInstance newNodeInstance = helper.getNodeFactory()
532: .createRouteNodeInstance(getRouteHeaderId(),
533: returnInstance.getRouteNode());
534: newNodeInstance.setBranch(returnInstance.getBranch());
535: newNodeInstance.setProcess(returnInstance.getProcess());
536: newNodeInstance.setComplete(false);
537: newNodeInstance.setActive(true);
538: for (Iterator iterator = startingNodes.iterator(); iterator
539: .hasNext();) {
540: RouteNodeInstance activeNodeInstance = (RouteNodeInstance) iterator
541: .next();
542: // TODO what if the activeNodeInstance already has next nodes?
543: activeNodeInstance.setComplete(true);
544: activeNodeInstance.setActive(false);
545: activeNodeInstance.setInitial(false);
546: activeNodeInstance.addNextNodeInstance(newNodeInstance);
547: }
548: for (Iterator iterator = startingNodes.iterator(); iterator
549: .hasNext();) {
550: RouteNodeInstance activeNodeInstance = (RouteNodeInstance) iterator
551: .next();
552: nodeService.save(activeNodeInstance);
553: }
554: // TODO really we need to call transitionTo on this node, how can we do that?
555: // this isn't an issue yet because we only allow simple nodes and split nodes at the moment which do no real
556: // work on transitionTo but we may need to enhance that in the future
557: return newNodeInstance;
558: }
559:
560: public boolean isSuperUserUsage() {
561: return super UserUsage;
562: }
563:
564: public void setSuperUserUsage(boolean superUserUsage) {
565: this.superUserUsage = superUserUsage;
566: }
567:
568: }
|