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