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