/*
* Copyright 2015 ThoughtWorks, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.thoughtworks.go.util;
import java.util.Hashtable;
import java.util.Map;
import java.util.Stack;
import com.thoughtworks.go.config.CaseInsensitiveString;
public class DFSCycleDetector {
public final void topoSort(final CaseInsensitiveString root, final PipelineDependencyState pipelineDependencyState) throws Exception {
Hashtable<CaseInsensitiveString, CycleState> state = new Hashtable<>();
Stack<CaseInsensitiveString> visiting = new Stack<>();
if (!state.containsKey(root)) {
tsort(root, pipelineDependencyState, state, visiting);
} else if (state.get(root) == CycleState.VISITING) {
throw ExceptionUtils.bomb("Unexpected node in visiting state: " + root);
}
assertHasVisitedAllNodesInTree(state);
}
private void tsort(final CaseInsensitiveString root, final PipelineDependencyState pipelineDependencyState, final Hashtable<CaseInsensitiveString, CycleState> state, Stack<CaseInsensitiveString> visiting) throws Exception {
state.put(root, CycleState.VISITING);
visiting.push(root);
// Make sure we exist
validateRootExists(root, pipelineDependencyState, visiting);
Node stage = pipelineDependencyState.getDependencyMaterials(root);
for (Node.DependencyNode cur : stage.getDependencies()) {
if (!state.containsKey(cur.getPipelineName())) {
// Not been visited
tsort(cur.getPipelineName(), pipelineDependencyState, state, visiting);
} else if (state.get(cur.getPipelineName()) == CycleState.VISITING) {
// Currently visiting this node, so have a cycle
throwCircularException(cur.getPipelineName(), visiting);
}
}
popAndAssertTopIsConsistent(visiting, root);
state.put(root, CycleState.VISITED);
}
private void assertHasVisitedAllNodesInTree(Hashtable<CaseInsensitiveString, CycleState> state) {
for (Map.Entry<CaseInsensitiveString, CycleState> cycleStateEntry : state.entrySet()) {
if (cycleStateEntry.getValue() == CycleState.VISITING) {
throw ExceptionUtils.bomb("Unexpected node in visiting state: " + cycleStateEntry.getKey());
}
}
}
private void popAndAssertTopIsConsistent(Stack<CaseInsensitiveString> visiting, CaseInsensitiveString root) {
CaseInsensitiveString p = visiting.pop();
if (!root.equals(p)) {
throw ExceptionUtils.bomb("Unexpected internal error: expected to pop " + root + " but got " + p);
}
}
private void validateRootExists(CaseInsensitiveString root, PipelineDependencyState pipelineDependencyState, Stack<CaseInsensitiveString> visiting) throws Exception {
if (!pipelineDependencyState.hasPipeline(root)) {
StringBuffer sb = new StringBuffer("Pipeline \"");
sb.append(root);
sb.append("\" does not exist.");
visiting.pop();
if (!visiting.empty()) {
CaseInsensitiveString parent = visiting.peek();
sb.append(" It is used from pipeline \"");
sb.append(parent);
sb.append("\".");
}
throw new Exception(sb.toString());
}
}
private static void throwCircularException(CaseInsensitiveString end, Stack<CaseInsensitiveString> stk) throws Exception {
StringBuffer sb = new StringBuffer("Circular dependency: ");
sb.append(end);
CaseInsensitiveString c;
do {
c = stk.pop();
sb.append(" <- ");
sb.append(c);
} while (!c.equals(end));
throw new Exception(new String(sb));
}
private static enum CycleState {
VISITED, VISITING
}
}