diff --git a/javac-plugin/pom.xml b/javac-plugin/pom.xml index 608f3ec..d7c8c75 100644 --- a/javac-plugin/pom.xml +++ b/javac-plugin/pom.xml @@ -45,6 +45,10 @@ com.fasterxml.jackson.core jackson-databind + + com.google.guava + guava + junit diff --git a/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/Dependency.java b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/Dependency.java new file mode 100644 index 0000000..cea0f65 --- /dev/null +++ b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/Dependency.java @@ -0,0 +1,53 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph; + +import org.moditect.deptective.internal.graph.Dependency; + +/** + * @author Gerd Wütherich (gw@code-kontor.io) + */ +public class Dependency { + + private Node from; + private Node to; + private int aggregatdWeight; + + public Dependency(Node from, Node to, int aggregatdWeight) { + this.from = from; + this.to = to; + this.aggregatdWeight = aggregatdWeight; + + from.addOutgoingDependency(this); + } + + public Node getFrom() { + return from; + } + + public Node getTo() { + return to; + } + + public int getAggregatedWeight() { + return aggregatdWeight; + } + + @Override + public String toString() { + return "SimpleDependency [from=" + from + ", to=" + to + ", aggregatdWeight=" + aggregatdWeight + "]"; + } +} diff --git a/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/GraphUtils.java b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/GraphUtils.java new file mode 100644 index 0000000..2e1110c --- /dev/null +++ b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/GraphUtils.java @@ -0,0 +1,158 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.moditect.deptective.internal.graph.impl.DependencyStructureMatrix; +import org.moditect.deptective.internal.graph.impl.FastFasSorter; +import org.moditect.deptective.internal.graph.impl.Tarjan; + +/** + * + * @author Gerd Wütherich (gw@code-kontor.io) + */ +public class GraphUtils { + + /** + * A directed graph is called strongly connected if there is a path in each direction between each pair of vertices + * of the graph. A strongly connected component (SCC) of a directed graph is a maximal strongly connected subgraph. + * + * @param nodes the collection of nodes (the directed graph) + * @return a list of strongly connected components (SCCs). Note that the result also contains components that + * contain just a single node. If you want to detect 'real' cycle (size > 1) please use {@link GraphUtils#detectCycles(Collection)}. + */ + public static List> detectStronglyConnectedComponents(Collection nodes) { + return new Tarjan().detectStronglyConnectedComponents(checkNotNull(nodes)); + } + + /** + * Returns all strongly connected subgraphs (size > 1) of the specified graph. + * + * @param nodes + * @return a list of strongly connected components (SCCs) with a size > 1. + */ + public static List> detectCycles(Collection nodes) { + return new Tarjan().detectStronglyConnectedComponents(nodes).stream().filter(cycle -> cycle.size() > 1) + .collect(Collectors.toList()); + } + + /** + * Creates a dependency structure matrix (DSM) for the given graph nodes. + * + * @param nodes the collection of nodes + * @return + */ + public static IDependencyStructureMatrix createDependencyStructureMatrix( + Collection nodes) { + return new DependencyStructureMatrix(nodes); + } + + /** + * An adjacency matrix is a square matrix used to represent a finite graph. The elements of the matrix + * indicate whether pairs of vertices are connected (adjacent) or not in the graph. + * + * @param nodes the collection of nodes + * @return the adjacency matrix for the given list of nodes + */ + public static int[][] computeAdjacencyMatrix(List nodes) { + checkNotNull(nodes); + return computeAdjacencyMatrix((Node[]) nodes.toArray(new Node[nodes.size()])); + } + + /** + * An adjacency matrix is a square matrix used to represent a finite graph. The elements of the matrix + * indicate whether pairs of vertices are connected (adjacent) or not in the graph. + * + * @param nodes the array of nodes + * @return the adjacency matrix for the given list of nodes + */ + public static int[][] computeAdjacencyMatrix(Node... nodes) { + int[][] result = new int[nodes.length][nodes.length]; + for (int i = 0; i < result.length; i++) { + for (int j = 0; j < result.length; j++) { + Dependency dependency = nodes[i].getOutgoingDependencyTo(nodes[j]); + result[i][j] = dependency != null ? dependency.getAggregatedWeight() : 0; + } + } + return result; + } + + /** + * An adjacency list is a collection of (unordered) lists used to represent a finite graph. Each list + * describes the set of neighbors of a node. + * + * @param nodes the array of nodes + * @return the adjacency list for the given list of nodes + */ + public static int[][] computeAdjacencyList(Collection nodes) { + checkNotNull(nodes); + return computeAdjacencyList((Node[]) nodes.toArray(new Node[nodes.size()])); + } + + /** + * An adjacency list is a collection of (unordered) lists used to represent a finite graph. Each list + * describes the set of neighbors of a node. + * + * @param nodes the array of nodes + * @return the adjacency list for the given list of nodes + */ + public static int[][] computeAdjacencyList(Node... nodes) { + + int[][] matrix; + + // prepare + int i = 0; + Map map = new HashMap(); + for (Node iArtifact : nodes) { + map.put(iArtifact, i); + i++; + } + matrix = new int[nodes.length][]; + + for (Node node : nodes) { + Collection dependencies = node.getOutgoingDependenciesTo(Arrays.asList(nodes)); + if (dependencies == null) { + dependencies = Collections.emptyList(); + } + int index = map.get(node); + matrix[index] = new int[dependencies.size()]; + int count = 0; + for (Dependency dependency : dependencies) { + matrix[index][count] = map.get(dependency.getTo()); + count++; + } + } + return matrix; + } + + /** + * Creates a FastFAS based {@link INodeSorter}. + * + * @return a FastFAS based {@link INodeSorter}. + */ + public static INodeSorter createFasNodeSorter() { + return new FastFasSorter(); + } +} diff --git a/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/IDependencyStructureMatrix.java b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/IDependencyStructureMatrix.java new file mode 100644 index 0000000..5f1d104 --- /dev/null +++ b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/IDependencyStructureMatrix.java @@ -0,0 +1,36 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph; + +import java.util.List; + +/** + * @author Gerd Wütherich (gw@code-kontor.io) + */ +public interface IDependencyStructureMatrix { + + List getOrderedNodes(); + + List getUpwardDependencies(); + + List> getCycles(); + + boolean isCellInCycle(int i, int j); + + boolean isRowInCycle(int i); + + int getWeight(int i, int j); +} diff --git a/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/INodeSorter.java b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/INodeSorter.java new file mode 100644 index 0000000..ca767cc --- /dev/null +++ b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/INodeSorter.java @@ -0,0 +1,33 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph; + +import java.util.List; + +/** + * @author Gerd Wütherich (gw@code-kontor.io) + */ +public interface INodeSorter { + + SortResult sort(List node); + + public interface SortResult { + + List getOrderedNodes(); + + List getUpwardsDependencies(); + } +} diff --git a/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/Node.java b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/Node.java new file mode 100644 index 0000000..2383947 --- /dev/null +++ b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/Node.java @@ -0,0 +1,105 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.moditect.deptective.internal.graph.Dependency; +import org.moditect.deptective.internal.graph.Node; + +/** + * @author Gerd Wütherich (gw@code-kontor.io) + */ +public class Node { + + private Map outgoingDependencies; + private String id; + + public Node(String id) { + this.id = checkNotNull(id); + } + + public String getId() { + return id; + } + + public Dependency getOutgoingDependencyTo(Node node) { + + if (!hasOutgoingDependencies() || !outgoingDependencies.containsKey(checkNotNull(node))) { + return null; + } + + return outgoingDependencies.get(node); + } + + public Set getOutgoingDependenciesTo(Collection nodes) { + return checkNotNull(nodes).stream().map(node -> getOutgoingDependencyTo(node)).filter(dep -> dep != null) + .collect(Collectors.toSet()); + } + + public boolean hasOutgoingDependencies() { + return outgoingDependencies != null && !outgoingDependencies.isEmpty(); + } + + public void addOutgoingDependency(Dependency dependency) { + checkNotNull(dependency); + outgoingDependencies().put(dependency.getTo(), dependency); + } + + @Override + public String toString() { + return "Node [id=" + id + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Node other = (Node) obj; + if (id == null) { + if (other.id != null) + return false; + } + else if (!id.equals(other.id)) + return false; + return true; + } + + private Map outgoingDependencies() { + if (outgoingDependencies == null) { + outgoingDependencies = new HashMap<>(); + } + return outgoingDependencies; + } +} diff --git a/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/impl/DependencyStructureMatrix.java b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/impl/DependencyStructureMatrix.java new file mode 100644 index 0000000..e0b72ca --- /dev/null +++ b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/impl/DependencyStructureMatrix.java @@ -0,0 +1,130 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph.impl; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.moditect.deptective.internal.graph.Dependency; +import org.moditect.deptective.internal.graph.GraphUtils; +import org.moditect.deptective.internal.graph.IDependencyStructureMatrix; +import org.moditect.deptective.internal.graph.INodeSorter; +import org.moditect.deptective.internal.graph.INodeSorter.SortResult; +import org.moditect.deptective.internal.graph.Node; + +public class DependencyStructureMatrix implements IDependencyStructureMatrix { + + private List> cycles; + + private List nodes; + + private List upwardDependencies; + + public DependencyStructureMatrix(Collection nodes) { + initialize(nodes); + } + + @Override + public List getUpwardDependencies() { + return upwardDependencies; + } + + @Override + public int getWeight(int i, int j) { + + if (i < 0 || i >= nodes.size() || j < 0 || j >= nodes.size()) { + return -1; + } + + Dependency dependency = nodes.get(i).getOutgoingDependencyTo(nodes.get(j)); + + return dependency != null ? dependency.getAggregatedWeight() : 0; + } + + @Override + public List getOrderedNodes() { + return nodes; + } + + @Override + public boolean isRowInCycle(int i) { + return isCellInCycle(i, i); + } + + @Override + public boolean isCellInCycle(int i, int j) { + + if (i < 0 || i >= nodes.size() || j < 0 || j >= nodes.size()) { + return false; + } + + for (List cycle : cycles) { + if (cycle.size() > 1 && cycle.contains(nodes.get(i)) && cycle.contains(nodes.get(j))) { + return true; + } + } + + return false; + } + + @Override + public List> getCycles() { + return cycles; + } + + private void initialize(Collection unorderedArtifacts) { + + checkNotNull(unorderedArtifacts); + + upwardDependencies = new ArrayList<>(); + + List> c = GraphUtils.detectStronglyConnectedComponents(unorderedArtifacts); + INodeSorter artifactSorter = new FastFasSorter(); + for (List cycle : c) { + SortResult sortResult = artifactSorter.sort(cycle); + cycle.clear(); + cycle.addAll(sortResult.getOrderedNodes()); + upwardDependencies.addAll(sortResult.getUpwardsDependencies()); + } + + List orderedArtifacts = new ArrayList<>(); + + // optimize: un-cycled artifacts without dependencies first + for (List artifactList : c) { + if (artifactList.size() == 1 && !artifactList.get(0).hasOutgoingDependencies()) { + orderedArtifacts.add(artifactList.get(0)); + } + } + + for (List cycle : c) { + for (Node node : cycle) { + if (!orderedArtifacts.contains(node)) { + orderedArtifacts.add(node); + } + } + } + Collections.reverse(orderedArtifacts); + nodes = orderedArtifacts; + + // + cycles = c.stream().filter(nodeList -> nodeList.size() > 1).collect(Collectors.toList()); + } +} diff --git a/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/impl/FastFAS.java b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/impl/FastFAS.java new file mode 100644 index 0000000..73cf770 --- /dev/null +++ b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/impl/FastFAS.java @@ -0,0 +1,274 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph.impl; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + *

+ * http://dl.acm.org/citation.cfm?id=595057 + *

+ * + * @author Gerd Wütherich (gerd@gerd-wuetherich.de) + */ +public class FastFAS { + + /** the set of vertices */ + private Set vertices; + + /** the adjacency matrix */ + int[][] adjacencyMatrix; + + /** the result list 's1' */ + private List s1; + + /** the result list 's2' */ + private List s2; + + /** the skipped edges */ + private List skippedEdge; + + /** + *

+ * Creates a new instance of type {@link FastFAS}. + *

+ * + * @param adjacencyMatrix the adjacency matrix + */ + public FastFAS(int[][] adjacencyMatrix) { + this.adjacencyMatrix = checkNotNull(adjacencyMatrix); + } + + /** + *

+ * Returns the ordered sequence. + *

+ * + * @return the ordered sequence. + */ + public int[] getOrderedSequence() { + + // create the skipped edges list + skippedEdge = new ArrayList<>(); + + // create the vertices set + vertices = new HashSet(); + for (int i = 0; i < this.adjacencyMatrix.length; i++) { + vertices.add(i); + } + + // create the internal result lists + s1 = new ArrayList(); + s2 = new ArrayList(); + + // the main loop + while (!vertices.isEmpty()) { + if (findSink()) { + continue; + } + else if (findSource()) { + continue; + } + else if (findVertexToRemove()) { + continue; + } + } + + // convert to result array + return convertToArray(s1, s2); + } + + public List getSkippedEdge() { + return skippedEdge; + } + + public static int[] reverse(int[] sequence) { + + // + checkNotNull(sequence); + + // + int[] result = new int[sequence.length]; + for (int i = 0; i < sequence.length; i++) { + result[sequence.length - (1 + i)] = sequence[i]; + } + + // + return result; + } + + /** + *

+ * Tries to find and remove a sink. + *

+ * + * @return true if a sink was found and removed. + */ + private boolean findSink() { + + // initialize the sink + int sink = -1; + + // try to find a sink... + for (Integer i : vertices) { + sink = i; + for (Integer j : vertices) { + if (i != j && this.adjacencyMatrix[i][j] != 0) { + sink = -1; + break; + } + } + if (sink != -1) { + break; + } + } + + // if a sink was found, remove it and return true... + if (sink != -1) { + vertices.remove(sink); + s2.add(0, sink); + return true; + } + // ...otherwise return false + else { + return false; + } + } + + /** + *

+ * Tries to find and remove a source. + *

+ * + * @return true if a source was found and removed. + */ + private boolean findSource() { + + // initialize the source + int source = -1; + + // try to find a source... + for (Integer i : vertices) { + source = i; + for (Integer j : vertices) { + if (i != j && this.adjacencyMatrix[j][i] != 0) { + source = -1; + break; + } + } + if (source != -1) { + break; + } + } + + // if a source was found, remove it and return true... + if (source != -1) { + vertices.remove(source); + s1.add(source); + return true; + } + // ...otherwise return false + else { + return false; + } + } + + /** + *

+ *

+ * + * @return + */ + private boolean findVertexToRemove() { + + // initialize current maximum and vertex + int currentMaximum = Integer.MIN_VALUE; + int currentVertex = Integer.MIN_VALUE; + + // find the vertex with the highest maximum + for (Integer vertex : vertices) { + int delta = getDelta(vertex); + if (currentVertex == Integer.MIN_VALUE || currentMaximum < delta) { + currentMaximum = delta; + currentVertex = vertex; + } + } + + // remove vertex and return true... + vertices.remove(currentVertex); + + // + for (Integer j : vertices) { + if (currentVertex != j && this.adjacencyMatrix[j][currentVertex] != 0) { + skippedEdge.add(new Integer[] { j, currentVertex }); + } + } + + s1.add(currentVertex); + return false; + } + + /** + *

+ *

+ * + * @param vertex + * @return + */ + private int getDelta(int vertex) { + + int in = 0; + int out = 0; + + for (Integer j : vertices) { + if (vertex != j) { + in = in + this.adjacencyMatrix[j][vertex]; + out = out + this.adjacencyMatrix[vertex][j]; + } + } + + // + return out - in; + } + + /** + *

+ * Helper method. Concatenates the given lists and returns them as one array. + *

+ * + * @param s1 the list s1 + * @param s2 the list s2 + * @return the result array. + */ + private int[] convertToArray(List s1, List s2) { + int[] result = new int[s1.size() + s2.size()]; + int index = 0; + for (int i : s1) { + result[index] = i; + index++; + } + for (int i : s2) { + result[index] = i; + index++; + } + return result; + } +} diff --git a/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/impl/FastFasSorter.java b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/impl/FastFasSorter.java new file mode 100644 index 0000000..cdc254f --- /dev/null +++ b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/impl/FastFasSorter.java @@ -0,0 +1,92 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.moditect.deptective.internal.graph.GraphUtils; +import org.moditect.deptective.internal.graph.Dependency; +import org.moditect.deptective.internal.graph.Node; +import org.moditect.deptective.internal.graph.INodeSorter; + +public class FastFasSorter implements INodeSorter { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public SortResult sort(List artifacts) { + + // we have to compute the adjacency matrix first + int[][] adjacencyMatrix = GraphUtils.computeAdjacencyMatrix(artifacts); + + // the ordered sequence (highest first!) + FastFAS fastFAS = new FastFAS(adjacencyMatrix); + int[] ordered = fastFAS.getOrderedSequence(); + + // Bubbles + for (int outerIndex = 1; outerIndex < ordered.length; outerIndex++) { + for (int index = outerIndex; index >= 1; index--) { + + // + if (adjacencyMatrix[ordered[index]][ordered[index + - 1]] > adjacencyMatrix[ordered[index - 1]][ordered[index]]) { + + // swap... + int temp = ordered[index]; + ordered[index] = ordered[index - 1]; + ordered[index - 1] = temp; + + } + else { + + // stop bubbling... + break; + } + } + } + + // reverse it + ordered = FastFAS.reverse(ordered); + + // create the result nodes list + List resultNodes = new ArrayList<>(artifacts.size()); + for (int index : ordered) { + resultNodes.add(artifacts.get(index)); + } + + // create the list of upwards dependencies + List upwardsDependencies = new ArrayList<>(); + for (Integer[] values : fastFAS.getSkippedEdge()) { + Node source = artifacts.get(values[0]); + Node target = artifacts.get(values[1]); + upwardsDependencies.add(source.getOutgoingDependencyTo(target)); + } + + // return the result + return new SortResult() { + + @Override + public List getOrderedNodes() { + return resultNodes; + } + + @Override + public List getUpwardsDependencies() { + return upwardsDependencies; + } + }; + } +} diff --git a/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/impl/Tarjan.java b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/impl/Tarjan.java new file mode 100644 index 0000000..faaa28d --- /dev/null +++ b/javac-plugin/src/main/java/org/moditect/deptective/internal/graph/impl/Tarjan.java @@ -0,0 +1,97 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph.impl; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.moditect.deptective.internal.graph.GraphUtils; +import org.moditect.deptective.internal.graph.Node; + +public class Tarjan { + + private int _index = 0; + private ArrayList _stack = new ArrayList(); + private List> _stronglyConnectedComponents = new ArrayList>(); + int[] _vlowlink; + int[] _vindex; + + private Node[] _artifacts; + + public List> detectStronglyConnectedComponents(Collection artifacts) { + checkNotNull(artifacts); + + _artifacts = artifacts.toArray(new Node[0]); + int[][] adjacencyList = GraphUtils.computeAdjacencyList(_artifacts); + return executeTarjan(adjacencyList); + } + + private List> executeTarjan(int[][] graph) { + checkNotNull(graph); + + _stronglyConnectedComponents.clear(); + _index = 0; + _stack.clear(); + _vlowlink = new int[graph.length]; + _vindex = new int[graph.length]; + for (int i = 0; i < _vlowlink.length; i++) { + _vlowlink[i] = -1; + _vindex[i] = -1; + } + + for (int i = 0; i < graph.length; i++) { + if (_vindex[i] == -1) { + tarjan(i, graph); + } + } + + return _stronglyConnectedComponents; + } + + @SuppressWarnings("unchecked") + private void tarjan(int v, int[][] graph) { + checkNotNull(v); + checkNotNull(graph); + + _vindex[v] = _index; + _vlowlink[v] = _index; + + _index++; + _stack.add(0, v); + for (int n : graph[v]) { + if (_vindex[n] == -1) { + tarjan(n, graph); + _vlowlink[v] = Math.min(_vlowlink[v], _vlowlink[n]); + } + else if (_stack.contains(n)) { + _vlowlink[v] = Math.min(_vlowlink[v], _vindex[n]); + } + } + if (_vlowlink[v] == _vindex[v]) { + int n; + ArrayList component = new ArrayList(); + do { + n = _stack.remove(0); + component.add((T) _artifacts[n]); + } + while (n != v); + _stronglyConnectedComponents.add(component); + } + } +} diff --git a/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/CyclesTest.java b/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/CyclesTest.java new file mode 100644 index 0000000..dea2928 --- /dev/null +++ b/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/CyclesTest.java @@ -0,0 +1,45 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.Test; + +/** + * + * @author Gerd Wütherich (gw@code-kontor.io) + */ +public class CyclesTest { + + @Test + public void detectCycle() { + + // + List nodes = TestModelCreator.createDummyModel(); + + // + List> cycles = GraphUtils.detectCycles(nodes); + + // + assertThat(cycles).hasSize(1); + + // + assertThat(cycles.get(0)).contains(nodes.get(2)).contains(nodes.get(3)); + } +} diff --git a/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/DependencyStructureMatrixTest.java b/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/DependencyStructureMatrixTest.java new file mode 100644 index 0000000..66d70cd --- /dev/null +++ b/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/DependencyStructureMatrixTest.java @@ -0,0 +1,53 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.Test; + +/** + * + * @author Gerd Wütherich (gw@code-kontor.io) + */ +public class DependencyStructureMatrixTest { + + @Test + public void detectCycle() { + + // + List nodes = TestModelCreator.createDummyModel(); + + // + IDependencyStructureMatrix dsm = GraphUtils.createDependencyStructureMatrix(nodes); + + // assert ordered nodes + assertThat(dsm.getOrderedNodes()).hasSize(4).containsExactly( + nodes.get(0), nodes.get(1), nodes.get(2), + nodes.get(3) + ); + + // assert upward dependencies + assertThat(dsm.getUpwardDependencies()).hasSize(1) + .containsExactly(nodes.get(3).getOutgoingDependencyTo(nodes.get(2))); + + // assert cycles + assertThat(dsm.getCycles()).hasSize(1); + assertThat(dsm.getCycles().get(0)).containsExactly(nodes.get(3), nodes.get(2)); + } +} diff --git a/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/FasNodeSorterTest.java b/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/FasNodeSorterTest.java new file mode 100644 index 0000000..1971754 --- /dev/null +++ b/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/FasNodeSorterTest.java @@ -0,0 +1,42 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.Test; +import org.moditect.deptective.internal.graph.INodeSorter.SortResult; + +public class FasNodeSorterTest { + + @Test + public void sortNodes() { + + // + List nodes = TestModelCreator.createDummyModel(); + + // + INodeSorter nodeSorter = GraphUtils.createFasNodeSorter(); + + // + SortResult sortResult = nodeSorter.sort(nodes); + + // + assertThat(sortResult.getUpwardsDependencies()).hasSize(1); + } +} diff --git a/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/StronglyConnectedComponentsTest.java b/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/StronglyConnectedComponentsTest.java new file mode 100644 index 0000000..21d2fbf --- /dev/null +++ b/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/StronglyConnectedComponentsTest.java @@ -0,0 +1,49 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.Test; + +/** + * + * @author Gerd Wütherich (gw@code-kontor.io) + */ +public class StronglyConnectedComponentsTest { + + @Test + public void detectCycle() { + + // + List nodes = TestModelCreator.createDummyModel(); + + // + List> stronglyConnectedComponents = GraphUtils.detectStronglyConnectedComponents(nodes); + + // + assertThat(stronglyConnectedComponents).hasSize(3); + + // + for (List scc : stronglyConnectedComponents) { + if (scc.size() == 2) { + assertThat(scc).contains(nodes.get(2)).contains(nodes.get(3)); + } + } + } +} diff --git a/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/TestModelCreator.java b/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/TestModelCreator.java new file mode 100644 index 0000000..87dde64 --- /dev/null +++ b/javac-plugin/src/test/java/org/moditect/deptective/internal/graph/TestModelCreator.java @@ -0,0 +1,49 @@ +/** + * Copyright 2019 The ModiTect authors + * + * 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 org.moditect.deptective.internal.graph; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.moditect.deptective.internal.graph.Dependency; +import org.moditect.deptective.internal.graph.Node; + +/** + * + * @author Gerd Wütherich (gw@code-kontor.io) + */ +public class TestModelCreator { + + /** + * + * @return + */ + public static List createDummyModel() { + + Node p1 = new Node("p1"); + Node p2 = new Node("p2"); + Node p3 = new Node("p3"); + Node p4 = new Node("p4"); + + new Dependency(p1, p2, 13); + new Dependency(p2, p3, 57); + new Dependency(p3, p4, 45); + new Dependency(p4, p3, 3); + + return new ArrayList<>(Arrays.asList(p1, p2, p3, p4)); + } +} diff --git a/pom.xml b/pom.xml index 9b24588..29fb116 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,11 @@ jackson-databind 2.9.8 + + com.google.guava + guava + 27.0.1-jre +