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        @Override
532        public Object getDatabaseVendorObject() {
533            return neo4j;
534        }
535    
536        // ------------------------------------------------------------------------------------------------- Private Methods
537    
538    
539    
540        // === Value Storage ===
541    
542        private void storeAndIndexTopicUri(Node topicNode, String uri) {
543            storeAndIndexExactValue(topicNode, KEY_URI, uri, topicContentExact);
544        }
545    
546        private void storeAndIndexAssociationUri(Node assocNode, String uri) {
547            storeAndIndexExactValue(assocNode, KEY_URI, uri, assocContentExact);
548        }
549    
550        // ---
551    
552        private void storeAndIndexTopicTypeUri(Node topicNode, String topicTypeUri) {
553            storeAndIndexExactValue(topicNode, KEY_TPYE_URI, topicTypeUri, topicContentExact);
554        }
555    
556        private void storeAndIndexAssociationTypeUri(Node assocNode, String assocTypeUri) {
557            storeAndIndexExactValue(assocNode, KEY_TPYE_URI, assocTypeUri, assocContentExact);
558        }
559    
560        // ---
561    
562        /**
563         * Stores a node value under the specified key and adds the value to the specified index (under the same key).
564         * <code>IndexMode.KEY</code> is used for indexing.
565         * <p>
566         * Used for URIs, type URIs, and properties.
567         *
568         * @param   node        a topic node, or an association node.
569         * @param   exactIndex  the index to add the value to. If <code>null</code> no indexing is performed.
570         */
571        private void storeAndIndexExactValue(Node node, String key, Object value, Index<Node> exactIndex) {
572            // store
573            node.setProperty(key, value);
574            // index
575            if (exactIndex != null) {
576                // Note: numbers are indexed numerically to allow range queries.
577                if (value instanceof Number) {
578                    value = ValueContext.numeric((Number) value);
579                }
580                indexNodeValue(node, value, asList(IndexMode.KEY), key, exactIndex, null);      // fulltextIndex=null
581            }
582        }
583    
584        // ---
585    
586        private void indexTopicNodeValue(Node topicNode, List<IndexMode> indexModes, String indexKey, Object indexValue) {
587            indexNodeValue(topicNode, indexValue, indexModes, indexKey, topicContentExact, topicContentFulltext);
588        }
589    
590        private void indexAssociationNodeValue(Node assocNode, List<IndexMode> indexModes, String indexKey,
591                                                                                           Object indexValue) {
592            indexNodeValue(assocNode, indexValue, indexModes, indexKey, assocContentExact, assocContentFulltext);
593        }
594    
595        // ---
596    
597        private Object getIndexValue(SimpleValue value, SimpleValue indexValue) {
598            return indexValue != null ? indexValue.value() : value.value();
599        }
600    
601    
602    
603        // === Indexing ===
604    
605        private void indexNodeValue(Node node, Object value, List<IndexMode> indexModes, String indexKey,
606                                                             Index<Node> exactIndex, Index<Node> fulltextIndex) {
607            for (IndexMode indexMode : indexModes) {
608                if (indexMode == IndexMode.OFF) {
609                    return;
610                } else if (indexMode == IndexMode.KEY) {
611                    exactIndex.remove(node, indexKey);              // remove old
612                    exactIndex.add(node, indexKey, value);          // index new
613                } else if (indexMode == IndexMode.FULLTEXT) {
614                    fulltextIndex.remove(node, KEY_FULLTEXT);       // remove old
615                    fulltextIndex.add(node, KEY_FULLTEXT, value);   // index new
616                } else if (indexMode == IndexMode.FULLTEXT_KEY) {
617                    fulltextIndex.remove(node, indexKey);           // remove old
618                    fulltextIndex.add(node, indexKey, value);       // index new
619                } else {
620                    throw new RuntimeException("Unexpected index mode: \"" + indexMode + "\"");
621                }
622            }
623        }
624    
625        // ---
626    
627        private void indexAssociation(Node assocNode, String roleTypeUri1, Node playerNode1,
628                                                      String roleTypeUri2, Node playerNode2) {
629            indexAssociationId(assocNode);
630            indexAssociationType(assocNode, typeUri(assocNode));
631            //
632            indexAssociationRole(assocNode, 1, roleTypeUri1, playerNode1);
633            indexAssociationRole(assocNode, 2, roleTypeUri2, playerNode2);
634        }
635    
636        private void indexAssociationId(Node assocNode) {
637            assocMetadata.add(assocNode, KEY_ASSOC_ID, assocNode.getId());
638        }
639    
640        private void indexAssociationType(Node assocNode, String assocTypeUri) {
641            reindexValue(assocNode, KEY_ASSOC_TPYE_URI, assocTypeUri);
642        }
643    
644        private void indexAssociationRole(Node assocNode, int pos, String roleTypeUri, Node playerNode) {
645            assocMetadata.add(assocNode, KEY_ROLE_TPYE_URI + pos, roleTypeUri);
646            assocMetadata.add(assocNode, KEY_PLAYER_TPYE + pos, NodeType.of(playerNode).stringify());
647            assocMetadata.add(assocNode, KEY_PLAYER_ID + pos, playerNode.getId());
648            assocMetadata.add(assocNode, KEY_PLAYER_TYPE_URI + pos, typeUri(playerNode));
649        }
650    
651        // ---
652    
653        private void indexAssociationRoleType(Node assocNode, long playerId, String roleTypeUri) {
654            int pos = lookupPlayerPosition(assocNode.getId(), playerId);
655            reindexValue(assocNode, KEY_ROLE_TPYE_URI, pos, roleTypeUri);
656        }
657    
658        private int lookupPlayerPosition(long assocId, long playerId) {
659            boolean pos1 = isPlayerAtPosition(1, assocId, playerId);
660            boolean pos2 = isPlayerAtPosition(2, assocId, playerId);
661            if (pos1 && pos2) {
662                throw new RuntimeException("Ambiguity: both players have ID " + playerId + " in association " + assocId);
663            } else if (pos1) {
664                return 1;
665            } else if (pos2) {
666                return 2;
667            } else {
668                throw new IllegalArgumentException("ID " + playerId + " is not a player in association " + assocId);
669            }
670        }
671    
672        private boolean isPlayerAtPosition(int pos, long assocId, long playerId) {
673            BooleanQuery query = new BooleanQuery();
674            addTermQuery(KEY_ASSOC_ID, assocId, query);
675            addTermQuery(KEY_PLAYER_ID + pos, playerId, query);
676            return assocMetadata.query(query).getSingle() != null;
677        }
678    
679        // ---
680    
681        private void reindexTypeUri(Node playerNode, String typeUri) {
682            reindexTypeUri(1, playerNode, typeUri);
683            reindexTypeUri(2, playerNode, typeUri);
684        }
685    
686        /**
687         * Re-indexes the KEY_PLAYER_TYPE_URI of all associations in which the specified node
688         * is a player at the specified position.
689         *
690         * @param   playerNode  a topic node or an association node.
691         * @param   typeUri     the new type URI to be indexed for the player node.
692         */
693        private void reindexTypeUri(int pos, Node playerNode, String typeUri) {
694            for (Node assocNode : lookupAssociations(pos, playerNode)) {
695                reindexValue(assocNode, KEY_PLAYER_TYPE_URI, pos, typeUri);
696            }
697        }
698    
699        private IndexHits<Node> lookupAssociations(int pos, Node playerNode) {
700            return assocMetadata.get(KEY_PLAYER_ID + pos, playerNode.getId());
701        }
702    
703        // ---
704    
705        private void reindexValue(Node assocNode, String key, int pos, String value) {
706            reindexValue(assocNode, key + pos, value);
707        }
708    
709        private void reindexValue(Node assocNode, String key, String value) {
710            assocMetadata.remove(assocNode, key);
711            assocMetadata.add(assocNode, key, value);
712        }
713    
714        // --- Query indexes ---
715    
716        private IndexHits<Node> queryIndexByProperty(Index<Node> index, String propUri, Object propValue) {
717            // Note: numbers must be queried as numeric value as they are indexed numerically.
718            if (propValue instanceof Number) {
719                propValue = ValueContext.numeric((Number) propValue);
720            }
721            return index.get(propUri, propValue);
722        }
723    
724        private IndexHits<Node> queryIndexByPropertyRange(Index<Node> index, String propUri, Number from, Number to) {
725            return index.query(buildNumericRangeQuery(propUri, from, to));
726        }
727    
728        // ---
729    
730        private List<AssociationModel> queryAssociationIndex(String assocTypeUri,
731                                         String roleTypeUri1, NodeType playerType1, long playerId1, String playerTypeUri1,
732                                         String roleTypeUri2, NodeType playerType2, long playerId2, String playerTypeUri2) {
733            return buildAssociations(assocMetadata.query(buildAssociationQuery(assocTypeUri,
734                roleTypeUri1, playerType1, playerId1, playerTypeUri1,
735                roleTypeUri2, playerType2, playerId2, playerTypeUri2
736            )));
737        }
738    
739        // --- Build index queries ---
740    
741        private QueryContext buildNumericRangeQuery(String propUri, Number from, Number to) {
742            return QueryContext.numericRange(propUri, from, to);
743        }
744    
745        // ---
746    
747        private Query buildAssociationQuery(String assocTypeUri,
748                                         String roleTypeUri1, NodeType playerType1, long playerId1, String playerTypeUri1,
749                                         String roleTypeUri2, NodeType playerType2, long playerId2, String playerTypeUri2) {
750            // query bidirectional
751            BooleanQuery direction1 = new BooleanQuery();
752            addRole(direction1, 1, roleTypeUri1, playerType1, playerId1, playerTypeUri1);
753            addRole(direction1, 2, roleTypeUri2, playerType2, playerId2, playerTypeUri2);
754            BooleanQuery direction2 = new BooleanQuery();
755            addRole(direction2, 1, roleTypeUri2, playerType2, playerId2, playerTypeUri2);
756            addRole(direction2, 2, roleTypeUri1, playerType1, playerId1, playerTypeUri1);
757            //
758            BooleanQuery roleQuery = new BooleanQuery();
759            roleQuery.add(direction1, Occur.SHOULD);
760            roleQuery.add(direction2, Occur.SHOULD);
761            //
762            BooleanQuery query = new BooleanQuery();
763            if (assocTypeUri != null) {
764                addTermQuery(KEY_ASSOC_TPYE_URI, assocTypeUri, query);
765            }
766            query.add(roleQuery, Occur.MUST);
767            //
768            return query;
769        }
770    
771        private void addRole(BooleanQuery query, int pos, String roleTypeUri, NodeType playerType, long playerId,
772                                                                                                   String playerTypeUri) {
773            if (roleTypeUri != null)   addTermQuery(KEY_ROLE_TPYE_URI + pos,   roleTypeUri,   query);
774            if (playerType != null)    addTermQuery(KEY_PLAYER_TPYE + pos,     playerType,    query);
775            if (playerId != -1)        addTermQuery(KEY_PLAYER_ID + pos,       playerId,      query);
776            if (playerTypeUri != null) addTermQuery(KEY_PLAYER_TYPE_URI + pos, playerTypeUri, query);
777        }
778    
779        // ---
780    
781        private void addTermQuery(String key, long value, BooleanQuery query) {
782            addTermQuery(key, Long.toString(value), query);
783        }
784    
785        private void addTermQuery(String key, NodeType nodeType, BooleanQuery query) {
786            addTermQuery(key, nodeType.stringify(), query);
787        }
788    
789        private void addTermQuery(String key, String value, BooleanQuery query) {
790            query.add(new TermQuery(new Term(key, value)), Occur.MUST);
791        }
792    
793        // --- Remove index entries ---
794    
795        private void removeTopicFromIndex(Node topicNode) {
796            topicContentExact.remove(topicNode);
797            topicContentFulltext.remove(topicNode);
798        }
799    
800        private void removeAssociationFromIndex(Node assocNode) {
801            assocContentExact.remove(assocNode);
802            assocContentFulltext.remove(assocNode);
803            //
804            assocMetadata.remove(assocNode);
805        }
806    
807        // ---
808    
809        private void removeTopicPropertyFromIndex(Node topicNode, String propUri) {
810            topicContentExact.remove(topicNode, propUri);
811        }
812    
813        private void removeAssociationPropertyFromIndex(Node assocNode, String propUri) {
814            assocContentExact.remove(assocNode, propUri);
815        }
816    
817        // --- Create indexes ---
818    
819        private Index<Node> createExactIndex(String name) {
820            return neo4j.index().forNodes(name);
821        }
822    
823        private Index<Node> createFulltextIndex(String name) {
824            if (neo4j.index().existsForNodes(name)) {
825                return neo4j.index().forNodes(name);
826            } else {
827                Map<String, String> configuration = stringMap(IndexManager.PROVIDER, "lucene", "type", "fulltext");
828                return neo4j.index().forNodes(name, configuration);
829            }
830        }
831    
832    
833    
834        // === Helper ===
835    
836        // --- Neo4j -> DeepaMehta Bridge ---
837    
838        TopicModel buildTopic(Node topicNode) {
839            return new TopicModel(
840                topicNode.getId(),
841                uri(topicNode),
842                typeUri(topicNode),
843                simpleValue(topicNode),
844                null    // composite=null
845            );
846        }
847    
848        private List<TopicModel> buildTopics(Iterable<Node> topicNodes) {
849            List<TopicModel> topics = new ArrayList();
850            for (Node topicNode : topicNodes) {
851                topics.add(buildTopic(topicNode));
852            }
853            return topics;
854        }
855    
856        // ---
857    
858        AssociationModel buildAssociation(Node assocNode) {
859            List<RoleModel> roleModels = buildRoleModels(assocNode);
860            return new AssociationModel(
861                assocNode.getId(),
862                uri(assocNode),
863                typeUri(assocNode),
864                roleModels.get(0), roleModels.get(1),
865                simpleValue(assocNode),
866                null    // composite=null
867            );
868        }
869    
870        private List<AssociationModel> buildAssociations(Iterable<Node> assocNodes) {
871            List<AssociationModel> assocs = new ArrayList();
872            for (Node assocNode : assocNodes) {
873                assocs.add(buildAssociation(assocNode));
874            }
875            return assocs;
876        }
877    
878        private List<RoleModel> buildRoleModels(Node assocNode) {
879            List<RoleModel> roleModels = new ArrayList();
880            for (Relationship rel : fetchRelationships(assocNode)) {
881                Node node = rel.getEndNode();
882                String roleTypeUri = rel.getType().name();
883                RoleModel roleModel = NodeType.of(node).createRoleModel(node, roleTypeUri);
884                roleModels.add(roleModel);
885            }
886            return roleModels;
887        }
888    
889    
890    
891        // --- DeepaMehta -> Neo4j Bridge ---
892    
893        private Node storePlayerRelationship(Node assocNode, RoleModel roleModel) {
894            Node playerNode = fetchPlayerNode(roleModel);
895            assocNode.createRelationshipTo(
896                playerNode,
897                getRelationshipType(roleModel.getRoleTypeUri())
898            );
899            return playerNode;
900        }
901    
902        private Node fetchPlayerNode(RoleModel roleModel) {
903            if (roleModel instanceof TopicRoleModel) {
904                return fetchTopicPlayerNode((TopicRoleModel) roleModel);
905            } else if (roleModel instanceof AssociationRoleModel) {
906                return fetchAssociationNode(roleModel.getPlayerId());
907            } else {
908                throw new RuntimeException("Unexpected role model: " + roleModel);
909            }
910        }
911    
912        private Node fetchTopicPlayerNode(TopicRoleModel roleModel) {
913            if (roleModel.topicIdentifiedByUri()) {
914                return fetchTopicNodeByUri(roleModel.getTopicUri());
915            } else {
916                return fetchTopicNode(roleModel.getPlayerId());
917            }
918        }
919    
920    
921    
922        // --- Neo4j Helper ---
923    
924        private Relationship fetchRelationship(Node assocNode, long playerId) {
925            List<Relationship> rels = fetchRelationships(assocNode);
926            boolean match1 = rels.get(0).getEndNode().getId() == playerId;
927            boolean match2 = rels.get(1).getEndNode().getId() == playerId;
928            if (match1 && match2) {
929                throw new RuntimeException("Ambiguity: both players have ID " + playerId + " in association " +
930                    assocNode.getId());
931            } else if (match1) {
932                return rels.get(0);
933            } else if (match2) {
934                return rels.get(1);
935            } else {
936                throw new IllegalArgumentException("ID " + playerId + " is not a player in association " +
937                    assocNode.getId());
938            }
939        }
940    
941        private List<Relationship> fetchRelationships(Node assocNode) {
942            List<Relationship> rels = new ArrayList();
943            for (Relationship rel : assocNode.getRelationships(Direction.OUTGOING)) {
944                rels.add(rel);
945            }
946            // sanity check
947            if (rels.size() != 2) {
948                throw new RuntimeException("Association " + assocNode.getId() + " connects " + rels.size() +
949                    " player instead of 2");
950            }
951            //
952            return rels;
953        }
954    
955        // ---
956    
957        private List<AssociationModel> fetchAssociations(Node node) {
958            List<AssociationModel> assocs = new ArrayList();
959            for (Relationship rel : node.getRelationships(Direction.INCOMING)) {
960                Node assocNode = rel.getStartNode();
961                assocs.add(buildAssociation(assocNode));
962            }
963            return assocs;
964        }
965    
966        // ---
967    
968        private Node fetchTopicNode(long topicId) {
969            return checkType(
970                fetchNode(topicId), NodeType.TOPIC
971            );
972        }
973    
974        private Node fetchAssociationNode(long assocId) {
975            return checkType(
976                fetchNode(assocId), NodeType.ASSOC
977            );
978        }
979    
980        // ---
981    
982        private Node fetchNode(long id) {
983            return neo4j.getNodeById(id);
984        }
985    
986        private Node fetchTopicNodeByUri(String uri) {
987            Node node = topicContentExact.get(KEY_URI, uri).getSingle();
988            //
989            if (node == null) {
990                throw new RuntimeException("Topic with URI \"" + uri + "\" not found in DB");
991            }
992            //
993            return checkType(node, NodeType.TOPIC);
994        }
995    
996        private Node checkType(Node node, NodeType type) {
997            if (NodeType.of(node) != type) {
998                throw new IllegalArgumentException(type.error(node));
999            }
1000            return node;
1001        }
1002    
1003        // ---
1004    
1005        private RelationshipType getRelationshipType(String typeName) {
1006            return relTypeCache.get(typeName);
1007        }
1008    
1009        // ---
1010    
1011        private String uri(Node node) {
1012            return (String) node.getProperty(KEY_URI);
1013        }
1014    
1015        private String typeUri(Node node) {
1016            return (String) node.getProperty(KEY_TPYE_URI);
1017        }
1018    
1019        private SimpleValue simpleValue(Node node) {
1020            return new SimpleValue(node.getProperty(KEY_VALUE));
1021        }
1022    
1023    
1024    
1025        // --- DeepaMehta Helper ---
1026    
1027        // ### TODO: this is a DB agnostic helper method. It could be moved e.g. to a common base class.
1028        private List<RelatedTopicModel> buildRelatedTopics(List<AssociationModel> assocs, long playerId) {
1029            List<RelatedTopicModel> relTopics = new ArrayList();
1030            for (AssociationModel assoc : assocs) {
1031                relTopics.add(new RelatedTopicModel(
1032                    fetchTopic(
1033                        assoc.getOtherPlayerId(playerId)
1034                    ), assoc)
1035                );
1036            }
1037            return relTopics;
1038        }
1039    
1040        // ### TODO: this is a DB agnostic helper method. It could be moved e.g. to a common base class.
1041        private List<RelatedAssociationModel> buildRelatedAssociations(List<AssociationModel> assocs, long playerId) {
1042            List<RelatedAssociationModel> relAssocs = new ArrayList();
1043            for (AssociationModel assoc : assocs) {
1044                relAssocs.add(new RelatedAssociationModel(
1045                    fetchAssociation(
1046                        assoc.getOtherPlayerId(playerId)
1047                    ), assoc)
1048                );
1049            }
1050            return relAssocs;
1051        }
1052    
1053        // ---
1054    
1055        // ### TODO: a principal copy exists in DeepaMehtaObjectModel
1056        private void setDefaults(DeepaMehtaObjectModel model) {
1057            if (model.getUri() == null) {
1058                model.setUri("");
1059            }
1060            if (model.getSimpleValue() == null) {
1061                model.setSimpleValue("");
1062            }
1063        }
1064    
1065        /**
1066         * Checks if a topic or an association with the given URI exists in the DB, and
1067         * throws an exception if so. If an empty URI ("") is given no check is performed.
1068         *
1069         * @param   uri     The URI to check. Must not be null.
1070         */
1071        private void checkUriUniqueness(String uri) {
1072            if (uri.equals("")) {
1073                return;
1074            }
1075            Node n1 = topicContentExact.get(KEY_URI, uri).getSingle();
1076            Node n2 = assocContentExact.get(KEY_URI, uri).getSingle();
1077            if (n1 != null || n2 != null) {
1078                throw new RuntimeException("URI \"" + uri + "\" is not unique");
1079            }
1080        }
1081    }