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