/*
* LensKit, an open source recommender systems toolkit.
* Copyright 2010-2014 LensKit Contributors. See CONTRIBUTORS.md.
* Work on LensKit has been funded by the National Science Foundation under
* grants IIS 05-34939, 08-08692, 08-12148, and 10-17697.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the
* License, or (at your option) any later version.
*
* This program 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
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.grouplens.lenskit.eval.graph;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.Closer;
import org.apache.commons.lang3.tuple.Pair;
import org.grouplens.grapht.Component;
import org.grouplens.grapht.Dependency;
import org.grouplens.grapht.graph.DAGEdge;
import org.grouplens.grapht.graph.DAGNode;
import org.grouplens.grapht.reflect.AbstractSatisfactionVisitor;
import org.grouplens.grapht.reflect.Desire;
import org.grouplens.grapht.reflect.Satisfaction;
import org.grouplens.grapht.reflect.SatisfactionVisitor;
import org.grouplens.lenskit.RecommenderBuildException;
import org.grouplens.lenskit.core.Parameter;
import org.grouplens.lenskit.inject.GraphtUtils;
import org.grouplens.lenskit.inject.RecommenderInstantiator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Provider;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.util.*;
/**
* Class to manage traversing nodes. It is not used to handle the root node, but rather handles
* the rest of them.
*
* @since 2.1
* @author <a href="http://www.grouplens.org">GroupLens Research</a>
*/
public class GraphDumper {
private static final Logger logger = LoggerFactory.getLogger(GraphDumper.class);
private static final String ROOT_ID = "root";
private final GraphWriter writer;
private final DAGNode<Component, Dependency> graph;
private final HashSet<DAGNode<Component, Dependency>> unsharedNodes;
private final Map<DAGNode<Component, Dependency>, String> nodeIds;
private final Map<String, String> nodeTargets;
private final Queue<GVEdge> edgeQueue;
GraphDumper(DAGNode<Component, Dependency> g, Set<DAGNode<Component, Dependency>> unshared, GraphWriter gw) {
writer = gw;
graph = g;
unsharedNodes = Sets.newHashSet(unshared);
unsharedNodes.retainAll(g.getReachableNodes());
logger.debug("{} shared nodes", unsharedNodes.size());
nodeIds = new HashMap<DAGNode<Component, Dependency>, String>();
nodeTargets = new HashMap<String, String>();
edgeQueue = new LinkedList<GVEdge>();
}
/**
* Set the root node for this dumper. This must be called before any other methods.
*
* @param root The root node.
* @return The ID of the root node.
*/
String setRoot(DAGNode<Component, Dependency> root) throws IOException {
if (!nodeTargets.isEmpty()) {
throw new IllegalStateException("root node already specificied");
}
nodeIds.put(root, ROOT_ID);
nodeTargets.put(ROOT_ID, ROOT_ID);
writer.putNode(NodeBuilder.create(ROOT_ID)
.setLabel("root")
.setShape("box")
.add("style", "rounded")
.build());
return ROOT_ID;
}
/**
* Process a node.
*
* @param node The node to process
* @return The node's target descriptor (ID, possibly with port).
*/
String process(DAGNode<Component, Dependency> node) throws IOException {
Preconditions.checkNotNull(node, "node must not be null");
if (nodeTargets.isEmpty()) {
throw new IllegalStateException("root node has not been set");
}
String id = nodeIds.get(node);
String tgt;
if (id == null) {
id = "N" + nodeIds.size();
nodeIds.put(node, id);
Component csat = node.getLabel();
assert csat != null;
Satisfaction sat = csat.getSatisfaction();
try {
tgt = sat.visit(new Visitor(node, id));
} catch (RuntimeException e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
} else {
throw e;
}
}
Preconditions.checkNotNull(tgt, "the returned target was null");
nodeTargets.put(id, tgt);
} else {
tgt = nodeTargets.get(id);
if (tgt == null) {
// tentatively use the node ID, we might remap it later
tgt = id;
}
}
return tgt;
}
/**
* Finish the graph, writing the edges.
*/
void finish() throws IOException {
while (!edgeQueue.isEmpty()) {
GVEdge e = edgeQueue.remove();
String newTarget = nodeTargets.get(e.getTarget());
if (newTarget != null) {
e = EdgeBuilder.of(e).setTarget(newTarget).build();
}
writer.putEdge(e);
}
}
private class Visitor implements SatisfactionVisitor<String> {
private final DAGNode<Component, Dependency> currentNode;
private final String nodeId;
private final Satisfaction satisfaction;
private Visitor(DAGNode<Component, Dependency> nd, String id) {
currentNode = nd;
nodeId = id;
if (currentNode == null) {
throw new IllegalStateException("dumper not running");
}
Component csat = currentNode.getLabel();
assert csat != null;
satisfaction = csat.getSatisfaction();
}
@Override
public String visitNull() {
NodeBuilder nb = NodeBuilder.create(nodeId);
nb.setShape("ellipse");
nb.setLabel("null");
GVNode node = nb.build();
try {
writer.putNode(node);
} catch (IOException e) {
throw Throwables.propagate(e);
}
return node.getTarget();
}
@Override
public String visitClass(Class<?> clazz) {
GVNode node = componentNode(clazz, null);
try {
writer.putNode(node);
} catch (IOException e) {
throw Throwables.propagate(e);
}
return node.getTarget();
}
@Override
public String visitInstance(Object instance) {
GVNode node = NodeBuilder.create(nodeId)
.setLabel(instance.toString())
.setShape("ellipse")
.build();
try {
writer.putNode(node);
} catch (IOException e) {
throw Throwables.propagate(e);
}
return node.getId();
}
/**
* Create a provided node from the current node, and queue an edge for it.
*
* @param pid The ID of the provider node for targeting the provision edge.
* @return The provided node and the edge connect it to the provider node.
*/
private Pair<GVNode, GVEdge> providedNode(String pid) {
GVNode pNode = ComponentNodeBuilder.create(nodeId, satisfaction.getErasedType())
.setShareable(GraphtUtils.isShareable(currentNode))
.setShared(!unsharedNodes.contains(currentNode))
.setIsProvided(true)
.build();
GVEdge pEdge = EdgeBuilder.create(pNode.getTarget() + ":e", pid)
.set("style", "dotted")
.set("dir", "back")
.set("arrowhead", "vee")
.build();
return Pair.of(pNode, pEdge);
}
@Override
public String visitProviderClass(Class<? extends Provider<?>> pclass) {
String pid = nodeId + "P";
// we create a comp. node for the provider, and a provided node for its target
GVNode pnode = componentNode(pclass, pid);
Pair<GVNode, GVEdge> provided = providedNode(pid);
try {
SubgraphBuilder sgb = new SubgraphBuilder();
writer.putSubgraph(sgb.setName("sgp_" + pid)
.addNode(pnode)
.addNode(provided.getLeft())
.addEdge(provided.getRight())
.build());
} catch (IOException e) {
throw Throwables.propagate(e);
}
// return *provided* node's ID
return provided.getLeft().getTarget();
}
@Override
public String visitProviderInstance(Provider<?> provider) {
String pid = nodeId + "P";
GVNode pnode = NodeBuilder.create(pid)
.setLabel(provider.toString())
.setShape("ellipse")
.set("style", "dashed")
.build();
Pair<GVNode, GVEdge> provided = providedNode(pid);
try {
SubgraphBuilder sgb = new SubgraphBuilder();
writer.putSubgraph(sgb.setName("sgp_" + pid)
.addNode(pnode)
.addNode(provided.getLeft())
.addEdge(provided.getRight())
.build());
} catch (IOException e) {
throw Throwables.propagate(e);
}
// return *provided* node's ID
return provided.getLeft().getTarget();
}
private GVNode componentNode(Class<?> type, String pid) {
String id = pid == null ? nodeId : pid;
ComponentNodeBuilder bld = ComponentNodeBuilder.create(id, type);
bld.setShareable(pid == null && GraphtUtils.isShareable(currentNode))
.setShared(!unsharedNodes.contains(currentNode))
.setIsProvider(pid != null);
List<DAGEdge<Component, Dependency>> edges = Lists.newArrayList(currentNode.getOutgoingEdges());
Collections.sort(edges, GraphtUtils.DEP_EDGE_ORDER);
for (DAGEdge<Component, Dependency> e: edges) {
Desire dep = e.getLabel().getInitialDesire();
Annotation q = dep.getInjectionPoint().getQualifier();
DAGNode<Component, Dependency> targetNode = e.getTail();
if (q != null && q.annotationType().getAnnotation(Parameter.class) != null) {
logger.debug("dumping parameter {}", q);
Component tcsat = targetNode.getLabel();
assert tcsat != null;
Satisfaction tsat = tcsat.getSatisfaction();
Object val = tsat.visit(new AbstractSatisfactionVisitor<Object>(null) {
@Override
public Object visitInstance(Object instance) {
return instance;
}
});
if (val == null) {
logger.warn("parameter {} not bound", q);
}
bld.addParameter(q, val);
} else {
logger.debug("dumping dependency {}", dep);
bld.addDependency(dep);
String tid = null;
try {
tid = process(targetNode);
} catch (IOException exc) {
throw Throwables.propagate(exc);
}
String port = String.format("%s:%d", id, bld.getLastDependencyPort());
EdgeBuilder eb = EdgeBuilder.create(port, tid)
.set("arrowhead", "vee");
if (e.getLabel().isFixed()) {
eb.set("arrowtail", "crow");
}
if (GraphtUtils.desireIsTransient(dep)) {
eb.set("style", "dashed");
}
edgeQueue.add(eb.build());
}
}
GVNode node = bld.build();
return node;
}
}
/**
* Render a graph to a file.
*
* @param graph The graph to render.
* @param graphvizFile The file to write the graph to.
* @throws IOException
*/
public static void renderGraph(DAGNode<Component,Dependency> graph,
File graphvizFile) throws IOException, RecommenderBuildException {
logger.debug("graph has {} nodes", graph.getReachableNodes().size());
logger.debug("simulating instantiation");
RecommenderInstantiator instantiator = RecommenderInstantiator.create(graph);
DAGNode<Component, Dependency> unshared = instantiator.simulate();
logger.debug("unshared graph has {} nodes", unshared.getReachableNodes().size());
Closer close = Closer.create();
try {
Writer writer = close.register(new FileWriter(graphvizFile));
GraphWriter gw = close.register(new GraphWriter(writer));
GraphDumper dumper = new GraphDumper(graph, unshared.getReachableNodes(), gw);
logger.debug("writing root node");
String rid = dumper.setRoot(graph);
// process each other node & add an edge
for (DAGEdge<Component, Dependency> e: graph.getOutgoingEdges()) {
DAGNode<Component, Dependency> target = e.getTail();
Component csat = target.getLabel();
if (!satIsNull(csat.getSatisfaction())) {
logger.debug("processing node {}", csat.getSatisfaction());
String id = dumper.process(target);
gw.putEdge(EdgeBuilder.create(rid, id)
.set("arrowhead", "vee")
.build());
}
}
// and we're done
dumper.finish();
} catch (Throwable th) {
throw close.rethrow(th);
} finally {
close.close();
}
}
private static boolean satIsNull(Satisfaction sat) {
return sat.visit(new AbstractSatisfactionVisitor<Boolean>(false) {
@Override
public Boolean visitNull() {
return true;
}
});
}
}