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