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