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.actions; 017 018import java.util.Collection; 019import java.util.HashSet; 020import java.util.List; 021import java.util.Set; 022 023import org.apache.log4j.MDC; 024import org.kuali.rice.kew.actionrequest.ActionRequestValue; 025import org.kuali.rice.kew.actionrequest.Recipient; 026import org.kuali.rice.kew.actiontaken.ActionTakenValue; 027import org.kuali.rice.kew.api.KewApiServiceLocator; 028import org.kuali.rice.kew.api.action.MovePoint; 029import org.kuali.rice.kew.api.document.DocumentOrchestrationQueue; 030import org.kuali.rice.kew.api.document.DocumentProcessingOptions; 031import org.kuali.rice.kew.api.exception.InvalidActionTakenException; 032import org.kuali.rice.kew.engine.RouteContext; 033import org.kuali.rice.kew.engine.node.RouteNode; 034import org.kuali.rice.kew.engine.node.RouteNodeInstance; 035import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 036import org.kuali.rice.kew.service.KEWServiceLocator; 037import org.kuali.rice.kew.api.KewApiConstants; 038import org.kuali.rice.kim.api.identity.principal.PrincipalContract; 039 040 041/** 042 * Returns a document to a previous node in the route. 043 * 044 * Current implementation only supports returning to a node on the main branch of the 045 * document. 046 * 047 * @author Kuali Rice Team (rice.collab@kuali.org) 048 */ 049public class MoveDocumentAction extends ActionTakenEvent { 050 051 protected final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(getClass()); 052 053 private MovePoint movePoint; 054 055 public MoveDocumentAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal) { 056 super(KewApiConstants.ACTION_TAKEN_MOVE_CD, routeHeader, principal); 057 } 058 059 public MoveDocumentAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, MovePoint movePoint) { 060 super(KewApiConstants.ACTION_TAKEN_MOVE_CD, routeHeader, principal, annotation); 061 this.movePoint = movePoint; 062 } 063 064 /* (non-Javadoc) 065 * @see org.kuali.rice.kew.actions.ActionTakenEvent#isActionCompatibleRequest(java.util.List) 066 */ 067 @Override 068 public String validateActionRules() { 069 return validateActionRules(getActionRequestService().findAllPendingRequests(routeHeader.getDocumentId()), KEWServiceLocator.getRouteNodeService().getActiveRouteNodeNames(getRouteHeader().getDocumentId())); 070 } 071 072 @Override 073 public String validateActionRules(List<ActionRequestValue> actionRequests) { 074 return validateActionRules(actionRequests, KEWServiceLocator.getRouteNodeService().getActiveRouteNodeNames(getRouteHeader().getDocumentId())); 075 } 076 077 private String validateActionRules(List<ActionRequestValue> actionRequests, Collection<String> activeNodes) { 078 if (!getRouteHeader().isValidActionToTake(getActionPerformedCode())) { 079 return "Document is not in a state to be moved"; 080 } 081 if (activeNodes.isEmpty()) { 082 return "Document has no active nodes."; 083 } 084 List<ActionRequestValue> filteredActionRequests = filterActionRequestsByCode(actionRequests, KewApiConstants.ACTION_REQUEST_COMPLETE_REQ); 085 if (!isActionCompatibleRequest(filteredActionRequests)) { 086 return "No request for the user is compatible with the MOVE action"; 087 } 088 return ""; 089 } 090 091 092 /* (non-Javadoc) 093 * @see org.kuali.rice.kew.actions.ActionTakenEvent#isActionCompatibleRequest(java.util.List) 094 */ 095 public boolean isActionCompatibleRequest(List<ActionRequestValue> requests) { 096 //Move is always correct because the client application has authorized it 097 return true; 098 } 099 100 public void recordAction() throws InvalidActionTakenException { 101 MDC.put("docId", getRouteHeader().getDocumentId()); 102 updateSearchableAttributesIfPossible(); 103 LOG.debug("Moving document " + getRouteHeader().getDocumentId() + " to point: " + displayMovePoint(movePoint) + ", annotation: " + annotation); 104 105 List actionRequests = getActionRequestService().findAllValidRequests(getPrincipal().getPrincipalId(), getDocumentId(), KewApiConstants.ACTION_REQUEST_COMPLETE_REQ); 106 Collection activeNodes = KEWServiceLocator.getRouteNodeService().getActiveNodeInstances(getRouteHeader().getDocumentId()); 107 String errorMessage = validateActionRules(actionRequests,activeNodes); 108 if (!org.apache.commons.lang.StringUtils.isEmpty(errorMessage)) { 109 throw new InvalidActionTakenException(errorMessage); 110 } 111 112 //KULRICE-12283:Modified the logic so this action moves the document to enroute status before attempting to move it to another node if it is initialized or saved 113 if (getRouteHeader().isStateInitiated() || getRouteHeader().isStateSaved()) { 114 markDocumentEnroute(getRouteHeader()); 115 getRouteHeader().setRoutedByUserWorkflowId(getPrincipal().getPrincipalId()); 116 } 117 RouteNodeInstance startNodeInstance = determineStartNode(activeNodes, movePoint); 118 119 LOG.debug("Record the move action"); 120 Recipient delegator = findDelegatorForActionRequests(actionRequests); 121 ActionTakenValue actionTaken = saveActionTaken(delegator); 122 getActionRequestService().deactivateRequests(actionTaken, actionRequests); 123 notifyActionTaken(actionTaken); 124 125 // TODO this whole bit is a bit hacky at the moment 126 if (movePoint.getStepsToMove() > 0) { 127 Set<String> targetNodeNames = new HashSet<String>(); 128 targetNodeNames.add(determineFutureNodeName(startNodeInstance, movePoint)); 129 130 final boolean shouldIndex = getRouteHeader().getDocumentType().hasSearchableAttributes() && RouteContext.getCurrentRouteContext().isSearchIndexingRequestedForContext(); 131 String applicationId = routeHeader.getDocumentType().getApplicationId(); 132 DocumentOrchestrationQueue orchestrationQueue = KewApiServiceLocator.getDocumentOrchestrationQueue( 133 routeHeader.getDocumentId(), applicationId); 134 org.kuali.rice.kew.api.document.OrchestrationConfig orchestrationConfig = 135 org.kuali.rice.kew.api.document.OrchestrationConfig.create(actionTaken.getActionTakenId(), targetNodeNames); 136 //KULRICE-12283: Modified this to pass along two new flags to indicate that acks and FYIs should be deactivated with the move 137 DocumentProcessingOptions options = DocumentProcessingOptions.create(true, shouldIndex, false, true, true); 138 orchestrationQueue.orchestrateDocument(routeHeader.getDocumentId(), getPrincipal().getPrincipalId(), orchestrationConfig, options); 139 } else { 140 String targetNodeName = determineReturnNodeName(startNodeInstance, movePoint); 141 ReturnToPreviousNodeAction returnAction = new ReturnToPreviousNodeAction(KewApiConstants.ACTION_TAKEN_MOVE_CD, getRouteHeader(), getPrincipal(), annotation, targetNodeName, false); 142 143 returnAction.recordAction(); 144 } 145 } 146 147 private RouteNodeInstance determineStartNode(Collection<RouteNodeInstance> activeNodes, MovePoint movePoint) throws InvalidActionTakenException { 148 RouteNodeInstance startNodeInstance = null; 149 for (RouteNodeInstance nodeInstance : activeNodes) 150 { 151 if (nodeInstance.getName().equals(movePoint.getStartNodeName())) 152 { 153 if (startNodeInstance != null) 154 { 155 throw new InvalidActionTakenException("More than one active node exists with the given name: " + movePoint.getStartNodeName()); 156 } 157 startNodeInstance = nodeInstance; 158 } 159 } 160 if (startNodeInstance == null) { 161 throw new InvalidActionTakenException("Could not locate an active node with the given name: " + movePoint.getStartNodeName()); 162 } 163 return startNodeInstance; 164 } 165 166 private String determineFutureNodeName(RouteNodeInstance startNodeInstance, MovePoint movePoint) throws InvalidActionTakenException { 167 return determineFutureNodeName(startNodeInstance.getRouteNode(), movePoint, 0, new HashSet()); 168 } 169 170 private String determineFutureNodeName(RouteNode node, MovePoint movePoint, int currentStep, Set nodesProcessed) throws InvalidActionTakenException { 171 if (nodesProcessed.contains(node.getRouteNodeId())) { 172 throw new InvalidActionTakenException("Detected a cycle at node " + node.getRouteNodeName() + " when attempting to move document."); 173 } 174 nodesProcessed.add(node.getRouteNodeId()); 175 if (currentStep == movePoint.getStepsToMove()) { 176 return node.getRouteNodeName(); 177 } 178 List nextNodes = node.getNextNodes(); 179 if (nextNodes.size() == 0) { 180 throw new InvalidActionTakenException("Could not proceed forward, there are no more nodes in the route. Halted on step " + currentStep); 181 } 182 if (nextNodes.size() != 1) { 183 throw new InvalidActionTakenException("Cannot move forward in a multi-branch path. Located "+nextNodes.size()+" branches. Halted on step " + currentStep); 184 } 185 return determineFutureNodeName((RouteNode)nextNodes.get(0), movePoint, currentStep+1, nodesProcessed); 186 } 187 188 private String determineReturnNodeName(RouteNodeInstance startNodeInstance, MovePoint movePoint) throws InvalidActionTakenException { 189 return determineReturnNodeName(startNodeInstance.getRouteNode(), movePoint, 0); 190 } 191 192 private String determineReturnNodeName(RouteNode node, MovePoint movePoint, int currentStep) throws InvalidActionTakenException { 193 if (currentStep == movePoint.getStepsToMove()) { 194 return node.getRouteNodeName(); 195 } 196 List previousNodes = node.getPreviousNodes(); 197 if (previousNodes.size() == 0) { 198 throw new InvalidActionTakenException("Could not locate the named target node in the document's past route. Halted on step " + currentStep); 199 } 200 if (previousNodes.size() != 1) { 201 throw new InvalidActionTakenException("Located a multi-branch path, could not proceed backward past this point. Halted on step " + currentStep); 202 } 203 return determineReturnNodeName((RouteNode)previousNodes.get(0), movePoint, currentStep-1); 204 } 205 206 private String displayMovePoint(MovePoint movePoint) { 207 return "fromNode="+movePoint.getStartNodeName()+", stepsToMove="+movePoint.getStepsToMove(); 208 } 209 //KULRICE-12283: Copied a method from the BlanketApproveAction which moves a document to enroute status so we can perform a move on it 210 protected void markDocumentEnroute(DocumentRouteHeaderValue routeHeader) throws InvalidActionTakenException { 211 String oldStatus = routeHeader.getDocRouteStatus(); 212 routeHeader.markDocumentEnroute(); 213 String newStatus = routeHeader.getDocRouteStatus(); 214 notifyStatusChange(newStatus, oldStatus); 215 KEWServiceLocator.getRouteHeaderService().saveRouteHeader(routeHeader); 216 } 217}