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