changeset 2446:bc7fbc2c3512

Use graph API in storage populator command. Reviewed-by: jerboaa Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-September/020829.html
author Joshua Matsuoka <jmatsuok@redhat.com>
date Mon, 12 Sep 2016 12:53:41 -0400
parents 710a1d37c0ed
children b620db651e19
files dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/TopologicalSort.java dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/actions/ListDependenciesAction.java dev/storage-populator/command/pom.xml dev/storage-populator/command/src/main/java/com/redhat/thermostat/storage/populator/internal/config/PopulationConfig.java dev/storage-populator/command/src/main/java/com/redhat/thermostat/storage/populator/internal/config/typeadapter/PopulationConfigTypeAdapter.java dev/storage-populator/command/src/main/java/com/redhat/thermostat/storage/populator/internal/config/typeadapter/RelationShipTypeAdapter.java dev/storage-populator/command/src/main/java/com/redhat/thermostat/storage/populator/internal/dependencies/Relationship.java dev/storage-populator/command/src/test/java/com/redhat/thermostat/storage/populator/internal/config/PopulationConfigTest.java dev/storage-populator/command/src/test/java/com/redhat/thermostat/storage/populator/internal/config/typeadapter/PopulationConfigTypeAdapterTest.java dev/storage-populator/command/src/test/java/com/redhat/thermostat/storage/populator/internal/config/typeadapter/RelationShipTypeAdapterTest.java dev/storage-populator/command/src/test/java/com/redhat/thermostat/storage/populator/internal/dependencies/RelationshipTest.java platform/collections/src/main/java/com/redhat/thermostat/collections/graph/TopologicalSort.java
diffstat 12 files changed, 219 insertions(+), 404 deletions(-) [+]
line wrap: on
line diff
--- a/dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/TopologicalSort.java	Fri Sep 09 08:51:50 2016 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-/*
- * Copyright 2012-2016 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code give
- * you permission to link this code with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also
- * meet, for each linked independent module, the terms and conditions
- * of the license of that module.  An independent module is a module
- * which is not derived from or based on this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-package com.redhat.thermostat.tools.dependency.internal;
-
-import com.redhat.thermostat.collections.graph.DFSPayload;
-import com.redhat.thermostat.collections.graph.DefaultTraversalListener;
-import com.redhat.thermostat.collections.graph.DepthFirstSearch;
-import com.redhat.thermostat.collections.graph.Node;
-import com.redhat.thermostat.collections.graph.Relationship;
-import com.redhat.thermostat.collections.graph.RelationshipType;
-import com.redhat.thermostat.collections.graph.SearchPayload;
-
-import java.util.HashSet;
-import java.util.Set;
-import java.util.Stack;
-
-/**
- */
-public class TopologicalSort extends DefaultTraversalListener {
-    private Stack<Node> ordered;
-    private Set<Node> discovered;
-
-    public TopologicalSort(Stack<Node> ordered, Set<Node> discovered) {
-        this.ordered = ordered;
-        this.discovered = discovered;
-    }
-
-    public TopologicalSort() {
-        this.ordered = new Stack<>();
-        this.discovered = new HashSet<>();
-    }
-
-    public Stack<Node> getOrdered() {
-        return ordered;
-    }
-
-    public Set<Node> getDiscovered() {
-        return discovered;
-    }
-
-    @Override
-    protected void preProcessNodeImpl(Node node) {
-        discovered.add(node);
-    }
-
-    @Override
-    protected void postProcessNodeImpl(Node node) {
-        ordered.add(node);
-    }
-
-    @Override
-    public Status processRelationship(Relationship relationship,
-                                      SearchPayload payload)
-    {
-        RelationshipType type =
-                DepthFirstSearch.classify(relationship, (DFSPayload) payload);
-
-        if (type.equals(RelationshipType.BACK)) {
-            throw new IllegalStateException("back node! " + relationship);
-        }
-
-        return Status.CONTINUE;
-    }
-}
--- a/dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/actions/ListDependenciesAction.java	Fri Sep 09 08:51:50 2016 -0400
+++ b/dependency-tool/command/src/main/java/com/redhat/thermostat/tools/dependency/internal/actions/ListDependenciesAction.java	Mon Sep 12 12:53:41 2016 -0400
@@ -42,7 +42,7 @@
 import com.redhat.thermostat.common.cli.CommandContext;
 import com.redhat.thermostat.tools.dependency.internal.DependencyGraphBuilder;
 import com.redhat.thermostat.tools.dependency.internal.PathProcessorHandler;
