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