001    /**
002     * Copyright 2005-2014 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     */
016    package org.kuali.rice.kew.actions;
017    
018    import java.util.Collection;
019    import java.util.HashSet;
020    import java.util.List;
021    import java.util.Set;
022    
023    import org.apache.log4j.MDC;
024    import org.kuali.rice.kew.actionrequest.ActionRequestValue;
025    import org.kuali.rice.kew.actionrequest.Recipient;
026    import org.kuali.rice.kew.actiontaken.ActionTakenValue;
027    import org.kuali.rice.kew.api.KewApiServiceLocator;
028    import org.kuali.rice.kew.api.action.MovePoint;
029    import org.kuali.rice.kew.api.document.DocumentOrchestrationQueue;
030    import org.kuali.rice.kew.api.document.DocumentProcessingOptions;
031    import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
032    import org.kuali.rice.kew.engine.RouteContext;
033    import org.kuali.rice.kew.engine.node.RouteNode;
034    import org.kuali.rice.kew.engine.node.RouteNodeInstance;
035    import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
036    import org.kuali.rice.kew.service.KEWServiceLocator;
037    import org.kuali.rice.kew.api.KewApiConstants;
038    import 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     */
049    public 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                RouteNodeInstance startNodeInstance = determineStartNode(activeNodes, movePoint);
113    
114                LOG.debug("Record the move action");
115                Recipient delegator = findDelegatorForActionRequests(actionRequests);
116                ActionTakenValue actionTaken = saveActionTaken(delegator);
117                getActionRequestService().deactivateRequests(actionTaken, actionRequests);
118                notifyActionTaken(actionTaken);
119    
120                // TODO this whole bit is a bit hacky at the moment
121                if (movePoint.getStepsToMove() > 0) {
122                    Set<String> targetNodeNames = new HashSet<String>();
123                    targetNodeNames.add(determineFutureNodeName(startNodeInstance, movePoint));
124    
125                        final boolean shouldIndex = getRouteHeader().getDocumentType().hasSearchableAttributes() && RouteContext.getCurrentRouteContext().isSearchIndexingRequestedForContext();
126                    String applicationId = routeHeader.getDocumentType().getApplicationId();
127                    DocumentOrchestrationQueue orchestrationQueue = KewApiServiceLocator.getDocumentOrchestrationQueue(
128                            routeHeader.getDocumentId(), applicationId);
129                    org.kuali.rice.kew.api.document.OrchestrationConfig orchestrationConfig =
130                        org.kuali.rice.kew.api.document.OrchestrationConfig.create(actionTaken.getActionTakenId(), targetNodeNames);
131                    DocumentProcessingOptions options = DocumentProcessingOptions.create(true, shouldIndex, false);
132                    orchestrationQueue.orchestrateDocument(routeHeader.getDocumentId(), getPrincipal().getPrincipalId(), orchestrationConfig, options);
133                } else {
134                    String targetNodeName = determineReturnNodeName(startNodeInstance, movePoint);
135                    ReturnToPreviousNodeAction returnAction = new ReturnToPreviousNodeAction(KewApiConstants.ACTION_TAKEN_MOVE_CD, getRouteHeader(), getPrincipal(), annotation, targetNodeName, false);
136                    
137                    returnAction.recordAction();
138                }
139        }
140    
141        private RouteNodeInstance determineStartNode(Collection<RouteNodeInstance> activeNodes, MovePoint movePoint) throws InvalidActionTakenException {
142            RouteNodeInstance startNodeInstance = null;
143            for (RouteNodeInstance nodeInstance : activeNodes)
144            {
145                if (nodeInstance.getName().equals(movePoint.getStartNodeName()))
146                {
147                    if (startNodeInstance != null)
148                    {
149                        throw new InvalidActionTakenException("More than one active node exists with the given name:  " + movePoint.getStartNodeName());
150                    }
151                    startNodeInstance = nodeInstance;
152                }
153            }
154            if (startNodeInstance == null) {
155                throw new InvalidActionTakenException("Could not locate an active node with the given name: " + movePoint.getStartNodeName());
156            }
157            return startNodeInstance;
158        }
159    
160        private String determineFutureNodeName(RouteNodeInstance startNodeInstance, MovePoint movePoint) throws InvalidActionTakenException {
161            return determineFutureNodeName(startNodeInstance.getRouteNode(), movePoint, 0, new HashSet());
162        }
163    
164        private String determineFutureNodeName(RouteNode node, MovePoint movePoint, int currentStep, Set nodesProcessed) throws InvalidActionTakenException {
165            if (nodesProcessed.contains(node.getRouteNodeId())) {
166                throw new InvalidActionTakenException("Detected a cycle at node " + node.getRouteNodeName() + " when attempting to move document.");
167            }
168            nodesProcessed.add(node.getRouteNodeId());
169            if (currentStep == movePoint.getStepsToMove()) {
170                return node.getRouteNodeName();
171            }
172            List nextNodes = node.getNextNodes();
173            if (nextNodes.size() == 0) {
174                throw new InvalidActionTakenException("Could not proceed forward, there are no more nodes in the route.  Halted on step " + currentStep);
175            }
176            if (nextNodes.size() != 1) {
177                throw new InvalidActionTakenException("Cannot move forward in a multi-branch path.  Located "+nextNodes.size()+" branches.  Halted on step " + currentStep);
178            }
179            return determineFutureNodeName((RouteNode)nextNodes.get(0), movePoint, currentStep+1, nodesProcessed);
180        }
181    
182        private String determineReturnNodeName(RouteNodeInstance startNodeInstance, MovePoint movePoint) throws InvalidActionTakenException {
183            return determineReturnNodeName(startNodeInstance.getRouteNode(), movePoint, 0);
184        }
185    
186        private String determineReturnNodeName(RouteNode node, MovePoint movePoint, int currentStep) throws InvalidActionTakenException {
187            if (currentStep == movePoint.getStepsToMove()) {
188                return node.getRouteNodeName();
189            }
190            List previousNodes = node.getPreviousNodes();
191            if (previousNodes.size() == 0) {
192                throw new InvalidActionTakenException("Could not locate the named target node in the document's past route.  Halted on step " + currentStep);
193            }
194            if (previousNodes.size() != 1) {
195                throw new InvalidActionTakenException("Located a multi-branch path, could not proceed backward past this point.  Halted on step " + currentStep);
196            }
197            return determineReturnNodeName((RouteNode)previousNodes.get(0), movePoint, currentStep-1);
198        }
199    
200        private String displayMovePoint(MovePoint movePoint) {
201            return "fromNode="+movePoint.getStartNodeName()+", stepsToMove="+movePoint.getStepsToMove();
202        }
203    
204    }