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