001package de.deepamehta.core.impl; 002 003import de.deepamehta.core.DeepaMehtaObject; 004import de.deepamehta.core.model.AssociationDefinitionModel; 005import de.deepamehta.core.model.AssociationModel; 006import de.deepamehta.core.model.ChildTopicsModel; 007import de.deepamehta.core.model.DeepaMehtaObjectModel; 008import de.deepamehta.core.model.IndexMode; 009import de.deepamehta.core.model.RelatedTopicModel; 010import de.deepamehta.core.model.RoleModel; 011import de.deepamehta.core.model.SimpleValue; 012import de.deepamehta.core.model.TopicModel; 013import de.deepamehta.core.model.TopicDeletionModel; 014import de.deepamehta.core.model.TopicReferenceModel; 015import de.deepamehta.core.model.TypeModel; 016import de.deepamehta.core.service.DeepaMehtaEvent; 017import de.deepamehta.core.service.Directive; 018import de.deepamehta.core.service.Directives; 019import de.deepamehta.core.util.JavaUtils; 020 021import org.codehaus.jettison.json.JSONObject; 022 023import java.util.List; 024import java.util.logging.Logger; 025 026 027 028class DeepaMehtaObjectModelImpl implements DeepaMehtaObjectModel { 029 030 // ---------------------------------------------------------------------------------------------- Instance Variables 031 032 long id; // is -1 in models used for a create operation. ### FIXDOC 033 // is never -1 in models used for an update operation. 034 String uri; // is never null in models used for a create operation, may be empty. ### FIXDOC 035 // may be null in models used for an update operation. 036 String typeUri; // is never null in models used for a create operation. ### FIXDOC 037 // may be null in models used for an update operation. 038 SimpleValue value; // is never null in models used for a create operation, may be constructed 039 // on empty string. ### FIXDOC 040 // may be null in models used for an update operation. 041 ChildTopicsModelImpl childTopics; // is never null, may be empty. ### FIXDOC 042 043 // --- 044 045 PersistenceLayer pl; 046 EventManager em; 047 ModelFactoryImpl mf; 048 049 Logger logger = Logger.getLogger(getClass().getName()); 050 051 // ---------------------------------------------------------------------------------------------------- Constructors 052 053 DeepaMehtaObjectModelImpl(long id, String uri, String typeUri, SimpleValue value, ChildTopicsModelImpl childTopics, 054 PersistenceLayer pl) { 055 this.id = id; 056 this.uri = uri; 057 this.typeUri = typeUri; 058 this.value = value; 059 this.childTopics = childTopics != null ? childTopics : pl.mf.newChildTopicsModel(); 060 // 061 this.pl = pl; 062 this.em = pl.em; 063 this.mf = pl.mf; 064 } 065 066 DeepaMehtaObjectModelImpl(DeepaMehtaObjectModelImpl object) { 067 this(object.getId(), object.getUri(), object.getTypeUri(), object.getSimpleValue(), 068 object.getChildTopicsModel(), object.pl); 069 } 070 071 // -------------------------------------------------------------------------------------------------- Public Methods 072 073 // --- ID --- 074 075 @Override 076 public long getId() { 077 return id; 078 } 079 080 @Override 081 public void setId(long id) { 082 this.id = id; 083 } 084 085 // --- URI --- 086 087 @Override 088 public String getUri() { 089 return uri; 090 } 091 092 @Override 093 public void setUri(String uri) { 094 this.uri = uri; 095 } 096 097 // --- Type URI --- 098 099 @Override 100 public String getTypeUri() { 101 return typeUri; 102 } 103 104 @Override 105 public void setTypeUri(String typeUri) { 106 this.typeUri = typeUri; 107 } 108 109 // --- Simple Value --- 110 111 @Override 112 public SimpleValue getSimpleValue() { 113 return value; 114 } 115 116 // --- 117 118 @Override 119 public void setSimpleValue(String value) { 120 setSimpleValue(new SimpleValue(value)); 121 } 122 123 @Override 124 public void setSimpleValue(int value) { 125 setSimpleValue(new SimpleValue(value)); 126 } 127 128 @Override 129 public void setSimpleValue(long value) { 130 setSimpleValue(new SimpleValue(value)); 131 } 132 133 @Override 134 public void setSimpleValue(boolean value) { 135 setSimpleValue(new SimpleValue(value)); 136 } 137 138 @Override 139 public void setSimpleValue(SimpleValue value) { 140 this.value = value; 141 } 142 143 // --- Child Topics --- 144 145 @Override 146 public ChildTopicsModelImpl getChildTopicsModel() { 147 return childTopics; 148 } 149 150 @Override 151 public void setChildTopicsModel(ChildTopicsModel childTopics) { 152 this.childTopics = (ChildTopicsModelImpl) childTopics; 153 } 154 155 // --- misc --- 156 157 @Override 158 public void set(DeepaMehtaObjectModel object) { 159 setId(object.getId()); 160 setUri(object.getUri()); 161 setTypeUri(object.getTypeUri()); 162 setSimpleValue(object.getSimpleValue()); 163 setChildTopicsModel(object.getChildTopicsModel()); 164 } 165 166 // --- 167 168 @Override 169 public RoleModel createRoleModel(String roleTypeUri) { 170 throw new RuntimeException("Not implemented"); // only implemented in subclasses 171 // Note: technically this class is not abstract. It is instantiated by the ModelFactory. 172 } 173 174 175 176 // === Serialization === 177 178 @Override 179 public JSONObject toJSON() { 180 try { 181 // Note: for models used for topic/association enrichment (e.g. timestamps, permissions) 182 // default values must be set in case they are not fully initialized. 183 setDefaults(); 184 // 185 JSONObject o = new JSONObject(); 186 o.put("id", id); 187 o.put("uri", uri); 188 o.put("type_uri", typeUri); 189 o.put("value", value.value()); 190 o.put("childs", childTopics.toJSON()); 191 return o; 192 } catch (Exception e) { 193 throw new RuntimeException("Serialization failed (" + this + ")", e); 194 } 195 } 196 197 198 199 // === Java API === 200 201 @Override 202 public DeepaMehtaObjectModel clone() { 203 try { 204 DeepaMehtaObjectModel object = (DeepaMehtaObjectModel) super.clone(); 205 object.setChildTopicsModel(childTopics.clone()); 206 return object; 207 } catch (Exception e) { 208 throw new RuntimeException("Cloning a DeepaMehtaObjectModel failed", e); 209 } 210 } 211 212 @Override 213 public boolean equals(Object o) { 214 return ((DeepaMehtaObjectModel) o).getId() == id; 215 } 216 217 @Override 218 public int hashCode() { 219 return ((Long) id).hashCode(); 220 } 221 222 @Override 223 public String toString() { 224 return "id=" + id + ", uri=\"" + uri + "\", typeUri=\"" + typeUri + "\", value=\"" + value + 225 "\", childTopics=" + childTopics; 226 } 227 228 // ----------------------------------------------------------------------------------------- Package Private Methods 229 230 231 232 // === Abstract Methods === 233 234 String className() { 235 throw new UnsupportedOperationException(); 236 } 237 238 DeepaMehtaObject instantiate() { 239 throw new UnsupportedOperationException(); 240 } 241 242 // --- 243 244 TypeModel getType() { 245 throw new UnsupportedOperationException(); 246 } 247 248 List<AssociationModelImpl> getAssociations() { 249 throw new UnsupportedOperationException(); 250 } 251 252 // --- 253 254 RelatedTopicModelImpl getRelatedTopic(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri, 255 String othersTopicTypeUri) { 256 throw new UnsupportedOperationException(); 257 } 258 259 List<RelatedTopicModelImpl> getRelatedTopics(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri, 260 String othersTopicTypeUri) { 261 throw new UnsupportedOperationException(); 262 } 263 264 List<RelatedTopicModelImpl> getRelatedTopics(List assocTypeUris, String myRoleTypeUri, String othersRoleTypeUri, 265 String othersTopicTypeUri) { 266 throw new UnsupportedOperationException(); 267 } 268 269 // --- 270 271 void storeUri() { 272 throw new UnsupportedOperationException(); 273 } 274 275 void storeTypeUri() { 276 throw new UnsupportedOperationException(); 277 } 278 279 /** 280 * Stores and indexes the simple value of the specified topic or association model. 281 * Determines the index key and index modes. 282 */ 283 void storeSimpleValue() { 284 throw new UnsupportedOperationException(); 285 } 286 287 /** 288 * Indexes the simple value of the given object model according to the given index mode. 289 * <p> 290 * Called to index existing topics/associations once an index mode has been added to a type definition. 291 */ 292 void indexSimpleValue(IndexMode indexMode) { 293 throw new UnsupportedOperationException(); 294 } 295 296 // --- 297 298 void updateChildTopics(ChildTopicsModel childTopics) { 299 throw new UnsupportedOperationException(); 300 } 301 302 void _delete() { 303 throw new UnsupportedOperationException(); 304 } 305 306 // --- 307 308 DeepaMehtaEvent getReadAccessEvent() { 309 throw new UnsupportedOperationException(); 310 } 311 312 DeepaMehtaEvent getPreUpdateEvent() { 313 throw new UnsupportedOperationException(); 314 } 315 316 DeepaMehtaEvent getPostUpdateEvent() { 317 throw new UnsupportedOperationException(); 318 } 319 320 DeepaMehtaEvent getPreDeleteEvent() { 321 throw new UnsupportedOperationException(); 322 } 323 324 DeepaMehtaEvent getPostDeleteEvent() { 325 throw new UnsupportedOperationException(); 326 } 327 328 // --- 329 330 Directive getUpdateDirective() { 331 throw new UnsupportedOperationException(); 332 } 333 334 Directive getDeleteDirective() { 335 throw new UnsupportedOperationException(); 336 } 337 338 339 340 // === Core Internal Hooks === 341 342 void preUpdate(DeepaMehtaObjectModel newModel) { 343 } 344 345 void postUpdate(DeepaMehtaObjectModel newModel, DeepaMehtaObjectModel oldModel) { 346 } 347 348 // --- 349 350 void preDelete() { 351 } 352 353 void postDelete() { 354 } 355 356 357 358 // === Update === 359 360 /** 361 * @param newModel The data to update. 362 * If the URI is <code>null</code> it is not updated. 363 * If the type URI is <code>null</code> it is not updated. 364 * If the simple value is <code>null</code> it is not updated. 365 */ 366 final void update(DeepaMehtaObjectModel newModel) { 367 try { 368 logger.info("Updating " + className() + " " + id + " (typeUri=\"" + typeUri + "\")"); 369 DeepaMehtaObject object = instantiate(); 370 DeepaMehtaObjectModel oldModel = clone(); 371 em.fireEvent(getPreUpdateEvent(), object, newModel); 372 // 373 preUpdate(newModel); 374 // 375 _updateUri(newModel.getUri()); 376 _updateTypeUri(newModel.getTypeUri()); 377 if (isSimple()) { 378 _updateSimpleValue(newModel.getSimpleValue()); 379 } else { 380 _updateChildTopics(newModel.getChildTopicsModel()); 381 } 382 // 383 postUpdate(newModel, oldModel); 384 // 385 Directives.get().add(getUpdateDirective(), object); 386 em.fireEvent(getPostUpdateEvent(), object, newModel, oldModel); 387 } catch (Exception e) { 388 throw new RuntimeException("Updating " + className() + " " + id + " failed (typeUri=\"" + typeUri + "\")", 389 e); 390 } 391 } 392 393 // --- 394 395 void updateUri(String uri) { 396 setUri(uri); // update memory 397 storeUri(); // update DB, "abstract" 398 } 399 400 void updateTypeUri(String typeUri) { 401 setTypeUri(typeUri); // update memory 402 storeTypeUri(); // update DB, "abstract" 403 } 404 405 void updateSimpleValue(SimpleValue value) { 406 if (value == null) { 407 throw new IllegalArgumentException("Tried to set a null SimpleValue (" + this + ")"); 408 } 409 setSimpleValue(value); // update memory 410 storeSimpleValue(); // update DB, "abstract" 411 } 412 413 414 415 // === Delete === 416 417 /** 418 * Deletes 1) this DeepaMehta object's child topics (recursively) which have an underlying association definition of 419 * type "Composition Definition" and 2) deletes all the remaining direct associations of this DeepaMehta object. 420 * <p> 421 * Note: deletion of the object itself is up to the subclasses. ### FIXDOC 422 */ 423 final void delete() { 424 try { 425 em.fireEvent(getPreDeleteEvent(), instantiate()); 426 preDelete(); 427 // 428 // delete child topics (recursively) 429 for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) { 430 if (assocDef.getTypeUri().equals("dm4.core.composition_def")) { 431 for (TopicModel childTopic : getRelatedTopics(assocDef.getInstanceLevelAssocTypeUri(), 432 "dm4.core.parent", "dm4.core.child", assocDef.getChildTypeUri())) { 433 ((DeepaMehtaObjectModelImpl) childTopic).delete(); 434 } 435 } 436 } 437 // delete direct associations 438 for (AssociationModel assoc : getAssociations()) { 439 ((DeepaMehtaObjectModelImpl) assoc).delete(); 440 } 441 // delete object itself 442 logger.info("Deleting " + className() + " " + id + " (typeUri=\"" + typeUri + "\")"); 443 _delete(); 444 // 445 postDelete(); 446 // 447 Directives.get().add(getDeleteDirective(), this); 448 em.fireEvent(getPostDeleteEvent(), this); 449 } catch (IllegalStateException e) { 450 // Note: getAssociations() might throw IllegalStateException and is no problem. 451 // This can happen when this object is an association which is already deleted. 452 // 453 // Consider this particular situation: let A1 and A2 be associations of this object and let A2 point to A1. 454 // If A1 gets deleted first (the association set order is non-deterministic), A2 is implicitely deleted 455 // with it (because it is a direct association of A1 as well). Then when the loop comes to A2 456 // "IllegalStateException: Node[1327] has been deleted in this tx" is thrown because A2 has been deleted 457 // already. (The Node appearing in the exception is the middle node of A2.) If, on the other hand, A2 458 // gets deleted first no error would occur. 459 // 460 // This particular situation exists when e.g. a topicmap is deleted while one of its mapcontext 461 // associations is also a part of the topicmap itself. This originates e.g. when the user reveals 462 // a topicmap's mapcontext association and then deletes the topicmap. 463 // 464 if (e.getMessage().equals("Node[" + id + "] has been deleted in this tx")) { 465 logger.info("### Association " + id + " has already been deleted in this transaction. This can " + 466 "happen while deleting a topic with associations A1 and A2 while A2 points to A1 (" + this + ")"); 467 } else { 468 throw e; 469 } 470 } catch (Exception e) { 471 throw new RuntimeException("Deleting " + className() + " " + id + " failed (" + this + ")", e); 472 } 473 } 474 475 476 477 // === Update Child Topics === 478 479 // ### TODO: make this private. See comment in DeepaMehtaObjectImpl.setChildTopics() 480 void _updateChildTopics(ChildTopicsModel newModel) { 481 try { 482 for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) { 483 String assocDefUri = assocDef.getAssocDefUri(); 484 String cardinalityUri = assocDef.getChildCardinalityUri(); 485 RelatedTopicModel newChildTopic = null; // only used for "one" 486 List<? extends RelatedTopicModel> newChildTopics = null; // only used for "many" 487 if (cardinalityUri.equals("dm4.core.one")) { 488 newChildTopic = newModel.getTopicOrNull(assocDefUri); 489 // skip if not contained in update request 490 if (newChildTopic == null) { 491 continue; 492 } 493 } else if (cardinalityUri.equals("dm4.core.many")) { 494 newChildTopics = newModel.getTopicsOrNull(assocDefUri); 495 // skip if not contained in update request 496 if (newChildTopics == null) { 497 continue; 498 } 499 } else { 500 throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI"); 501 } 502 // 503 updateChildTopics(newChildTopic, newChildTopics, assocDef); 504 } 505 // 506 recalculateParentLabel(); 507 // 508 } catch (Exception e) { 509 throw new RuntimeException("Updating the child topics of " + objectInfo() + " failed", e); 510 } 511 } 512 513 // Note: the given association definition must not necessarily originate from the parent object's type definition. 514 // It may originate from a facet definition as well. 515 // Called from DeepaMehtaObjectImpl.updateChildTopic() and DeepaMehtaObjectImpl.updateChildTopics(). 516 // ### TODO: make this private? See comments in DeepaMehtaObjectImpl. 517 void updateChildTopics(RelatedTopicModel newChildTopic, List<? extends RelatedTopicModel> newChildTopics, 518 AssociationDefinitionModel assocDef) { 519 // Note: updating the child topics requires them to be loaded 520 loadChildTopics(assocDef); 521 // 522 String assocTypeUri = assocDef.getTypeUri(); 523 boolean one = newChildTopic != null; 524 if (assocTypeUri.equals("dm4.core.composition_def")) { 525 if (one) { 526 updateCompositionOne(newChildTopic, assocDef); 527 } else { 528 updateCompositionMany(newChildTopics, assocDef); 529 } 530 } else if (assocTypeUri.equals("dm4.core.aggregation_def")) { 531 if (one) { 532 updateAggregationOne(newChildTopic, assocDef); 533 } else { 534 updateAggregationMany(newChildTopics, assocDef); 535 } 536 } else { 537 throw new RuntimeException("Association type \"" + assocTypeUri + "\" not supported"); 538 } 539 } 540 541 // --- 542 543 /** 544 * Loads the child topics which are not loaded already. 545 */ 546 void loadChildTopics() { 547 for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) { 548 loadChildTopics(assocDef); 549 } 550 } 551 552 /** 553 * Loads the child topics for the given assoc def, provided they are not loaded already. 554 */ 555 void loadChildTopics(String assocDefUri) { 556 try { 557 loadChildTopics(getAssocDef(assocDefUri)); 558 } catch (Exception e) { 559 throw new RuntimeException("Loading \"" + assocDefUri + "\" child topics of " + objectInfo() + " failed", 560 e); 561 } 562 } 563 564 // --- 565 566 /** 567 * Calculates the simple value that is to be indexed for this object. 568 * 569 * HTML tags are stripped from HTML values. Non-HTML values are returned directly. 570 */ 571 SimpleValue getIndexValue() { 572 SimpleValue value = getSimpleValue(); 573 if (getType().getDataTypeUri().equals("dm4.core.html")) { 574 return new SimpleValue(JavaUtils.stripHTML(value.toString())); 575 } else { 576 return value; 577 } 578 } 579 580 boolean uriChange(String newUri, String compareUri) { 581 return newUri != null && !newUri.equals(compareUri); 582 } 583 584 boolean isSimple() { 585 return !getType().getDataTypeUri().equals("dm4.core.composite"); 586 } 587 588 589 590 // ------------------------------------------------------------------------------------------------- Private Methods 591 592 // ### TODO: a principal copy exists in Neo4jStorage. 593 // Should this be package private? Should Neo4jStorage have access to the Core's impl package? 594 private void setDefaults() { 595 if (getUri() == null) { 596 setUri(""); 597 } 598 if (getSimpleValue() == null) { 599 setSimpleValue(""); 600 } 601 } 602 603 /** 604 * Recursively loads child topics (model) and updates this attached object cache accordingly. ### FIXDOC 605 * If the child topics are loaded already nothing is performed. 606 * 607 * @param assocDef the child topics according to this association definition are loaded. 608 * <p> 609 * Note: the association definition must not necessarily originate from the parent object's 610 * type definition. It may originate from a facet definition as well. 611 */ 612 private void loadChildTopics(AssociationDefinitionModel assocDef) { 613 String assocDefUri = assocDef.getAssocDefUri(); 614 if (!childTopics.has(assocDefUri)) { 615 logger.fine("### Lazy-loading \"" + assocDefUri + "\" child topic(s) of " + objectInfo()); 616 pl.valueStorage.fetchChildTopics(this, assocDef); 617 } 618 } 619 620 private void recalculateParentLabel() { 621 List<String> labelAssocDefUris = null; 622 try { 623 // load required childs 624 labelAssocDefUris = pl.valueStorage.getLabelAssocDefUris(this); 625 for (String assocDefUri : labelAssocDefUris) { 626 loadChildTopics(assocDefUri); 627 } 628 // 629 pl.valueStorage.recalculateLabel(this); 630 } catch (Exception e) { 631 throw new RuntimeException("Recalculating the label of " + objectInfo() + 632 " failed (assoc defs involved: " + labelAssocDefUris + ")", e); 633 } 634 } 635 636 637 638 // === Update (memory + DB) === 639 640 private void _updateUri(String newUri) { 641 if (uriChange(newUri, uri)) { // abort if no update is requested 642 logger.info("### Changing URI of " + className() + " " + id + " from \"" + uri + 643 "\" -> \"" + newUri + "\""); 644 updateUri(newUri); 645 } 646 } 647 648 private void _updateTypeUri(String newTypeUri) { 649 if (newTypeUri != null && !newTypeUri.equals(typeUri)) { // abort if no update is requested 650 logger.info("### Changing type URI of " + className() + " " + id + " from \"" + typeUri + 651 "\" -> \"" + newTypeUri + "\""); 652 updateTypeUri(newTypeUri); 653 } 654 } 655 656 private void _updateSimpleValue(SimpleValue newValue) { 657 if (newValue != null && !newValue.equals(value)) { // abort if no update is requested 658 logger.info("### Changing simple value of " + className() + " " + id + " from \"" + value + 659 "\" -> \"" + newValue + "\""); 660 updateSimpleValue(newValue); 661 } 662 } 663 664 665 666 // === Update Child Topics === 667 668 // --- Composition --- 669 670 private void updateCompositionOne(RelatedTopicModel newChildTopic, AssociationDefinitionModel assocDef) { 671 RelatedTopicModelImpl childTopic = childTopics.getTopicOrNull(assocDef.getAssocDefUri()); 672 // Note: for cardinality one the simple request format is sufficient. The child's topic ID is not required. 673 // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic. 674 if (newChildTopic instanceof TopicDeletionModel) { 675 deleteChildTopicOne(childTopic, assocDef, true); // deleteChild=true 676 } else if (newChildTopic instanceof TopicReferenceModel) { 677 createAssignmentOne(childTopic, (TopicReferenceModel) newChildTopic, assocDef, true); // deleteChild=true 678 } else if (childTopic != null) { 679 updateRelatedTopic(childTopic, newChildTopic); 680 } else { 681 createChildTopicOne(newChildTopic, assocDef); 682 } 683 } 684 685 private void updateCompositionMany(List<? extends RelatedTopicModel> newChildTopics, 686 AssociationDefinitionModel assocDef) { 687 for (RelatedTopicModel newChildTopic : newChildTopics) { 688 long childTopicId = newChildTopic.getId(); 689 if (newChildTopic instanceof TopicDeletionModel) { 690 deleteChildTopicMany(childTopicId, assocDef, true); // deleteChild=true 691 } else if (newChildTopic instanceof TopicReferenceModel) { 692 createAssignmentMany((TopicReferenceModelImpl) newChildTopic, assocDef); 693 } else if (childTopicId != -1) { 694 updateChildTopicMany(newChildTopic, assocDef); 695 } else { 696 createChildTopicMany(newChildTopic, assocDef); 697 } 698 } 699 } 700 701 // --- Aggregation --- 702 703 private void updateAggregationOne(RelatedTopicModel newChildTopic, AssociationDefinitionModel assocDef) { 704 RelatedTopicModelImpl childTopic = childTopics.getTopicOrNull(assocDef.getAssocDefUri()); 705 // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic. 706 if (newChildTopic instanceof TopicDeletionModel) { 707 deleteChildTopicOne(childTopic, assocDef, false); // deleteChild=false 708 } else if (newChildTopic instanceof TopicReferenceModel) { 709 createAssignmentOne(childTopic, (TopicReferenceModel) newChildTopic, assocDef, false); // deleteChild=false 710 } else if (newChildTopic.getId() != -1) { 711 updateChildTopicOne(newChildTopic, assocDef); 712 } else { 713 if (childTopic != null) { 714 childTopic.getRelatingAssociation().delete(); 715 } 716 createChildTopicOne(newChildTopic, assocDef); 717 } 718 } 719 720 private void updateAggregationMany(List<? extends RelatedTopicModel> newChildTopics, 721 AssociationDefinitionModel assocDef) { 722 for (RelatedTopicModel newChildTopic : newChildTopics) { 723 long childTopicId = newChildTopic.getId(); 724 if (newChildTopic instanceof TopicDeletionModel) { 725 deleteChildTopicMany(childTopicId, assocDef, false); // deleteChild=false 726 } else if (newChildTopic instanceof TopicReferenceModel) { 727 createAssignmentMany((TopicReferenceModelImpl) newChildTopic, assocDef); 728 } else if (childTopicId != -1) { 729 updateChildTopicMany(newChildTopic, assocDef); 730 } else { 731 createChildTopicMany(newChildTopic, assocDef); 732 } 733 } 734 } 735 736 // --- Update --- 737 738 private void updateChildTopicOne(RelatedTopicModel newChildTopic, AssociationDefinitionModel assocDef) { 739 RelatedTopicModelImpl childTopic = childTopics.getTopicOrNull(assocDef.getAssocDefUri()); 740 // 741 if (childTopic == null || childTopic.getId() != newChildTopic.getId()) { 742 throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " + 743 objectInfo() + " according to " + assocDef); 744 } 745 // 746 updateRelatedTopic(childTopic, newChildTopic); 747 // Note: memory is already up-to-date. The child topic is updated in-place of parent. 748 } 749 750 private void updateChildTopicMany(RelatedTopicModel newChildTopic, AssociationDefinitionModel assocDef) { 751 RelatedTopicModelImpl childTopic = childTopics.findChildTopicById(newChildTopic.getId(), assocDef); 752 // 753 if (childTopic == null) { 754 throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " + 755 objectInfo() + " according to " + assocDef); 756 } 757 // 758 updateRelatedTopic(childTopic, newChildTopic); 759 // Note: memory is already up-to-date. The child topic is updated in-place of parent. 760 } 761 762 // --- 763 764 private void updateRelatedTopic(RelatedTopicModelImpl childTopic, RelatedTopicModel newChildTopic) { 765 // update topic 766 childTopic.update(newChildTopic); 767 // update association 768 updateRelatingAssociation(childTopic, newChildTopic); 769 } 770 771 private void updateRelatingAssociation(RelatedTopicModelImpl childTopic, RelatedTopicModel newChildTopic) { 772 childTopic.getRelatingAssociation().update(newChildTopic.getRelatingAssociation()); 773 } 774 775 // --- Create --- 776 777 private void createChildTopicOne(RelatedTopicModel newChildTopic, AssociationDefinitionModel assocDef) { 778 // update DB 779 createAndAssociateChildTopic(newChildTopic, assocDef); 780 // update memory 781 childTopics.putInChildTopics(newChildTopic, assocDef); 782 } 783 784 private void createChildTopicMany(RelatedTopicModel newChildTopic, AssociationDefinitionModel assocDef) { 785 // update DB 786 createAndAssociateChildTopic(newChildTopic, assocDef); 787 // update memory 788 childTopics.addToChildTopics(newChildTopic, assocDef); 789 } 790 791 // --- 792 793 private void createAndAssociateChildTopic(RelatedTopicModel childTopic, AssociationDefinitionModel assocDef) { 794 pl.createTopic(childTopic); 795 associateChildTopic(childTopic, assocDef); 796 } 797 798 // --- Assignment --- 799 800 private void createAssignmentOne(RelatedTopicModelImpl childTopic, TopicReferenceModel newChildTopic, 801 AssociationDefinitionModel assocDef, boolean deleteChildTopic) { 802 if (childTopic != null) { 803 if (newChildTopic.isReferingTo(childTopic)) { 804 updateRelatingAssociation(childTopic, newChildTopic); 805 // Note: memory is already up-to-date. The association is updated in-place of parent. 806 return; 807 } 808 if (deleteChildTopic) { 809 childTopic.delete(); 810 } else { 811 childTopic.getRelatingAssociation().delete(); 812 } 813 } 814 // update DB 815 resolveRefAndAssociateChildTopic(newChildTopic, assocDef); 816 // update memory 817 childTopics.putInChildTopics(newChildTopic, assocDef); 818 } 819 820 private void createAssignmentMany(TopicReferenceModelImpl newChildTopic, AssociationDefinitionModel assocDef) { 821 RelatedTopicModelImpl childTopic = childTopics.findChildTopicByRef(newChildTopic, assocDef); 822 if (childTopic != null) { 823 // Note: "create assignment" is an idempotent operation. A create request for an assignment which 824 // exists already is not an error. Instead, nothing is performed. 825 updateRelatingAssociation(childTopic, newChildTopic); 826 // Note: memory is already up-to-date. The association is updated in-place of parent. 827 return; 828 } 829 // update DB 830 resolveRefAndAssociateChildTopic(newChildTopic, assocDef); 831 // update memory 832 childTopics.addToChildTopics(newChildTopic, assocDef); 833 } 834 835 // --- 836 837 /** 838 * Creates an association between our parent object ("Parent" role) and the referenced topic ("Child" role). 839 * The association type is taken from the given association definition. 840 * 841 * @return the resolved child topic. 842 */ 843 private void resolveRefAndAssociateChildTopic(TopicReferenceModel childTopicRef, 844 AssociationDefinitionModel assocDef) { 845 pl.valueStorage.resolveReference(childTopicRef); 846 associateChildTopic(childTopicRef, assocDef); 847 } 848 849 private void associateChildTopic(RelatedTopicModel childTopic, AssociationDefinitionModel assocDef) { 850 pl.valueStorage.associateChildTopic(this, childTopic, assocDef); 851 } 852 853 // --- Delete --- 854 855 private void deleteChildTopicOne(RelatedTopicModelImpl childTopic, AssociationDefinitionModel assocDef, 856 boolean deleteChildTopic) { 857 if (childTopic == null) { 858 // Note: "delete child"/"delete assignment" is an idempotent operation. A delete request for a 859 // child/assignment which has been deleted already (resp. is non-existing) is not an error. 860 // Instead, nothing is performed. 861 return; 862 } 863 // update DB 864 if (deleteChildTopic) { 865 childTopic.delete(); 866 } else { 867 childTopic.getRelatingAssociation().delete(); 868 } 869 // update memory 870 childTopics.removeChildTopic(assocDef); 871 } 872 873 private void deleteChildTopicMany(long childTopicId, AssociationDefinitionModel assocDef, 874 boolean deleteChildTopic) { 875 RelatedTopicModelImpl childTopic = childTopics.findChildTopicById(childTopicId, assocDef); 876 if (childTopic == null) { 877 // Note: "delete child"/"delete assignment" is an idempotent operation. A delete request for a 878 // child/assignment which has been deleted already (resp. is non-existing) is not an error. 879 // Instead, nothing is performed. 880 return; 881 } 882 // update DB 883 if (deleteChildTopic) { 884 childTopic.delete(); 885 } else { 886 childTopic.getRelatingAssociation().delete(); 887 } 888 // update memory 889 childTopics.removeFromChildTopics(childTopic, assocDef); 890 } 891 892 893 894 // === Helper === 895 896 private AssociationDefinitionModel getAssocDef(String assocDefUri) { 897 // Note: doesn't work for facets 898 return getType().getAssocDef(assocDefUri); 899 } 900 901 private String objectInfo() { 902 return className() + " " + id; 903 } 904}