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        // ---
249    
250        @Override
251        public void storeAssociation(AssociationModel assocModel) {
252            setDefaults(assocModel);
253            String uri = assocModel.getUri();
254            checkUriUniqueness(uri);
255            //
256            // 1) update DB
257            Node assocNode = neo4j.createNode();
258            assocNode.setProperty(KEY_NODE_TYPE, "assoc");
259            //
260            storeAndIndexAssociationUri(assocNode, uri);
261            storeAndIndexAssociationTypeUri(assocNode, assocModel.getTypeUri());
262            //
263            RoleModel role1 = assocModel.getRoleModel1();
264            RoleModel role2 = assocModel.getRoleModel2();
265            Node playerNode1 = storePlayerRelationship(assocNode, role1);
266            Node playerNode2 = storePlayerRelationship(assocNode, role2);
267            //
268            // 2) update index
269            indexAssociation(assocNode, role1.getRoleTypeUri(), playerNode1,
270                                        role2.getRoleTypeUri(), playerNode2);
271            // 3) update model
272            assocModel.setId(assocNode.getId());
273        }
274    
275        @Override
276        public void storeAssociationUri(long assocId, String uri) {
277            storeAndIndexAssociationUri(fetchAssociationNode(assocId), uri);
278        }
279    
280        // Note: a storage implementation is not responsible for maintaining the "Instantiation" associations.
281        // This is performed at the application layer.
282        @Override
283        public void storeAssociationTypeUri(long assocId, String assocTypeUri) {
284            Node assocNode = fetchAssociationNode(assocId);
285            //
286            // 1) update DB and content index
287            storeAndIndexAssociationTypeUri(assocNode, assocTypeUri);
288            //
289            // 2) update association metadata index
290            indexAssociationType(assocNode, assocTypeUri);  // update association entry itself
291            reindexTypeUri(assocNode, assocTypeUri);        // update all association entries the association is a player of
292        }
293    
294        @Override
295        public void storeAssociationValue(long assocId, SimpleValue value, List<IndexMode> indexModes,
296                                                                           String indexKey, SimpleValue indexValue) {
297            Node assocNode = fetchAssociationNode(assocId);
298            // store
299            assocNode.setProperty(KEY_VALUE, value.value());
300            // index
301            indexAssociationNodeValue(assocNode, indexModes, indexKey, getIndexValue(value, indexValue));
302        }
303    
304        @Override
305        public void indexAssociationValue(long assocId, IndexMode indexMode, String indexKey, SimpleValue indexValue) {
306            indexAssociationNodeValue(fetchAssociationNode(assocId), asList(indexMode), indexKey, indexValue.value());
307        }
308    
309        @Override
310        public void storeRoleTypeUri(long assocId, long playerId, String roleTypeUri) {
311            Node assocNode = fetchAssociationNode(assocId);
312            //
313            // 1) update DB
314            fetchRelationship(assocNode, playerId).delete();                                        // delete relationship
315            assocNode.createRelationshipTo(fetchNode(playerId), getRelationshipType(roleTypeUri));  // create new one
316            //
317            // 2) update association metadata index
318            indexAssociationRoleType(assocNode, playerId, roleTypeUri);
319        }
320    
321        // ---
322    
323        @Override
324        public void deleteAssociation(long assocId) {
325            // 1) update DB
326            Node assocNode = fetchAssociationNode(assocId);
327            // delete the 2 player relationships
328            for (Relationship rel : fetchRelationships(assocNode)) {
329                rel.delete();
330            }
331            //
332            assocNode.delete();
333            //
334            // 2) update index
335            removeAssociationFromIndex(assocNode);  
336        }
337    
338    
339    
340        // === Traversal ===
341    
342        @Override
343        public List<AssociationModel> fetchTopicAssociations(long topicId) {
344            return fetchAssociations(fetchTopicNode(topicId));
345        }
346    
347        @Override
348        public List<AssociationModel> fetchAssociationAssociations(long assocId) {
349            return fetchAssociations(fetchAssociationNode(assocId));
350        }
351    
352        // ---
353    
354        @Override
355        public List<RelatedTopicModel> fetchTopicRelatedTopics(long topicId, String assocTypeUri, String myRoleTypeUri,
356                                                               String othersRoleTypeUri, String othersTopicTypeUri) {
357            return buildRelatedTopics(queryAssociationIndex(
358                assocTypeUri,
359                myRoleTypeUri,     NodeType.TOPIC, topicId, null,
360                othersRoleTypeUri, NodeType.TOPIC, -1,      othersTopicTypeUri
361            ), topicId);
362        }
363    
364        @Override
365        public List<RelatedAssociationModel> fetchTopicRelatedAssociations(long topicId, String assocTypeUri,
366                                                String myRoleTypeUri, String othersRoleTypeUri, String othersAssocTypeUri) {
367            return buildRelatedAssociations(queryAssociationIndex(
368                assocTypeUri,
369                myRoleTypeUri,     NodeType.TOPIC, topicId, null,
370                othersRoleTypeUri, NodeType.ASSOC, -1,      othersAssocTypeUri
371            ), topicId);
372        }
373    
374        // ---
375    
376        @Override
377        public List<RelatedTopicModel> fetchAssociationRelatedTopics(long assocId, String assocTypeUri,
378                                                String myRoleTypeUri, String othersRoleTypeUri, String othersTopicTypeUri) {
379            return buildRelatedTopics(queryAssociationIndex(
380                assocTypeUri,
381                myRoleTypeUri,     NodeType.ASSOC, assocId, null,
382                othersRoleTypeUri, NodeType.TOPIC, -1,      othersTopicTypeUri
383            ), assocId);
384        }
385    
386        @Override
387        public List<RelatedAssociationModel> fetchAssociationRelatedAssociations(long assocId, String assocTypeUri,
388                                                String myRoleTypeUri, String othersRoleTypeUri, String othersAssocTypeUri) {
389            return buildRelatedAssociations(queryAssociationIndex(
390                assocTypeUri,
391                myRoleTypeUri,     NodeType.ASSOC, assocId, null,
392                othersRoleTypeUri, NodeType.ASSOC, -1,      othersAssocTypeUri
393            ), assocId);
394        }
395    
396        // ---
397    
398        @Override
399        public List<RelatedTopicModel> fetchRelatedTopics(long id, String assocTypeUri, String myRoleTypeUri,
400                                                          String othersRoleTypeUri, String othersTopicTypeUri) {
401            return buildRelatedTopics(queryAssociationIndex(
402                assocTypeUri,
403                myRoleTypeUri,     null,           id, null,
404                othersRoleTypeUri, NodeType.TOPIC, -1, othersTopicTypeUri
405            ), id);
406        }
407    
408        @Override
409        public List<RelatedAssociationModel> fetchRelatedAssociations(long id, String assocTypeUri, String myRoleTypeUri,
410                                                                      String othersRoleTypeUri, String othersAssocTypeUri) {
411            return buildRelatedAssociations(queryAssociationIndex(
412                assocTypeUri,
413                myRoleTypeUri,     null,           id, null,
414                othersRoleTypeUri, NodeType.ASSOC, -1, othersAssocTypeUri
415            ), id);
416        }
417    
418    
419    
420        // === Properties ===
421    
422        @Override
423        public Object fetchTopicProperty(long topicId, String propUri) {
424            return fetchTopicNode(topicId).getProperty(propUri);
425        }
426    
427        @Override
428        public Object fetchAssociationProperty(long assocId, String propUri) {
429            return fetchAssociationNode(assocId).getProperty(propUri);
430        }
431    
432        // ---
433    
434        @Override
435        public List<TopicModel> fetchTopicsByProperty(String propUri, Object propValue) {
436            return buildTopics(queryIndexByProperty(topicContentExact, propUri, propValue));
437        }
438    
439        @Override
440        public List<TopicModel> fetchTopicsByPropertyRange(String propUri, Number from, Number to) {
441            return buildTopics(queryIndexByPropertyRange(topicContentExact, propUri, from, to));
442        }
443    
444        @Override
445        public List<AssociationModel> fetchAssociationsByProperty(String propUri, Object propValue) {
446            return buildAssociations(queryIndexByProperty(assocContentExact, propUri, propValue));
447        }
448    
449        @Override
450        public List<AssociationModel> fetchAssociationsByPropertyRange(String propUri, Number from, Number to) {
451            return buildAssociations(queryIndexByPropertyRange(assocContentExact, propUri, from, to));
452        }
453    
454        // ---
455    
456        @Override
457        public void storeTopicProperty(long topicId, String propUri, Object propValue, boolean addToIndex) {
458            Index<Node> exactIndex = addToIndex ? topicContentExact : null;
459            storeAndIndexExactValue(fetchTopicNode(topicId), propUri, propValue, exactIndex);
460        }
461    
462        @Override
463        public void storeAssociationProperty(long assocId, String propUri, Object propValue, boolean addToIndex) {
464            Index<Node> exactIndex = addToIndex ? assocContentExact : null;
465            storeAndIndexExactValue(fetchAssociationNode(assocId), propUri, propValue, exactIndex);
466        }
467    
468        // ---
469    
470        @Override
471        public boolean hasTopicProperty(long topicId, String propUri) {
472            return fetchTopicNode(topicId).hasProperty(propUri);
473        }
474    
475        @Override
476        public boolean hasAssociationProperty(long assocId, String propUri) {
477            return fetchAssociationNode(assocId).hasProperty(propUri);
478        }
479    
480        // ---
481    
482        @Override
483        public void deleteTopicProperty(long topicId, String propUri) {
484            Node topicNode = fetchTopicNode(topicId);
485            topicNode.removeProperty(propUri);
486            removeTopicPropertyFromIndex(topicNode, propUri);
487        }
488    
489        @Override
490        public void deleteAssociationProperty(long assocId, String propUri) {
491            Node assocNode = fetchAssociationNode(assocId);
492            assocNode.removeProperty(propUri);
493            removeAssociationPropertyFromIndex(assocNode, propUri);
494        }
495    
496    
497    
498        // === DB ===
499    
500        @Override
501        public DeepaMehtaTransaction beginTx() {
502            return new Neo4jTransactionAdapter(neo4j);
503        }
504    
505        @Override
506        public boolean setupRootNode() {
507            try {
508                Node rootNode = fetchNode(0);
509                //
510                if (rootNode.getProperty(KEY_NODE_TYPE, null) != null) {
511                    return false;
512                }
513                //
514                rootNode.setProperty(KEY_NODE_TYPE, "topic");
515                rootNode.setProperty(KEY_VALUE, "Meta Type");
516                storeAndIndexTopicUri(rootNode, "dm4.core.meta_type");
517                storeAndIndexTopicTypeUri(rootNode, "dm4.core.meta_meta_type");
518                //
519                return true;
520            } catch (Exception e) {
521                throw new RuntimeException("Setting up the root node (0) failed", e);
522            }
523        }
524    
525        @Override
526        public void shutdown() {
527            logger.info("Shutdown Neo4j");
528            neo4j.shutdown();
529        }
530    
531        // ---
532    
533        @Override
534        public Object getDatabaseVendorObject() {
535            return neo4j;
536        }
537    
538        @Override
539        public Object getDatabaseVendorObject(long objectId) {
540            return fetchNode(objectId);
541        }
542    
543        // ------------------------------------------------------------------------------------------------- Private Methods
544    
545    
546    
547        // === Value Storage ===
548    
549        private void storeAndIndexTopicUri(Node topicNode, String uri) {
550            storeAndIndexExactValue(topicNode, KEY_URI, uri, topicContentExact);
551        }
552    
553        private void storeAndIndexAssociationUri(Node assocNode, String uri) {
554            storeAndIndexExactValue(assocNode, KEY_URI, uri, assocContentExact);
555        }
556    
557        // ---
558    
559        private void storeAndIndexTopicTypeUri(Node topicNode, String topicTypeUri) {
560            storeAndIndexExactValue(topicNode, KEY_TPYE_URI, topicTypeUri, topicContentExact);
561        }
562    
563        private void storeAndIndexAssociationTypeUri(Node assocNode, String assocTypeUri) {
564            storeAndIndexExactValue(assocNode, KEY_TPYE_URI, assocTypeUri, assocContentExact);
565        }
566    
567        // ---
568    
569        /**
570         * Stores a node value under the specified key and adds the value to the specified index (under the same key).
571         * <code>IndexMode.KEY</code> is used for indexing.
572         * <p>
573         * Used for URIs, type URIs, and properties.
574         *
575         * @param   node        a topic node, or an association node.
576         * @param   exactIndex  the index to add the value to. If <code>null</code> no indexing is performed.
577         */
578        private void storeAndIndexExactValue(Node node, String key, Object value, Index<Node> exactIndex) {
579            // store
580            node.setProperty(key, value);
581            // index
582            if (exactIndex != null) {
583                // Note: numbers are indexed numerically to allow range queries.
584                if (value instanceof Number) {
585                    value = ValueContext.numeric((Number) value);
586                }
587                indexNodeValue(node, value, asList(IndexMode.KEY), key, exactIndex, null);      // fulltextIndex=null
588            }
589        }
590    
591        // ---
592    
593        private void indexTopicNodeValue(Node topicNode, List<IndexMode> indexModes, String indexKey, Object indexValue) {
594            indexNodeValue(topicNode, indexValue, indexModes, indexKey, topicContentExact, topicContentFulltext);
595        }
596    
597        private void indexAssociationNodeValue(Node assocNode, List<IndexMode> indexModes, String indexKey,
598                                                                                           Object indexValue) {
599            indexNodeValue(assocNode, indexValue, indexModes, indexKey, assocContentExact, assocContentFulltext);
600        }
601    
602        // ---
603    
604        private Object getIndexValue(SimpleValue value, SimpleValue indexValue) {
605            return indexValue != null ? indexValue.value() : value.value();
606        }
607    
608    
609    
610        // === Indexing ===
611    
612        private void indexNodeValue(Node node, Object value, List<IndexMode> indexModes, String indexKey,
613                                                             Index<Node> exactIndex, Index<Node> fulltextIndex) {
614            for (IndexMode indexMode : indexModes) {
615                if (indexMode == IndexMode.OFF) {
616                    return;
617                } else if (indexMode == IndexMode.KEY) {
618                    exactIndex.remove(node, indexKey);              // remove old
619                    exactIndex.add(node, indexKey, value);          // index new
620                } else if (indexMode == IndexMode.FULLTEXT) {
621                    fulltextIndex.remove(node, KEY_FULLTEXT);       // remove old
622                    fulltextIndex.add(node, KEY_FULLTEXT, value);   // index new
623                } else if (indexMode == IndexMode.FULLTEXT_KEY) {
624                    fulltextIndex.remove(node, indexKey);           // remove old
625                    fulltextIndex.add(node, indexKey, value);       // index new
626                } else {
627                    throw new RuntimeException("Unexpected index mode: \"" + indexMode + "\"");
628                }
629            }
630        }
631    
632        // ---
633    
634        private void indexAssociation(Node assocNode, String roleTypeUri1, Node playerNode1,
635                                                      String roleTypeUri2, Node playerNode2) {
636            indexAssociationId(assocNode);
637            indexAssociationType(assocNode, typeUri(assocNode));
638            //
639            indexAssociationRole(assocNode, 1, roleTypeUri1, playerNode1);
640            indexAssociationRole(assocNode, 2, roleTypeUri2, playerNode2);
641        }
642    
643        private void indexAssociationId(Node assocNode) {
644            assocMetadata.add(assocNode, KEY_ASSOC_ID, assocNode.getId());
645        }
646    
647        private void indexAssociationType(Node assocNode, String assocTypeUri) {
648            reindexValue(assocNode, KEY_ASSOC_TPYE_URI, assocTypeUri);
649        }
650    
651        private void indexAssociationRole(Node assocNode, int pos, String roleTypeUri, Node playerNode) {
652            assocMetadata.add(assocNode, KEY_ROLE_TPYE_URI + pos, roleTypeUri);
653            assocMetadata.add(assocNode, KEY_PLAYER_TPYE + pos, NodeType.of(playerNode).stringify());
654            assocMetadata.add(assocNode, KEY_PLAYER_ID + pos, playerNode.getId());
655            assocMetadata.add(assocNode, KEY_PLAYER_TYPE_URI + pos, typeUri(playerNode));
656        }
657    
658        // ---
659    
660        private void indexAssociationRoleType(Node assocNode, long playerId, String roleTypeUri) {
661            int pos = lookupPlayerPosition(assocNode.getId(), playerId);
662            reindexValue(assocNode, KEY_ROLE_TPYE_URI, pos, roleTypeUri);
663        }
664    
665        private int lookupPlayerPosition(long assocId, long playerId) {
666            boolean pos1 = isPlayerAtPosition(1, assocId, playerId);
667            boolean pos2 = isPlayerAtPosition(2, assocId, playerId);
668            if (pos1 && pos2) {
669                throw new RuntimeException("Ambiguity: both players have ID " + playerId + " in association " + assocId);
670            } else if (pos1) {
671                return 1;
672            } else if (pos2) {
673                return 2;
674            } else {
675                throw new IllegalArgumentException("ID " + playerId + " is not a player in association " + assocId);
676            }
677        }
678    
679        private boolean isPlayerAtPosition(int pos, long assocId, long playerId) {
680            BooleanQuery query = new BooleanQuery();
681            addTermQuery(KEY_ASSOC_ID, assocId, query);
682            addTermQuery(KEY_PLAYER_ID + pos, playerId, query);
683            return assocMetadata.query(query).getSingle() != null;
684        }
685    
686        // ---
687    
688        private void reindexTypeUri(Node playerNode, String typeUri) {
689            reindexTypeUri(1, playerNode, typeUri);
690            reindexTypeUri(2, playerNode, typeUri);
691        }
692    
693        /**
694         * Re-indexes the KEY_PLAYER_TYPE_URI of all associations in which the specified node
695         * is a player at the specified position.
696         *
697         * @param   playerNode  a topic node or an association node.
698         * @param   typeUri     the new type URI to be indexed for the player node.
699         */
700        private void reindexTypeUri(int pos, Node playerNode, String typeUri) {
701            for (Node assocNode : lookupAssociations(pos, playerNode)) {
702                reindexValue(assocNode, KEY_PLAYER_TYPE_URI, pos, typeUri);
703            }
704        }
705    
706        private IndexHits<Node> lookupAssociations(int pos, Node playerNode) {
707            return assocMetadata.get(KEY_PLAYER_ID + pos, playerNode.getId());
708        }
709    
710        // ---
711    
712        private void reindexValue(Node assocNode, String key, int pos, String value) {
713            reindexValue(assocNode, key + pos, value);
714        }
715    
716        private void reindexValue(Node assocNode, String key, String value) {
717            assocMetadata.remove(assocNode, key);
718            assocMetadata.add(assocNode, key, value);
719        }
720    
721        // --- Query indexes ---
722    
723        private IndexHits<Node> queryIndexByProperty(Index<Node> index, String propUri, Object propValue) {
724            // Note: numbers must be queried as numeric value as they are indexed numerically.
725            if (propValue instanceof Number) {
726                propValue = ValueContext.numeric((Number) propValue);
727            }
728            return index.get(propUri, propValue);
729        }
730    
731        private IndexHits<Node> queryIndexByPropertyRange(Index<Node> index, String propUri, Number from, Number to) {
732            return index.query(buildNumericRangeQuery(propUri, from, to));
733        }
734    
735        // ---
736    
737        private List<AssociationModel> queryAssociationIndex(String assocTypeUri,
738                                         String roleTypeUri1, NodeType playerType1, long playerId1, String playerTypeUri1,
739                                         String roleTypeUri2, NodeType playerType2, long playerId2, String playerTypeUri2) {
740            return buildAssociations(assocMetadata.query(buildAssociationQuery(assocTypeUri,
741                roleTypeUri1, playerType1, playerId1, playerTypeUri1,
742                roleTypeUri2, playerType2, playerId2, playerTypeUri2
743            )));
744        }
745    
746        // --- Build index queries ---
747    
748        private QueryContext buildNumericRangeQuery(String propUri, Number from, Number to) {
749            return QueryContext.numericRange(propUri, from, to);
750        }
751    
752        // ---
753    
754        private Query buildAssociationQuery(String assocTypeUri,
755                                         String roleTypeUri1, NodeType playerType1, long playerId1, String playerTypeUri1,
756                                         String roleTypeUri2, NodeType playerType2, long playerId2, String playerTypeUri2) {
757            // query bidirectional
758            BooleanQuery direction1 = new BooleanQuery();
759            addRole(direction1, 1, roleTypeUri1, playerType1, playerId1, playerTypeUri1);
760            addRole(direction1, 2, roleTypeUri2, playerType2, playerId2, playerTypeUri2);
761            BooleanQuery direction2 = new BooleanQuery();
762            addRole(direction2, 1, roleTypeUri2, playerType2, playerId2, playerTypeUri2);
763            addRole(direction2, 2, roleTypeUri1, playerType1, playerId1, playerTypeUri1);
764            //
765            BooleanQuery roleQuery = new BooleanQuery();
766            roleQuery.add(direction1, Occur.SHOULD);
767            roleQuery.add(direction2, Occur.SHOULD);
768            //
769            BooleanQuery query = new BooleanQuery();
770            if (assocTypeUri != null) {
771                addTermQuery(KEY_ASSOC_TPYE_URI, assocTypeUri, query);
772            }
773            query.add(roleQuery, Occur.MUST);
774            //
775            return query;
776        }
777    
778        private void addRole(BooleanQuery query, int pos, String roleTypeUri, NodeType playerType, long playerId,
779                                                                                                   String playerTypeUri) {
780            if (roleTypeUri != null)   addTermQuery(KEY_ROLE_TPYE_URI + pos,   roleTypeUri,   query);
781            if (playerType != null)    addTermQuery(KEY_PLAYER_TPYE + pos,     playerType,    query);
782            if (playerId != -1)        addTermQuery(KEY_PLAYER_ID + pos,       playerId,      query);
783            if (playerTypeUri != null) addTermQuery(KEY_PLAYER_TYPE_URI + pos, playerTypeUri, query);
784        }
785    
786        // ---
787    
788        private void addTermQuery(String key, long value, BooleanQuery query) {
789            addTermQuery(key, Long.toString(value), query);
790        }
791    
792        private void addTermQuery(String key, NodeType nodeType, BooleanQuery query) {
793            addTermQuery(key, nodeType.stringify(), query);
794        }
795    
796        private void addTermQuery(String key, String value, BooleanQuery query) {
797            query.add(new TermQuery(new Term(key, value)), Occur.MUST);
798        }
799    
800        // --- Remove index entries ---
801    
802        private void removeTopicFromIndex(Node topicNode) {
803            topicContentExact.remove(topicNode);
804            topicContentFulltext.remove(topicNode);
805        }
806    
807        private void removeAssociationFromIndex(Node assocNode) {
808            assocContentExact.remove(assocNode);
809            assocContentFulltext.remove(assocNode);
810            //
811            assocMetadata.remove(assocNode);
812        }
813    
814        // ---
815    
816        private void removeTopicPropertyFromIndex(Node topicNode, String propUri) {
817            topicContentExact.remove(topicNode, propUri);
818        }
819    
820        private void removeAssociationPropertyFromIndex(Node assocNode, String propUri) {
821            assocContentExact.remove(assocNode, propUri);
822        }
823    
824        // --- Create indexes ---
825    
826        private Index<Node> createExactIndex(String name) {
827            return neo4j.index().forNodes(name);
828        }
829    
830        private Index<Node> createFulltextIndex(String name) {
831            if (neo4j.index().existsForNodes(name)) {
832                return neo4j.index().forNodes(name);
833            } else {
834                Map<String, String> configuration = stringMap(IndexManager.PROVIDER, "lucene", "type", "fulltext");
835                return neo4j.index().forNodes(name, configuration);
836            }
837        }
838    
839    
840    
841        // === Helper ===
842    
843        // --- Neo4j -> DeepaMehta Bridge ---
844    
845        TopicModel buildTopic(Node topicNode) {
846            return new TopicModel(
847                topicNode.getId(),
848                uri(topicNode),
849                typeUri(topicNode),
850                simpleValue(topicNode),
851                null    // childTopics=null
852            );
853        }
854    
855        private List<TopicModel> buildTopics(Iterable<Node> topicNodes) {
856            List<TopicModel> topics = new ArrayList();
857            for (Node topicNode : topicNodes) {
858                topics.add(buildTopic(topicNode));
859            }
860            return topics;
861        }
862    
863        // ---
864    
865        AssociationModel buildAssociation(Node assocNode) {
866            List<RoleModel> roleModels = buildRoleModels(assocNode);
867            return new AssociationModel(
868                assocNode.getId(),
869                uri(assocNode),
870                typeUri(assocNode),
871                roleModels.get(0), roleModels.get(1),
872                simpleValue(assocNode),
873                null    // childTopics=null
874            );
875        }
876    
877        private List<AssociationModel> buildAssociations(Iterable<Node> assocNodes) {
878            List<AssociationModel> assocs = new ArrayList();
879            for (Node assocNode : assocNodes) {
880                assocs.add(buildAssociation(assocNode));
881            }
882            return assocs;
883        }
884    
885        private List<RoleModel> buildRoleModels(Node assocNode) {
886            List<RoleModel> roleModels = new ArrayList();
887            for (Relationship rel : fetchRelationships(assocNode)) {
888                Node node = rel.getEndNode();
889                String roleTypeUri = rel.getType().name();
890                RoleModel roleModel = NodeType.of(node).createRoleModel(node, roleTypeUri);
891                roleModels.add(roleModel);
892            }
893            return roleModels;
894        }
895    
896    
897    
898        // --- DeepaMehta -> Neo4j Bridge ---
899    
900        private Node storePlayerRelationship(Node assocNode, RoleModel roleModel) {
901            Node playerNode = fetchPlayerNode(roleModel);
902            assocNode.createRelationshipTo(
903                playerNode,
904                getRelationshipType(roleModel.getRoleTypeUri())
905            );
906            return playerNode;
907        }
908    
909        private Node fetchPlayerNode(RoleModel roleModel) {
910            if (roleModel instanceof TopicRoleModel) {
911                return fetchTopicPlayerNode((TopicRoleModel) roleModel);
912            } else if (roleModel instanceof AssociationRoleModel) {
913                return fetchAssociationNode(roleModel.getPlayerId());
914            } else {
915                throw new RuntimeException("Unexpected role model: " + roleModel);
916            }
917        }
918    
919        private Node fetchTopicPlayerNode(TopicRoleModel roleModel) {
920            if (roleModel.topicIdentifiedByUri()) {
921                return fetchTopicNodeByUri(roleModel.getTopicUri());
922            } else {
923                return fetchTopicNode(roleModel.getPlayerId());
924            }
925        }
926    
927    
928    
929        // --- Neo4j Helper ---
930    
931        private Relationship fetchRelationship(Node assocNode, long playerId) {
932            List<Relationship> rels = fetchRelationships(assocNode);
933            boolean match1 = rels.get(0).getEndNode().getId() == playerId;
934            boolean match2 = rels.get(1).getEndNode().getId() == playerId;
935            if (match1 && match2) {
936                throw new RuntimeException("Ambiguity: both players have ID " + playerId + " in association " +
937                    assocNode.getId());
938            } else if (match1) {
939                return rels.get(0);
940            } else if (match2) {
941                return rels.get(1);
942            } else {
943                throw new IllegalArgumentException("ID " + playerId + " is not a player in association " +
944                    assocNode.getId());
945            }
946        }
947    
948        private List<Relationship> fetchRelationships(Node assocNode) {
949            List<Relationship> rels = new ArrayList();
950            for (Relationship rel : assocNode.getRelationships(Direction.OUTGOING)) {
951                rels.add(rel);
952            }
953            // sanity check
954            if (rels.size() != 2) {
955                throw new RuntimeException("Association " + assocNode.getId() + " connects " + rels.size() +
956                    " player instead of 2");
957            }
958            //
959            return rels;
960        }
961    
962        // ---
963    
964        /**
965         * Fetches all associations the given topic or association is involved in.
966         *
967         * @param   node    a topic node or an association node.
968         */
969        private List<AssociationModel> fetchAssociations(Node node) {
970            List<AssociationModel> assocs = new ArrayList();
971            for (Relationship rel : node.getRelationships(Direction.INCOMING)) {
972                Node assocNode = rel.getStartNode();
973                // skip non-DM nodes stored by 3rd-party components (e.g. Neo4j Spatial)
974                if (!assocNode.hasProperty(KEY_NODE_TYPE)) {
975                    continue;
976                }
977                //
978                assocs.add(buildAssociation(assocNode));
979            }
980            return assocs;
981        }
982    
983        // ---
984    
985        private Node fetchTopicNode(long topicId) {
986            return checkType(
987                fetchNode(topicId), NodeType.TOPIC
988            );
989        }
990    
991        private Node fetchAssociationNode(long assocId) {
992            return checkType(
993                fetchNode(assocId), NodeType.ASSOC
994            );
995        }
996    
997        // ---
998    
999        private Node fetchNode(long id) {
1000            return neo4j.getNodeById(id);
1001        }
1002    
1003        private Node fetchTopicNodeByUri(String uri) {
1004            Node node = topicContentExact.get(KEY_URI, uri).getSingle();
1005            //
1006            if (node == null) {
1007                throw new RuntimeException("Topic with URI \"" + uri + "\" not found in DB");
1008            }
1009            //
1010            return checkType(node, NodeType.TOPIC);
1011        }
1012    
1013        private Node checkType(Node node, NodeType type) {
1014            if (NodeType.of(node) != type) {
1015                throw new IllegalArgumentException(type.error(node));
1016            }
1017            return node;
1018        }
1019    
1020        // ---
1021    
1022        private RelationshipType getRelationshipType(String typeName) {
1023            return relTypeCache.get(typeName);
1024        }
1025    
1026        // ---
1027    
1028        private String uri(Node node) {
1029            return (String) node.getProperty(KEY_URI);
1030        }
1031    
1032        private String typeUri(Node node) {
1033            return (String) node.getProperty(KEY_TPYE_URI);
1034        }
1035    
1036        private SimpleValue simpleValue(Node node) {
1037            return new SimpleValue(node.getProperty(KEY_VALUE));
1038        }
1039    
1040    
1041    
1042        // --- DeepaMehta Helper ---
1043    
1044        // ### TODO: this is a DB agnostic helper method. It could be moved e.g. to a common base class.
1045        private List<RelatedTopicModel> buildRelatedTopics(List<AssociationModel> assocs, long playerId) {
1046            List<RelatedTopicModel> relTopics = new ArrayList();
1047            for (AssociationModel assoc : assocs) {
1048                relTopics.add(new RelatedTopicModel(
1049                    fetchTopic(
1050                        assoc.getOtherPlayerId(playerId)
1051                    ), assoc)
1052                );
1053            }
1054            return relTopics;
1055        }
1056    
1057        // ### TODO: this is a DB agnostic helper method. It could be moved e.g. to a common base class.
1058        private List<RelatedAssociationModel> buildRelatedAssociations(List<AssociationModel> assocs, long playerId) {
1059            List<RelatedAssociationModel> relAssocs = new ArrayList();
1060            for (AssociationModel assoc : assocs) {
1061                relAssocs.add(new RelatedAssociationModel(
1062                    fetchAssociation(
1063                        assoc.getOtherPlayerId(playerId)
1064                    ), assoc)
1065                );
1066            }
1067            return relAssocs;
1068        }
1069    
1070        // ---
1071    
1072        // ### TODO: a principal copy exists in DeepaMehtaObjectModel
1073        private void setDefaults(DeepaMehtaObjectModel model) {
1074            if (model.getUri() == null) {
1075                model.setUri("");
1076            }
1077            if (model.getSimpleValue() == null) {
1078                model.setSimpleValue("");
1079            }
1080        }
1081    
1082        /**
1083         * Checks if a topic or an association with the given URI exists in the DB, and
1084         * throws an exception if so. If an empty URI ("") is given no check is performed.
1085         *
1086         * @param   uri     The URI to check. Must not be null.
1087         */
1088        private void checkUriUniqueness(String uri) {
1089            if (uri.equals("")) {
1090                return;
1091            }
1092            Node n1 = topicContentExact.get(KEY_URI, uri).getSingle();
1093            Node n2 = assocContentExact.get(KEY_URI, uri).getSingle();
1094            if (n1 != null || n2 != null) {
1095                throw new RuntimeException("URI \"" + uri + "\" is not unique");
1096            }
1097        }
1098    }