From 087a6f1ce342d47c15d8430f1f7b8271f9e3d140 Mon Sep 17 00:00:00 2001
From: jakubs <jakubs>
Date: Thu, 27 Sep 2012 13:24:08 +0000
Subject: [PATCH] BIS-201, SP-277 implement grouping dag

SVN: 26836
---
 .../cisd/common/collections/GroupingDAG.java  | 185 ++++++++++++++++++
 .../common/collections/GroupingDAGTest.java   | 133 +++++++++++++
 2 files changed, 318 insertions(+)
 create mode 100644 common/source/java/ch/systemsx/cisd/common/collections/GroupingDAG.java
 create mode 100644 common/sourceTest/java/ch/systemsx/cisd/common/collections/GroupingDAGTest.java

diff --git a/common/source/java/ch/systemsx/cisd/common/collections/GroupingDAG.java b/common/source/java/ch/systemsx/cisd/common/collections/GroupingDAG.java
new file mode 100644
index 00000000000..b6387704292
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/collections/GroupingDAG.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2012 ETH Zuerich, CISD
+ *
+ * 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 ch.systemsx.cisd.common.collections;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+
+/**
+ * The implementation of a DAG, that returns the group of items, where the first group includes the
+ * items without dependencies, the second group contain only dependencies to the first group etc.
+ * 
+ * @author Jakub Straszewski
+ */
+public class GroupingDAG<T>
+{
+
+    /**
+     * The algorithm works on three internal structures.
+     * <p>
+     * <code>graph</code> as the adjacency list, where vertice from A to B means that a must be
+     * executed before B. <code> dependenciesCount </code> is the priority table, which keeps
+     * information for each item, how many items should be still taken before this one.
+     * <code>queue </code> is the priority queue, that uses <code> dependenciesCount </code> as the
+     * priority key.
+     * <p>
+     * At each level algorithm takes the items that have zero dependencies, then updates the
+     * dependency counts, and repeats the procedure until there are no more vertices. If it cannot
+     * proceed at some stage - it means that there is a circular dependency and the exception is
+     * being thrown.
+     */
+    private final Map<T, Integer> dependenciesCount;
+
+    private final Map<T, Collection<T>> graph;
+
+    private final PriorityQueue<T> queue;
+
+    private final List<List<T>> sortedGroups;
+
+    private class DependenciesComparator implements Comparator<T>
+    {
+        @Override
+        public int compare(T o1, T o2)
+        {
+            return dependenciesCount.get(o1).compareTo(dependenciesCount.get(o2));
+        }
+    }
+
+    /**
+     * @param graph must be non-empty graph
+     */
+    private GroupingDAG(Map<T, Collection<T>> graph)
+    {
+        this.graph = graph;
+        this.dependenciesCount = new HashMap<T, Integer>();
+        this.queue = new PriorityQueue<T>(graph.size(), new DependenciesComparator());
+        this.sortedGroups = new LinkedList<List<T>>();
+        initialize();
+        sort();
+    }
+
+    /**
+     * Return the items in the list of groups, where the earlier groups are independent on the
+     * latter ones.
+     * 
+     * @param graph the connection graph(A).contains(B) means that A must be scheduled BEFORE B
+     */
+    public static <T> List<List<T>> groupByDepencies(Map<T, Collection<T>> graph)
+    {
+        if (graph.size() == 0)
+        {
+            return Collections.emptyList();
+        }
+
+        GroupingDAG<T> dag = new GroupingDAG<T>(graph);
+        return dag.sortedGroups;
+    }
+
+    private void addNoDependency(T item)
+    {
+        if (!dependenciesCount.containsKey(item))
+        {
+            dependenciesCount.put(item, 0);
+        }
+    }
+
+    private void addDependency(T dependant)
+    {
+        int count = 0;
+        if (dependenciesCount.containsKey(dependant))
+        {
+            count = dependenciesCount.get(dependant);
+        }
+        dependenciesCount.put(dependant, count + 1);
+    }
+
+    private void initialize()
+    {
+        for (Map.Entry<T, Collection<T>> entry : graph.entrySet())
+        {
+            addNoDependency(entry.getKey());
+            for (T dependant : entry.getValue())
+            {
+                addDependency(dependant);
+            }
+        }
+
+        for (T item : graph.keySet())
+        {
+            queue.add(item);
+        }
+    }
+
+    private void sort()
+    {
+        while (!queue.isEmpty())
+        {
+            List<T> levelItems = new LinkedList<T>();
+
+            if (dependenciesCount.get(queue.peek()) > 0)
+            {
+                throw new UserFailureException("Circular dependency found!");
+            }
+
+            while (!queue.isEmpty() && dependenciesCount.get(queue.peek()) == 0)
+            {
+                T item = queue.poll();
+                levelItems.add(item);
+            }
+            sortedGroups.add(levelItems);
+
+            if (!queue.isEmpty())
+            {
+                // we don't need to clean if we know, that this is the last loop
+                updateQueueAfterTheLevelCompleted(levelItems);
+            }
+        }
+    }
+
+    /**
+     * after all items that have no dependencies have been taken, we remove further dependencies to
+     * those items
+     */
+    private void updateQueueAfterTheLevelCompleted(List<T> levelItems)
+    {
+        HashSet<T> allSonsInTheLevel = new HashSet<T>();
+
+        for (T item : levelItems)
+        {
+            for (T son : graph.get(item))
+            {
+                allSonsInTheLevel.add(son);
+                dependenciesCount.put(son, dependenciesCount.get(son) - 1);
+            }
+        }
+
+        for (T son : allSonsInTheLevel)
+        {
+            queue.remove(son);
+            queue.add(son);
+        }
+    }
+}
diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/collections/GroupingDAGTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/collections/GroupingDAGTest.java
new file mode 100644
index 00000000000..082fbff252b
--- /dev/null
+++ b/common/sourceTest/java/ch/systemsx/cisd/common/collections/GroupingDAGTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2012 ETH Zuerich, CISD
+ *
+ * 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 ch.systemsx.cisd.common.collections;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.AssertJUnit;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+
+/**
+ * @author Jakub Straszewski
+ */
+public class GroupingDAGTest extends AssertJUnit
+{
+
+    @Test
+    public void testIndependent()
+    {
+        HashMap<String, Collection<String>> adjacencyMap =
+                new HashMap<String, Collection<String>>();
+
+        adjacencyMap.put("A", new ArrayList<String>());
+        adjacencyMap.put("B", Arrays.asList("A"));
+        adjacencyMap.put("C", Arrays.asList("A", "B"));
+        adjacencyMap.put("D", Arrays.asList("C", "B", "A"));
+        adjacencyMap.put("E", Arrays.asList("C", "D"));
+
+        List<List<String>> groups = sortTopologically(adjacencyMap);
+
+        assertAllEntitiesPresent(adjacencyMap.keySet(), groups);
+
+        assertEquals("[[E], [D], [C], [B], [A]]", groups.toString());
+    }
+
+    @Test
+    public void testWithParent()
+    {
+        HashMap<String, Collection<String>> adjacencyMap =
+                new HashMap<String, Collection<String>>();
+
+        adjacencyMap.put("P1", Arrays.asList("A1", "A2", "A3"));
+        adjacencyMap.put("P2", Arrays.asList("A1", "A2", "A3"));
+        adjacencyMap.put("A1", new ArrayList<String>());
+        adjacencyMap.put("A2", new ArrayList<String>());
+        adjacencyMap.put("A3", new ArrayList<String>());
+        adjacencyMap.put("I", new ArrayList<String>());
+
+        List<List<String>> groups = sortTopologically(adjacencyMap);
+
+        assertAllEntitiesPresent(adjacencyMap.keySet(), groups);
+
+        for (List<String> list : groups)
+        {
+            Collections.sort(list);
+        }
+
+        assertEquals("[[I, P1, P2], [A1, A2, A3]]", groups.toString());
+    }
+
+    @Test
+    public void testEmpty()
+    {
+        HashMap<String, Collection<String>> adjacencyMap =
+                new HashMap<String, Collection<String>>();
+
+        List<List<String>> groups = sortTopologically(adjacencyMap);
+
+        assertEquals(0, groups.size());
+    }
+
+    private void assertAllEntitiesPresent(Collection<String> original, List<List<String>> groups)
+    {
+        HashSet<String> allItems = new HashSet<String>(original);
+
+        for (List<String> group : groups)
+        {
+            for (String item : group)
+            {
+                if (!allItems.contains(item))
+                {
+                    fail("The items in original and groped lists do not match! (" + item + ") "
+                            + original + " " + groups);
+                }
+                allItems.remove(item);
+            }
+        }
+        if (allItems.size() > 0)
+        {
+            fail("The items in original and groped lists do not match! " + original + " " + groups);
+        }
+    }
+
+    @Test(expectedExceptions = UserFailureException.class)
+    public void testCyclicGraph()
+    {
+        HashMap<String, Collection<String>> adjacencyMap =
+                new HashMap<String, Collection<String>>();
+
+        adjacencyMap.put("A", Arrays.asList("B"));
+        adjacencyMap.put("B", Arrays.asList("C"));
+        adjacencyMap.put("C", Arrays.asList("A"));
+
+        sortTopologically(adjacencyMap);
+    }
+
+    private List<List<String>> sortTopologically(final Map<String, Collection<String>> adjacencyMap)
+    {
+        return GroupingDAG.groupByDepencies(adjacencyMap);
+    }
+}
-- 
GitLab