View Javadoc

1   package eu.ehri.project.core.impl.neo4j;
2   
3   import com.google.common.base.Preconditions;
4   import com.tinkerpop.blueprints.CloseableIterable;
5   import com.tinkerpop.blueprints.Edge;
6   import com.tinkerpop.blueprints.Features;
7   import com.tinkerpop.blueprints.GraphQuery;
8   import com.tinkerpop.blueprints.MetaGraph;
9   import com.tinkerpop.blueprints.TransactionalGraph;
10  import com.tinkerpop.blueprints.Vertex;
11  import com.tinkerpop.blueprints.util.DefaultGraphQuery;
12  import com.tinkerpop.blueprints.util.ExceptionFactory;
13  import com.tinkerpop.blueprints.util.PropertyFilteredIterable;
14  import com.tinkerpop.blueprints.util.StringFactory;
15  import com.tinkerpop.blueprints.util.WrappingCloseableIterable;
16  import org.apache.commons.configuration.Configuration;
17  import org.apache.commons.configuration.ConfigurationConverter;
18  import org.neo4j.graphdb.GraphDatabaseService;
19  import org.neo4j.graphdb.Label;
20  import org.neo4j.graphdb.Node;
21  import org.neo4j.graphdb.NotFoundException;
22  import org.neo4j.graphdb.Relationship;
23  import org.neo4j.graphdb.RelationshipType;
24  import org.neo4j.graphdb.ResourceIterable;
25  import org.neo4j.graphdb.ResourceIterator;
26  import org.neo4j.graphdb.Transaction;
27  import org.neo4j.graphdb.TransactionFailureException;
28  import org.neo4j.graphdb.factory.GraphDatabaseBuilder;
29  import org.neo4j.graphdb.factory.GraphDatabaseFactory;
30  
31  import java.io.File;
32  import java.util.Collections;
33  import java.util.Map;
34  import java.util.logging.Logger;
35  
36  /**
37   * A Blueprints implementation of the graph database Neo4j (http://neo4j.org)
38   */
39  public class Neo4j2Graph implements TransactionalGraph, MetaGraph<GraphDatabaseService> {
40      private static final Logger logger = Logger.getLogger(Neo4j2Graph.class.getName());
41  
42      private GraphDatabaseService rawGraph;
43  
44      protected final ThreadLocal<Transaction> tx = new ThreadLocal<Transaction>() {
45          protected Transaction initialValue() {
46              return null;
47          }
48      };
49  
50      protected final ThreadLocal<Boolean> checkElementsInTransaction = new ThreadLocal<Boolean>() {
51          protected Boolean initialValue() {
52              return false;
53          }
54      };
55  
56      private static final Features FEATURES = new Features();
57  
58      static {
59  
60          FEATURES.supportsSerializableObjectProperty = false;
61          FEATURES.supportsBooleanProperty = true;
62          FEATURES.supportsDoubleProperty = true;
63          FEATURES.supportsFloatProperty = true;
64          FEATURES.supportsIntegerProperty = true;
65          FEATURES.supportsPrimitiveArrayProperty = true;
66          FEATURES.supportsUniformListProperty = true;
67          FEATURES.supportsMixedListProperty = false;
68          FEATURES.supportsLongProperty = true;
69          FEATURES.supportsMapProperty = false;
70          FEATURES.supportsStringProperty = true;
71  
72          FEATURES.supportsDuplicateEdges = true;
73          FEATURES.supportsSelfLoops = true;
74          FEATURES.isPersistent = true;
75          FEATURES.isWrapper = false;
76          FEATURES.supportsVertexIteration = true;
77          FEATURES.supportsEdgeIteration = true;
78          FEATURES.supportsVertexIndex = false;
79          FEATURES.supportsEdgeIndex = true;
80          FEATURES.ignoresSuppliedIds = true;
81          FEATURES.supportsTransactions = true;
82          FEATURES.supportsIndices = true;
83          FEATURES.supportsKeyIndices = true;
84          FEATURES.supportsVertexKeyIndex = false;
85          FEATURES.supportsEdgeKeyIndex = false;
86          FEATURES.supportsEdgeRetrieval = true;
87          FEATURES.supportsVertexProperties = true;
88          FEATURES.supportsEdgeProperties = true;
89          FEATURES.supportsThreadedTransactions = false;
90          FEATURES.supportsThreadIsolatedTransactions = true;
91      }
92  
93      protected boolean checkElementsInTransaction() {
94          if (this.tx.get() == null) {
95              return false;
96          } else {
97              return this.checkElementsInTransaction.get();
98          }
99      }
100 
101     public Neo4j2Graph(String directory) {
102         this(directory, null);
103     }
104 
105     public Neo4j2Graph(GraphDatabaseService rawGraph) {
106         this.rawGraph = rawGraph;
107 
108         init();
109     }
110 
111     public Neo4j2Graph(String directory, Map<String, String> configuration) {
112         try {
113             GraphDatabaseBuilder builder = new GraphDatabaseFactory().newEmbeddedDatabaseBuilder(new File(directory));
114             if (null != configuration)
115                 this.rawGraph = builder.setConfig(configuration).newGraphDatabase();
116             else
117                 this.rawGraph = builder.newGraphDatabase();
118 
119             init();
120 
121         } catch (Exception e) {
122             if (this.rawGraph != null)
123                 this.rawGraph.shutdown();
124             throw new RuntimeException(e.getMessage(), e);
125         }
126     }
127 
128     protected void init() {
129     }
130 
131     public Neo4j2Graph(Configuration configuration) {
132         this(configuration.getString("blueprints.neo4j.directory", null),
133                 ConfigurationConverter.getMap(configuration.subset("blueprints.neo4j.conf")));
134     }
135 
136     @Override
137     public Neo4j2Vertex addVertex(Object id) {
138         this.autoStartTransaction(true);
139         return new Neo4j2Vertex(this.rawGraph.createNode(), this);
140     }
141 
142     @Override
143     public Neo4j2Vertex getVertex(Object id) {
144         this.autoStartTransaction(false);
145 
146         if (null == id)
147             throw ExceptionFactory.vertexIdCanNotBeNull();
148 
149         try {
150             Long longId;
151             if (id instanceof Long)
152                 longId = (Long) id;
153             else if (id instanceof Number)
154                 longId = ((Number) id).longValue();
155             else
156                 longId = Double.valueOf(id.toString()).longValue();
157             return new Neo4j2Vertex(this.rawGraph.getNodeById(longId), this);
158         } catch (NotFoundException e) {
159             return null;
160         } catch (NumberFormatException e) {
161             return null;
162         }
163     }
164 
165     /**
166      * {@inheritDoc}
167      * <p>
168      * The underlying Neo4j graph does not natively support this method within a
169      * transaction. If the graph is not currently in a transaction, then the
170      * operation runs efficiently and correctly. If the graph is currently in a
171      * transaction, please use setCheckElementsInTransaction() if it is
172      * necessary to ensure proper transactional semantics. Note that it is
173      * costly to check if an element is in the transaction.
174      *
175      * @return all the vertices in the graph
176      */
177     @Override
178     public Iterable<Vertex> getVertices() {
179         this.autoStartTransaction(false);
180         return new Neo4j2VertexIterable(rawGraph.getAllNodes(), this);
181     }
182 
183     public CloseableIterable<Vertex> getVerticesByLabel(final String label) {
184         this.autoStartTransaction(false);
185         ResourceIterable<Node> wrap = new ResourceIterable<Node>() {
186             @Override
187             public ResourceIterator<Node> iterator() {
188                 return rawGraph.<Node>findNodes(Label.label(label));
189             }
190         };
191         return new Neo4j2VertexIterable(wrap, this);
192     }
193 
194     public CloseableIterable<Vertex> getVerticesByLabelKeyValue(
195             final String label, final String key, final Object value) {
196         ResourceIterable<Node> wrap = new ResourceIterable<Node>() {
197             @Override
198             public ResourceIterator<Node> iterator() {
199                 autoStartTransaction(false);
200                 return rawGraph.<Node>findNodes(Label.label(label), key, value);
201             }
202         };
203         return new Neo4j2VertexIterable(wrap, this);
204     }
205 
206     /**
207      * Get an iterable of vertices via a Cypher query.
208      *
209      * @param query  the cypher query
210      * @param params a map of parameters
211      * @param column the name of the column from which to extract the vertices. The column
212      *               must be a node or a class cast exception will be thrown when the
213      *               iterable is accessed
214      * @return an iterable of vertices
215      */
216     public CloseableIterable<Vertex> getVerticesByQuery(final String query, final Map<String, Object> params, String column) {
217         Preconditions.checkNotNull(query, "Query cannot be null");
218         Preconditions.checkNotNull(column, "Column cannot be null");
219         ResourceIterable<Node> wrap = () -> {
220             autoStartTransaction(false);
221             return rawGraph
222                     .execute(query, params != null ? params : Collections.emptyMap())
223                     .columnAs(column);
224         };
225         return new Neo4j2VertexIterable(wrap, this);
226     }
227 
228     @Override
229     public Iterable<Vertex> getVertices(String key, Object value) {
230         this.autoStartTransaction(false);
231         return new PropertyFilteredIterable<>(key, value, this.getVertices());
232     }
233 
234     /**
235      * {@inheritDoc}
236      * <p>
237      * The underlying Neo4j graph does not natively support this method within a
238      * transaction. If the graph is not currently in a transaction, then the
239      * operation runs efficiently and correctly. If the graph is currently in a
240      * transaction, please use setCheckElementsInTransaction() if it is
241      * necessary to ensure proper transactional semantics. Note that it is
242      * costly to check if an element is in the transaction.
243      *
244      * @return all the edges in the graph
245      */
246     @Override
247     public Iterable<Edge> getEdges() {
248         this.autoStartTransaction(false);
249         return new Neo4j2EdgeIterable(rawGraph.getAllRelationships(), this);
250     }
251 
252     @Override
253     public Iterable<Edge> getEdges(String key, Object value) {
254         this.autoStartTransaction(false);
255         return new PropertyFilteredIterable<>(key, value, this.getEdges());
256     }
257 
258     @Override
259     public void removeVertex(Vertex vertex) {
260         this.autoStartTransaction(true);
261 
262         try {
263             Node node = ((Neo4j2Vertex) vertex).getRawVertex();
264             for (Relationship relationship : node.getRelationships(org.neo4j.graphdb.Direction.BOTH)) {
265                 relationship.delete();
266             }
267             node.delete();
268         } catch (NotFoundException | IllegalStateException nfe) {
269             throw ExceptionFactory.vertexWithIdDoesNotExist(vertex.getId());
270         }
271     }
272 
273     @Override
274     public Neo4j2Edge addEdge(Object id, Vertex outVertex, Vertex inVertex, String label) {
275         if (label == null)
276             throw ExceptionFactory.edgeLabelCanNotBeNull();
277 
278         this.autoStartTransaction(true);
279         return new Neo4j2Edge(((Neo4j2Vertex) outVertex).getRawVertex().createRelationshipTo(((Neo4j2Vertex) inVertex).getRawVertex(),
280                 RelationshipType.withName(label)), this);
281     }
282 
283     @Override
284     public Neo4j2Edge getEdge(Object id) {
285         if (null == id)
286             throw ExceptionFactory.edgeIdCanNotBeNull();
287 
288         this.autoStartTransaction(true);
289         try {
290             Long longId;
291             if (id instanceof Long)
292                 longId = (Long) id;
293             else
294                 longId = Double.valueOf(id.toString()).longValue();
295             return new Neo4j2Edge(this.rawGraph.getRelationshipById(longId), this);
296         } catch (NotFoundException e) {
297             return null;
298         } catch (NumberFormatException e) {
299             return null;
300         }
301     }
302 
303     @Override
304     public void removeEdge(Edge edge) {
305         this.autoStartTransaction(true);
306         ((Relationship) ((Neo4j2Edge) edge).getRawElement()).delete();
307     }
308 
309     @Override
310     public void stopTransaction(Conclusion conclusion) {
311         if (Conclusion.SUCCESS == conclusion)
312             commit();
313         else
314             rollback();
315     }
316 
317     @Override
318     public void commit() {
319         if (null == tx.get()) {
320             return;
321         }
322 
323         try {
324             tx.get().success();
325         } finally {
326             tx.get().close();
327             tx.remove();
328         }
329     }
330 
331     @Override
332     public void rollback() {
333         if (null == tx.get()) {
334             return;
335         }
336 
337         try {
338             tx.get().failure();
339         } finally {
340             tx.get().close();
341             tx.remove();
342         }
343     }
344 
345     @Override
346     public void shutdown() {
347         try {
348             this.commit();
349         } catch (TransactionFailureException e) {
350             logger.warning("Failure on shutdown " + e.getMessage());
351             // TODO: inspect why certain transactions fail
352         }
353         this.rawGraph.shutdown();
354     }
355 
356     // The forWrite flag is true when the autoStartTransaction method is
357     // called before any operation which will modify the graph in any way. It
358     // is not used in this simple implementation but is required in subclasses
359     // which enforce transaction rules. Now that Neo4j reads also require a
360     // transaction to be open it is otherwise impossible to tell the difference
361     // between the beginning of a write operation and the beginning of a read
362     // operation.
363     public void autoStartTransaction(boolean forWrite) {
364         if (tx.get() == null)
365             tx.set(this.rawGraph.beginTx());
366     }
367 
368     public GraphDatabaseService getRawGraph() {
369         return this.rawGraph;
370     }
371 
372     public Features getFeatures() {
373         return FEATURES;
374     }
375 
376     public String toString() {
377         return StringFactory.graphString(this, this.rawGraph.toString());
378     }
379 
380     public GraphQuery query() {
381         return new DefaultGraphQuery(this);
382     }
383 
384     public CloseableIterable<Map<String, Object>> query(String query, Map<String, Object> params) {
385         ResourceIterable<Map<String, Object>> wrap = () -> {
386             autoStartTransaction(false);
387             return rawGraph.execute(query, params == null ? Collections.<String, Object>emptyMap() : params);
388         };
389         return new WrappingCloseableIterable<>(wrap);
390     }
391 }