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