-import com.redhat.thermostat.tools.dependency.internal.TopologicalSort;
+import com.redhat.thermostat.collections.graph.TopologicalSort;
 import com.redhat.thermostat.tools.dependency.internal.Utils;
 
 import java.nio.file.Path;
--- a/dev/storage-populator/command/pom.xml	Fri Sep 09 08:51:50 2016 -0400
+++ b/dev/storage-populator/command/pom.xml	Mon Sep 12 12:53:41 2016 -0400
@@ -131,6 +131,11 @@
         <groupId>com.google.code.gson</groupId>
         <artifactId>gson</artifactId>
     </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-platform-collections</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 
 </project>
--- a/dev/storage-populator/command/src/main/java/com/redhat/thermostat/storage/populator/internal/config/PopulationConfig.java	Fri Sep 09 08:51:50 2016 -0400
+++ b/dev/storage-populator/command/src/main/java/com/redhat/thermostat/storage/populator/internal/config/PopulationConfig.java	Mon Sep 12 12:53:41 2016 -0400
@@ -37,156 +37,118 @@
 package com.redhat.thermostat.storage.populator.internal.config;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.ArrayList;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.redhat.thermostat.collections.graph.DepthFirstSearch;
+import com.redhat.thermostat.collections.graph.HashGraph;
 import com.redhat.thermostat.storage.populator.internal.config.typeadapter.ConfigItemTypeAdapter;
 import com.redhat.thermostat.storage.populator.internal.config.typeadapter.PopulationConfigTypeAdapterFactory;
 import com.redhat.thermostat.storage.populator.internal.config.typeadapter.RelationShipTypeAdapter;
