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}