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 }