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