package dgm; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.tinkerpop.blueprints.*; import com.tinkerpop.blueprints.impls.neo4j.Neo4jGraph; import com.tinkerpop.blueprints.impls.neo4j.Neo4jVertexIterable; import dgm.trees.*; import dgm.trees2.Trees2; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.index.ReadableIndex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Iterator; import java.util.Map; public final class GraphUtilities { private static final Logger log = LoggerFactory.getLogger(GraphUtilities.class); public static final String PREFIX = "_"; public static final String IDENTIFIER = PREFIX + "identifier"; public static final String SYMBOLIC_IDENTIFER = PREFIX + "symbolic"; public static final String OWNER = PREFIX + "owner"; public static final String SYMBOLIC_OWNER = PREFIX + "symbolicOwner"; public static final String KEY_INDEX = PREFIX + "index"; public static final String KEY_TYPE = PREFIX + "type"; public static final String KEY_ID = PREFIX + "id"; public static final String KEY_VERSION = PREFIX + "version"; public static final int RESERVED_COUNT = 8; private GraphUtilities() {} /** * Compute the vertices reached from <code>s</code> in one step in direction <code>d</code>. * * @param s Initial vertex * @param d Direction to follow edges * * @return Tree with value (null, s) and then all children as specified * */ public static Tree<Pair<Edge,Vertex>> childrenFrom(Vertex s, Direction d) { // view the graph as a tree final TreeViewer<Pair<Edge,Vertex>> tv = new GraphTreeViewer(d); // build a copy of that tree by BFS visiting it final TreeBuilder<Pair<Edge,Vertex>> tb = new TreeBuilder<Pair<Edge, Vertex>>(); // but don't visit same node twice, ie. kill cycles final OccurrenceTracker<Pair<Edge,Vertex>> ot = new NodeAlreadyVisitedTracker(); final CycleKiller<Pair<Edge,Vertex>> cktb = new CycleKiller<Pair<Edge, Vertex>>(tb, ot); Trees2.bfsVisit(new Pair<Edge, Vertex>(null, s), tv, cktb); return tb.tree(); } public static Iterable<Vertex> findVerticesInIndex(Graph graph, String index) { return graph.getVertices(KEY_INDEX,index); } private static <_,A> boolean inTree(Tree<Pair<_,A>> tree, A thing) { if(tree.value() != null) { if(tree.value().b == null && thing == null) return true; if(tree.value().b.equals(thing)) return true; } for(Tree<Pair<_,A>> c : tree.children()) if(inTree(c, thing)) return true; return false; } public static Iterable<Vertex> findVerticesInIndex(Graph graph, String index, String type) { if (graph instanceof MetaGraph && graph instanceof Neo4jGraph) { GraphDatabaseService graphDatabaseService = ((MetaGraph<GraphDatabaseService>)graph).getRawGraph(); ReadableIndex<Node> indexer = graphDatabaseService.index().getNodeAutoIndexer().getAutoIndex(); Iterable<Node> itty = indexer.query(KEY_INDEX+":"+index+" AND "+KEY_TYPE+":"+type); // TODO check for transaction, this is done in Neo4jgraph normally but we can't access that method. return new Neo4jVertexIterable( itty, (Neo4jGraph)graph, false); } return null; } /** * Set the property of an element to a json value. * * If the value is an object the property is set to the JSON string, otherwise the property is set to the native * value represented by the JsonNode. * * <ul> * <li>So "'foo'" is stored as a String "foo" * <li>A boolean "true" is stored as Boolean.TRUE, etc. * <li>But "{'foo':123}" is stored as a String "{'foo':123}" * </ul> * * Normally you would just store (JSON) values, not objects or arrays. * * @param elt The node or edge of which to set the property * @param property Property name * @param value Property value */ public static void setProperty(Element elt, String property, JsonNode value) { // TODO use com.tinkerpop.blueprints.Features , supportsBooleanProperty, etc... checkPropertyName(property); // values are directly inserted as strings if(value.isValueNode()) { elt.setProperty(property, value.asText()); return; } // objects are inserted as json strings elt.setProperty(property, value.toString()); } public static JsonNode getProperty(ObjectMapper om, Element elt, String property) { checkPropertyName(property); final Object obj = elt.getProperty(property); if(! (obj instanceof String)) throw new RuntimeException("Property " + property + " is not in the expected format (String), it's a " + obj.getClass().getSimpleName()); final String s = String.valueOf(obj); try { return om.readTree(s); } catch (IOException z) { try { // TODO this is very shady.. anything that doesn't parse as JSON is read as string... return om.readTree("\"" + s + "\""); } catch (IOException e) { throw new RuntimeException("Failed to parse property " + property + " from '" + s +"'", z); } } } /** * This method removes all properties (except the id and owner ones) and sets new properties. */ public static void setProperties(Element element, Map<String, JsonNode> properties) { for (String key : element.getPropertyKeys()) { if (! key.startsWith(GraphUtilities.PREFIX)) element.removeProperty(key); } for (Map.Entry<String, JsonNode> e : properties.entrySet()) setProperty(element, e.getKey(), e.getValue()); } public static void checkPropertyName(String name) { if(name.equals(IDENTIFIER) || name.equals(OWNER) || name.equals(SYMBOLIC_IDENTIFER) || name.equals(SYMBOLIC_OWNER)) throw new IllegalArgumentException("Property name '" + name + "' is a reserved name"); } public static Edge findEdge(ObjectMapper om, Graph G, EdgeID edgeID) { final String id = getStringRepresentation(om, edgeID); final Iterator<Edge> ei = G.getEdges(IDENTIFIER, id).iterator(); if(!ei.hasNext()) return null; final Edge e = ei.next(); if(ei.hasNext()) throw new RuntimeException("Graph inconsistency! More than one edge with (head,label,tail) coordinate " + id); // TODO: Consistently handle these inconsistencies return e; } private static String getStringRepresentation(ObjectMapper om, final EdgeID edgeID) { final StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(toJSON(om, edgeID.tail()).toString()) .append("--") .append(edgeID.label()) .append("->") .append(toJSON(om, edgeID.head()).toString()); return stringBuilder.toString(); } private static Vertex findVertexOnProperty(ObjectMapper om, Graph G, ID id, String propertyName) { final String idStr = toJSON(om, id).toString(); final Iterator<Vertex> vi = G.getVertices(propertyName, idStr).iterator(); if(!vi.hasNext()) return null; final Vertex v = vi.next(); if(vi.hasNext()) throw new RuntimeException("Graph inconsistency! More than one vertex with identifier " + id); return v; } public static Vertex findVertex(ObjectMapper om, Graph G, ID id) { return findVertexOnProperty(om, G, id, IDENTIFIER); } /** * Find all vertices owner by the specified ID, don't look at versions. * This method will not return the ownable symbolic vertices. */ public static Iterable<Vertex> findOwnedVertices(ObjectMapper om, Graph G, ID owner) { return G.getVertices(SYMBOLIC_OWNER, toJSON(om, getSymbolicID(owner)).toString()); } /** * Find all edge owner by the specified ID, don't look at versions. */ public static Iterable<Edge> findOwnedEdges(ObjectMapper om, Graph G, ID owner) { return G.getEdges(SYMBOLIC_OWNER, toJSON(om, getSymbolicID(owner)).toString()); } /** * Find vertex without considering the version specified in the ID. */ public static Vertex resolveVertex(ObjectMapper om, Graph G, ID id) { return findVertexOnProperty(om, G, getSymbolicID(id), SYMBOLIC_IDENTIFER); } public static EdgeID getEdgeID(ObjectMapper om, Edge edge) { final Vertex tail = edge.getVertex(Direction.OUT); final Vertex head = edge.getVertex(Direction.IN); final ID tailID = getID(om, tail); final ID headID = getID(om, head); if (tailID != null && headID != null) return new EdgeID(tailID, edge.getLabel(), headID); return null; } /** * TODO: return null if ID cannot be found */ public static ID getID(ObjectMapper om, Vertex vertex) { final String json = String.valueOf(vertex.getProperty(IDENTIFIER)); try { final JsonNode node = om.readTree(json); return JSONUtilities.fromJSON(node); } catch (IOException e) { log.trace("Failed to parse ID from string '{}' (should be JSON Array)", json); return null; } } public static void setID(ObjectMapper om, Vertex vertex, ID id) { final String json = toJSON(om, id).toString(); vertex.setProperty(IDENTIFIER, json); final String symbolic = toJSON(om, getSymbolicID(id)).toString(); vertex.setProperty(SYMBOLIC_IDENTIFER, symbolic); setKey(vertex, id); } public static void setEdgeId(ObjectMapper om, EdgeID edgeID, Edge edge) { edge.setProperty(IDENTIFIER, getStringRepresentation(om, edgeID)); // TODO keys for the edges ? } private static void setKey(Element element, ID id) { element.setProperty(KEY_INDEX, id.index()); element.setProperty(KEY_TYPE, id.type()); element.setProperty(KEY_ID, id.id()); element.setProperty(KEY_VERSION, id.version()); } public static ID getSymbolicID(ID id) { return new ID(id.index(), id.type(), id.id(), 0); } public static void setOwner(ObjectMapper om, Element element, ID id) { element.setProperty(OWNER, toJSON(om, id).toString()); element.setProperty(SYMBOLIC_OWNER, toJSON(om, getSymbolicID(id)).toString()); } /** * Find the owner of an edge or a vertex. */ public static ID getOwner(ObjectMapper om, Element element) { final Object owner = element.getProperty(OWNER); if (owner == null) return null; try { final JsonNode node = om.readTree(String.valueOf(owner)); return JSONUtilities.fromJSON(node); } catch (IOException e) { log.trace("Failed to parse ID from '{}' (should be JSON Array)", String.valueOf(owner)); return null; } } /** * Create an edge in the graph and set its identifier. */ public static Edge createEdge(ObjectMapper om, Graph G, EdgeID edgeID) { final Vertex tv = findVertex(om, G, edgeID.tail()); final Vertex hv = findVertex(om, G, edgeID.head()); if(tv == null || hv == null) throw new RuntimeException("Head or tail of edge doesn't exist!"); final Edge e = G.addEdge(null, tv, hv, edgeID.label()); setEdgeId(om, edgeID, e); return e; } /** * Create a vertex and set it's identifier. */ public static Vertex createVertex(ObjectMapper om, Graph G, ID id) { final Vertex v = G.addVertex(null); setID(om, v, id); return v; } public static boolean isSymbolic(ObjectMapper om, final Vertex v) { return getID(om, v).isSymbolic(); } /** * Return true if {@link GraphUtilities#setOwner} can be called on this vertex without violating semantics: * * You should only set the owner if: * <ul> * <li>The vertex does not have an owner yet</li> * <li>An older version of your subset owns the vertex</li> * <li>The vertex is a symbolic vertex ({@code version() == 0})</li> * </ul> */ public static boolean isOwnable(ObjectMapper om, final Vertex v, final ID owner) { final ID id = getOwner(om, v); return id == null || onlyVersionDiffers(id, owner) || isSymbolic(om, v); } /** * Tests if a.version is smaller than b.version */ public static boolean isOlder(final ID a, final ID b) { return a.version() < b.version(); } /** * Determine whether two ID instances are equal when ignoring their versions. */ public static boolean onlyVersionDiffers(final ID id, final ID other) { return id.id().equals(other.id()) && id.index().equals(other.index()) && id.type().equals(other.type()); } /** * From the edge ID, return the ID that is not {@code notThisOne}. */ public static ID getOppositeId(final EdgeID id, final ID notThisOne) { return id.head().equals(notThisOne) ? id.tail() : id.head(); } /** * Return the direction <i>d</i> such that {@link Edge#getVertex(com.tinkerpop.blueprints.Direction d)} * return the vertex {@code !=} to the vertex with {@code getID() == vertexId}. */ public static Direction directionOppositeTo(final EdgeID id, final ID vertexId) { return id.head().equals(vertexId) ? Direction.OUT : Direction.IN; } public static EdgeID createOppositeId(final EdgeID id, final ID notThisOne, final ID targetId) { if (id.tail().equals(notThisOne)) return new EdgeID(id.tail(), id.label(), targetId); else return new EdgeID(targetId, id.label(), id.head()); } public static void dumpGraph(ObjectMapper om, Graph graph) { log.trace("Graph dump start"); for(Edge e : graph.getEdges()) { final EdgeID edgeId = getEdgeID(om, e); if(edgeId == null) log.trace(e.toString()); else log.trace(edgeId.toString()); } log.trace("Graph dump done"); } public static void makeSymbolic(ObjectMapper om, Vertex vertex) { final ID symbolicID = getSymbolicID(getID(om, vertex)); setID(om, vertex, symbolicID); setOwner(om, vertex, symbolicID); for (Edge edge : vertex.getEdges(Direction.IN)) { EdgeID id = getEdgeID(om, edge); EdgeID idWithSymbolicHead = new EdgeID(id.tail(), id.label(), symbolicID); setEdgeId(om, idWithSymbolicHead, edge); } for (Edge edge : vertex.getEdges(Direction.OUT)) { EdgeID id = getEdgeID(om, edge); EdgeID idWithSymbolicHead = new EdgeID(symbolicID, id.label(), id.head()); setEdgeId(om, idWithSymbolicHead, edge); } } public static ArrayNode toJSON(ObjectMapper om, ID id) { return om.createArrayNode() .add(id.index()) .add(id.type()) .add(id.id()) .add(id.version()); } public static ObjectNode toJSON(final ObjectMapper om, final Edge edge) { final ObjectNode objectNode = om.createObjectNode(); final EdgeID edgeID = GraphUtilities.getEdgeID(om, edge); final ID tail = edgeID.tail(); objectNode.put("tail", toJSON(om, tail)); final String label = edgeID.label(); objectNode.put("label", label); final ID head = edgeID.head(); objectNode.put("head", toJSON(om, head)); return objectNode; } public static ArrayNode toJSON(ObjectMapper om, Vertex vertex) { final ID id = GraphUtilities.getID(om, vertex); return toJSON(om, id); } }