package com.tinkerpop.blueprints.impls.neo4j2; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.ConfigurationConverter; import org.neo4j.graphdb.DynamicLabel; import org.neo4j.graphdb.DynamicRelationshipType; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.NotFoundException; import org.neo4j.graphdb.PropertyContainer; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.TransactionFailureException; import org.neo4j.graphdb.factory.GraphDatabaseBuilder; import org.neo4j.graphdb.factory.GraphDatabaseFactory; import org.neo4j.graphdb.index.AutoIndexer; import org.neo4j.graphdb.index.RelationshipIndex; import org.neo4j.helpers.Settings; import org.neo4j.kernel.GraphDatabaseAPI; import org.neo4j.kernel.impl.core.NodeManager; import org.neo4j.tooling.GlobalGraphOperations; import com.tinkerpop.blueprints.Edge; import com.tinkerpop.blueprints.Element; import com.tinkerpop.blueprints.Features; import com.tinkerpop.blueprints.GraphQuery; import com.tinkerpop.blueprints.Index; import com.tinkerpop.blueprints.IndexableGraph; import com.tinkerpop.blueprints.KeyIndexableGraph; import com.tinkerpop.blueprints.MetaGraph; import com.tinkerpop.blueprints.Parameter; import com.tinkerpop.blueprints.TransactionalGraph; import com.tinkerpop.blueprints.Vertex; import com.tinkerpop.blueprints.impls.neo4j2.index.Neo4j2EdgeIndex; import com.tinkerpop.blueprints.impls.neo4j2.index.Neo4j2VertexIndex; import com.tinkerpop.blueprints.impls.neo4j2.iterate.Neo4j2EdgeIterable; import com.tinkerpop.blueprints.impls.neo4j2.iterate.Neo4j2VertexIterable; import com.tinkerpop.blueprints.util.DefaultGraphQuery; import com.tinkerpop.blueprints.util.ExceptionFactory; import com.tinkerpop.blueprints.util.KeyIndexableGraphHelper; import com.tinkerpop.blueprints.util.PropertyFilteredIterable; import com.tinkerpop.blueprints.util.StringFactory; /** * A Blueprints implementation of the graph database Neo4j (http://neo4j.org) * * @author Marko A. Rodriguez (http://markorodriguez.com) */ public class Neo4j2Graph implements TransactionalGraph, IndexableGraph, KeyIndexableGraph, MetaGraph<GraphDatabaseService> { private static final Logger logger = Logger.getLogger(Neo4j2Graph.class.getName()); public static GraphDatabaseBuilder createGraphDatabaseBuilder(String directory, Map<String, String> configuration){ GraphDatabaseBuilder builder = new GraphDatabaseFactory().newEmbeddedDatabaseBuilder(directory); if (null != configuration){ for(String key: configuration.keySet()){ builder.setConfig(Settings.setting(key, Settings.STRING, (String) null), configuration.get(key)); } } return builder; } //========================================================================= // Element wrapper public static interface ElementWrapper<T extends Element, S extends PropertyContainer> { public T wrap(S rawElement); } public static interface VertexWrapper<V extends Vertex> extends ElementWrapper<V, Node>{ } public static interface EdgeWrapper<E extends Edge> extends ElementWrapper<E, Relationship>{ } private static VertexWrapper<Neo4j2Vertex> createDefaultVertexWrapper(final Neo4j2Graph graph){ return new VertexWrapper<Neo4j2Vertex>() { @Override public Neo4j2Vertex wrap(Node rawVertex) { return new Neo4j2Vertex(rawVertex, graph); } }; } private static EdgeWrapper<Neo4j2Edge> createDefaultEdgeWrapper(final Neo4j2Graph graph){ return new EdgeWrapper<Neo4j2Edge>() { @Override public Neo4j2Edge wrap(Relationship rawEdge) { return new Neo4j2Edge(rawEdge, graph); } }; } //========================================================================= private GraphDatabaseService rawGraph; private Neo4j2GraphInternalIndexKeys indexKeys; private VertexWrapper<? extends Vertex> vertexWrapper; private EdgeWrapper<? extends Edge> edgeWrapper; protected final ThreadLocal<Transaction> tx = new ThreadLocal<Transaction>() { protected Transaction initialValue() { return null; } }; private static final Features FEATURES = new Features(); static { FEATURES.supportsSerializableObjectProperty = false; FEATURES.supportsBooleanProperty = true; FEATURES.supportsDoubleProperty = true; FEATURES.supportsFloatProperty = true; FEATURES.supportsIntegerProperty = true; FEATURES.supportsPrimitiveArrayProperty = true; FEATURES.supportsUniformListProperty = true; FEATURES.supportsMixedListProperty = false; FEATURES.supportsLongProperty = true; FEATURES.supportsMapProperty = false; FEATURES.supportsStringProperty = true; FEATURES.supportsDuplicateEdges = true; FEATURES.supportsSelfLoops = true; FEATURES.isPersistent = true; FEATURES.isWrapper = false; FEATURES.supportsVertexIteration = true; FEATURES.supportsEdgeIteration = true; FEATURES.supportsVertexIndex = true; FEATURES.supportsEdgeIndex = true; FEATURES.ignoresSuppliedIds = true; FEATURES.supportsTransactions = true; FEATURES.supportsIndices = true; FEATURES.supportsKeyIndices = true; FEATURES.supportsVertexKeyIndex = true; FEATURES.supportsEdgeKeyIndex = true; FEATURES.supportsEdgeRetrieval = true; FEATURES.supportsVertexProperties = true; FEATURES.supportsEdgeProperties = true; FEATURES.supportsThreadedTransactions = false; FEATURES.supportsThreadIsolatedTransactions = true; } /** * @deprecated since Blueprints 2.7.0/Neo4j 2.2.x this method is * no longer required since Neo4j indexes no longer return * deleted elements. It will always return false. */ @Deprecated protected boolean checkElementsInTransaction() { return false; } /** * @deprecated since Blueprints 2.7.0/Neo4j 2.2.x this method is * no longer required - Neo4j indexes no longer return deleted elements. * * This method is now a no-op. */ @Deprecated public void setCheckElementsInTransaction(final boolean checkElementsInTransaction) { } public Neo4j2Graph(final String directory) { this(directory, null); } public Neo4j2Graph(final Configuration configuration) { this(configuration.getString("blueprints.neo4j.directory", null), ConfigurationConverter.getMap(configuration.subset("blueprints.neo4j.conf"))); } public Neo4j2Graph(final String directory, final Map<String, String> configuration) { this(createGraphDatabaseBuilder(directory, configuration).newGraphDatabase()); } public Neo4j2Graph(final GraphDatabaseService rawGraph) { try{ this.rawGraph = rawGraph; this.indexKeys = new Neo4j2GraphInternalIndexKeys(this.rawGraph); this.vertexWrapper = createDefaultVertexWrapper(this); this.edgeWrapper = createDefaultEdgeWrapper(this); init(); } catch (Exception e) { if (this.rawGraph != null) this.rawGraph.shutdown(); throw new RuntimeException(e.getMessage(), e); } } public VertexWrapper<? extends Vertex> getVertexWrapper() { return vertexWrapper; } public EdgeWrapper<? extends Edge> getEdgeWrapper() { return edgeWrapper; } public void setVertexWrapper(VertexWrapper<? extends Vertex> vertexWrapper) { this.vertexWrapper = vertexWrapper; } public void setEdgeWrapper(EdgeWrapper<? extends Edge> edgeWrapper) { this.edgeWrapper = edgeWrapper; } protected void init() { this.loadKeyIndices(); this.commit(); } private void loadKeyIndices() { this.autoStartTransaction(true); for (final String key : this.indexKeys.getKeys(Vertex.class)) { this.createKeyIndex(key, Vertex.class); } for (final String key : this.indexKeys.getKeys(Edge.class)) { this.createKeyIndex(key, Edge.class); } this.commit(); } /** * Helper method, here only to support the existing methods that pass that Class<T> as an argument. */ @SuppressWarnings({ "unchecked", "rawtypes" }) private <T extends Element> Index<T> _createIndex(final String indexName, final Class<T> indexClass, final Parameter... indexParameters) { if (Vertex.class.isAssignableFrom(indexClass)) { return (Index<T>) new Neo4j2VertexIndex(indexName, this, indexParameters); } else { return (Index<T>) new Neo4j2EdgeIndex(indexName, this, indexParameters); } } @SuppressWarnings("rawtypes") @Override public synchronized <T extends Element> Index<T> createIndex(final String indexName, final Class<T> indexClass, final Parameter... indexParameters) { this.autoStartTransaction(true); if (this.rawGraph.index().existsForNodes(indexName) || this.rawGraph.index().existsForRelationships(indexName)) { throw ExceptionFactory.indexAlreadyExists(indexName); } return _createIndex(indexName, indexClass, indexParameters); } public <T extends Element> Index<T> getIndex(final String indexName, final Class<T> indexClass) { this.autoStartTransaction(false); if (Vertex.class.isAssignableFrom(indexClass)) { if (this.rawGraph.index().existsForNodes(indexName)) { return _createIndex(indexName, indexClass); } else if (this.rawGraph.index().existsForRelationships(indexName)) { throw ExceptionFactory.indexDoesNotSupportClass(indexName, indexClass); } else { return null; } } else if (Edge.class.isAssignableFrom(indexClass)) { if (this.rawGraph.index().existsForRelationships(indexName)) { return _createIndex(indexName, indexClass); } else if (this.rawGraph.index().existsForNodes(indexName)) { throw ExceptionFactory.indexDoesNotSupportClass(indexName, indexClass); } else { return null; } } else { return null; } } /** * {@inheritDoc} * <p/> * Note that this method will force a successful closing of the current * thread's transaction. As such, once the index is dropped, the operation * is committed. * * @param indexName the name of the index to drop */ public synchronized void dropIndex(final String indexName) { this.autoStartTransaction(true); if (this.rawGraph.index().existsForNodes(indexName)) { org.neo4j.graphdb.index.Index<Node> nodeIndex = this.rawGraph.index().forNodes(indexName); if (nodeIndex.isWriteable()) { nodeIndex.delete(); } } else if (this.rawGraph.index().existsForRelationships(indexName)) { RelationshipIndex relationshipIndex = this.rawGraph.index().forRelationships(indexName); if (relationshipIndex.isWriteable()) { relationshipIndex.delete(); } } this.commit(); } public Iterable<Index<? extends Element>> getIndices() { this.autoStartTransaction(false); final List<Index<? extends Element>> indices = new ArrayList<Index<? extends Element>>(); for (final String name : this.rawGraph.index().nodeIndexNames()) { if (!name.equals(Neo4j2Tokens.NODE_AUTO_INDEX)) indices.add(new Neo4j2VertexIndex(name, this)); } for (final String name : this.rawGraph.index().relationshipIndexNames()) { if (!name.equals(Neo4j2Tokens.RELATIONSHIP_AUTO_INDEX)) indices.add(new Neo4j2EdgeIndex(name, this)); } return indices; } public Neo4j2Vertex addVertex(final Object id) { this.autoStartTransaction(true); return new Neo4j2Vertex(this.rawGraph.createNode(), this); } public Neo4j2Vertex getVertex(final Object id) { this.autoStartTransaction(false); if (null == id) throw ExceptionFactory.vertexIdCanNotBeNull(); try { final Long longId; if (id instanceof Long) longId = (Long) id; else if (id instanceof Number) longId = ((Number) id).longValue(); else longId = Double.valueOf(id.toString()).longValue(); return new Neo4j2Vertex(this.rawGraph.getNodeById(longId), this); } catch (NotFoundException e) { return null; } catch (NumberFormatException e) { return null; } } /** * @return all the vertices in the graph */ public Iterable<Vertex> getVertices() { this.autoStartTransaction(false); return new Neo4j2VertexIterable(GlobalGraphOperations.at(rawGraph).getAllNodes(), this); } public Iterable<Vertex> getVertices(final String label) { Iterable<Node> nodes = new Iterable<Node>() { @Override public Iterator<Node> iterator() { return rawGraph.findNodes(DynamicLabel.label(label)); } }; return new Neo4j2VertexIterable(nodes , this); } public Iterable<Vertex> getVertices(final String key, final Object value) { this.autoStartTransaction(false); final AutoIndexer<?> indexer = this.rawGraph.index().getNodeAutoIndexer(); if (indexer.isEnabled() && indexer.getAutoIndexedProperties().contains(key)) return new Neo4j2VertexIterable(this.rawGraph.index().getNodeAutoIndexer().getAutoIndex().get(key, value), this); else return new PropertyFilteredIterable<Vertex>(key, value, this.getVertices()); } /** * @return all the edges in the graph */ public Iterable<Edge> getEdges() { this.autoStartTransaction(false); return new Neo4j2EdgeIterable(GlobalGraphOperations.at(rawGraph).getAllRelationships(), this); } public Iterable<Edge> getEdges(final String key, final Object value) { this.autoStartTransaction(false); final AutoIndexer<?> indexer = this.rawGraph.index().getRelationshipAutoIndexer(); if (indexer.isEnabled() && indexer.getAutoIndexedProperties().contains(key)) return new Neo4j2EdgeIterable(this.rawGraph.index().getRelationshipAutoIndexer().getAutoIndex().get(key, value), this); else return new PropertyFilteredIterable<Edge>(key, value, this.getEdges()); } @Override public <T extends Element> Set<String> getIndexedKeys(Class<T> elementClass) { this.autoStartTransaction(false); AutoIndexer<?> indexer = getAutoIndexer(elementClass); if(indexer.isEnabled()){ return indexer.getAutoIndexedProperties(); } else { return Collections.emptySet(); } } @Override public <T extends Element> void dropKeyIndex(final String key, final Class<T> elementClass) { this.autoStartTransaction(true); AutoIndexer<?> autoIndexer = getAutoIndexer(elementClass); if (!autoIndexer.isEnabled()){ return; } autoIndexer.stopAutoIndexingProperty(key); this.indexKeys.removeKey(key, elementClass); } @SuppressWarnings("rawtypes") @Override public <T extends Element> void createKeyIndex(final String key, final Class<T> elementClass, final Parameter... indexParameters) { this.autoStartTransaction(true); AutoIndexer<?> indexer = getAutoIndexer(elementClass); if(indexer.isEnabled() && indexer.getAutoIndexedProperties().contains(key)){ return; } if (! indexer.isEnabled()){ indexer.setEnabled(true); } indexer.startAutoIndexingProperty(key); Iterable<? extends Element> elements = Vertex.class.isAssignableFrom(elementClass) ? this.getVertices() : this.getEdges(); KeyIndexableGraphHelper.reIndexElements(this, elements, new HashSet<String>(Arrays.asList(key))); this.autoStartTransaction(true); this.indexKeys.addKey(key, elementClass); } private <T extends Element> AutoIndexer<?> getAutoIndexer(final Class<T> elementClass){ if (elementClass == null){ throw ExceptionFactory.classForElementCannotBeNull(); } else if (Vertex.class.isAssignableFrom(elementClass)) { return this.rawGraph.index().getNodeAutoIndexer(); } else if (Edge.class.isAssignableFrom(elementClass)) { return this.rawGraph.index().getRelationshipAutoIndexer(); } else { throw ExceptionFactory.classIsNotIndexable(elementClass); } } public void removeVertex(final Vertex vertex) { this.autoStartTransaction(true); try { final Node node = ((Neo4j2Vertex) vertex).getRawElement(); for (final Relationship relationship : node.getRelationships(org.neo4j.graphdb.Direction.BOTH)) { relationship.delete(); } node.delete(); } catch (NotFoundException nfe) { throw ExceptionFactory.vertexWithIdDoesNotExist(vertex.getId()); } catch (IllegalStateException ise) { // wrap the neo4j exception so that the message is consistent in blueprints. throw ExceptionFactory.vertexWithIdDoesNotExist(vertex.getId()); } } public Neo4j2Edge addEdge(final Object id, final Vertex outVertex, final Vertex inVertex, final String label) { if (label == null) throw ExceptionFactory.edgeLabelCanNotBeNull(); this.autoStartTransaction(true); return new Neo4j2Edge(((Neo4j2Vertex) outVertex).getRawElement().createRelationshipTo(((Neo4j2Vertex) inVertex).getRawElement(), DynamicRelationshipType.withName(label)), this); } public Neo4j2Edge getEdge(final Object id) { if (null == id) throw ExceptionFactory.edgeIdCanNotBeNull(); this.autoStartTransaction(false); try { final Long longId; if (id instanceof Long) longId = (Long) id; else longId = Double.valueOf(id.toString()).longValue(); return new Neo4j2Edge(this.rawGraph.getRelationshipById(longId), this); } catch (NotFoundException e) { return null; } catch (NumberFormatException e) { return null; } } public void removeEdge(final Edge edge) { this.autoStartTransaction(true); ((Relationship) ((Neo4j2Edge) edge).getRawElement()).delete(); } @SuppressWarnings("deprecation") @Override public void stopTransaction(Conclusion conclusion) { if (Conclusion.SUCCESS == conclusion) commit(); else rollback(); } public void commit() { if (null == tx.get()) { return; } try { tx.get().success(); } finally { tx.get().close(); tx.remove(); } } public void rollback() { if (null == tx.get()) { return; } try { tx.get().failure(); } finally { tx.get().close(); tx.remove(); } } public void shutdown() { try { this.commit(); } catch (TransactionFailureException e) { logger.warning("Failure on shutdown "+e.getMessage()); // TODO: inspect why certain transactions fail } this.rawGraph.shutdown(); } // The forWrite flag is true when the autoStartTransaction method is // called before any operation which will modify the graph in any way. It // is not used in this simple implementation but is required in subclasses // which enforce transaction rules. Now that Neo4j reads also require a // transaction to be open it is otherwise impossible to tell the difference // between the beginning of a write operation and the beginning of a read // operation. public void autoStartTransaction(boolean forWrite) { if (tx.get() == null) tx.set(this.rawGraph.beginTx()); } public GraphDatabaseService getRawGraph() { return this.rawGraph; } public Features getFeatures() { return FEATURES; } public String toString() { return StringFactory.graphString(this, this.rawGraph.toString()); } public GraphQuery query() { return new DefaultGraphQuery(this); } public Iterator<Map<String,Object>> query(String query, Map<String,Object> params) { return rawGraph.execute(query,params==null ? Collections.<String,Object>emptyMap() : params); } /** * A class that encapsulates some deprecated method calls, * and other "hackish" bits, leftover from previous implementation. * * @author Joey Freund */ private class Neo4j2GraphInternalIndexKeys { private GraphDatabaseService rawGraph; public Neo4j2GraphInternalIndexKeys(GraphDatabaseService rawGraph) { this.rawGraph = rawGraph; } private <T extends Element> String getIndexKeysPropertyName(final Class<T> elementClass){ return elementClass.getSimpleName() + ":indexed_keys"; } public <T extends Element> void addKey(String key, Class<T> elementClass){ Set<String> keys = getKeys(elementClass); keys.add(key); setKeys(elementClass, keys); } public <T extends Element> void removeKey(String key, Class<T> elementClass){ try { Set<String> keys = getKeys(elementClass); keys.remove(key); setKeys(elementClass, keys); } catch (Exception e) { // no indexed_keys kernel data property } } public <T extends Element> Set<String> getKeys(Class<T> elementClass){ try { PropertyContainer pc = tryToGetGraphProperties(); final String[] keys = (String[]) pc.getProperty(getIndexKeysPropertyName(elementClass)); return new HashSet<String>(Arrays.asList(keys)); } catch (Exception e) { return new HashSet<String>(); } } private <T extends Element> void setKeys(Class<T> elementClass, Set<String> keys){ PropertyContainer pc = getGraphProperties(); pc.setProperty(getIndexKeysPropertyName(elementClass), keys.toArray(new String[keys.size()])); } private PropertyContainer getGraphProperties() { if (rawGraph instanceof GraphDatabaseAPI) { return ((GraphDatabaseAPI) this.rawGraph).getDependencyResolver().resolveDependency(NodeManager.class).newGraphProperties(); } else { logNotGraphDatabaseAPI(); throw new UnsupportedOperationException("Cannot get graph properties of a non-GraphDatabaseAPI graph"); } } private PropertyContainer tryToGetGraphProperties() { try{ return getGraphProperties(); } catch (UnsupportedOperationException e){ return null; } } private void logNotGraphDatabaseAPI() { if (logger.isLoggable(Level.WARNING)) { logger.log(Level.WARNING, "Indices are not available on non-GraphDatabaseAPI instances" + " Current graph class is " + rawGraph.getClass().getName()); } } } }