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