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