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 // --- 249 250 @Override 251 public void storeAssociation(AssociationModel assocModel) { 252 setDefaults(assocModel); 253 String uri = assocModel.getUri(); 254 checkUriUniqueness(uri); 255 // 256 // 1) update DB 257 Node assocNode = neo4j.createNode(); 258 assocNode.setProperty(KEY_NODE_TYPE, "assoc"); 259 // 260 storeAndIndexAssociationUri(assocNode, uri); 261 storeAndIndexAssociationTypeUri(assocNode, assocModel.getTypeUri()); 262 // 263 RoleModel role1 = assocModel.getRoleModel1(); 264 RoleModel role2 = assocModel.getRoleModel2(); 265 Node playerNode1 = storePlayerRelationship(assocNode, role1); 266 Node playerNode2 = storePlayerRelationship(assocNode, role2); 267 // 268 // 2) update index 269 indexAssociation(assocNode, role1.getRoleTypeUri(), playerNode1, 270 role2.getRoleTypeUri(), playerNode2); 271 // 3) update model 272 assocModel.setId(assocNode.getId()); 273 } 274 275 @Override 276 public void storeAssociationUri(long assocId, String uri) { 277 storeAndIndexAssociationUri(fetchAssociationNode(assocId), uri); 278 } 279 280 // Note: a storage implementation is not responsible for maintaining the "Instantiation" associations. 281 // This is performed at the application layer. 282 @Override 283 public void storeAssociationTypeUri(long assocId, String assocTypeUri) { 284 Node assocNode = fetchAssociationNode(assocId); 285 // 286 // 1) update DB and content index 287 storeAndIndexAssociationTypeUri(assocNode, assocTypeUri); 288 // 289 // 2) update association metadata index 290 indexAssociationType(assocNode, assocTypeUri); // update association entry itself 291 reindexTypeUri(assocNode, assocTypeUri); // update all association entries the association is a player of 292 } 293 294 @Override 295 public void storeAssociationValue(long assocId, SimpleValue value, List<IndexMode> indexModes, 296 String indexKey, SimpleValue indexValue) { 297 Node assocNode = fetchAssociationNode(assocId); 298 // store 299 assocNode.setProperty(KEY_VALUE, value.value()); 300 // index 301 indexAssociationNodeValue(assocNode, indexModes, indexKey, getIndexValue(value, indexValue)); 302 } 303 304 @Override 305 public void indexAssociationValue(long assocId, IndexMode indexMode, String indexKey, SimpleValue indexValue) { 306 indexAssociationNodeValue(fetchAssociationNode(assocId), asList(indexMode), indexKey, indexValue.value()); 307 } 308 309 @Override 310 public void storeRoleTypeUri(long assocId, long playerId, String roleTypeUri) { 311 Node assocNode = fetchAssociationNode(assocId); 312 // 313 // 1) update DB 314 fetchRelationship(assocNode, playerId).delete(); // delete relationship 315 assocNode.createRelationshipTo(fetchNode(playerId), getRelationshipType(roleTypeUri)); // create new one 316 // 317 // 2) update association metadata index 318 indexAssociationRoleType(assocNode, playerId, roleTypeUri); 319 } 320 321 // --- 322 323 @Override 324 public void deleteAssociation(long assocId) { 325 // 1) update DB 326 Node assocNode = fetchAssociationNode(assocId); 327 // delete the 2 player relationships 328 for (Relationship rel : fetchRelationships(assocNode)) { 329 rel.delete(); 330 } 331 // 332 assocNode.delete(); 333 // 334 // 2) update index 335 removeAssociationFromIndex(assocNode); 336 } 337 338 339 340 // === Traversal === 341 342 @Override 343 public List<AssociationModel> fetchTopicAssociations(long topicId) { 344 return fetchAssociations(fetchTopicNode(topicId)); 345 } 346 347 @Override 348 public List<AssociationModel> fetchAssociationAssociations(long assocId) { 349 return fetchAssociations(fetchAssociationNode(assocId)); 350 } 351 352 // --- 353 354 @Override 355 public List<RelatedTopicModel> fetchTopicRelatedTopics(long topicId, String assocTypeUri, String myRoleTypeUri, 356 String othersRoleTypeUri, String othersTopicTypeUri) { 357 return buildRelatedTopics(queryAssociationIndex( 358 assocTypeUri, 359 myRoleTypeUri, NodeType.TOPIC, topicId, null, 360 othersRoleTypeUri, NodeType.TOPIC, -1, othersTopicTypeUri 361 ), topicId); 362 } 363 364 @Override 365 public List<RelatedAssociationModel> fetchTopicRelatedAssociations(long topicId, String assocTypeUri, 366 String myRoleTypeUri, String othersRoleTypeUri, String othersAssocTypeUri) { 367 return buildRelatedAssociations(queryAssociationIndex( 368 assocTypeUri, 369 myRoleTypeUri, NodeType.TOPIC, topicId, null, 370 othersRoleTypeUri, NodeType.ASSOC, -1, othersAssocTypeUri 371 ), topicId); 372 } 373 374 // --- 375 376 @Override 377 public List<RelatedTopicModel> fetchAssociationRelatedTopics(long assocId, String assocTypeUri, 378 String myRoleTypeUri, String othersRoleTypeUri, String othersTopicTypeUri) { 379 return buildRelatedTopics(queryAssociationIndex( 380 assocTypeUri, 381 myRoleTypeUri, NodeType.ASSOC, assocId, null, 382 othersRoleTypeUri, NodeType.TOPIC, -1, othersTopicTypeUri 383 ), assocId); 384 } 385 386 @Override 387 public List<RelatedAssociationModel> fetchAssociationRelatedAssociations(long assocId, String assocTypeUri, 388 String myRoleTypeUri, String othersRoleTypeUri, String othersAssocTypeUri) { 389 return buildRelatedAssociations(queryAssociationIndex( 390 assocTypeUri, 391 myRoleTypeUri, NodeType.ASSOC, assocId, null, 392 othersRoleTypeUri, NodeType.ASSOC, -1, othersAssocTypeUri 393 ), assocId); 394 } 395 396 // --- 397 398 @Override 399 public List<RelatedTopicModel> fetchRelatedTopics(long id, String assocTypeUri, String myRoleTypeUri, 400 String othersRoleTypeUri, String othersTopicTypeUri) { 401 return buildRelatedTopics(queryAssociationIndex( 402 assocTypeUri, 403 myRoleTypeUri, null, id, null, 404 othersRoleTypeUri, NodeType.TOPIC, -1, othersTopicTypeUri 405 ), id); 406 } 407 408 @Override 409 public List<RelatedAssociationModel> fetchRelatedAssociations(long id, String assocTypeUri, String myRoleTypeUri, 410 String othersRoleTypeUri, String othersAssocTypeUri) { 411 return buildRelatedAssociations(queryAssociationIndex( 412 assocTypeUri, 413 myRoleTypeUri, null, id, null, 414 othersRoleTypeUri, NodeType.ASSOC, -1, othersAssocTypeUri 415 ), id); 416 } 417 418 419 420 // === Properties === 421 422 @Override 423 public Object fetchTopicProperty(long topicId, String propUri) { 424 return fetchTopicNode(topicId).getProperty(propUri); 425 } 426 427 @Override 428 public Object fetchAssociationProperty(long assocId, String propUri) { 429 return fetchAssociationNode(assocId).getProperty(propUri); 430 } 431 432 // --- 433 434 @Override 435 public List<TopicModel> fetchTopicsByProperty(String propUri, Object propValue) { 436 return buildTopics(queryIndexByProperty(topicContentExact, propUri, propValue)); 437 } 438 439 @Override 440 public List<TopicModel> fetchTopicsByPropertyRange(String propUri, Number from, Number to) { 441 return buildTopics(queryIndexByPropertyRange(topicContentExact, propUri, from, to)); 442 } 443 444 @Override 445 public List<AssociationModel> fetchAssociationsByProperty(String propUri, Object propValue) { 446 return buildAssociations(queryIndexByProperty(assocContentExact, propUri, propValue)); 447 } 448 449 @Override 450 public List<AssociationModel> fetchAssociationsByPropertyRange(String propUri, Number from, Number to) { 451 return buildAssociations(queryIndexByPropertyRange(assocContentExact, propUri, from, to)); 452 } 453 454 // --- 455 456 @Override 457 public void storeTopicProperty(long topicId, String propUri, Object propValue, boolean addToIndex) { 458 Index<Node> exactIndex = addToIndex ? topicContentExact : null; 459 storeAndIndexExactValue(fetchTopicNode(topicId), propUri, propValue, exactIndex); 460 } 461 462 @Override 463 public void storeAssociationProperty(long assocId, String propUri, Object propValue, boolean addToIndex) { 464 Index<Node> exactIndex = addToIndex ? assocContentExact : null; 465 storeAndIndexExactValue(fetchAssociationNode(assocId), propUri, propValue, exactIndex); 466 } 467 468 // --- 469 470 @Override 471 public boolean hasTopicProperty(long topicId, String propUri) { 472 return fetchTopicNode(topicId).hasProperty(propUri); 473 } 474 475 @Override 476 public boolean hasAssociationProperty(long assocId, String propUri) { 477 return fetchAssociationNode(assocId).hasProperty(propUri); 478 } 479 480 // --- 481 482 @Override 483 public void deleteTopicProperty(long topicId, String propUri) { 484 Node topicNode = fetchTopicNode(topicId); 485 topicNode.removeProperty(propUri); 486 removeTopicPropertyFromIndex(topicNode, propUri); 487 } 488 489 @Override 490 public void deleteAssociationProperty(long assocId, String propUri) { 491 Node assocNode = fetchAssociationNode(assocId); 492 assocNode.removeProperty(propUri); 493 removeAssociationPropertyFromIndex(assocNode, propUri); 494 } 495 496 497 498 // === DB === 499 500 @Override 501 public DeepaMehtaTransaction beginTx() { 502 return new Neo4jTransactionAdapter(neo4j); 503 } 504 505 @Override 506 public boolean setupRootNode() { 507 try { 508 Node rootNode = fetchNode(0); 509 // 510 if (rootNode.getProperty(KEY_NODE_TYPE, null) != null) { 511 return false; 512 } 513 // 514 rootNode.setProperty(KEY_NODE_TYPE, "topic"); 515 rootNode.setProperty(KEY_VALUE, "Meta Type"); 516 storeAndIndexTopicUri(rootNode, "dm4.core.meta_type"); 517 storeAndIndexTopicTypeUri(rootNode, "dm4.core.meta_meta_type"); 518 // 519 return true; 520 } catch (Exception e) { 521 throw new RuntimeException("Setting up the root node (0) failed", e); 522 } 523 } 524 525 @Override 526 public void shutdown() { 527 logger.info("Shutdown Neo4j"); 528 neo4j.shutdown(); 529 } 530 531 // --- 532 533 @Override 534 public Object getDatabaseVendorObject() { 535 return neo4j; 536 } 537 538 @Override 539 public Object getDatabaseVendorObject(long objectId) { 540 return fetchNode(objectId); 541 } 542 543 // ------------------------------------------------------------------------------------------------- Private Methods 544 545 546 547 // === Value Storage === 548 549 private void storeAndIndexTopicUri(Node topicNode, String uri) { 550 storeAndIndexExactValue(topicNode, KEY_URI, uri, topicContentExact); 551 } 552 553 private void storeAndIndexAssociationUri(Node assocNode, String uri) { 554 storeAndIndexExactValue(assocNode, KEY_URI, uri, assocContentExact); 555 } 556 557 // --- 558 559 private void storeAndIndexTopicTypeUri(Node topicNode, String topicTypeUri) { 560 storeAndIndexExactValue(topicNode, KEY_TPYE_URI, topicTypeUri, topicContentExact); 561 } 562 563 private void storeAndIndexAssociationTypeUri(Node assocNode, String assocTypeUri) { 564 storeAndIndexExactValue(assocNode, KEY_TPYE_URI, assocTypeUri, assocContentExact); 565 } 566 567 // --- 568 569 /** 570 * Stores a node value under the specified key and adds the value to the specified index (under the same key). 571 * <code>IndexMode.KEY</code> is used for indexing. 572 * <p> 573 * Used for URIs, type URIs, and properties. 574 * 575 * @param node a topic node, or an association node. 576 * @param exactIndex the index to add the value to. If <code>null</code> no indexing is performed. 577 */ 578 private void storeAndIndexExactValue(Node node, String key, Object value, Index<Node> exactIndex) { 579 // store 580 node.setProperty(key, value); 581 // index 582 if (exactIndex != null) { 583 // Note: numbers are indexed numerically to allow range queries. 584 if (value instanceof Number) { 585 value = ValueContext.numeric((Number) value); 586 } 587 indexNodeValue(node, value, asList(IndexMode.KEY), key, exactIndex, null); // fulltextIndex=null 588 } 589 } 590 591 // --- 592 593 private void indexTopicNodeValue(Node topicNode, List<IndexMode> indexModes, String indexKey, Object indexValue) { 594 indexNodeValue(topicNode, indexValue, indexModes, indexKey, topicContentExact, topicContentFulltext); 595 } 596 597 private void indexAssociationNodeValue(Node assocNode, List<IndexMode> indexModes, String indexKey, 598 Object indexValue) { 599 indexNodeValue(assocNode, indexValue, indexModes, indexKey, assocContentExact, assocContentFulltext); 600 } 601 602 // --- 603 604 private Object getIndexValue(SimpleValue value, SimpleValue indexValue) { 605 return indexValue != null ? indexValue.value() : value.value(); 606 } 607 608 609 610 // === Indexing === 611 612 private void indexNodeValue(Node node, Object value, List<IndexMode> indexModes, String indexKey, 613 Index<Node> exactIndex, Index<Node> fulltextIndex) { 614 for (IndexMode indexMode : indexModes) { 615 if (indexMode == IndexMode.OFF) { 616 return; 617 } else if (indexMode == IndexMode.KEY) { 618 exactIndex.remove(node, indexKey); // remove old 619 exactIndex.add(node, indexKey, value); // index new 620 } else if (indexMode == IndexMode.FULLTEXT) { 621 fulltextIndex.remove(node, KEY_FULLTEXT); // remove old 622 fulltextIndex.add(node, KEY_FULLTEXT, value); // index new 623 } else if (indexMode == IndexMode.FULLTEXT_KEY) { 624 fulltextIndex.remove(node, indexKey); // remove old 625 fulltextIndex.add(node, indexKey, value); // index new 626 } else { 627 throw new RuntimeException("Unexpected index mode: \"" + indexMode + "\""); 628 } 629 } 630 } 631 632 // --- 633 634 private void indexAssociation(Node assocNode, String roleTypeUri1, Node playerNode1, 635 String roleTypeUri2, Node playerNode2) { 636 indexAssociationId(assocNode); 637 indexAssociationType(assocNode, typeUri(assocNode)); 638 // 639 indexAssociationRole(assocNode, 1, roleTypeUri1, playerNode1); 640 indexAssociationRole(assocNode, 2, roleTypeUri2, playerNode2); 641 } 642 643 private void indexAssociationId(Node assocNode) { 644 assocMetadata.add(assocNode, KEY_ASSOC_ID, assocNode.getId()); 645 } 646 647 private void indexAssociationType(Node assocNode, String assocTypeUri) { 648 reindexValue(assocNode, KEY_ASSOC_TPYE_URI, assocTypeUri); 649 } 650 651 private void indexAssociationRole(Node assocNode, int pos, String roleTypeUri, Node playerNode) { 652 assocMetadata.add(assocNode, KEY_ROLE_TPYE_URI + pos, roleTypeUri); 653 assocMetadata.add(assocNode, KEY_PLAYER_TPYE + pos, NodeType.of(playerNode).stringify()); 654 assocMetadata.add(assocNode, KEY_PLAYER_ID + pos, playerNode.getId()); 655 assocMetadata.add(assocNode, KEY_PLAYER_TYPE_URI + pos, typeUri(playerNode)); 656 } 657 658 // --- 659 660 private void indexAssociationRoleType(Node assocNode, long playerId, String roleTypeUri) { 661 int pos = lookupPlayerPosition(assocNode.getId(), playerId); 662 reindexValue(assocNode, KEY_ROLE_TPYE_URI, pos, roleTypeUri); 663 } 664 665 private int lookupPlayerPosition(long assocId, long playerId) { 666 boolean pos1 = isPlayerAtPosition(1, assocId, playerId); 667 boolean pos2 = isPlayerAtPosition(2, assocId, playerId); 668 if (pos1 && pos2) { 669 throw new RuntimeException("Ambiguity: both players have ID " + playerId + " in association " + assocId); 670 } else if (pos1) { 671 return 1; 672 } else if (pos2) { 673 return 2; 674 } else { 675 throw new IllegalArgumentException("ID " + playerId + " is not a player in association " + assocId); 676 } 677 } 678 679 private boolean isPlayerAtPosition(int pos, long assocId, long playerId) { 680 BooleanQuery query = new BooleanQuery(); 681 addTermQuery(KEY_ASSOC_ID, assocId, query); 682 addTermQuery(KEY_PLAYER_ID + pos, playerId, query); 683 return assocMetadata.query(query).getSingle() != null; 684 } 685 686 // --- 687 688 private void reindexTypeUri(Node playerNode, String typeUri) { 689 reindexTypeUri(1, playerNode, typeUri); 690 reindexTypeUri(2, playerNode, typeUri); 691 } 692 693 /** 694 * Re-indexes the KEY_PLAYER_TYPE_URI of all associations in which the specified node 695 * is a player at the specified position. 696 * 697 * @param playerNode a topic node or an association node. 698 * @param typeUri the new type URI to be indexed for the player node. 699 */ 700 private void reindexTypeUri(int pos, Node playerNode, String typeUri) { 701 for (Node assocNode : lookupAssociations(pos, playerNode)) { 702 reindexValue(assocNode, KEY_PLAYER_TYPE_URI, pos, typeUri); 703 } 704 } 705 706 private IndexHits<Node> lookupAssociations(int pos, Node playerNode) { 707 return assocMetadata.get(KEY_PLAYER_ID + pos, playerNode.getId()); 708 } 709 710 // --- 711 712 private void reindexValue(Node assocNode, String key, int pos, String value) { 713 reindexValue(assocNode, key + pos, value); 714 } 715 716 private void reindexValue(Node assocNode, String key, String value) { 717 assocMetadata.remove(assocNode, key); 718 assocMetadata.add(assocNode, key, value); 719 } 720 721 // --- Query indexes --- 722 723 private IndexHits<Node> queryIndexByProperty(Index<Node> index, String propUri, Object propValue) { 724 // Note: numbers must be queried as numeric value as they are indexed numerically. 725 if (propValue instanceof Number) { 726 propValue = ValueContext.numeric((Number) propValue); 727 } 728 return index.get(propUri, propValue); 729 } 730 731 private IndexHits<Node> queryIndexByPropertyRange(Index<Node> index, String propUri, Number from, Number to) { 732 return index.query(buildNumericRangeQuery(propUri, from, to)); 733 } 734 735 // --- 736 737 private List<AssociationModel> queryAssociationIndex(String assocTypeUri, 738 String roleTypeUri1, NodeType playerType1, long playerId1, String playerTypeUri1, 739 String roleTypeUri2, NodeType playerType2, long playerId2, String playerTypeUri2) { 740 return buildAssociations(assocMetadata.query(buildAssociationQuery(assocTypeUri, 741 roleTypeUri1, playerType1, playerId1, playerTypeUri1, 742 roleTypeUri2, playerType2, playerId2, playerTypeUri2 743 ))); 744 } 745 746 // --- Build index queries --- 747 748 private QueryContext buildNumericRangeQuery(String propUri, Number from, Number to) { 749 return QueryContext.numericRange(propUri, from, to); 750 } 751 752 // --- 753 754 private Query buildAssociationQuery(String assocTypeUri, 755 String roleTypeUri1, NodeType playerType1, long playerId1, String playerTypeUri1, 756 String roleTypeUri2, NodeType playerType2, long playerId2, String playerTypeUri2) { 757 // query bidirectional 758 BooleanQuery direction1 = new BooleanQuery(); 759 addRole(direction1, 1, roleTypeUri1, playerType1, playerId1, playerTypeUri1); 760 addRole(direction1, 2, roleTypeUri2, playerType2, playerId2, playerTypeUri2); 761 BooleanQuery direction2 = new BooleanQuery(); 762 addRole(direction2, 1, roleTypeUri2, playerType2, playerId2, playerTypeUri2); 763 addRole(direction2, 2, roleTypeUri1, playerType1, playerId1, playerTypeUri1); 764 // 765 BooleanQuery roleQuery = new BooleanQuery(); 766 roleQuery.add(direction1, Occur.SHOULD); 767 roleQuery.add(direction2, Occur.SHOULD); 768 // 769 BooleanQuery query = new BooleanQuery(); 770 if (assocTypeUri != null) { 771 addTermQuery(KEY_ASSOC_TPYE_URI, assocTypeUri, query); 772 } 773 query.add(roleQuery, Occur.MUST); 774 // 775 return query; 776 } 777 778 private void addRole(BooleanQuery query, int pos, String roleTypeUri, NodeType playerType, long playerId, 779 String playerTypeUri) { 780 if (roleTypeUri != null) addTermQuery(KEY_ROLE_TPYE_URI + pos, roleTypeUri, query); 781 if (playerType != null) addTermQuery(KEY_PLAYER_TPYE + pos, playerType, query); 782 if (playerId != -1) addTermQuery(KEY_PLAYER_ID + pos, playerId, query); 783 if (playerTypeUri != null) addTermQuery(KEY_PLAYER_TYPE_URI + pos, playerTypeUri, query); 784 } 785 786 // --- 787 788 private void addTermQuery(String key, long value, BooleanQuery query) { 789 addTermQuery(key, Long.toString(value), query); 790 } 791 792 private void addTermQuery(String key, NodeType nodeType, BooleanQuery query) { 793 addTermQuery(key, nodeType.stringify(), query); 794 } 795 796 private void addTermQuery(String key, String value, BooleanQuery query) { 797 query.add(new TermQuery(new Term(key, value)), Occur.MUST); 798 } 799 800 // --- Remove index entries --- 801 802 private void removeTopicFromIndex(Node topicNode) { 803 topicContentExact.remove(topicNode); 804 topicContentFulltext.remove(topicNode); 805 } 806 807 private void removeAssociationFromIndex(Node assocNode) { 808 assocContentExact.remove(assocNode); 809 assocContentFulltext.remove(assocNode); 810 // 811 assocMetadata.remove(assocNode); 812 } 813 814 // --- 815 816 private void removeTopicPropertyFromIndex(Node topicNode, String propUri) { 817 topicContentExact.remove(topicNode, propUri); 818 } 819 820 private void removeAssociationPropertyFromIndex(Node assocNode, String propUri) { 821 assocContentExact.remove(assocNode, propUri); 822 } 823 824 // --- Create indexes --- 825 826 private Index<Node> createExactIndex(String name) { 827 return neo4j.index().forNodes(name); 828 } 829 830 private Index<Node> createFulltextIndex(String name) { 831 if (neo4j.index().existsForNodes(name)) { 832 return neo4j.index().forNodes(name); 833 } else { 834 Map<String, String> configuration = stringMap(IndexManager.PROVIDER, "lucene", "type", "fulltext"); 835 return neo4j.index().forNodes(name, configuration); 836 } 837 } 838 839 840 841 // === Helper === 842 843 // --- Neo4j -> DeepaMehta Bridge --- 844 845 TopicModel buildTopic(Node topicNode) { 846 return new TopicModel( 847 topicNode.getId(), 848 uri(topicNode), 849 typeUri(topicNode), 850 simpleValue(topicNode), 851 null // childTopics=null 852 ); 853 } 854 855 private List<TopicModel> buildTopics(Iterable<Node> topicNodes) { 856 List<TopicModel> topics = new ArrayList(); 857 for (Node topicNode : topicNodes) { 858 topics.add(buildTopic(topicNode)); 859 } 860 return topics; 861 } 862 863 // --- 864 865 AssociationModel buildAssociation(Node assocNode) { 866 List<RoleModel> roleModels = buildRoleModels(assocNode); 867 return new AssociationModel( 868 assocNode.getId(), 869 uri(assocNode), 870 typeUri(assocNode), 871 roleModels.get(0), roleModels.get(1), 872 simpleValue(assocNode), 873 null // childTopics=null 874 ); 875 } 876 877 private List<AssociationModel> buildAssociations(Iterable<Node> assocNodes) { 878 List<AssociationModel> assocs = new ArrayList(); 879 for (Node assocNode : assocNodes) { 880 assocs.add(buildAssociation(assocNode)); 881 } 882 return assocs; 883 } 884 885 private List<RoleModel> buildRoleModels(Node assocNode) { 886 List<RoleModel> roleModels = new ArrayList(); 887 for (Relationship rel : fetchRelationships(assocNode)) { 888 Node node = rel.getEndNode(); 889 String roleTypeUri = rel.getType().name(); 890 RoleModel roleModel = NodeType.of(node).createRoleModel(node, roleTypeUri); 891 roleModels.add(roleModel); 892 } 893 return roleModels; 894 } 895 896 897 898 // --- DeepaMehta -> Neo4j Bridge --- 899 900 private Node storePlayerRelationship(Node assocNode, RoleModel roleModel) { 901 Node playerNode = fetchPlayerNode(roleModel); 902 assocNode.createRelationshipTo( 903 playerNode, 904 getRelationshipType(roleModel.getRoleTypeUri()) 905 ); 906 return playerNode; 907 } 908 909 private Node fetchPlayerNode(RoleModel roleModel) { 910 if (roleModel instanceof TopicRoleModel) { 911 return fetchTopicPlayerNode((TopicRoleModel) roleModel); 912 } else if (roleModel instanceof AssociationRoleModel) { 913 return fetchAssociationNode(roleModel.getPlayerId()); 914 } else { 915 throw new RuntimeException("Unexpected role model: " + roleModel); 916 } 917 } 918 919 private Node fetchTopicPlayerNode(TopicRoleModel roleModel) { 920 if (roleModel.topicIdentifiedByUri()) { 921 return fetchTopicNodeByUri(roleModel.getTopicUri()); 922 } else { 923 return fetchTopicNode(roleModel.getPlayerId()); 924 } 925 } 926 927 928 929 // --- Neo4j Helper --- 930 931 private Relationship fetchRelationship(Node assocNode, long playerId) { 932 List<Relationship> rels = fetchRelationships(assocNode); 933 boolean match1 = rels.get(0).getEndNode().getId() == playerId; 934 boolean match2 = rels.get(1).getEndNode().getId() == playerId; 935 if (match1 && match2) { 936 throw new RuntimeException("Ambiguity: both players have ID " + playerId + " in association " + 937 assocNode.getId()); 938 } else if (match1) { 939 return rels.get(0); 940 } else if (match2) { 941 return rels.get(1); 942 } else { 943 throw new IllegalArgumentException("ID " + playerId + " is not a player in association " + 944 assocNode.getId()); 945 } 946 } 947 948 private List<Relationship> fetchRelationships(Node assocNode) { 949 List<Relationship> rels = new ArrayList(); 950 for (Relationship rel : assocNode.getRelationships(Direction.OUTGOING)) { 951 rels.add(rel); 952 } 953 // sanity check 954 if (rels.size() != 2) { 955 throw new RuntimeException("Association " + assocNode.getId() + " connects " + rels.size() + 956 " player instead of 2"); 957 } 958 // 959 return rels; 960 } 961 962 // --- 963 964 /** 965 * Fetches all associations the given topic or association is involved in. 966 * 967 * @param node a topic node or an association node. 968 */ 969 private List<AssociationModel> fetchAssociations(Node node) { 970 List<AssociationModel> assocs = new ArrayList(); 971 for (Relationship rel : node.getRelationships(Direction.INCOMING)) { 972 Node assocNode = rel.getStartNode(); 973 // skip non-DM nodes stored by 3rd-party components (e.g. Neo4j Spatial) 974 if (!assocNode.hasProperty(KEY_NODE_TYPE)) { 975 continue; 976 } 977 // 978 assocs.add(buildAssociation(assocNode)); 979 } 980 return assocs; 981 } 982 983 // --- 984 985 private Node fetchTopicNode(long topicId) { 986 return checkType( 987 fetchNode(topicId), NodeType.TOPIC 988 ); 989 } 990 991 private Node fetchAssociationNode(long assocId) { 992 return checkType( 993 fetchNode(assocId), NodeType.ASSOC 994 ); 995 } 996 997 // --- 998 999 private Node fetchNode(long id) { 1000 return neo4j.getNodeById(id); 1001 } 1002 1003 private Node fetchTopicNodeByUri(String uri) { 1004 Node node = topicContentExact.get(KEY_URI, uri).getSingle(); 1005 // 1006 if (node == null) { 1007 throw new RuntimeException("Topic with URI \"" + uri + "\" not found in DB"); 1008 } 1009 // 1010 return checkType(node, NodeType.TOPIC); 1011 } 1012 1013 private Node checkType(Node node, NodeType type) { 1014 if (NodeType.of(node) != type) { 1015 throw new IllegalArgumentException(type.error(node)); 1016 } 1017 return node; 1018 } 1019 1020 // --- 1021 1022 private RelationshipType getRelationshipType(String typeName) { 1023 return relTypeCache.get(typeName); 1024 } 1025 1026 // --- 1027 1028 private String uri(Node node) { 1029 return (String) node.getProperty(KEY_URI); 1030 } 1031 1032 private String typeUri(Node node) { 1033 return (String) node.getProperty(KEY_TPYE_URI); 1034 } 1035 1036 private SimpleValue simpleValue(Node node) { 1037 return new SimpleValue(node.getProperty(KEY_VALUE)); 1038 } 1039 1040 1041 1042 // --- DeepaMehta Helper --- 1043 1044 // ### TODO: this is a DB agnostic helper method. It could be moved e.g. to a common base class. 1045 private List<RelatedTopicModel> buildRelatedTopics(List<AssociationModel> assocs, long playerId) { 1046 List<RelatedTopicModel> relTopics = new ArrayList(); 1047 for (AssociationModel assoc : assocs) { 1048 relTopics.add(new RelatedTopicModel( 1049 fetchTopic( 1050 assoc.getOtherPlayerId(playerId) 1051 ), assoc) 1052 ); 1053 } 1054 return relTopics; 1055 } 1056 1057 // ### TODO: this is a DB agnostic helper method. It could be moved e.g. to a common base class. 1058 private List<RelatedAssociationModel> buildRelatedAssociations(List<AssociationModel> assocs, long playerId) { 1059 List<RelatedAssociationModel> relAssocs = new ArrayList(); 1060 for (AssociationModel assoc : assocs) { 1061 relAssocs.add(new RelatedAssociationModel( 1062 fetchAssociation( 1063 assoc.getOtherPlayerId(playerId) 1064 ), assoc) 1065 ); 1066 } 1067 return relAssocs; 1068 } 1069 1070 // --- 1071 1072 // ### TODO: a principal copy exists in DeepaMehtaObjectModel 1073 private void setDefaults(DeepaMehtaObjectModel model) { 1074 if (model.getUri() == null) { 1075 model.setUri(""); 1076 } 1077 if (model.getSimpleValue() == null) { 1078 model.setSimpleValue(""); 1079 } 1080 } 1081 1082 /** 1083 * Checks if a topic or an association with the given URI exists in the DB, and 1084 * throws an exception if so. If an empty URI ("") is given no check is performed. 1085 * 1086 * @param uri The URI to check. Must not be null. 1087 */ 1088 private void checkUriUniqueness(String uri) { 1089 if (uri.equals("")) { 1090 return; 1091 } 1092 Node n1 = topicContentExact.get(KEY_URI, uri).getSingle(); 1093 Node n2 = assocContentExact.get(KEY_URI, uri).getSingle(); 1094 if (n1 != null || n2 != null) { 1095 throw new RuntimeException("URI \"" + uri + "\" is not unique"); 1096 } 1097 } 1098 }