This is an automated email from the ASF dual-hosted git repository. mthl pushed a commit to branch trunk in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
commit 7044af8a6fe9261f4fc5806fd6fefd9e537ee5be Author: Samuel Trégouët <samuel.trego...@nereide.fr> AuthorDate: Wed Oct 30 17:21:40 2019 +0100 Implemented: Add a generic directed graph utilitary class (OFBIZ-11275) --- build.gradle | 2 + config/checkstyle/checkstyle.xml | 5 +- .../java/org/apache/ofbiz/base/util/Digraph.java | 118 +++++++++++++++++++++ .../org/apache/ofbiz/base/util/DiGraphTest.java | 93 ++++++++++++++++ 4 files changed, 217 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1f14f95..a4d9466 100644 --- a/build.gradle +++ b/build.gradle @@ -207,6 +207,8 @@ dependencies { testImplementation 'org.hamcrest:hamcrest:2.1' testImplementation 'org.hamcrest:hamcrest-library:2.1' // Enable junit4 to not depend on hamcrest-1.3 testImplementation 'org.mockito:mockito-core:3.1.0' + testImplementation 'com.pholser:junit-quickcheck-core:0.9' + testImplementation 'com.pholser:junit-quickcheck-generators:0.9' runtimeOnly 'javax.xml.soap:javax.xml.soap-api:1.4.0' runtimeOnly 'de.odysseus.juel:juel-spi:2.2.7' runtimeOnly 'net.sf.barcode4j:barcode4j-fop-ext:2.1' diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 3e3606b..426c5c3 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -109,7 +109,10 @@ under the License. <module name="SimplifyBooleanReturn"/> <!-- Checks for class design --> - <module name="DesignForExtension"/> + <module name="DesignForExtension"> + <property name="ignoredAnnotations" + value="After, AfterClass, Before, BeforeClass, Test, Property"/> + </module> <module name="FinalClass"/> <module name="HideUtilityClassConstructor"/> <module name="InterfaceIsType"/> diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/util/Digraph.java b/framework/base/src/main/java/org/apache/ofbiz/base/util/Digraph.java new file mode 100644 index 0000000..8314ef1 --- /dev/null +++ b/framework/base/src/main/java/org/apache/ofbiz/base/util/Digraph.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.ofbiz.base.util; + +import static java.util.stream.Collectors.toSet; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A basic directed graph utilitary. + * <p> + * A directed graph is a data structure consisting of nodes and arrows connecting those nodes + * which are called <em>edges</em>. In a directed graph edges are ordered pairs of respectively + * source and target nodes. + * <p> + * This implementation is adapted to small in-memory graphs. + * + * @param <T> the type of the nodes + * @see <a href="https://www.wikipedia.org/wiki/Directed_graph">Directed graph</a> + */ +public class Digraph<T> { + /** The map associating source nodes to their adjacent target nodes. */ + private final Map<T, Collection<T>> edges; + /** The set of nodes */ + private final Set<T> nodes; + + /** + * Constructs a directed graph from a specification Map. + * + * @param spec the map defining a set of source nodes (keys) that are linked to a collection + * of adjacent target nodes (values). Both keys and values must not be {@code null}. + * @throws IllegalArgumentException when a target node is not present in the sources nodes. + */ + public Digraph(Map<T, Collection<T>> spec) throws IllegalArgumentException { + this.edges = spec; + this.nodes = spec.keySet(); + // Check that all adjacent nodes are present as keys in the map. + Set<T> undeclaredNodes = spec.values().stream() + .flatMap(Collection::stream) + .filter(child -> !nodes.contains(child)) + .collect(toSet()); + if (!undeclaredNodes.isEmpty()) { + String msg = String.format("%s nodes are not present in the graph", undeclaredNodes); + throw new IllegalArgumentException(msg); + } + } + + /** + * Sort nodes in a topological ordering assuming that this graph is acyclic. + * <p> + * A graph without cycles is often called a <em>Directed Acyclic Graph</em> (DAG). + * + * @return a linear ordering of nodes such for every edge in the graph its target node + * is present before its source node. + * @throws IllegalStateException when this graph contains a cycle. + * @see <a href="https://www.wikipedia.org/wiki/Topological_sorting">topological sorting</a> + * @see <a href="https://www.wikipedia.org/wiki/Directed_acyclic_graph">DAG</a> + */ + public List<T> sort() throws IllegalStateException { + Set<T> permanents = new HashSet<>(); + Set<T> temporaries = new HashSet<>(); + List<T> result = new ArrayList<>(); + for (T node : nodes) { + if (!permanents.contains(node)) { + visit(result, node, permanents, temporaries); + } + } + return result; + } + + /** + * Traverses the graph using <em>Depth First Search</em> (DFS) to construct a topological ordering. + * + * @param res the ordered list that we are building + * @param root the current node we are visiting + * @param permanents the nodes that have been successfully been visited + * @param temporaries the nodes that we have started to visit but might contain cycles. + * @throws IllegalStateException when a cycle is found. + * @see #sort + * @see <a href="https://www.wikipedia.org/wiki/Depth-first_search">Depth Dirst Search</a> + */ + private void visit(List<T> res, T root, Set<T> permanents, Set<T> temporaries) throws IllegalStateException { + if (permanents.contains(root)) { + return; + } else if (temporaries.contains(root)) { + throw new IllegalStateException("A cycle has been found"); + } else { + temporaries.add(root); + for (T next : edges.get(root)) { + visit(res, next, permanents, temporaries); + } + temporaries.remove(root); + permanents.add(root); + res.add(root); + } + } +} diff --git a/framework/base/src/test/java/org/apache/ofbiz/base/util/DiGraphTest.java b/framework/base/src/test/java/org/apache/ofbiz/base/util/DiGraphTest.java new file mode 100644 index 0000000..de299cd --- /dev/null +++ b/framework/base/src/test/java/org/apache/ofbiz/base/util/DiGraphTest.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.ofbiz.base.util; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.in; +import static org.hamcrest.Matchers.is; +import static org.junit.Assume.assumeNoException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; + +@RunWith(JUnitQuickcheck.class) +public class DiGraphTest { + + @Test + public void testAcyclic() { + checkTopologicalOrder(UtilMisc.toMap( + "a", asList("b"), + "b", asList("c", "d"), + "c", emptyList(), + "d", emptyList(), + "z", emptyList(), + "f", emptyList(), + "e", asList("z", "b"))); + } + + @Test(expected = IllegalStateException.class) + public void testWithCycle() { + Map<String, Collection<String>> g = UtilMisc.toMap( + "a", asList("b"), + "b", asList("c"), + "c", asList("a")); + Digraph<String> dg = new Digraph<>(g); + dg.sort(); + } + + @Test + public void testMultipleParents() { + checkTopologicalOrder(UtilMisc.toMap( + "a", asList("b"), + "b", emptyList(), + "c", asList("b"))); + } + + @Property + public <T> void topologicalOrderProperty(Map<T, Collection<T>> graphspec) { + try { + checkTopologicalOrder(graphspec); + } catch (IllegalArgumentException e) { + assumeNoException("Invalid Graph", e); + } catch (IllegalStateException e) { + assumeNoException("Not a directed acyclic graph", e); + } + } + + private static <T> void checkTopologicalOrder(Map<T, Collection<T>> graphspec) { + Digraph<T> g = new Digraph<>(graphspec); + List<T> seen = new ArrayList<>(); + for (T node : g.sort()) { + for (T adjacent : graphspec.get(node)) { + assertThat("child nodes are before their parents", adjacent, is(in(seen))); + } + seen.add(node); + } + } +}