-import com.redhat.thermostat.storage.populator.internal.dependencies.Relationship;
+import com.redhat.thermostat.collections.graph.Relationship;
+import com.redhat.thermostat.collections.graph.TopologicalSort;
+import com.redhat.thermostat.collections.graph.Graph;
+import com.redhat.thermostat.collections.graph.Node;
 
 public class PopulationConfig {
     
     public static final String RECORDS = "records";
     public static final String RELATIONSHIPS = "relationships";
-    private final Map<String, ConfigItem> configs;
-    private final List<ConfigItem> allConfigs;
-    private final Map<String, List<Relationship>> incomingRelationShips;
-    private final Map<String, List<Relationship>> outgoingRelationShips;
+    public static final String ITEM = "item";
+    private final Map<String, Node> configs;
+    private final Map<Node, Set<Relationship>> relationshipMap;
+    private final Graph graph;
     
     private PopulationConfig() {
         configs = new HashMap<>();
-        allConfigs = new ArrayList<>();
-        incomingRelationShips = new HashMap<>();
-        outgoingRelationShips = new HashMap<>();
+        graph = new HashGraph();
+        relationshipMap = new HashMap<>();
     }
 
-    public ConfigItem getConfig(String name) {
-        return configs.get(name);
+    public Node getConfig(String configItem) {
+        Node cfg = configs.get(configItem);
+        if (cfg == null) {
+            throw new InvalidConfigurationException("Config is missing from collection!");
+        }
+        return cfg;
     }
     
     public List<ConfigItem> getConfigsTopologicallySorted() {
-        return topologicallySort(allConfigs);
+        return topologicallySort();
     }
-    
+
     /**
      * Sort config items topologically. That is, get them in an order so as
      * to process collections with no incoming links to other collections.
-     * 
-     * It uses Kahn's algorithm to do so.
-     * 
-     * @param configs unsorted configs.
+     *
+     * It uses the standard DFS algorithm to do so.
+     *
      * @return A topologically sorted list.
      */
-    private List<ConfigItem> topologicallySort(List<ConfigItem> configs) {
-        LinkedList<ConfigItem> sortedList = new LinkedList<>();
-        LinkedList<ConfigItem> queue = getConfigItemsWithNoIncomingEdge(configs);
-        while (!queue.isEmpty()) {
-            ConfigItem n = queue.remove(0);
-            sortedList.addLast(n);
-            List<Relationship> source = getOutgoingRelationShipsForConfig(n);
-            List<Relationship> outgoingRels = new LinkedList<>(source);
-            for (Relationship edge: outgoingRels) {
-                ConfigItem m = getConfig(edge.getTo());
-                removeEdge(edge, n, m);
-                if (hasNoIncomingEdge(m)) {
-                    queue.addLast(m);
-                }
-            }
+    private List<ConfigItem> topologicallySort() {
+        DepthFirstSearch dfs = new DepthFirstSearch(graph);
+        TopologicalSort tsort = new TopologicalSort();
+        List<Node> roots = getRoots();
+        Set<ConfigItem> cfgs = new LinkedHashSet<>();
+        // If no roots were found to start from, the graph must be cyclic or empty.
+        if (roots.size() == 0) {
+            throw new InvalidConfigurationException("Relationships form cycle!");
         }
-        if (hasRelationShips(configs)) {
-            throw new AssertionError("Relationships form at least one cycle! Excpected an acyclic directed graph.");
-        }
-        return sortedList;
-    }
 
-    private boolean hasRelationShips(List<ConfigItem> configs) {
-        for (ConfigItem i: configs) {
-            List<Relationship> incoming = getIncomingRelationShipsForConfig(i);
-            List<Relationship> outgoing = getOutgoingRelationShipsForConfig(i);
-            if (!incoming.isEmpty() ||
-                    !outgoing.isEmpty()) {
-                // We are going to bomb, print some debug info.
-                System.err.println("outgoings for " + i.getName() + " " + outgoing);
-                System.err.println("incomings for " + i.getName() + " " + incoming);
-                return true;
+        for (Node root : roots) {
+            dfs.search(getConfig(root.getName()), tsort);
+            for (Node n : tsort.getOrdered()) {
+                Node cfg = getConfig(n.getName());
+                cfgs.add((ConfigItem) cfg.getProperty(ITEM));
             }
+            // Isolated nodes will return empty from DFS
+            if (tsort.getOrdered().size() == 0) {
+                cfgs.add((ConfigItem) root.getProperty(ITEM));
+            }
+            dfs.reset();
+            tsort.reset();
         }
-        return false;
-    }
 
-    private void removeEdge(Relationship toRemove, ConfigItem from, ConfigItem to) {
-        if (from == null || to == null) {
-            throw new InvalidConfigurationException("Found relationship [" +
-                                                    toRemove +
-                    "] but either incoming or outgoing records config is missing");
-        }
-        List<Relationship> incoming = getIncomingRelationShipsForConfig(to);
-        List<Relationship> outgoing = getOutgoingRelationShipsForConfig(from);
-        incoming.remove(toRemove);
-        outgoing.remove(toRemove);
+        List<ConfigItem> ordered = new ArrayList<>(cfgs);
+        Collections.reverse(ordered);
+        return ordered;
     }
 
-    private LinkedList<ConfigItem> getConfigItemsWithNoIncomingEdge(List<ConfigItem> allConfigs) {
-        LinkedList<ConfigItem> startNodes = new LinkedList<>();
-        for (ConfigItem item: allConfigs) {
-            if (hasNoIncomingEdge(item)) {
-                startNodes.add(item);
+    private List<Node> getRoots() {
+        List<Node> roots = new ArrayList<>();
+        for (Node n : relationshipMap.keySet()) {
+            if (relationshipMap.get(n).size() == 0) {
+                roots.add(n);
             }
         }
-        return startNodes;
+        return roots;
     }
 
-    private boolean hasNoIncomingEdge(ConfigItem item) {
-        List<Relationship> rels = getIncomingRelationShipsForConfig(item);
-        if (rels == null || rels.isEmpty()) {
-            return true;
-        }
-        return false;
-    }
-
-    private List<Relationship> getIncomingRelationShipsForConfig(ConfigItem item) {
-        List<Relationship> result = incomingRelationShips.get(item.getName());
-        if (result == null) {
-            return Collections.emptyList();
-        }
-        return result;
+    private void addConfig(ConfigItem configItem) {
+        Node item = new Node(configItem.getName());
+        item.setProperty(ITEM, configItem);
+        configs.put(item.getName(), item);
+        relationshipMap.put(item, new HashSet<Relationship>());
     }
     
-    private List<Relationship> getOutgoingRelationShipsForConfig(ConfigItem item) {
-        List<Relationship> result = outgoingRelationShips.get(item.getName());
-        if (result == null) {
-            return Collections.emptyList();
+    private void addRelationShip(Relationship relationship) {
+        graph.addRelationship(relationship);
+        if (relationshipMap.get(relationship.getTo()) == null) {
+            Set<Relationship> rels = new HashSet<>();
+            rels.add(relationship);
+            relationshipMap.put(relationship.getTo(), rels);
         }
-        return result;
-    }
-    
-    private void addConfig(ConfigItem item) {
-        configs.put(item.getName(), item);
-        allConfigs.add(item);
-    }
-    
-    private void addIncomingRelationShipForConfig(ConfigItem item, Relationship relationship) {
-        addRelationShip(item, relationship, incomingRelationShips);
-    }
-    
-    private void addOutgoingRelationShipForConfig(ConfigItem item, Relationship relationship) {
-        addRelationShip(item, relationship, outgoingRelationShips);
-    }
-    
-    private void addRelationShip(ConfigItem item, Relationship relationship, Map<String, List<Relationship>> map) {
-        List<Relationship> existingRels = map.get(item.getName());
-        if (existingRels == null) {
-            existingRels = new LinkedList<>();
-            map.put(item.getName(), existingRels);
+        else {
+            relationshipMap.get(relationship.getTo()).add(relationship);
         }
-        existingRels.add(relationship);
     }
     
     public static PopulationConfig parseFromJsonString(String json) throws IOException {
@@ -204,14 +166,7 @@
         for (ConfigItem item: items) {
             pc.addConfig(item);
             for (Relationship r: rels) {
-                // keep track of incoming relationships (incoming edge)
-                if (r.getTo().equals(item.getName())) {
-                    pc.addIncomingRelationShipForConfig(item, r);
-                }
-                // keep track of outgoing relationships (outgoing edge)
-                if (r.getFrom().equals(item.getName())) {
-                    pc.addOutgoingRelationShipForConfig(item, r);
-                }
+                pc.addRelationShip(r);
             }
         }
         return pc;
--- a/dev/storage-populator/command/src/main/java/com/redhat/thermostat/storage/populator/internal/config/typeadapter/PopulationConfigTypeAdapter.java	Fri Sep 09 08:51:50 2016 -0400
+++ b/dev/storage-populator/command/src/main/java/com/redhat/thermostat/storage/populator/internal/config/typeadapter/PopulationConfigTypeAdapter.java	Mon Sep 12 12:53:41 2016 -0400
@@ -50,7 +50,7 @@
 import com.google.gson.stream.JsonWriter;
 import com.redhat.thermostat.storage.populator.internal.config.ConfigItem;
 import com.redhat.thermostat.storage.populator.internal.config.PopulationConfig;
-import com.redhat.thermostat.storage.populator.internal.dependencies.Relationship;
+import com.redhat.thermostat.collections.graph.Relationship;
 
 public class PopulationConfigTypeAdapter extends TypeAdapter<PopulationConfig> {
     
--- a/dev/storage-populator/command/src/main/java/com/redhat/thermostat/storage/populator/internal/config/typeadapter/RelationShipTypeAdapter.java	Fri Sep 09 08:51:50 2016 -0400
+++ b/dev/storage-populator/command/src/main/java/com/redhat/thermostat/storage/populator/internal/config/typeadapter/RelationShipTypeAdapter.java	Mon Sep 12 12:53:41 2016 -0400
@@ -44,10 +44,15 @@
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonToken;
 import com.google.gson.stream.JsonWriter;
-import com.redhat.thermostat.storage.populator.internal.dependencies.Relationship;
+import com.redhat.thermostat.collections.graph.Relationship;
+import com.redhat.thermostat.collections.graph.Node;
 
 public class RelationShipTypeAdapter extends TypeAdapter<Relationship> {
 
+    private static final String TO = "to";
+    private static final String FROM = "from";
+    private static final String KEY = "key";
+
     @Override
     public Relationship read(JsonReader in) throws IOException {
         // handle null
@@ -68,20 +73,22 @@
         while (in.hasNext()) {
             String name = in.nextName();
             switch(name) {
-            case Relationship.FROM:
-                values.put(Relationship.FROM, in.nextString());
+            case FROM:
+                values.put(FROM, in.nextString());
                 break;
-            case Relationship.TO:
-                values.put(Relationship.TO, in.nextString());
+            case TO:
+                values.put(TO, in.nextString());
                 break;
-            case Relationship.KEY:
-                values.put(Relationship.KEY, in.nextString());
+            case KEY:
+                values.put(KEY, in.nextString());
                 break;
             default:
                 throw new IllegalStateException("Unknown relationship value: '" + name + "'");
             }
         }
-        return new Relationship(values.get(Relationship.FROM), values.get(Relationship.TO), values.get(Relationship.KEY));
+        Relationship r = new Relationship(new Node(values.get(FROM)), "->", new Node(values.get(TO)));
+        r.setProperty(KEY, values.get(KEY));
+        return r;
     }
 
     @Override
--- a/dev/storage-populator/command/src/main/java/com/redhat/thermostat/storage/populator/internal/dependencies/Relationship.java	Fri Sep 09 08:51:50 2016 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-/*
- * Copyright 2012-2016 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code give
- * you permission to link this code with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also
- * meet, for each linked independent module, the terms and conditions
- * of the license of that module.  An independent module is a module
- * which is not derived from or based on this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-package com.redhat.thermostat.storage.populator.internal.dependencies;
-
-import java.util.Objects;
-
-public class Relationship {
-
-    public static final String TO = "to";
-    public static final String FROM = "from";
-    public static final String KEY = "key";
-    private final String from;
-    private final String to;
-    private final String key;
-    
-    public Relationship(String from, String to, String key) {
-        this.from = from;
-        this.to = to;
-        this.key = key;
-    }
-
-    public String getFrom() {
-        return from;
-    }
-
-    public String getTo() {
-        return to;
-    }
-
-    public String getKey() {
-        return key;
-    }
-    
-    @Override
-    public boolean equals(Object o) {
-        if (o == null || o.getClass() != Relationship.class) {
-            return false;
-        }
-        Relationship other = (Relationship)o;
-        return Objects.equals(to, other.to) &&
-                Objects.equals(from, other.from) &&
-                Objects.equals(key, other.key);
-    }
-    
-    @Override
-    public int hashCode() {
-        return Objects.hash(to, from, key);
-    }
-    
-    @Override
-    public String toString() {
-        return from + " -> " + to + "(" + key + ")";
-    }
-}
--- a/dev/storage-populator/command/src/test/java/com/redhat/thermostat/storage/populator/internal/config/PopulationConfigTest.java	Fri Sep 09 08:51:50 2016 -0400
+++ b/dev/storage-populator/command/src/test/java/com/redhat/thermostat/storage/populator/internal/config/PopulationConfigTest.java	Mon Sep 12 12:53:41 2016 -0400
@@ -49,7 +49,8 @@
 
 import org.junit.Test;
 
-import com.redhat.thermostat.storage.populator.internal.dependencies.Relationship;
+import com.redhat.thermostat.collections.graph.Relationship;
+import com.redhat.thermostat.collections.graph.Node;
 import com.redhat.thermostat.storage.populator.internal.config.ConfigItem;
 import com.redhat.thermostat.storage.populator.internal.config.PopulationConfig;
 
@@ -67,16 +68,16 @@
     public void canParseFromFile() throws IOException {
         String json = new String(Files.readAllBytes(new File(getClass().getResource("/testconfig").getFile()).toPath()));
         PopulationConfig config = PopulationConfig.parseFromJsonString(json);
-        ConfigItem item = config.getConfig("agent-config");
+        ConfigItem item = config.getConfig("agent-config").getProperty("item");
         assertNotNull(item);
         assertEquals(100, item.getNumber());
         assertEquals(20, item.getAliveItems());
         List<ConfigItem> items = config.getConfigsTopologicallySorted();
         assertEquals(7, items.size());
-        item = config.getConfig("vm-info");
+        item = config.getConfig("vm-info").getProperty("item");
         assertEquals(50, item.getNumber());
         assertEquals(40, item.getAliveItems());
-        item = config.getConfig("vm-gc-stats");
+        item = config.getConfig("vm-gc-stats").getProperty("item");
         assertNotNull(item);
     }
     
@@ -149,15 +150,20 @@
 
     private List<Relationship> buildGoodRels() {
         List<Relationship> relationships = new LinkedList<>();
-        Relationship agentConfigToNetworkInfo = new Relationship(AGENT_CONFIG_NAME, NETWORK_INFO_NAME, AGENT_KEY);
+        Relationship agentConfigToNetworkInfo = new Relationship(new Node(AGENT_CONFIG_NAME), "->", new Node(NETWORK_INFO_NAME));
+        agentConfigToNetworkInfo.setProperty("key", AGENT_KEY);
         relationships.add(agentConfigToNetworkInfo);
-        Relationship agentConfigToVmGcStat = new Relationship(AGENT_CONFIG_NAME, VM_GC_STAT_NAME, AGENT_KEY);
+        Relationship agentConfigToVmGcStat = new Relationship(new Node(AGENT_CONFIG_NAME), "->", new Node(VM_GC_STAT_NAME));
+        agentConfigToVmGcStat.setProperty("key", AGENT_KEY);
         relationships.add(agentConfigToVmGcStat);
-        Relationship agentConfigToHostInfo = new Relationship(AGENT_CONFIG_NAME, HOST_INFO_NAME, AGENT_KEY);
+        Relationship agentConfigToHostInfo = new Relationship(new Node(AGENT_CONFIG_NAME), "->", new Node(HOST_INFO_NAME));
+        agentConfigToHostInfo.setProperty("key", AGENT_KEY);
         relationships.add(agentConfigToHostInfo);
-        Relationship agentConfigToVmInfo = new Relationship(AGENT_CONFIG_NAME, VM_INFO_NAME, AGENT_KEY);
+        Relationship agentConfigToVmInfo = new Relationship(new Node(AGENT_CONFIG_NAME), "->", new Node(VM_INFO_NAME));
+        agentConfigToVmInfo.setProperty("key", AGENT_KEY);
         relationships.add(agentConfigToVmInfo);
-        Relationship vmInfoToVmGcStat = new Relationship(VM_INFO_NAME, VM_GC_STAT_NAME, VM_KEY);
+        Relationship vmInfoToVmGcStat = new Relationship(new Node(VM_INFO_NAME), "->", new Node(VM_GC_STAT_NAME));
+        vmInfoToVmGcStat.setProperty("key", VM_KEY);
         relationships.add(vmInfoToVmGcStat);
         return relationships;
     }
@@ -180,14 +186,16 @@
     /**
      * Tests topological sorting with a cycle. Expected to fail.
      */
-    @Test(expected = AssertionError.class)
+    @Test(expected = PopulationConfig.InvalidConfigurationException.class)
     public void testTopogicalSortCycle() throws IOException {
         List<ConfigItem> records = buildGoodRecords();
         List<Relationship> rels = buildGoodRels();
         // add a cycle between vm-info -> host-info -> agent-config
-        Relationship cyclePartOne = new Relationship(VM_INFO_NAME, HOST_INFO_NAME, VM_KEY);
+        Relationship cyclePartOne = new Relationship(new Node(VM_INFO_NAME), "->", new Node(HOST_INFO_NAME));
+        cyclePartOne.setProperty("key", VM_KEY);
         rels.add(cyclePartOne);
-        Relationship cyclePartTwo = new Relationship(HOST_INFO_NAME, AGENT_CONFIG_NAME, AGENT_KEY);
+        Relationship cyclePartTwo = new Relationship(new Node(HOST_INFO_NAME), "->", new Node(AGENT_CONFIG_NAME));
+        cyclePartTwo.setProperty("key", AGENT_KEY);
         rels.add(cyclePartTwo);
         PopulationConfig pc = PopulationConfig.createFromLists(records, rels);
         // This is expected to fail
--- a/dev/storage-populator/command/src/test/java/com/redhat/thermostat/storage/populator/internal/config/typeadapter/PopulationConfigTypeAdapterTest.java	Fri Sep 09 08:51:50 2016 -0400
+++ b/dev/storage-populator/command/src/test/java/com/redhat/thermostat/storage/populator/internal/config/typeadapter/PopulationConfigTypeAdapterTest.java	Mon Sep 12 12:53:41 2016 -0400
@@ -47,7 +47,7 @@
 import com.google.gson.GsonBuilder;
 import com.redhat.thermostat.storage.populator.internal.config.ConfigItem;
 import com.redhat.thermostat.storage.populator.internal.config.PopulationConfig;
-import com.redhat.thermostat.storage.populator.internal.dependencies.Relationship;
+import com.redhat.thermostat.collections.graph.Relationship;
 import com.redhat.thermostat.storage.populator.internal.config.typeadapter.ConfigItemTypeAdapter;
 import com.redhat.thermostat.storage.populator.internal.config.typeadapter.PopulationConfigTypeAdapterFactory;
 import com.redhat.thermostat.storage.populator.internal.config.typeadapter.RelationShipTypeAdapter;
@@ -72,11 +72,11 @@
                 "{ \"name\": \"agent-config\", \"number\": 10, \"alive\": 3 }]" +
                 "}";
         PopulationConfig config = gson.fromJson(json, PopulationConfig.class);
-        ConfigItem item = config.getConfig("config-item");
+        ConfigItem item = config.getConfig("config-item").getProperty("item");
         assertEquals(3, item.getNumber());
         assertEquals(1, item.getAliveItems());
         assertEquals("config-item", item.getName());
-        item = config.getConfig("agent-config");
+        item = config.getConfig("agent-config").getProperty("item");
         assertEquals(10, item.getNumber());
         assertEquals(3, item.getAliveItems());
         assertEquals("agent-config", item.getName());
@@ -91,11 +91,11 @@
                 "[{ \"from\": \"config-item\", \"to\": \"agent-config\", \"key\": \"foo\"}]" +
                 "}";
         PopulationConfig config = gson.fromJson(json, PopulationConfig.class);
-        ConfigItem item = config.getConfig("config-item");
+        ConfigItem item = config.getConfig("config-item").getProperty("item");
         assertEquals(3, item.getNumber());
         assertEquals(1, item.getAliveItems());
         assertEquals("config-item", item.getName());
-        item = config.getConfig("agent-config");
+        item = config.getConfig("agent-config").getProperty("item");
         assertEquals(10, item.getNumber());
         assertEquals(3, item.getAliveItems());
         assertEquals("agent-config", item.getName());
--- a/dev/storage-populator/command/src/test/java/com/redhat/thermostat/storage/populator/internal/config/typeadapter/RelationShipTypeAdapterTest.java	Fri Sep 09 08:51:50 2016 -0400
+++ b/dev/storage-populator/command/src/test/java/com/redhat/thermostat/storage/populator/internal/config/typeadapter/RelationShipTypeAdapterTest.java	Mon Sep 12 12:53:41 2016 -0400
@@ -43,7 +43,7 @@
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
-import com.redhat.thermostat.storage.populator.internal.dependencies.Relationship;
+import com.redhat.thermostat.collections.graph.Relationship;
 import com.redhat.thermostat.storage.populator.internal.config.typeadapter.RelationShipTypeAdapter;
 
 public class RelationShipTypeAdapterTest {
@@ -60,8 +60,8 @@
     @Test
     public void testBasicNonNull() {
         Relationship rel = gson.fromJson("{ \"from\": \"foo\", \"to\": \"bar\", \"key\": \"baz\"}", Relationship.class);
-        assertEquals("foo", rel.getFrom());
-        assertEquals("bar", rel.getTo());
-        assertEquals("baz", rel.getKey());
+        assertEquals("foo", rel.getFrom().getName());
+        assertEquals("bar", rel.getTo().getName());
+        assertEquals("baz", rel.getProperty("key"));
     }
 }
--- a/dev/storage-populator/command/src/test/java/com/redhat/thermostat/storage/populator/internal/dependencies/RelationshipTest.java	Fri Sep 09 08:51:50 2016 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-/*
- * Copyright 2012-2016 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code give
- * you permission to link this code with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also
- * meet, for each linked independent module, the terms and conditions
- * of the license of that module.  An independent module is a module
- * which is not derived from or based on this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-package com.redhat.thermostat.storage.populator.internal.dependencies;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import org.junit.Test;
-
-import com.redhat.thermostat.storage.populator.internal.dependencies.Relationship;
-
-public class RelationshipTest {
-
-    @Test
-    public void testEquals() {
-        Relationship one = new Relationship("foo", "bar", "key");
-        Relationship two = new Relationship("foo", "bar", "key");
-        Relationship different = new Relationship("foo", "bar", "something");
-        assertTrue(one.equals(two));
-        assertTrue(one.equals(one));
-        assertFalse(one.equals(different));
-        assertFalse(one.equals(null));
-        assertTrue(two.equals(one));
-    }
-    
-    @Test
-    public void testHashCode() {
-        Relationship one = new Relationship("foo", "bar", "key");
-        Relationship two = new Relationship("foo", "bar", "key");
-        Relationship different = new Relationship("foo", "bar", "something");
-        assertEquals(one.hashCode(), two.hashCode());
-        assertFalse(different.hashCode() == one.hashCode());
-        assertFalse(two.hashCode() == different.hashCode());
-        assertEquals(one.hashCode(), one.hashCode());
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/platform/collections/src/main/java/com/redhat/thermostat/collections/graph/TopologicalSort.java	Mon Sep 12 12:53:41 2016 -0400
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.collections.graph;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * A {@link DefaultTraversalListener} used by {@link DepthFirstSearch} to construct a
+ * topological ordering of a {@link Graph}.
+ */
+public class TopologicalSort extends DefaultTraversalListener {
+    private Stack<Node> ordered;
+    private Set<Node> discovered;
+
+    public TopologicalSort(Stack<Node> ordered, Set<Node> discovered) {
+        this.ordered = ordered;
+        this.discovered = discovered;
+    }
+
+    public TopologicalSort() {
+        this.ordered = new Stack<>();
+        this.discovered = new HashSet<>();
+    }
+
+    public void reset() {
+        ordered.clear();
+        discovered.clear();
+    }
+
+    public Stack<Node> getOrdered() {
+        return ordered;
+    }
+
+    public Set<Node> getDiscovered() {
+        return discovered;
+    }
+
+    @Override
+    protected void preProcessNodeImpl(Node node) {
+        discovered.add(node);
+    }
+
+    @Override
+    protected void postProcessNodeImpl(Node node) {
+        ordered.add(node);
+    }
+
+    @Override
+    public Status processRelationship(Relationship relationship,
+                                      SearchPayload payload)
+    {
+        RelationshipType type =
+                DepthFirstSearch.classify(relationship, (DFSPayload) payload);
+
+        if (type.equals(RelationshipType.BACK)) {
+            throw new IllegalStateException("back node! " + relationship);
+        }
+
+        return Status.CONTINUE;
+    }
+}