001    package de.deepamehta.storage.neo4j;
002    
003    import de.deepamehta.core.model.AssociationModel;
004    import de.deepamehta.core.model.AssociationRoleModel;
005    import de.deepamehta.core.model.IndexMode;
006    import de.deepamehta.core.model.RelatedAssociationModel;
007    import de.deepamehta.core.model.RelatedTopicModel;
008    import de.deepamehta.core.model.RoleModel;
009    import de.deepamehta.core.model.SimpleValue;
010    import de.deepamehta.core.model.TopicModel;
011    import de.deepamehta.core.model.TopicRoleModel;
012    import de.deepamehta.core.storage.spi.DeepaMehtaStorage;
013    import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
014    
015    import org.neo4j.graphdb.Direction;
016    import org.neo4j.graphdb.GraphDatabaseService;
017    import org.neo4j.graphdb.Node;
018    import org.neo4j.graphdb.Relationship;
019    import org.neo4j.graphdb.RelationshipType;
020    import org.neo4j.graphdb.factory.GraphDatabaseFactory;
021    import org.neo4j.graphdb.index.Index;
022    import org.neo4j.graphdb.index.IndexHits;
023    import org.neo4j.graphdb.index.IndexManager;
024    import static org.neo4j.helpers.collection.MapUtil.stringMap;
025    import org.neo4j.index.lucene.QueryContext;
026    import org.neo4j.index.lucene.ValueContext;
027    
028    import org.apache.lucene.index.Term;
029    import org.apache.lucene.search.BooleanClause.Occur;
030    import org.apache.lucene.search.BooleanQuery;
031    import org.apache.lucene.search.Query;
032    import org.apache.lucene.search.TermQuery;
033    
034    import java.util.ArrayList;
035    import static java.util.Arrays.asList;
036    import java.util.Iterator;
037    import java.util.List;
038    import java.util.Map;
039    import java.util.logging.Logger;
040    
041    
042    
043    public class Neo4jStorage implements DeepaMehtaStorage {
044    
045        // ------------------------------------------------------------------------------------------------------- Constants
046    
047        // --- DB Property Keys ---
048        private static final String KEY_NODE_TYPE = "node_type";
049        private static final String KEY_VALUE     = "value";
050    
051        // --- Content Index Keys ---
052        private static final String KEY_URI      = "uri";                       // used as property key as well
053        private static final String KEY_TPYE_URI = "type_uri";                  // used as property key as well
054        private static final String KEY_FULLTEXT = "_fulltext_";
055    
056        // --- Association Metadata Index Keys ---
057        private static final String KEY_ASSOC_ID       = "assoc_id";
058        private static final String KEY_ASSOC_TPYE_URI = "assoc_type_uri";
059        // role 1 & 2
060        private static final String KEY_ROLE_TPYE_URI   = "role_type_uri_";     // "1" or "2" is appended programatically
061        private static final String KEY_PLAYER_TPYE     = "player_type_";       // "1" or "2" is appended programatically
062        private static final String KEY_PLAYER_ID       = "player_id_";         // "1" or "2" is appended programatically
063        private static final String KEY_PLAYER_TYPE_URI = "player_type_uri_";   // "1" or "2" is appended programatically
064    
065        // ---------------------------------------------------------------------------------------------- Instance Variables
066    
067                GraphDatabaseService neo4j = null;
068        private RelationtypeCache relTypeCache;
069        
070        private Index<Node> topicContentExact;      // topic URI, topic type URI, topic value (index mode KEY), properties
071        private Index<Node> topicContentFulltext;   // topic value (index modes FULLTEXT or FULLTEXT_KEY)
072        private Index<Node> assocContentExact;      // assoc URI, assoc type URI, assoc value (index mode KEY), properties
073        private Index<Node> assocContentFulltext;   // assoc value (index modes FULLTEXT or FULLTEXT_KEY)
074        private Index<Node> assocMetadata;
075    
076        private final Logger logger = Logger.getLogger(getClass().getName());
077    
078        // ---------------------------------------------------------------------------------------------------- Constructors
079    
080        Neo4jStorage(String databasePath) {
081            try {
082                this.neo4j = new GraphDatabaseFactory().newEmbeddedDatabase(databasePath);
083                this.relTypeCache = new RelationtypeCache(neo4j);
084                // indexes
085                this.topicContentExact    = createExactIndex("topic-content-exact");
086                this.topicContentFulltext = createFulltextIndex("topic-content-fulltext");
087                this.assocContentExact    = createExactIndex("assoc-content-exact");
088                this.assocContentFulltext = createFulltextIndex("assoc-content-fulltext");
089                this.assocMetadata = createExactIndex("assoc-metadata");
090            } catch (Exception e) {
091                if (neo4j != null) {
092                    shutdown();
093                }
094                throw new RuntimeException("Creating the Neo4j instance and indexes failed", e);
095            }
096        }
097    
098        // -------------------------------------------------------------------------------------------------- Public Methods
099    
100    
101    
102        // ****************************************
103        // *** DeepaMehtaStorage Implementation ***
104        // ****************************************
105    
106    
107    
108        // === Topics ===
109    
110        @Override
111        public TopicModel fetchTopic(long topicId) {
112            return buildTopic(fetchTopicNode(topicId));
113        }
114    
115        @Override
116        public TopicModel fetchTopic(String key, Object value) {
117            Node node = topicContentExact.get(key, value).getSingle();
118            return node != null ? buildTopic(node) : null;
119        }
120    
121        @Override
122        public List<TopicModel> fetchTopics(String key, Object value) {
123            return buildTopics(topicContentExact.query(key, value));
124        }
125    
126        @Override
127        public List<TopicModel> queryTopics(Object value) {
128            return queryTopics(null, value);
129        }
130    
131        @Override
132        public List<TopicModel> queryTopics(String key, Object value) {
133            if (key == null) {
134                key = KEY_FULLTEXT;
135            }
136            if (value == null) {
137                throw new IllegalArgumentException("Tried to call queryTopics() with a null value Object (key=\"" + key +
138                    "\")");
139            }
140            //
141            return buildTopics(topicContentFulltext.query(key, value));
142        }
143    
144        @Override
145        public Iterator<TopicModel> fetchAllTopics() {
146            return new TopicModelIterator(this);
147        }
148    
149        // ---
150    
151        @Override
152        public void storeTopic(TopicModel topicModel) {
153            String uri = topicModel.getUri();
154            checkUriUniqueness(uri);
155            //
156            // 1) update DB
157            Node topicNode = neo4j.createNode();
158            topicNode.setProperty(KEY_NODE_TYPE, "topic");
159            //
160            storeAndIndexTopicUri(topicNode, uri);
161            storeAndIndexTopicTypeUri(topicNode, topicModel.getTypeUri());
162            //
163            // 2) update model
164            topicModel.setId(topicNode.getId());
165        }
166    
167        @Override
168        public void storeTopicUri(long topicId, String uri) {
169            storeAndIndexTopicUri(fetchTopicNode(topicId), uri);
170        }
171    
172        // Note: a storage implementation is not responsible for maintaining the "Instantiation" associations.
173        // This is performed at the application layer.
174        @Override
175        public void storeTopicTypeUri(long topicId, String topicTypeUri) {
176            Node topicNode = fetchTopicNode(topicId);
177            //
178            // 1) update DB and content index
179            storeAndIndexTopicTypeUri(topicNode, topicTypeUri);
180            //
181            // 2) update association metadata index
182            reindexTypeUri(topicNode, topicTypeUri);
183        }
184    
185        @Override
186        public void storeTopicValue(long topicId, SimpleValue value, List<IndexMode> indexModes,
187                                                                     String indexKey, SimpleValue indexValue) {
188            storeAndIndexTopicValue(fetchTopicNode(topicId), value.value(), indexModes, indexKey,
189                getIndexValue(value, indexValue));
190        }
191    
192        // ---
193    
194        @Override
195        public void deleteTopic(long topicId) {
196            // 1) update DB
197            Node topicNode = fetchTopicNode(topicId);
198            topicNode.delete();
199            //
200            // 2) update index
201            removeTopicFromIndex(topicNode);
202        }
203    
204    
205    
206        // === Associations ===
207    
208        @Override
209        public AssociationModel fetchAssociation(long assocId) {
210            return buildAssociation(fetchAssociationNode(assocId));
211        }
212    
213        @Override
214        public List<AssociationModel> fetchAssociations(String assocTypeUri, long topicId1, long topicId2,
215                                                                             String roleTypeUri1, String roleTypeUri2) {
216            return queryAssociationIndex(
217                assocTypeUri,
218                roleTypeUri1, NodeType.TOPIC, topicId1, null,
219                roleTypeUri2, NodeType.TOPIC, topicId2, null
220            );
221        }
222    
223        @Override
224        public List<AssociationModel> fetchAssociationsBetweenTopicAndAssociation(String assocTypeUri, long topicId,
225                                                           long assocId, String topicRoleTypeUri, String assocRoleTypeUri) {
226            return queryAssociationIndex(
227                assocTypeUri,
228                topicRoleTypeUri, NodeType.TOPIC, topicId, null,
229                assocRoleTypeUri, NodeType.ASSOC, assocId, null
230            );
231        }
232    
233        @Override
234        public Iterator<AssociationModel> fetchAllAssociations() {
235            return new AssociationModelIterator(this);
236        }
237    
238        // ---
239    
240        @Override
241        public void storeAssociation(AssociationModel assocModel) {
242            String uri = assocModel.getUri();
243            checkUriUniqueness(uri);
244            //
245            // 1) update DB
246            Node assocNode = neo4j.createNode();
247            assocNode.setProperty(KEY_NODE_TYPE, "assoc");
248            //
249            storeAndIndexAssociationUri(assocNode, uri);
250            storeAndIndexAssociationTypeUri(assocNode, assocModel.getTypeUri());
251            //
252            RoleModel role1 = assocModel.getRoleModel1();
253            RoleModel role2 = assocModel.getRoleModel2();
254            Node playerNode1 = storePlayerRelationship(assocNode, role1);
255            Node playerNode2 = storePlayerRelationship(assocNode, role2);
256            //
257            // 2) update index
258            indexAssociation(assocNode, role1.getRoleTypeUri(), playerNode1,
259                                        role2.getRoleTypeUri(), playerNode2);
260            // 3) update model
261            assocModel.setId(assocNode.getId());
262        }
263    
264        @Override
265        public void storeAssociationUri(long assocId, String uri) {
266            storeAndIndexAssociationUri(fetchAssociationNode(assocId), uri);
267        }
268    
269        // Note: a storage implementation is not responsible for maintaining the "Instantiation" associations.
270        // This is performed at the application layer.
271        @Override
272        public void storeAssociationTypeUri(long assocId, String assocTypeUri) {
273            Node assocNode = fetchAssociationNode(assocId);
274            //
275            // 1) update DB and content index
276            storeAndIndexAssociationTypeUri(assocNode, assocTypeUri);
277            //
278            // 2) update association metadata index
279            indexAssociationType(assocNode, assocTypeUri);  // update association entry itself
280            reindexTypeUri(assocNode, assocTypeUri);        // update all association entries the association is a player of
281        }
282    
283        @Override
284        public void storeAssociationValue(long assocId, SimpleValue value, List<IndexMode> indexModes,
285                                                                           String indexKey, SimpleValue indexValue) {
286            storeAndIndexAssociationValue(fetchAssociationNode(assocId), value.value(), indexModes, indexKey,
287                getIndexValue(value, indexValue));
288        }
289    
290        @Override
291        public void storeRoleTypeUri(long assocId, long playerId, String roleTypeUri) {
292            Node assocNode = fetchAssociationNode(assocId);
293            //
294            // 1) update DB
295            fetchRelationship(assocNode, playerId).delete();                                        // delete relationship
296            assocNode.createRelationshipTo(fetchNode(playerId), getRelationshipType(roleTypeUri));  // create new one
297            //
298            // 2) update association metadata index
299            indexAssociationRoleType(assocNode, playerId, roleTypeUri);
300        }
301    
302        // ---
303    
304        @Override
305        public void deleteAssociation(long assocId) {
306            // 1) update DB
307            Node assocNode = fetchAssociationNode(assocId);
308            // delete the 2 player relationships
309            for (Relationship rel : fetchRelationships(assocNode)) {
310                rel.delete();
311            }
312            //
313            assocNode.delete();
314            //
315            // 2) update index
316            removeAssociationFromIndex(assocNode);  
317        }
318    
319    
320    
321        // === Traversal ===
322    
323        @Override
324        public List<AssociationModel> fetchTopicAssociations(long topicId) {
325            return fetchAssociations(fetchTopicNode(topicId));
326        }
327    
328        @Override
329        public List<AssociationModel> fetchAssociationAssociations(long assocId) {
330            return fetchAssociations(fetchAssociationNode(assocId));
331        }
332    
333        // ---
334    
335        @Override
336        public List<RelatedTopicModel> fetchTopicRelatedTopics(long topicId, String assocTypeUri, String myRoleTypeUri,
337                                                               String othersRoleTypeUri, String othersTopicTypeUri) {
338            return buildRelatedTopics(queryAssociationIndex(
339                assocTypeUri,
340                myRoleTypeUri,     NodeType.TOPIC, topicId, null,
341                othersRoleTypeUri, NodeType.TOPIC, -1,      othersTopicTypeUri
342            ), topicId);
343        }
344    
345        @Override
346        public List<RelatedAssociationModel> fetchTopicRelatedAssociations(long topicId, String assocTypeUri,
347                                                String myRoleTypeUri, String othersRoleTypeUri, String othersAssocTypeUri) {
348            return buildRelatedAssociations(queryAssociationIndex(
349                assocTypeUri,
350                myRoleTypeUri,     NodeType.TOPIC, topicId, null,
351                othersRoleTypeUri, NodeType.ASSOC, -1,      othersAssocTypeUri
352            ), topicId);
353        }
354    
355        // ---
356    
357        @Override
358        public List<RelatedTopicModel> fetchAssociationRelatedTopics(long assocId, String assocTypeUri,
359                                                String myRoleTypeUri, String othersRoleTypeUri, String othersTopicTypeUri) {
360            return buildRelatedTopics(queryAssociationIndex(
361                assocTypeUri,
362                myRoleTypeUri,     NodeType.ASSOC, assocId, null,
363                othersRoleTypeUri, NodeType.TOPIC, -1,      othersTopicTypeUri
364            ), assocId);
365        }
366    
367        @Override
368        public List<RelatedAssociationModel> fetchAssociationRelatedAssociations(long assocId, String assocTypeUri,
369                                                String myRoleTypeUri, String othersRoleTypeUri, String othersAssocTypeUri) {
370            return buildRelatedAssociations(queryAssociationIndex(
371                assocTypeUri,
372                myRoleTypeUri,     NodeType.ASSOC, assocId, null,
373                othersRoleTypeUri, NodeType.ASSOC, -1,      othersAssocTypeUri
374            ), assocId);
375        }
376    
377        // ---
378    
379        @Override
380        public List<RelatedTopicModel> fetchRelatedTopics(long id, String assocTypeUri, String myRoleTypeUri,
381                                                          String othersRoleTypeUri, String othersTopicTypeUri) {
382            return buildRelatedTopics(queryAssociationIndex(
383                assocTypeUri,
384                myRoleTypeUri,     null,           id, null,
385                othersRoleTypeUri, NodeType.TOPIC, -1, othersTopicTypeUri
386            ), id);
387        }
388    
389        @Override
390        public List<RelatedAssociationModel> fetchRelatedAssociations(long id, String assocTypeUri, String myRoleTypeUri,
391                                                                      String othersRoleTypeUri, String othersAssocTypeUri) {
392            return buildRelatedAssociations(queryAssociationIndex(
393                assocTypeUri,
394                myRoleTypeUri,     null,           id, null,
395                othersRoleTypeUri, NodeType.ASSOC, -1, othersAssocTypeUri
396            ), id);
397        }
398    
399    
400    
401        // === Properties ===
402    
403        @Override
404        public Object fetchTopicProperty(long topicId, String propUri) {
405            return fetchTopicNode(topicId).getProperty(propUri);
406        }
407    
408        @Override
409        public Object fetchAssociationProperty(long assocId, String propUri) {
410            return fetchAssociationNode(assocId).getProperty(propUri);
411        }
412    
413        // ---
414    
415        @Override
416        public List<TopicModel> fetchTopicsByProperty(String propUri, Object propValue) {
417            return buildTopics(queryIndexByProperty(topicContentExact, propUri, propValue));
418        }
419    
420        @Override
421        public List<TopicModel> fetchTopicsByPropertyRange(String propUri, Number from, Number to) {
422            return buildTopics(queryIndexByPropertyRange(topicContentExact, propUri, from, to));
423        }
424    
425        @Override
426        public List<AssociationModel> fetchAssociationsByProperty(String propUri, Object propValue) {
427            return buildAssociations(queryIndexByProperty(assocContentExact, propUri, propValue));
428        }
429    
430        @Override
431        public List<AssociationModel> fetchAssociationsByPropertyRange(String propUri, Number from, Number to) {
432            return buildAssociations(queryIndexByPropertyRange(assocContentExact, propUri, from, to));
433        }
434    
435        // ---
436    
437        @Override
438        public void storeTopicProperty(long topicId, String propUri, Object propValue, boolean addToIndex) {
439            Index<Node> exactIndex = addToIndex ? topicContentExact : null;
440            storeAndIndexExactValue(fetchTopicNode(topicId), propUri, propValue, exactIndex);
441        }
442    
443        @Override
444        public void storeAssociationProperty(long assocId, String propUri, Object propValue, boolean addToIndex) {
445            Index<Node> exactIndex = addToIndex ? assocContentExact : null;
446            storeAndIndexExactValue(fetchAssociationNode(assocId), propUri, propValue, exactIndex);
447        }
448    
449        // ---
450    
451        @Override
452        public boolean hasTopicProperty(long topicId, String propUri) {
453            return fetchTopicNode(topicId).hasProperty(propUri);
454        }
455    
456        @Override
457        public boolean hasAssociationProperty(long assocId, String propUri) {
458            return fetchAssociationNode(assocId).hasProperty(propUri);
459        }
460    
461        // ---
462    
463        @Override
464        public void deleteTopicProperty(long topicId, String propUri) {
465            Node topicNode = fetchTopicNode(topicId);
466            topicNode.removeProperty(propUri);
467            removeTopicPropertyFromIndex(topicNode, propUri);
468        }
469    
470        @Override
471        public void deleteAssociationProperty(long assocId, String propUri) {
472            Node assocNode = fetchAssociationNode(assocId);
473            assocNode.removeProperty(propUri);
474            removeAssociationPropertyFromIndex(assocNode, propUri);
475        }
476    
477    
478    
479        // === DB ===
480    
481        @Override
482        public DeepaMehtaTransaction beginTx() {
483            return new Neo4jTransactionAdapter(neo4j);
484        }
485    
486        @Override
487        public boolean setupRootNode() {
488            try {
489                Node rootNode = fetchNode(0);
490                //
491                if (rootNode.getProperty(KEY_NODE_TYPE, null) != null) {
492                    return false;
493                }
494                //
495                rootNode.setProperty(KEY_NODE_TYPE, "topic");
496                rootNode.setProperty(KEY_VALUE, "Meta Type");
497                storeAndIndexTopicUri(rootNode, "dm4.core.meta_type");
498                storeAndIndexTopicTypeUri(rootNode, "dm4.core.meta_meta_type");
499                //
500                return true;
501            } catch (Exception e) {
502                throw new RuntimeException("Setting up the root node (0) failed", e);
503            }
504        }
505    
506        @Override
507        public void shutdown() {
508            logger.info("Shutdown Neo4j");
509            neo4j.shutdown();
510        }
511    
512        // ------------------------------------------------------------------------------------------------- Private Methods
513    
514    
515    
516        // === Value Storage ===
517    
518        private void storeAndIndexTopicUri(Node topicNode, String uri) {
519            storeAndIndexExactValue(topicNode, KEY_URI, uri, topicContentExact);
520        }
521    
522        private void storeAndIndexAssociationUri(Node assocNode, String uri) {
523            storeAndIndexExactValue(assocNode, KEY_URI, uri, assocContentExact);
524        }
525    
526        // ---
527    
528        private void storeAndIndexTopicTypeUri(Node topicNode, String topicTypeUri) {
529            storeAndIndexExactValue(topicNode, KEY_TPYE_URI, topicTypeUri, topicContentExact);
530        }
531    
532        private void storeAndIndexAssociationTypeUri(Node assocNode, String assocTypeUri) {
533            storeAndIndexExactValue(assocNode, KEY_TPYE_URI, assocTypeUri, assocContentExact);
534        }
535    
536        // ---
537    
538        /**
539         * Stores a node value under the specified key and adds the value to the specified index (under the same key).
540         * <code>IndexMode.KEY</code> is used for indexing.
541         * <p>
542         * Used for URIs, type URIs, and properties.
543         *
544         * @param   node        a topic node, or an association node.
545         * @param   exactIndex  the index to add the value to. If <code>null</code> no indexing is performed.
546         */
547        private void storeAndIndexExactValue(Node node, String key, Object value, Index<Node> exactIndex) {
548            // store
549            node.setProperty(key, value);
550            // index
551            if (exactIndex != null) {
552                // Note: numbers are indexed numerically to allow range queries.
553                if (value instanceof Number) {
554                    value = ValueContext.numeric((Number) value);
555                }
556                indexNodeValue(node, value, asList(IndexMode.KEY), key, exactIndex, null);      // fulltextIndex=null
557            }
558        }
559    
560        // ---
561    
562        private void storeAndIndexTopicValue(Node topicNode, Object value, List<IndexMode> indexModes,
563                                                                           String indexKey, Object indexValue) {
564            // store
565            topicNode.setProperty(KEY_VALUE, value);
566            // index
567            indexNodeValue(topicNode, indexValue, indexModes, indexKey, topicContentExact, topicContentFulltext);
568        }
569    
570        private void storeAndIndexAssociationValue(Node assocNode, Object value, List<IndexMode> indexModes,
571                                                                                 String indexKey, Object indexValue) {
572            // store
573            assocNode.setProperty(KEY_VALUE, value);
574            // index
575            indexNodeValue(assocNode, indexValue, indexModes, indexKey, assocContentExact, assocContentFulltext);
576        }
577    
578        // ---
579    
580        private Object getIndexValue(SimpleValue value, SimpleValue indexValue) {
581            return indexValue != null ? indexValue.value() : value.value();
582        }
583    
584    
585    
586        // === Indexing ===
587    
588        private void indexNodeValue(Node node, Object value, List<IndexMode> indexModes, String indexKey,
589                                                             Index<Node> exactIndex, Index<Node> fulltextIndex) {
590            for (IndexMode indexMode : indexModes) {
591                if (indexMode == IndexMode.OFF) {
592                    return;
593                } else if (indexMode == IndexMode.KEY) {
594                    exactIndex.remove(node, indexKey);              // remove old
595                    exactIndex.add(node, indexKey, value);          // index new
596                } else if (indexMode == IndexMode.FULLTEXT) {
597                    fulltextIndex.remove(node, KEY_FULLTEXT);       // remove old
598                    fulltextIndex.add(node, KEY_FULLTEXT, value);   // index new
599                } else if (indexMode == IndexMode.FULLTEXT_KEY) {
600                    fulltextIndex.remove(node, indexKey);           // remove old
601                    fulltextIndex.add(node, indexKey, value);       // index new
602                } else {
603                    throw new RuntimeException("Unexpected index mode: \"" + indexMode + "\"");
604                }
605            }
606        }
607    
608        // ---
609    
610        private void indexAssociation(Node assocNode, String roleTypeUri1, Node playerNode1,
611                                                      String roleTypeUri2, Node playerNode2) {
612            indexAssociationId(assocNode);
613            indexAssociationType(assocNode, typeUri(assocNode));
614            //
615            indexAssociationRole(assocNode, 1, roleTypeUri1, playerNode1);
616            indexAssociationRole(assocNode, 2, roleTypeUri2, playerNode2);
617        }
618    
619        private void indexAssociationId(Node assocNode) {
620            assocMetadata.add(assocNode, KEY_ASSOC_ID, assocNode.getId());
621        }
622    
623        private void indexAssociationType(Node assocNode, String assocTypeUri) {
624            reindexValue(assocNode, KEY_ASSOC_TPYE_URI, assocTypeUri);
625        }
626    
627        private void indexAssociationRole(Node assocNode, int pos, String roleTypeUri, Node playerNode) {
628            assocMetadata.add(assocNode, KEY_ROLE_TPYE_URI + pos, roleTypeUri);
629            assocMetadata.add(assocNode, KEY_PLAYER_TPYE + pos, NodeType.of(playerNode).stringify());
630            assocMetadata.add(assocNode, KEY_PLAYER_ID + pos, playerNode.getId());
631            assocMetadata.add(assocNode, KEY_PLAYER_TYPE_URI + pos, typeUri(playerNode));
632        }
633    
634        // ---
635    
636        private void indexAssociationRoleType(Node assocNode, long playerId, String roleTypeUri) {
637            int pos = lookupPlayerPosition(assocNode.getId(), playerId);
638            reindexValue(assocNode, KEY_ROLE_TPYE_URI, pos, roleTypeUri);
639        }
640    
641        private int lookupPlayerPosition(long assocId, long playerId) {
642            boolean pos1 = isPlayerAtPosition(1, assocId, playerId);
643            boolean pos2 = isPlayerAtPosition(2, assocId, playerId);
644            if (pos1 && pos2) {
645                throw new RuntimeException("Ambiguity: both players have ID " + playerId + " in association " + assocId);
646            } else if (pos1) {
647                return 1;
648            } else if (pos2) {
649                return 2;
650            } else {
651                throw new IllegalArgumentException("ID " + playerId + " is not a player in association " + assocId);
652            }
653        }
654    
655        private boolean isPlayerAtPosition(int pos, long assocId, long playerId) {
656            BooleanQuery query = new BooleanQuery();
657            addTermQuery(KEY_ASSOC_ID, assocId, query);
658            addTermQuery(KEY_PLAYER_ID + pos, playerId, query);
659            return assocMetadata.query(query).getSingle() != null;
660        }
661    
662        // ---
663    
664        private void reindexTypeUri(Node playerNode, String typeUri) {
665            reindexTypeUri(1, playerNode, typeUri);
666            reindexTypeUri(2, playerNode, typeUri);
667        }
668    
669        /**
670         * Re-indexes the KEY_PLAYER_TYPE_URI of all associations in which the specified node
671         * is a player at the specified position.
672         *
673         * @param   playerNode  a topic node or an association node.
674         * @param   typeUri     the new type URI to be indexed for the player node.
675         */
676        private void reindexTypeUri(int pos, Node playerNode, String typeUri) {
677            for (Node assocNode : lookupAssociations(pos, playerNode)) {
678                reindexValue(assocNode, KEY_PLAYER_TYPE_URI, pos, typeUri);
679            }
680        }
681    
682        private IndexHits<Node> lookupAssociations(int pos, Node playerNode) {
683            return assocMetadata.get(KEY_PLAYER_ID + pos, playerNode.getId());
684        }
685    
686        // ---
687    
688        private void reindexValue(Node assocNode, String key, int pos, String value) {
689            reindexValue(assocNode, key + pos, value);
690        }
691    
692        private void reindexValue(Node assocNode, String key, String value) {
693            assocMetadata.remove(assocNode, key);
694            assocMetadata.add(assocNode, key, value);
695        }
696    
697        // --- Query indexes ---
698    
699        private IndexHits<Node> queryIndexByProperty(Index<Node> index, String propUri, Object propValue) {
700            // Note: numbers must be queried as numeric value as they are indexed numerically.
701            if (propValue instanceof Number) {
702                propValue = ValueContext.numeric((Number) propValue);
703            }
704            return index.get(propUri, propValue);
705        }
706    
707        private IndexHits<Node> queryIndexByPropertyRange(Index<Node> index, String propUri, Number from, Number to) {
708            return index.query(buildNumericRangeQuery(propUri, from, to));
709        }
710    
711        // ---
712    
713        private List<AssociationModel> queryAssociationIndex(String assocTypeUri,
714                                         String roleTypeUri1, NodeType playerType1, long playerId1, String playerTypeUri1,
715                                         String roleTypeUri2, NodeType playerType2, long playerId2, String playerTypeUri2) {
716            return buildAssociations(assocMetadata.query(buildAssociationQuery(assocTypeUri,
717                roleTypeUri1, playerType1, playerId1, playerTypeUri1,
718                roleTypeUri2, playerType2, playerId2, playerTypeUri2
719            )));
720        }
721    
722        // --- Build index queries ---
723    
724        private QueryContext buildNumericRangeQuery(String propUri, Number from, Number to) {
725            return QueryContext.numericRange(propUri, from, to);
726        }
727    
728        // ---
729    
730        private Query buildAssociationQuery(String assocTypeUri,
731                                         String roleTypeUri1, NodeType playerType1, long playerId1, String playerTypeUri1,
732                                         String roleTypeUri2, NodeType playerType2, long playerId2, String playerTypeUri2) {
733            // query bidirectional
734            BooleanQuery direction1 = new BooleanQuery();
735            addRole(direction1, 1, roleTypeUri1, playerType1, playerId1, playerTypeUri1);
736            addRole(direction1, 2, roleTypeUri2, playerType2, playerId2, playerTypeUri2);
737            BooleanQuery direction2 = new BooleanQuery();
738            addRole(direction2, 1, roleTypeUri2, playerType2, playerId2, playerTypeUri2);
739            addRole(direction2, 2, roleTypeUri1, playerType1, playerId1, playerTypeUri1);
740            //
741            BooleanQuery roleQuery = new BooleanQuery();
742            roleQuery.add(direction1, Occur.SHOULD);
743            roleQuery.add(direction2, Occur.SHOULD);
744            //
745            BooleanQuery query = new BooleanQuery();
746            if (assocTypeUri != null) {
747                addTermQuery(KEY_ASSOC_TPYE_URI, assocTypeUri, query);
748            }
749            query.add(roleQuery, Occur.MUST);
750            //
751            return query;
752        }
753    
754        private void addRole(BooleanQuery query, int pos, String roleTypeUri, NodeType playerType, long playerId,
755                                                                                                   String playerTypeUri) {
756            if (roleTypeUri != null)   addTermQuery(KEY_ROLE_TPYE_URI + pos,   roleTypeUri,   query);
757            if (playerType != null)    addTermQuery(KEY_PLAYER_TPYE + pos,     playerType,    query);
758            if (playerId != -1)        addTermQuery(KEY_PLAYER_ID + pos,       playerId,      query);
759            if (playerTypeUri != null) addTermQuery(KEY_PLAYER_TYPE_URI + pos, playerTypeUri, query);
760        }
761    
762        // ---
763    
764        private void addTermQuery(String key, long value, BooleanQuery query) {
765            addTermQuery(key, Long.toString(value), query);
766        }
767    
768        private void addTermQuery(String key, NodeType nodeType, BooleanQuery query) {
769            addTermQuery(key, nodeType.stringify(), query);
770        }
771    
772        private void addTermQuery(String key, String value, BooleanQuery query) {
773            query.add(new TermQuery(new Term(key, value)), Occur.MUST);
774        }
775    
776        // --- Remove index entries ---
777    
778        private void removeTopicFromIndex(Node topicNode) {
779            topicContentExact.remove(topicNode);
780            topicContentFulltext.remove(topicNode);
781        }
782    
783        private void removeAssociationFromIndex(Node assocNode) {
784            assocContentExact.remove(assocNode);
785            assocContentFulltext.remove(assocNode);
786            //
787            assocMetadata.remove(assocNode);
788        }
789    
790        // ---
791    
792        private void removeTopicPropertyFromIndex(Node topicNode, String propUri) {
793            topicContentExact.remove(topicNode, propUri);
794        }
795    
796        private void removeAssociationPropertyFromIndex(Node assocNode, String propUri) {
797            assocContentExact.remove(assocNode, propUri);
798        }
799    
800        // --- Create indexes ---
801    
802        private Index<Node> createExactIndex(String name) {
803            return neo4j.index().forNodes(name);
804        }
805    
806        private Index<Node> createFulltextIndex(String name) {
807            if (neo4j.index().existsForNodes(name)) {
808                return neo4j.index().forNodes(name);
809            } else {
810                Map<String, String> configuration = stringMap(IndexManager.PROVIDER, "lucene", "type", "fulltext");
811                return neo4j.index().forNodes(name, configuration);
812            }
813        }
814    
815    
816    
817        // === Helper ===
818    
819        // --- Neo4j -> DeepaMehta Bridge ---
820    
821        TopicModel buildTopic(Node topicNode) {
822            return new TopicModel(
823                topicNode.getId(),
824                uri(topicNode),
825                typeUri(topicNode),
826                simpleValue(topicNode),
827                null    // composite=null
828            );
829        }
830    
831        private List<TopicModel> buildTopics(Iterable<Node> topicNodes) {
832            List<TopicModel> topics = new ArrayList();
833            for (Node topicNode : topicNodes) {
834                topics.add(buildTopic(topicNode));
835            }
836            return topics;
837        }
838    
839        // ---
840    
841        AssociationModel buildAssociation(Node assocNode) {
842            List<RoleModel> roleModels = buildRoleModels(assocNode);
843            return new AssociationModel(
844                assocNode.getId(),
845                uri(assocNode),
846                typeUri(assocNode),
847                roleModels.get(0), roleModels.get(1),
848                simpleValue(assocNode),
849                null    // composite=null
850            );
851        }
852    
853        private List<AssociationModel> buildAssociations(Iterable<Node> assocNodes) {
854            List<AssociationModel> assocs = new ArrayList();
855            for (Node assocNode : assocNodes) {
856                assocs.add(buildAssociation(assocNode));
857            }
858            return assocs;
859        }
860    
861        private List<RoleModel> buildRoleModels(Node assocNode) {
862            List<RoleModel> roleModels = new ArrayList();
863            for (Relationship rel : fetchRelationships(assocNode)) {
864                Node node = rel.getEndNode();
865                String roleTypeUri = rel.getType().name();
866                RoleModel roleModel = NodeType.of(node).createRoleModel(node, roleTypeUri);
867                roleModels.add(roleModel);
868            }
869            return roleModels;
870        }
871    
872    
873    
874        // --- DeepaMehta -> Neo4j Bridge ---
875    
876        private Node storePlayerRelationship(Node assocNode, RoleModel roleModel) {
877            Node playerNode = fetchPlayerNode(roleModel);
878            assocNode.createRelationshipTo(
879                playerNode,
880                getRelationshipType(roleModel.getRoleTypeUri())
881            );
882            return playerNode;
883        }
884    
885        private Node fetchPlayerNode(RoleModel roleModel) {
886            if (roleModel instanceof TopicRoleModel) {
887                return fetchTopicPlayerNode((TopicRoleModel) roleModel);
888            } else if (roleModel instanceof AssociationRoleModel) {
889                return fetchAssociationNode(roleModel.getPlayerId());
890            } else {
891                throw new RuntimeException("Unexpected role model: " + roleModel);
892            }
893        }
894    
895        private Node fetchTopicPlayerNode(TopicRoleModel roleModel) {
896            if (roleModel.topicIdentifiedByUri()) {
897                return fetchTopicNodeByUri(roleModel.getTopicUri());
898            } else {
899                return fetchTopicNode(roleModel.getPlayerId());
900            }
901        }
902    
903    
904    
905        // --- Neo4j Helper ---
906    
907        private Relationship fetchRelationship(Node assocNode, long playerId) {
908            List<Relationship> rels = fetchRelationships(assocNode);
909            boolean match1 = rels.get(0).getEndNode().getId() == playerId;
910            boolean match2 = rels.get(1).getEndNode().getId() == playerId;
911            if (match1 && match2) {
912                throw new RuntimeException("Ambiguity: both players have ID " + playerId + " in association " +
913                    assocNode.getId());
914            } else if (match1) {
915                return rels.get(0);
916            } else if (match2) {
917                return rels.get(1);
918            } else {
919                throw new IllegalArgumentException("ID " + playerId + " is not a player in association " +
920                    assocNode.getId());
921            }
922        }
923    
924        private List<Relationship> fetchRelationships(Node assocNode) {
925            List<Relationship> rels = new ArrayList();
926            for (Relationship rel : assocNode.getRelationships(Direction.OUTGOING)) {
927                rels.add(rel);
928            }
929            // sanity check
930            if (rels.size() != 2) {
931                throw new RuntimeException("Association " + assocNode.getId() + " connects " + rels.size() +
932                    " player instead of 2");
933            }
934            //
935            return rels;
936        }
937    
938        // ---
939    
940        private List<AssociationModel> fetchAssociations(Node node) {
941            List<AssociationModel> assocs = new ArrayList();
942            for (Relationship rel : node.getRelationships(Direction.INCOMING)) {
943                Node assocNode = rel.getStartNode();
944                assocs.add(buildAssociation(assocNode));
945            }
946            return assocs;
947        }
948    
949        // ---
950    
951        private Node fetchTopicNode(long topicId) {
952            return checkType(
953                fetchNode(topicId), NodeType.TOPIC
954            );
955        }
956    
957        private Node fetchAssociationNode(long assocId) {
958            return checkType(
959                fetchNode(assocId), NodeType.ASSOC
960            );
961        }
962    
963        // ---
964    
965        private Node fetchNode(long id) {
966            return neo4j.getNodeById(id);
967        }
968    
969        private Node fetchTopicNodeByUri(String uri) {
970            Node node = topicContentExact.get(KEY_URI, uri).getSingle();
971            //
972            if (node == null) {
973                throw new RuntimeException("Topic with URI \"" + uri + "\" not found in DB");
974            }
975            //
976            return checkType(node, NodeType.TOPIC);
977        }
978    
979        private Node checkType(Node node, NodeType type) {
980            if (NodeType.of(node) != type) {
981                throw new IllegalArgumentException(type.error(node));
982            }
983            return node;
984        }
985    
986        // ---
987    
988        private RelationshipType getRelationshipType(String typeName) {
989            return relTypeCache.get(typeName);
990        }
991    
992        // ---
993    
994        private String uri(Node node) {
995            return (String) node.getProperty(KEY_URI);
996        }
997    
998        private String typeUri(Node node) {
999            return (String) node.getProperty(KEY_TPYE_URI);
1000        }
1001    
1002        private SimpleValue simpleValue(Node node) {
1003            return new SimpleValue(node.getProperty(KEY_VALUE));
1004        }
1005    
1006    
1007    
1008        // --- DeepaMehta Helper ---
1009    
1010        // ### TODO: this is a DB agnostic helper method. It could be moved e.g. to a common base class.
1011        private List<RelatedTopicModel> buildRelatedTopics(List<AssociationModel> assocs, long playerId) {
1012            List<RelatedTopicModel> relTopics = new ArrayList();
1013            for (AssociationModel assoc : assocs) {
1014                relTopics.add(new RelatedTopicModel(
1015                    fetchTopic(
1016                        assoc.getOtherPlayerId(playerId)
1017                    ), assoc)
1018                );
1019            }
1020            return relTopics;
1021        }
1022    
1023        // ### TODO: this is a DB agnostic helper method. It could be moved e.g. to a common base class.
1024        private List<RelatedAssociationModel> buildRelatedAssociations(List<AssociationModel> assocs, long playerId) {
1025            List<RelatedAssociationModel> relAssocs = new ArrayList();
1026            for (AssociationModel assoc : assocs) {
1027                relAssocs.add(new RelatedAssociationModel(
1028                    fetchAssociation(
1029                        assoc.getOtherPlayerId(playerId)
1030                    ), assoc)
1031                );
1032            }
1033            return relAssocs;
1034        }
1035    
1036        // ---
1037    
1038        /**
1039         * Checks if a topic with the given URI exists in the database, and if so, throws an exception.
1040         * If an empty string is given no check is performed. ### FIXDOC
1041         *
1042         * @param   uri     The URI to check. Must not be null.
1043         */
1044        private void checkUriUniqueness(String uri) {
1045            if (uri.equals("")) {
1046                return;
1047            }
1048            Node n1 = topicContentExact.get(KEY_URI, uri).getSingle();
1049            Node n2 = assocContentExact.get(KEY_URI, uri).getSingle();
1050            if (n1 != null || n2 != null) {
1051                throw new RuntimeException("URI \"" + uri + "\" is not unique");
1052            }
1053        }
1054    }