package com.tinkerpop.blueprints.impls.neo4j; 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.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; import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.ConfigurationConverter; 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.index.AutoIndexer; import org.neo4j.graphdb.index.RelationshipIndex; import org.neo4j.kernel.EmbeddedGraphDatabase; import org.neo4j.kernel.GraphDatabaseAPI; import org.neo4j.tooling.GlobalGraphOperations; import javax.transaction.Status; import javax.transaction.SystemException; import javax.transaction.TransactionManager; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * A Blueprints implementation of the graph database Neo4j (http://neo4j.org) * * @author Marko A. Rodriguez (http://markorodriguez.com) */ public class Neo4jGraph implements TransactionalGraph, IndexableGraph, KeyIndexableGraph, MetaGraph<GraphDatabaseService> { private static final Logger logger = Logger.getLogger(Neo4jGraph.class.getName()); private GraphDatabaseService rawGraph; private static final String INDEXED_KEYS_POSTFIX = ":indexed_keys"; protected final ThreadLocal<Transaction> tx = new ThreadLocal<Transaction>() { protected Transaction initialValue() { return null; } }; protected final ThreadLocal<Boolean> checkElementsInTransaction = new ThreadLocal<Boolean>() { protected Boolean initialValue() { return false; } }; 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; } protected boolean checkElementsInTransaction() { if (this.tx.get() == null) { return false; } else { return this.checkElementsInTransaction.get(); } } /** * Neo4j's transactions are not consistent between the graph and the graph * indices. Moreover, global graph operations are not consistent. For * example, if a vertex is removed and then an index is queried in the same * transaction, the removed vertex can be returned. This method allows the * developer to turn on/off a Neo4jGraph 'hack' that ensures transactional * consistency. The default behavior for Neo4jGraph is to use Neo4j's native * behavior which ensures speed at the expensive of consistency. Note that * this boolean switch is local to the current thread (i.e. a ThreadLocal * variable). * * @param checkElementsInTransaction check whether an element is in the transaction between * returning it */ public void setCheckElementsInTransaction(final boolean checkElementsInTransaction) { this.checkElementsInTransaction.set(checkElementsInTransaction); } public Neo4jGraph(final String directory) { this(directory, null); } public Neo4jGraph(final GraphDatabaseService rawGraph) { this.rawGraph = rawGraph; this.loadKeyIndices(); } public Neo4jGraph(final GraphDatabaseService rawGraph, boolean fresh) { this(rawGraph); if (fresh) this.freshLoad(); } public Neo4jGraph(final String directory, final Map<String, String> configuration) { boolean fresh = !new File(directory).exists(); try { if (null != configuration) this.rawGraph = new EmbeddedGraphDatabase(directory, configuration); else this.rawGraph = new EmbeddedGraphDatabase(directory); if (fresh) this.freshLoad(); this.loadKeyIndices(); } catch (Exception e) { if (this.rawGraph != null) this.rawGraph.shutdown(); throw new RuntimeException(e.getMessage(), e); } } public Neo4jGraph(final Configuration configuration) { this(configuration.getString("blueprints.neo4j.directory", null), ConfigurationConverter.getMap(configuration.subset("blueprints.neo4j.conf"))); } private void loadKeyIndices() { for (final String key : this.getInternalIndexKeys(Vertex.class)) { this.createKeyIndex(key, Vertex.class); } for (final String key : this.getInternalIndexKeys(Edge.class)) { this.createKeyIndex(key, Edge.class); } this.commit(); } private void freshLoad() { // remove reference node in a single transaction try { this.autoStartTransaction(); this.removeVertex(this.getVertex(0)); this.commit(); } catch (Exception e) { this.rollback(); } } private <T extends Element> void createInternalIndexKey(final String key, final Class<T> elementClass) { final String propertyName = elementClass.getSimpleName() + INDEXED_KEYS_POSTFIX; if (rawGraph instanceof GraphDatabaseAPI) { final PropertyContainer pc = ((GraphDatabaseAPI) this.rawGraph).getNodeManager().getGraphProperties(); try { final String[] keys = (String[]) pc.getProperty(propertyName); final Set<String> temp = new HashSet<String>(Arrays.asList(keys)); temp.add(key); pc.setProperty(propertyName, temp.toArray(new String[temp.size()])); } catch (Exception e) { // no indexed_keys kernel data property pc.setProperty(propertyName, new String[]{key}); } } else { throw new UnsupportedOperationException( "Unable to create an index on a non-GraphDatabaseAPI graph"); } } private <T extends Element> void dropInternalIndexKey(final String key, final Class<T> elementClass) { final String propertyName = elementClass.getSimpleName() + INDEXED_KEYS_POSTFIX; if (rawGraph instanceof GraphDatabaseAPI) { final PropertyContainer pc = ((GraphDatabaseAPI) this.rawGraph).getNodeManager().getGraphProperties(); try { final String[] keys = (String[]) pc.getProperty(propertyName); final Set<String> temp = new HashSet<String>(Arrays.asList(keys)); temp.remove(key); pc.setProperty(propertyName, temp.toArray(new String[temp.size()])); } catch (Exception e) { // no indexed_keys kernel data property } } else { logNotGraphDatabaseAPI(); } } public <T extends Element> Set<String> getInternalIndexKeys(final Class<T> elementClass) { final String propertyName = elementClass.getSimpleName() + INDEXED_KEYS_POSTFIX; if (rawGraph instanceof GraphDatabaseAPI) { final PropertyContainer pc = ((GraphDatabaseAPI) this.rawGraph).getNodeManager().getGraphProperties(); try { final String[] keys = (String[]) pc.getProperty(propertyName); return new HashSet<String>(Arrays.asList(keys)); } catch (Exception e) { // no indexed_keys kernel data property } } else { logNotGraphDatabaseAPI(); } return Collections.emptySet(); } 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()); } } public synchronized <T extends Element> Index<T> createIndex(final String indexName, final Class<T> indexClass, final Parameter... indexParameters) { if (this.rawGraph.index().existsForNodes(indexName) || this.rawGraph.index().existsForRelationships(indexName)) { throw ExceptionFactory.indexAlreadyExists(indexName); } this.autoStartTransaction(); return new Neo4jIndex(indexName, indexClass, this, indexParameters); } public <T extends Element> Index<T> getIndex(final String indexName, final Class<T> indexClass) { if (Vertex.class.isAssignableFrom(indexClass)) { if (this.rawGraph.index().existsForNodes(indexName)) { return new Neo4jIndex(indexName, indexClass, this); } 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 new Neo4jIndex(indexName, indexClass, this); } 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(); 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() { final List<Index<? extends Element>> indices = new ArrayList<Index<? extends Element>>(); for (final String name : this.rawGraph.index().nodeIndexNames()) { if (!name.equals(Neo4jTokens.NODE_AUTO_INDEX)) indices.add(new Neo4jIndex(name, Vertex.class, this)); } for (final String name : this.rawGraph.index().relationshipIndexNames()) { if (!name.equals(Neo4jTokens.RELATIONSHIP_AUTO_INDEX)) indices.add(new Neo4jIndex(name, Edge.class, this)); } return indices; } public Vertex addVertex(final Object id) { this.autoStartTransaction(); return new Neo4jVertex(this.rawGraph.createNode(), this); } public Vertex getVertex(final Object id) { 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 Neo4jVertex(this.rawGraph.getNodeById(longId), this); } catch (NotFoundException e) { return null; } catch (NumberFormatException e) { return null; } } /** * {@inheritDoc} * <p/> * The underlying Neo4j graph does not natively support this method within a * transaction. If the graph is not currently in a transaction, then the * operation runs efficiently and correctly. If the graph is currently in a * transaction, please use setCheckElementsInTransaction() if it is * necessary to ensure proper transactional semantics. Note that it is * costly to check if an element is in the transaction. * * @return all the vertices in the graph */ public Iterable<Vertex> getVertices() { return new Neo4jVertexIterable(GlobalGraphOperations.at(rawGraph).getAllNodes(), this, this.checkElementsInTransaction()); } public Iterable<Vertex> getVertices(final String key, final Object value) { final AutoIndexer indexer = this.rawGraph.index().getNodeAutoIndexer(); if (indexer.isEnabled() && indexer.getAutoIndexedProperties().contains(key)) return new Neo4jVertexIterable(this.rawGraph.index().getNodeAutoIndexer().getAutoIndex().get(key, value), this, this.checkElementsInTransaction()); else return new PropertyFilteredIterable<Vertex>(key, value, this.getVertices()); } /** * {@inheritDoc} * <p/> * The underlying Neo4j graph does not natively support this method within a * transaction. If the graph is not currently in a transaction, then the * operation runs efficiently and correctly. If the graph is currently in a * transaction, please use setCheckElementsInTransaction() if it is * necessary to ensure proper transactional semantics. Note that it is * costly to check if an element is in the transaction. * * @return all the edges in the graph */ public Iterable<Edge> getEdges() { return new Neo4jEdgeIterable(GlobalGraphOperations.at(rawGraph).getAllRelationships(), this, this.checkElementsInTransaction()); } public Iterable<Edge> getEdges(final String key, final Object value) { final AutoIndexer indexer = this.rawGraph.index().getRelationshipAutoIndexer(); if (indexer.isEnabled() && indexer.getAutoIndexedProperties().contains(key)) return new Neo4jEdgeIterable(this.rawGraph.index().getRelationshipAutoIndexer().getAutoIndex().get(key, value), this, this.checkElementsInTransaction()); else return new PropertyFilteredIterable<Edge>(key, value, this.getEdges()); } public <T extends Element> void dropKeyIndex(final String key, final Class<T> elementClass) { if (elementClass == null) throw ExceptionFactory.classForElementCannotBeNull(); this.autoStartTransaction(); if (Vertex.class.isAssignableFrom(elementClass)) { if (!this.rawGraph.index().getNodeAutoIndexer().isEnabled()) return; this.rawGraph.index().getNodeAutoIndexer().stopAutoIndexingProperty(key); } else if (Edge.class.isAssignableFrom(elementClass)) { if (!this.rawGraph.index().getRelationshipAutoIndexer().isEnabled()) return; this.rawGraph.index().getRelationshipAutoIndexer().stopAutoIndexingProperty(key); } else { throw ExceptionFactory.classIsNotIndexable(elementClass); } this.dropInternalIndexKey(key, elementClass); } public <T extends Element> void createKeyIndex(final String key, final Class<T> elementClass, final Parameter... indexParameters) { if (elementClass == null) throw ExceptionFactory.classForElementCannotBeNull(); if (Vertex.class.isAssignableFrom(elementClass)) { this.autoStartTransaction(); if (!this.rawGraph.index().getNodeAutoIndexer().isEnabled()) this.rawGraph.index().getNodeAutoIndexer().setEnabled(true); this.rawGraph.index().getNodeAutoIndexer().startAutoIndexingProperty(key); if (!this.getInternalIndexKeys(Vertex.class).contains(key)) { KeyIndexableGraphHelper.reIndexElements(this, this.getVertices(), new HashSet<String>(Arrays.asList(key))); this.autoStartTransaction(); this.createInternalIndexKey(key, elementClass); } } else if (Edge.class.isAssignableFrom(elementClass)) { this.autoStartTransaction(); if (!this.rawGraph.index().getRelationshipAutoIndexer().isEnabled()) this.rawGraph.index().getRelationshipAutoIndexer().setEnabled(true); this.rawGraph.index().getRelationshipAutoIndexer().startAutoIndexingProperty(key); if (!this.getInternalIndexKeys(Edge.class).contains(key)) { KeyIndexableGraphHelper.reIndexElements(this, this.getEdges(), new HashSet<String>(Arrays.asList(key))); this.autoStartTransaction(); this.createInternalIndexKey(key, elementClass); } } else { throw ExceptionFactory.classIsNotIndexable(elementClass); } } public <T extends Element> Set<String> getIndexedKeys(final Class<T> elementClass) { if (elementClass == null) throw ExceptionFactory.classForElementCannotBeNull(); if (Vertex.class.isAssignableFrom(elementClass)) { if (!this.rawGraph.index().getNodeAutoIndexer().isEnabled()) return Collections.emptySet(); return this.rawGraph.index().getNodeAutoIndexer().getAutoIndexedProperties(); } else if (Edge.class.isAssignableFrom(elementClass)) { if (!this.rawGraph.index().getRelationshipAutoIndexer().isEnabled()) return Collections.emptySet(); return this.rawGraph.index().getRelationshipAutoIndexer().getAutoIndexedProperties(); } else { throw ExceptionFactory.classIsNotIndexable(elementClass); } } public void removeVertex(final Vertex vertex) { this.autoStartTransaction(); try { final Node node = ((Neo4jVertex) vertex).getRawVertex(); 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 Edge addEdge(final Object id, final Vertex outVertex, final Vertex inVertex, final String label) { if (label == null) throw ExceptionFactory.edgeLabelCanNotBeNull(); this.autoStartTransaction(); return new Neo4jEdge(((Neo4jVertex) outVertex).getRawVertex().createRelationshipTo(((Neo4jVertex) inVertex).getRawVertex(), DynamicRelationshipType.withName(label)), this); } public Edge getEdge(final Object id) { if (null == id) throw ExceptionFactory.edgeIdCanNotBeNull(); try { final Long longId; if (id instanceof Long) longId = (Long) id; else longId = Double.valueOf(id.toString()).longValue(); return new Neo4jEdge(this.rawGraph.getRelationshipById(longId), this); } catch (NotFoundException e) { return null; } catch (NumberFormatException e) { return null; } } public void removeEdge(final Edge edge) { this.autoStartTransaction(); ((Relationship) ((Neo4jEdge) edge).getRawElement()).delete(); } 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().finish(); tx.remove(); } } public void rollback() { if (null == tx.get()) { return; } GraphDatabaseAPI graphDatabaseAPI = (GraphDatabaseAPI) getRawGraph(); TransactionManager transactionManager = graphDatabaseAPI.getTxManager(); try { javax.transaction.Transaction t = transactionManager.getTransaction(); if (t == null || t.getStatus() == Status.STATUS_ROLLEDBACK) { return; } tx.get().failure(); } catch (SystemException e) { throw new RuntimeException(e); } finally { tx.get().finish(); tx.remove(); } } public void shutdown() { try { this.commit(); } catch (TransactionFailureException e) { // TODO: inspect why certain transactions fail } this.rawGraph.shutdown(); } protected void autoStartTransaction() { 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); } }