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 335 DeepaMehtaEvent getPreUpdateEvent() { 336 throw new UnsupportedOperationException(); 337 } 338 339 DeepaMehtaEvent getPostUpdateEvent() { 340 throw new UnsupportedOperationException(); 341 } 342 343 DeepaMehtaEvent getPreDeleteEvent() { 344 throw new UnsupportedOperationException(); 345 } 346 347 DeepaMehtaEvent getPostDeleteEvent() { 348 throw new UnsupportedOperationException(); 349 } 350 351 // --- 352 353 Directive getUpdateDirective() { 354 throw new UnsupportedOperationException(); 355 } 356 357 Directive getDeleteDirective() { 358 throw new UnsupportedOperationException(); 359 } 360 361 362 363 // === Core Internal Hooks === 364 365 void preCreate() { 366 } 367 368 void postCreate() { 369 } 370 371 // --- 372 373 void preUpdate(DeepaMehtaObjectModel updateModel) { 374 } 375 376 void postUpdate(DeepaMehtaObjectModel updateModel, DeepaMehtaObjectModel oldObject) { 377 } 378 379 // --- 380 381 void preDelete() { 382 } 383 384 void postDelete() { 385 } 386 387 388 389 // === Update (memory + DB) === 390 391 final void updateWithChildTopics(ChildTopicsModel childTopics) { 392 update(createModelWithChildTopics(childTopics)); 393 } 394 395 /** 396 * @param updateModel The data to update. 397 * If the URI is <code>null</code> it is not updated. 398 * If the type URI is <code>null</code> it is not updated. 399 * If the simple value is <code>null</code> it is not updated. 400 */ 401 final void update(DeepaMehtaObjectModelImpl updateModel) { 402 try { 403 logger.info("Updating " + objectInfo() + " (typeUri=\"" + typeUri + "\")"); 404 DeepaMehtaObjectModel oldObject = clone(); 405 em.fireEvent(getPreUpdateEvent(), instantiate(), updateModel); 406 // 407 preUpdate(updateModel); 408 // 409 _updateUri(updateModel.getUri()); 410 _updateTypeUri(updateModel.getTypeUri()); 411 if (isSimple()) { 412 _updateSimpleValue(updateModel.getSimpleValue()); 413 } else { 414 _updateChildTopics(updateModel.getChildTopicsModel()); 415 } 416 // 417 postUpdate(updateModel, oldObject); 418 // 419 // Note: in case of a type topic the instantiate() call above creates a cloned model 420 // that doesn't reflect the update. Here we instantiate the now updated model. 421 DeepaMehtaObject object = instantiate(); 422 Directives.get().add(getUpdateDirective(), object); 423 em.fireEvent(getPostUpdateEvent(), object, updateModel, oldObject); 424 } catch (Exception e) { 425 throw new RuntimeException("Updating " + objectInfo() + " failed (typeUri=\"" + typeUri + "\")", e); 426 } 427 } 428 429 // --- 430 431 final void updateUri(String uri) { 432 setUri(uri); // update memory 433 storeUri(); // update DB, "abstract" 434 } 435 436 final void updateTypeUri(String typeUri) { 437 setTypeUri(typeUri); // update memory 438 storeTypeUri(); // update DB, "abstract" 439 } 440 441 final void updateSimpleValue(SimpleValue value) { 442 if (value == null) { 443 throw new IllegalArgumentException("Tried to set a null SimpleValue (" + this + ")"); 444 } 445 setSimpleValue(value); // update memory 446 storeSimpleValue(); // update DB, "abstract" 447 } 448 449 450 451 // === Delete === 452 453 /** 454 * Deletes 1) this DeepaMehta object's child topics (recursively) which have an underlying association definition of 455 * type "Composition Definition" and 2) deletes all the remaining direct associations of this DeepaMehta object. 456 * <p> 457 * Note: deletion of the object itself is up to the subclasses. ### FIXDOC 458 */ 459 final void delete() { 460 try { 461 em.fireEvent(getPreDeleteEvent(), instantiate()); 462 // 463 preDelete(); 464 // 465 // delete child topics (recursively) 466 for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) { 467 if (assocDef.getTypeUri().equals("dm4.core.composition_def")) { 468 for (TopicModelImpl childTopic : getRelatedTopics(assocDef.getInstanceLevelAssocTypeUri(), 469 "dm4.core.parent", "dm4.core.child", assocDef.getChildTypeUri())) { 470 childTopic.delete(); 471 } 472 } 473 } 474 // delete direct associations 475 for (AssociationModelImpl assoc : getAssociations()) { 476 assoc.delete(); 477 } 478 // delete object itself 479 logger.info("Deleting " + objectInfo() + " (typeUri=\"" + typeUri + "\")"); 480 _delete(); 481 // 482 postDelete(); 483 // 484 Directives.get().add(getDeleteDirective(), this); 485 em.fireEvent(getPostDeleteEvent(), this); 486 } catch (IllegalStateException e) { 487 // Note: getAssociations() might throw IllegalStateException and is no problem. 488 // This can happen when this object is an association which is already deleted. 489 // 490 // Consider this particular situation: let A1 and A2 be associations of this object and let A2 point to A1. 491 // If A1 gets deleted first (the association set order is non-deterministic), A2 is implicitely deleted 492 // with it (because it is a direct association of A1 as well). Then when the loop comes to A2 493 // "IllegalStateException: Node[1327] has been deleted in this tx" is thrown because A2 has been deleted 494 // already. (The Node appearing in the exception is the middle node of A2.) If, on the other hand, A2 495 // gets deleted first no error would occur. 496 // 497 // This particular situation exists when e.g. a topicmap is deleted while one of its mapcontext 498 // associations is also a part of the topicmap itself. This originates e.g. when the user reveals 499 // a topicmap's mapcontext association and then deletes the topicmap. 500 // 501 if (e.getMessage().equals("Node[" + id + "] has been deleted in this tx")) { 502 logger.info("### Association " + id + " has already been deleted in this transaction. This can " + 503 "happen while deleting a topic with associations A1 and A2 while A2 points to A1 (" + this + ")"); 504 } else { 505 throw e; 506 } 507 } catch (Exception e) { 508 throw new RuntimeException("Deleting " + objectInfo() + " failed (typeUri=\"" + typeUri + "\")", e); 509 } 510 } 511 512 513 514 // === Update Child Topics (memory + DB) === 515 516 // ### TODO: make this private. See comment in DeepaMehtaObjectImpl.setChildTopics() 517 final void _updateChildTopics(ChildTopicsModelImpl updateModel) { 518 try { 519 for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) { 520 String assocDefUri = assocDef.getAssocDefUri(); 521 String cardinalityUri = assocDef.getChildCardinalityUri(); 522 RelatedTopicModelImpl newChildTopic = null; // only used for "one" 523 List<RelatedTopicModelImpl> newChildTopics = null; // only used for "many" 524 if (cardinalityUri.equals("dm4.core.one")) { 525 newChildTopic = updateModel.getTopicOrNull(assocDefUri); 526 // skip if not contained in update request 527 if (newChildTopic == null) { 528 continue; 529 } 530 } else if (cardinalityUri.equals("dm4.core.many")) { 531 newChildTopics = updateModel.getTopicsOrNull(assocDefUri); 532 // skip if not contained in update request 533 if (newChildTopics == null) { 534 continue; 535 } 536 } else { 537 throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI"); 538 } 539 // 540 updateChildTopics(newChildTopic, newChildTopics, assocDef); 541 } 542 // 543 _calculateLabelAndUpdate(); 544 // 545 } catch (Exception e) { 546 throw new RuntimeException("Updating the child topics of " + objectInfo() + " failed", e); 547 } 548 } 549 550 // Note: the given association definition must not necessarily originate from the parent object's type definition. 551 // It may originate from a facet definition as well. 552 // Called from DeepaMehtaObjectImpl.updateChildTopic() and DeepaMehtaObjectImpl.updateChildTopics(). 553 // ### TODO: make this private? See comments in DeepaMehtaObjectImpl. 554 final void updateChildTopics(RelatedTopicModelImpl newChildTopic, List<RelatedTopicModelImpl> newChildTopics, 555 AssociationDefinitionModel assocDef) { 556 // Note: updating the child topics requires them to be loaded 557 loadChildTopics(assocDef); 558 // 559 String assocTypeUri = assocDef.getTypeUri(); 560 boolean one = newChildTopic != null; 561 if (assocTypeUri.equals("dm4.core.composition_def")) { 562 if (one) { 563 updateCompositionOne(newChildTopic, assocDef); 564 } else { 565 updateCompositionMany(newChildTopics, assocDef); 566 } 567 } else if (assocTypeUri.equals("dm4.core.aggregation_def")) { 568 if (one) { 569 updateAggregationOne(newChildTopic, assocDef); 570 } else { 571 updateAggregationMany(newChildTopics, assocDef); 572 } 573 } else { 574 throw new RuntimeException("Association type \"" + assocTypeUri + "\" not supported"); 575 } 576 } 577 578 // --- 579 580 /** 581 * Loads the child topics which are not loaded already. 582 */ 583 final DeepaMehtaObjectModel loadChildTopics() { 584 for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) { 585 loadChildTopics(assocDef); 586 } 587 return this; 588 } 589 590 /** 591 * Loads the child topics for the given assoc def, provided they are not loaded already. 592 */ 593 final DeepaMehtaObjectModel loadChildTopics(String assocDefUri) { 594 try { 595 return loadChildTopics(getAssocDef(assocDefUri)); 596 } catch (Exception e) { 597 throw new RuntimeException("Loading \"" + assocDefUri + "\" child topics of " + objectInfo() + " failed", 598 e); 599 } 600 } 601 602 // --- 603 604 /** 605 * Calculates the simple value that is to be indexed for this object. 606 * 607 * HTML tags are stripped from HTML values. Non-HTML values are returned directly. 608 */ 609 SimpleValue getIndexValue() { 610 SimpleValue value = getSimpleValue(); 611 if (getType().getDataTypeUri().equals("dm4.core.html")) { 612 return new SimpleValue(JavaUtils.stripHTML(value.toString())); 613 } else { 614 return value; 615 } 616 } 617 618 boolean uriChange(String newUri, String compareUri) { 619 return newUri != null && !newUri.equals(compareUri); 620 } 621 622 boolean isSimple() { 623 return !getType().getDataTypeUri().equals("dm4.core.composite"); 624 } 625 626 627 628 // ------------------------------------------------------------------------------------------------- Private Methods 629 630 // ### TODO: a principal copy exists in Neo4jStorage. 631 // Should this be package private? Should Neo4jStorage have access to the Core's impl package? 632 private void setDefaults() { 633 if (getUri() == null) { 634 setUri(""); 635 } 636 if (getSimpleValue() == null) { 637 setSimpleValue(""); 638 } 639 } 640 641 /** 642 * Recursively loads child topics (model) and updates this attached object cache accordingly. ### FIXDOC 643 * If the child topics are loaded already nothing is performed. 644 * 645 * @param assocDef the child topics according to this association definition are loaded. 646 * <p> 647 * Note: the association definition must not necessarily originate from the parent object's 648 * type definition. It may originate from a facet definition as well. 649 */ 650 private DeepaMehtaObjectModel loadChildTopics(AssociationDefinitionModel assocDef) { 651 String assocDefUri = assocDef.getAssocDefUri(); 652 if (!childTopics.has(assocDefUri)) { 653 logger.fine("### Lazy-loading \"" + assocDefUri + "\" child topic(s) of " + objectInfo()); 654 pl.valueStorage.fetchChildTopics(this, assocDef); 655 } 656 return this; 657 } 658 659 660 661 // === Update (memory + DB) === 662 663 private void _updateUri(String newUri) { 664 if (uriChange(newUri, uri)) { // abort if no update is requested 665 logger.info("### Changing URI of " + objectInfo() + " from \"" + uri + "\" -> \"" + newUri + "\""); 666 updateUri(newUri); 667 } 668 } 669 670 private void _updateTypeUri(String newTypeUri) { 671 if (newTypeUri != null && !newTypeUri.equals(typeUri)) { // abort if no update is requested 672 logger.info("### Changing type URI of " + objectInfo() + " from \"" + typeUri + "\" -> \"" + newTypeUri + 673 "\""); 674 updateTypeUri(newTypeUri); 675 } 676 } 677 678 private void _updateSimpleValue(SimpleValue newValue) { 679 if (newValue != null && !newValue.equals(value)) { // abort if no update is requested 680 logger.info("### Changing simple value of " + objectInfo() + " from \"" + value + "\" -> \"" + newValue + 681 "\""); 682 updateSimpleValue(newValue); 683 } 684 } 685 686 687 688 // === Update Child Topics (memory + DB) === 689 690 // --- Composition --- 691 692 private void updateCompositionOne(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) { 693 RelatedTopicModelImpl childTopic = childTopics.getTopicOrNull(assocDef.getAssocDefUri()); 694 // Note: for cardinality one the simple request format is sufficient. The child's topic ID is not required. 695 // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic. 696 if (newChildTopic instanceof TopicDeletionModel) { 697 deleteChildTopicOne(childTopic, assocDef, true); // deleteChild=true 698 } else if (newChildTopic instanceof TopicReferenceModel) { 699 createAssignmentOne(childTopic, (TopicReferenceModelImpl) newChildTopic, assocDef, true);// deleteChild=true 700 } else if (childTopic != null) { 701 updateRelatedTopic(childTopic, newChildTopic); 702 } else { 703 createChildTopicOne(newChildTopic, assocDef); 704 } 705 } 706 707 private void updateCompositionMany(List<RelatedTopicModelImpl> newChildTopics, 708 AssociationDefinitionModel assocDef) { 709 for (RelatedTopicModelImpl newChildTopic : newChildTopics) { 710 long childTopicId = newChildTopic.getId(); 711 if (newChildTopic instanceof TopicDeletionModel) { 712 deleteChildTopicMany(childTopicId, assocDef, true); // deleteChild=true 713 } else if (newChildTopic instanceof TopicReferenceModel) { 714 createAssignmentMany((TopicReferenceModelImpl) newChildTopic, assocDef); 715 } else if (childTopicId != -1) { 716 updateChildTopicMany(newChildTopic, assocDef); 717 } else { 718 createChildTopicMany(newChildTopic, assocDef); 719 } 720 } 721 } 722 723 // --- Aggregation --- 724 725 private void updateAggregationOne(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) { 726 RelatedTopicModelImpl childTopic = childTopics.getTopicOrNull(assocDef.getAssocDefUri()); 727 // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic. 728 if (newChildTopic instanceof TopicDeletionModel) { 729 deleteChildTopicOne(childTopic, assocDef, false); // deleteChild=false 730 } else if (newChildTopic instanceof TopicReferenceModel) { 731 createAssignmentOne(childTopic, (TopicReferenceModelImpl) newChildTopic, assocDef, false); 732 } else if (newChildTopic.getId() != -1) { // deleteChild=false 733 updateChildTopicOne(newChildTopic, assocDef); 734 } else { 735 if (childTopic != null) { 736 childTopic.getRelatingAssociation().delete(); 737 } 738 createChildTopicOne(newChildTopic, assocDef); 739 } 740 } 741 742 private void updateAggregationMany(List<RelatedTopicModelImpl> newChildTopics, 743 AssociationDefinitionModel assocDef) { 744 for (RelatedTopicModelImpl newChildTopic : newChildTopics) { 745 long childTopicId = newChildTopic.getId(); 746 if (newChildTopic instanceof TopicDeletionModel) { 747 deleteChildTopicMany(childTopicId, assocDef, false); // deleteChild=false 748 } else if (newChildTopic instanceof TopicReferenceModel) { 749 createAssignmentMany((TopicReferenceModelImpl) newChildTopic, assocDef); 750 } else if (childTopicId != -1) { 751 updateChildTopicMany(newChildTopic, assocDef); 752 } else { 753 createChildTopicMany(newChildTopic, assocDef); 754 } 755 } 756 } 757 758 // --- Update --- 759 760 private void updateChildTopicOne(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) { 761 RelatedTopicModelImpl childTopic = childTopics.getTopicOrNull(assocDef.getAssocDefUri()); 762 // 763 if (childTopic == null || childTopic.getId() != newChildTopic.getId()) { 764 throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " + objectInfo() + 765 " according to " + assocDef); 766 } 767 // 768 updateRelatedTopic(childTopic, newChildTopic); 769 // Note: memory is already up-to-date. The child topic is updated in-place of parent. 770 } 771 772 private void updateChildTopicMany(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) { 773 RelatedTopicModelImpl childTopic = childTopics.findChildTopicById(newChildTopic.getId(), assocDef); 774 // 775 if (childTopic == null) { 776 throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " + objectInfo() + 777 " according to " + assocDef); 778 } 779 // 780 updateRelatedTopic(childTopic, newChildTopic); 781 // Note: memory is already up-to-date. The child topic is updated in-place of parent. 782 } 783 784 // --- 785 786 private void updateRelatedTopic(RelatedTopicModelImpl childTopic, RelatedTopicModelImpl newChildTopic) { 787 // update topic 788 childTopic.update(newChildTopic); 789 // update association 790 updateRelatingAssociation(childTopic, newChildTopic); 791 } 792 793 private void updateRelatingAssociation(RelatedTopicModelImpl childTopic, RelatedTopicModelImpl newChildTopic) { 794 childTopic.getRelatingAssociation().update(newChildTopic.getRelatingAssociation()); 795 } 796 797 // --- Create --- 798 799 private void createChildTopicOne(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) { 800 // update DB 801 createAndAssociateChildTopic(newChildTopic, assocDef); 802 // update memory 803 childTopics.putInChildTopics(newChildTopic, assocDef); 804 } 805 806 private void createChildTopicMany(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) { 807 // update DB 808 createAndAssociateChildTopic(newChildTopic, assocDef); 809 // update memory 810 childTopics.addToChildTopics(newChildTopic, assocDef); 811 } 812 813 // --- 814 815 private void createAndAssociateChildTopic(RelatedTopicModelImpl childTopic, AssociationDefinitionModel assocDef) { 816 pl.createTopic(childTopic); 817 associateChildTopic(childTopic, assocDef); 818 } 819 820 // --- Assignment --- 821 822 private void createAssignmentOne(RelatedTopicModelImpl childTopic, TopicReferenceModelImpl newChildTopic, 823 AssociationDefinitionModel assocDef, boolean deleteChildTopic) { 824 if (childTopic != null) { 825 if (newChildTopic.isReferingTo(childTopic)) { 826 updateRelatingAssociation(childTopic, newChildTopic); 827 // Note: memory is already up-to-date. The association is updated in-place of parent. 828 return; 829 } 830 if (deleteChildTopic) { 831 childTopic.delete(); 832 } else { 833 childTopic.getRelatingAssociation().delete(); 834 } 835 } 836 // update DB 837 resolveRefAndAssociateChildTopic(newChildTopic, assocDef); 838 // update memory 839 childTopics.putInChildTopics(newChildTopic, assocDef); 840 } 841 842 private void createAssignmentMany(TopicReferenceModelImpl newChildTopic, AssociationDefinitionModel assocDef) { 843 RelatedTopicModelImpl childTopic = childTopics.findChildTopicByRef(newChildTopic, assocDef); 844 if (childTopic != null) { 845 // Note: "create assignment" is an idempotent operation. A create request for an assignment which 846 // exists already is not an error. Instead, nothing is performed. 847 updateRelatingAssociation(childTopic, newChildTopic); 848 // Note: memory is already up-to-date. The association is updated in-place of parent. 849 return; 850 } 851 // update DB 852 resolveRefAndAssociateChildTopic(newChildTopic, assocDef); 853 // update memory 854 childTopics.addToChildTopics(newChildTopic, assocDef); 855 } 856 857 // --- 858 859 /** 860 * Creates an association between our parent object ("Parent" role) and the referenced topic ("Child" role). 861 * The association type is taken from the given association definition. 862 * 863 * @return the resolved child topic. 864 */ 865 private void resolveRefAndAssociateChildTopic(TopicReferenceModel childTopicRef, 866 AssociationDefinitionModel assocDef) { 867 pl.valueStorage.resolveReference(childTopicRef); 868 associateChildTopic(childTopicRef, assocDef); 869 } 870 871 private void associateChildTopic(RelatedTopicModel childTopic, AssociationDefinitionModel assocDef) { 872 pl.valueStorage.associateChildTopic(this, childTopic, assocDef); 873 } 874 875 // --- Delete --- 876 877 private void deleteChildTopicOne(RelatedTopicModelImpl childTopic, AssociationDefinitionModel assocDef, 878 boolean deleteChildTopic) { 879 if (childTopic == null) { 880 // Note: "delete child"/"delete assignment" is an idempotent operation. A delete request for a 881 // child/assignment which has been deleted already (resp. is non-existing) is not an error. 882 // Instead, nothing is performed. 883 return; 884 } 885 // update DB 886 if (deleteChildTopic) { 887 childTopic.delete(); 888 } else { 889 childTopic.getRelatingAssociation().delete(); 890 } 891 // update memory 892 childTopics.removeChildTopic(assocDef); 893 } 894 895 private void deleteChildTopicMany(long childTopicId, AssociationDefinitionModel assocDef, 896 boolean deleteChildTopic) { 897 RelatedTopicModelImpl childTopic = childTopics.findChildTopicById(childTopicId, assocDef); 898 if (childTopic == null) { 899 // Note: "delete child"/"delete assignment" is an idempotent operation. A delete request for a 900 // child/assignment which has been deleted already (resp. is non-existing) is not an error. 901 // Instead, nothing is performed. 902 return; 903 } 904 // update DB 905 if (deleteChildTopic) { 906 childTopic.delete(); 907 } else { 908 childTopic.getRelatingAssociation().delete(); 909 } 910 // update memory 911 childTopics.removeFromChildTopics(childTopic, assocDef); 912 } 913 914 915 916 // === Label Calculation === 917 918 private void _calculateLabelAndUpdate() { 919 List<String> labelAssocDefUris = null; 920 try { 921 // load required childs 922 labelAssocDefUris = getLabelAssocDefUris(); 923 for (String assocDefUri : labelAssocDefUris) { 924 loadChildTopics(assocDefUri); 925 } 926 // 927 calculateLabelAndUpdate(); 928 } catch (Exception e) { 929 throw new RuntimeException("Calculating and updating label of " + objectInfo() + 930 " failed (assoc defs involved: " + labelAssocDefUris + ")", e); 931 } 932 } 933 934 /** 935 * Calculates the label for this object model and updates it in both, memory (in-place), and DB. 936 * <p> 937 * Prerequisites: 938 * 1) this object model is a composite. 939 * 2) this object model contains all the child topic models involved in the label calculation. 940 * Note: this method does not load any child topics from DB. 941 * 942 * ### TODO: make private 943 */ 944 void calculateLabelAndUpdate() { 945 try { 946 updateSimpleValue(new SimpleValue(calculateLabel())); 947 } catch (Exception e) { 948 throw new RuntimeException("Calculating and updating label of " + objectInfo() + " failed", e); 949 } 950 } 951 952 /** 953 * Calculates the label for this object model recursively. Recursion ends at a simple object model. 954 * <p> 955 * Note: called from this class only but can't be private as called on a different object. 956 */ 957 String calculateLabel() { 958 TypeModel type = getType(); 959 if (type.getDataTypeUri().equals("dm4.core.composite")) { 960 StringBuilder builder = new StringBuilder(); 961 for (String assocDefUri : getLabelAssocDefUris()) { 962 appendLabel(calculateChildLabel(assocDefUri), builder, LABEL_CHILD_SEPARATOR); 963 } 964 return builder.toString(); 965 } else { 966 return getSimpleValue().toString(); 967 } 968 } 969 970 private String calculateChildLabel(String assocDefUri) { 971 Object value = getChildTopicsModel().get(assocDefUri); 972 // Note: topics just created have no child topics yet 973 if (value == null) { 974 return ""; 975 } 976 // 977 if (value instanceof TopicModel) { 978 // single value 979 return ((TopicModelImpl) value).calculateLabel(); // recursion 980 } else if (value instanceof List) { 981 // multiple value 982 StringBuilder builder = new StringBuilder(); 983 for (TopicModelImpl childTopic : (List<TopicModelImpl>) value) { 984 appendLabel(childTopic.calculateLabel(), builder, LABEL_TOPIC_SEPARATOR); // recursion 985 } 986 return builder.toString(); 987 } else { 988 throw new RuntimeException("Unexpected value in a ChildTopicsModel: " + value); 989 } 990 } 991 992 private void appendLabel(String label, StringBuilder builder, String separator) { 993 // add separator 994 if (builder.length() > 0 && label.length() > 0) { 995 builder.append(separator); 996 } 997 // 998 builder.append(label); 999 } 1000 1001 /** 1002 * Prerequisite: this is a composite model. 1003 */ 1004 private List<String> getLabelAssocDefUris() { 1005 TypeModelImpl type = getType(); 1006 List<String> labelConfig = type.getLabelConfig(); 1007 if (labelConfig.size() > 0) { 1008 return labelConfig; 1009 } else { 1010 List<String> assocDefUris = new ArrayList(); 1011 Iterator<? extends AssociationDefinitionModel> i = type.getAssocDefs().iterator(); 1012 // Note: types just created might have no child types yet 1013 if (i.hasNext()) { 1014 assocDefUris.add(i.next().getAssocDefUri()); 1015 } 1016 return assocDefUris; 1017 } 1018 } 1019 1020 1021 1022 // === Helper === 1023 1024 private AssociationDefinitionModel getAssocDef(String assocDefUri) { 1025 // Note: doesn't work for facets 1026 return getType().getAssocDef(assocDefUri); 1027 } 1028 1029 private String objectInfo() { 1030 return className() + " " + id; 1031 } 1032}