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