001 package de.deepamehta.core.impl; 002 003 import de.deepamehta.core.Association; 004 import de.deepamehta.core.AssociationDefinition; 005 import de.deepamehta.core.ChildTopics; 006 import de.deepamehta.core.RelatedTopic; 007 import de.deepamehta.core.Topic; 008 import de.deepamehta.core.model.ChildTopicsModel; 009 import de.deepamehta.core.model.RelatedTopicModel; 010 import de.deepamehta.core.model.SimpleValue; 011 import de.deepamehta.core.model.TopicDeletionModel; 012 import de.deepamehta.core.model.TopicModel; 013 import de.deepamehta.core.model.TopicReferenceModel; 014 import de.deepamehta.core.model.TopicRoleModel; 015 016 import java.util.ArrayList; 017 import java.util.HashMap; 018 import java.util.List; 019 import java.util.Map; 020 import java.util.logging.Logger; 021 022 023 024 /** 025 * A composite value model that is attached to the DB. 026 */ 027 class AttachedChildTopics implements ChildTopics { 028 029 // ---------------------------------------------------------------------------------------------- Instance Variables 030 031 private ChildTopicsModel model; // underlying model 032 033 private AttachedDeepaMehtaObject parent; // attached object cache 034 035 /** 036 * Attached object cache. 037 * Key: child type URI (String), value: AttachedTopic or List<AttachedTopic> 038 */ 039 private Map<String, Object> childTopics = new HashMap(); // attached object cache 040 041 private EmbeddedService dms; 042 043 private Logger logger = Logger.getLogger(getClass().getName()); 044 045 // ---------------------------------------------------------------------------------------------------- Constructors 046 047 AttachedChildTopics(ChildTopicsModel model, AttachedDeepaMehtaObject parent, EmbeddedService dms) { 048 this.model = model; 049 this.parent = parent; 050 this.dms = dms; 051 initAttachedObjectCache(); 052 } 053 054 // -------------------------------------------------------------------------------------------------- Public Methods 055 056 057 058 // ********************************** 059 // *** ChildTopics Implementation *** 060 // ********************************** 061 062 063 064 // === Accessors === 065 066 @Override 067 public Topic getTopic(String childTypeUri) { 068 loadChildTopics(childTypeUri); 069 return _getTopic(childTypeUri); 070 } 071 072 @Override 073 public List<Topic> getTopics(String childTypeUri) { 074 loadChildTopics(childTypeUri); 075 return _getTopics(childTypeUri); 076 } 077 078 // --- 079 080 @Override 081 public Object get(String childTypeUri) { 082 return childTopics.get(childTypeUri); 083 } 084 085 @Override 086 public boolean has(String childTypeUri) { 087 return childTopics.containsKey(childTypeUri); 088 } 089 090 @Override 091 public Iterable<String> childTypeUris() { 092 return childTopics.keySet(); 093 } 094 095 @Override 096 public int size() { 097 return childTopics.size(); 098 } 099 100 // --- 101 102 @Override 103 public ChildTopicsModel getModel() { 104 return model; 105 } 106 107 108 109 // === Convenience Accessors === 110 111 @Override 112 public String getString(String childTypeUri) { 113 return getTopic(childTypeUri).getSimpleValue().toString(); 114 } 115 116 @Override 117 public int getInt(String childTypeUri) { 118 return getTopic(childTypeUri).getSimpleValue().intValue(); 119 } 120 121 @Override 122 public long getLong(String childTypeUri) { 123 return getTopic(childTypeUri).getSimpleValue().longValue(); 124 } 125 126 @Override 127 public double getDouble(String childTypeUri) { 128 return getTopic(childTypeUri).getSimpleValue().doubleValue(); 129 } 130 131 @Override 132 public boolean getBoolean(String childTypeUri) { 133 return getTopic(childTypeUri).getSimpleValue().booleanValue(); 134 } 135 136 @Override 137 public Object getObject(String childTypeUri) { 138 return getTopic(childTypeUri).getSimpleValue().value(); 139 } 140 141 // --- 142 143 @Override 144 public ChildTopics getChildTopics(String childTypeUri) { 145 return getTopic(childTypeUri).getChildTopics(); 146 } 147 148 // Note: there are no convenience accessors for a multiple-valued child. 149 150 151 152 // === Manipulators === 153 154 @Override 155 public ChildTopics set(String childTypeUri, TopicModel value) { 156 return _update(childTypeUri, value); 157 } 158 159 @Override 160 public ChildTopics set(String childTypeUri, Object value) { 161 return _update(childTypeUri, new TopicModel(childTypeUri, new SimpleValue(value))); 162 } 163 164 @Override 165 public ChildTopics set(String childTypeUri, ChildTopicsModel value) { 166 return _update(childTypeUri, new TopicModel(childTypeUri, value)); 167 } 168 169 // --- 170 171 @Override 172 public ChildTopics setRef(String childTypeUri, long refTopicId) { 173 return _update(childTypeUri, new TopicReferenceModel(refTopicId)); 174 } 175 176 @Override 177 public ChildTopics setRef(String childTypeUri, String refTopicUri) { 178 return _update(childTypeUri, new TopicReferenceModel(refTopicUri)); 179 } 180 181 // --- 182 183 @Override 184 public ChildTopics remove(String childTypeUri, long topicId) { 185 return _update(childTypeUri, new TopicDeletionModel(topicId)); 186 } 187 188 189 190 // ----------------------------------------------------------------------------------------- Package Private Methods 191 192 void update(ChildTopicsModel newComp) { 193 try { 194 for (AssociationDefinition assocDef : parent.getType().getAssocDefs()) { 195 String childTypeUri = assocDef.getChildTypeUri(); 196 String cardinalityUri = assocDef.getChildCardinalityUri(); 197 TopicModel newChildTopic = null; // only used for "one" 198 List<TopicModel> newChildTopics = null; // only used for "many" 199 if (cardinalityUri.equals("dm4.core.one")) { 200 newChildTopic = newComp.getTopic(childTypeUri, null); // defaultValue=null 201 // skip if not contained in update request 202 if (newChildTopic == null) { 203 continue; 204 } 205 } else if (cardinalityUri.equals("dm4.core.many")) { 206 newChildTopics = newComp.getTopics(childTypeUri, null); // defaultValue=null 207 // skip if not contained in update request 208 if (newChildTopics == null) { 209 continue; 210 } 211 } else { 212 throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI"); 213 } 214 // 215 updateChildTopics(newChildTopic, newChildTopics, assocDef); 216 } 217 // 218 dms.valueStorage.refreshLabel(parent.getModel()); 219 // 220 } catch (Exception e) { 221 throw new RuntimeException("Updating the child topics of " + parent.className() + " " + parent.getId() + 222 " failed (newComp=" + newComp + ")", e); 223 } 224 } 225 226 // Note: the given association definition must not necessarily originate from the parent object's type definition. 227 // It may originate from a facet definition as well. 228 // Called from AttachedDeepaMehtaObject.updateChildTopic() and AttachedDeepaMehtaObject.updateChildTopics(). 229 void updateChildTopics(TopicModel newChildTopic, List<TopicModel> newChildTopics, AssociationDefinition assocDef) { 230 // Note: updating the child topics requires them to be loaded 231 loadChildTopics(assocDef); 232 // 233 String assocTypeUri = assocDef.getTypeUri(); 234 boolean one = newChildTopic != null; 235 if (assocTypeUri.equals("dm4.core.composition_def")) { 236 if (one) { 237 updateCompositionOne(newChildTopic, assocDef); 238 } else { 239 updateCompositionMany(newChildTopics, assocDef); 240 } 241 } else if (assocTypeUri.equals("dm4.core.aggregation_def")) { 242 if (one) { 243 updateAggregationOne(newChildTopic, assocDef); 244 } else { 245 updateAggregationMany(newChildTopics, assocDef); 246 } 247 } else { 248 throw new RuntimeException("Association type \"" + assocTypeUri + "\" not supported"); 249 } 250 } 251 252 // --- 253 254 void loadChildTopics() { 255 dms.valueStorage.fetchChildTopics(parent.getModel()); 256 initAttachedObjectCache(); 257 } 258 259 void loadChildTopics(String childTypeUri) { 260 loadChildTopics(getAssocDef(childTypeUri)); 261 } 262 263 // ------------------------------------------------------------------------------------------------- Private Methods 264 265 /** 266 * Recursively loads child topics (model) and updates this attached object cache accordingly. 267 * If the child topics are loaded already nothing is performed. 268 * 269 * @param assocDef the child topics according to this association definition are loaded. 270 * <p> 271 * Note: the association definition must not necessarily originate from the parent object's 272 * type definition. It may originate from a facet definition as well. 273 */ 274 private void loadChildTopics(AssociationDefinition assocDef) { 275 String childTypeUri = assocDef.getChildTypeUri(); 276 if (!has(childTypeUri)) { 277 logger.fine("### Lazy-loading \"" + childTypeUri + "\" child topic(s) of " + parent.className() + " " + 278 parent.getId()); 279 dms.valueStorage.fetchChildTopics(parent.getModel(), assocDef); 280 initAttachedObjectCache(childTypeUri); 281 } 282 } 283 284 // --- Access this attached object cache --- 285 286 private Topic _getTopic(String childTypeUri) { 287 Topic topic = (Topic) childTopics.get(childTypeUri); 288 // error check 289 if (topic == null) { 290 throw new RuntimeException("Child topic of type \"" + childTypeUri + "\" not found in " + childTopics); 291 } 292 // 293 return topic; 294 } 295 296 private AttachedTopic _getTopic(String childTypeUri, AttachedTopic defaultTopic) { 297 AttachedTopic topic = (AttachedTopic) childTopics.get(childTypeUri); 298 return topic != null ? topic : defaultTopic; 299 } 300 301 // --- 302 303 private List<Topic> _getTopics(String childTypeUri) { 304 try { 305 List<Topic> topics = (List<Topic>) childTopics.get(childTypeUri); 306 // error check 307 if (topics == null) { 308 throw new RuntimeException("Child topics of type \"" + childTypeUri + "\" not found in " + childTopics); 309 } 310 // 311 return topics; 312 } catch (ClassCastException e) { 313 getModel().throwInvalidAccess(childTypeUri, e); 314 return null; // never reached 315 } 316 } 317 318 private List<Topic> _getTopics(String childTypeUri, List<Topic> defaultValue) { 319 try { 320 List<Topic> topics = (List<Topic>) childTopics.get(childTypeUri); 321 return topics != null ? topics : defaultValue; 322 } catch (ClassCastException e) { 323 getModel().throwInvalidAccess(childTypeUri, e); 324 return null; // never reached 325 } 326 } 327 328 // --- 329 330 private ChildTopics _update(String childTypeUri, TopicModel newChildTopic) { 331 parent.update(new TopicModel(parent.getTypeUri(), new ChildTopicsModel().put(childTypeUri, newChildTopic))); 332 return this; 333 } 334 335 // --- Composition --- 336 337 private void updateCompositionOne(TopicModel newChildTopic, AssociationDefinition assocDef) { 338 AttachedTopic childTopic = _getTopic(assocDef.getChildTypeUri(), null); 339 // Note: for cardinality one the simple request format is sufficient. The child's topic ID is not required. 340 // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic. 341 if (childTopic != null) { 342 // == update child == 343 // update DB 344 childTopic._update(newChildTopic); 345 // Note: memory is already up-to-date. The child topic is updated in-place of parent. 346 } else { 347 // == create child == 348 createChildTopicOne(newChildTopic, assocDef); 349 } 350 } 351 352 private void updateCompositionMany(List<TopicModel> newChildTopics, AssociationDefinition assocDef) { 353 for (TopicModel newChildTopic : newChildTopics) { 354 long childTopicId = newChildTopic.getId(); 355 if (newChildTopic instanceof TopicDeletionModel) { 356 Topic childTopic = findChildTopic(childTopicId, assocDef); 357 if (childTopic == null) { 358 // Note: "delete child" is an idempotent operation. A delete request for an child which has been 359 // deleted already (resp. is non-existing) is not an error. Instead, nothing is performed. 360 continue; 361 } 362 // == delete child == 363 // update DB 364 childTopic.delete(); 365 // update memory 366 removeFromChildTopics(childTopic, assocDef); 367 } else if (childTopicId != -1) { 368 // == update child == 369 updateChildTopicMany(newChildTopic, assocDef); 370 } else { 371 // == create child == 372 createChildTopicMany(newChildTopic, assocDef); 373 } 374 } 375 } 376 377 // --- Aggregation --- 378 379 private void updateAggregationOne(TopicModel newChildTopic, AssociationDefinition assocDef) { 380 RelatedTopic childTopic = (RelatedTopic) _getTopic(assocDef.getChildTypeUri(), null); 381 if (newChildTopic instanceof TopicReferenceModel) { 382 if (childTopic != null) { 383 if (((TopicReferenceModel) newChildTopic).isReferingTo(childTopic)) { 384 return; 385 } 386 // == update assignment == 387 // update DB 388 childTopic.getRelatingAssociation().delete(); 389 } else { 390 // == create assignment == 391 } 392 // update DB 393 Topic topic = associateReferencedChildTopic((TopicReferenceModel) newChildTopic, assocDef); 394 // update memory 395 putInChildTopics(topic, assocDef); 396 } else if (newChildTopic.getId() != -1) { 397 // == update child == 398 updateChildTopicOne(newChildTopic, assocDef); 399 } else { 400 // == create child == 401 // update DB 402 if (childTopic != null) { 403 childTopic.getRelatingAssociation().delete(); 404 } 405 createChildTopicOne(newChildTopic, assocDef); 406 } 407 } 408 409 private void updateAggregationMany(List<TopicModel> newChildTopics, AssociationDefinition assocDef) { 410 for (TopicModel newChildTopic : newChildTopics) { 411 long childTopicId = newChildTopic.getId(); 412 if (newChildTopic instanceof TopicDeletionModel) { 413 RelatedTopic childTopic = findChildTopic(childTopicId, assocDef); 414 if (childTopic == null) { 415 // Note: "delete assignment" is an idempotent operation. A delete request for an assignment which 416 // has been deleted already (resp. is non-existing) is not an error. Instead, nothing is performed. 417 continue; 418 } 419 // == delete assignment == 420 // update DB 421 childTopic.getRelatingAssociation().delete(); 422 // update memory 423 removeFromChildTopics(childTopic, assocDef); 424 } else if (newChildTopic instanceof TopicReferenceModel) { 425 if (isReferingToAny((TopicReferenceModel) newChildTopic, assocDef)) { 426 // Note: "create assignment" is an idempotent operation. A create request for an assignment which 427 // exists already is not an error. Instead, nothing is performed. 428 continue; 429 } 430 // == create assignment == 431 // update DB 432 Topic topic = associateReferencedChildTopic((TopicReferenceModel) newChildTopic, assocDef); 433 // update memory 434 addToChildTopics(topic, assocDef); 435 } else if (childTopicId != -1) { 436 // == update child == 437 updateChildTopicMany(newChildTopic, assocDef); 438 } else { 439 // == create child == 440 createChildTopicMany(newChildTopic, assocDef); 441 } 442 } 443 } 444 445 // --- ### TODO: avoid structural similar code, see ValueStorage 446 447 /** 448 * Creates an association between our parent object ("Parent" role) and the referenced topic ("Child" role). 449 * The association type is taken from the given association definition. 450 * 451 * @return the resolved child topic. 452 */ 453 RelatedTopic associateReferencedChildTopic(TopicReferenceModel childTopicRef, AssociationDefinition assocDef) { 454 if (childTopicRef.isReferenceById()) { 455 long childTopicId = childTopicRef.getId(); 456 // Note: the resolved topic must be fetched including its composite value. 457 // It might be required at client-side. ### FIXME: had fetchComposite=true 458 Topic childTopic = dms.getTopic(childTopicId); 459 Association assoc = associateChildTopic(childTopicId, assocDef); 460 return createRelatedTopic(childTopic, assoc); 461 } else if (childTopicRef.isReferenceByUri()) { 462 String childTopicUri = childTopicRef.getUri(); 463 // Note: the resolved topic must be fetched including its composite value. 464 // It might be required at client-side. ### FIXME: had fetchComposite=true 465 Topic childTopic = dms.getTopic("uri", new SimpleValue(childTopicUri)); 466 Association assoc = associateChildTopic(childTopicUri, assocDef); 467 return createRelatedTopic(childTopic, assoc); 468 } else { 469 throw new RuntimeException("Invalid topic reference (" + childTopicRef + ")"); 470 } 471 } 472 473 private Association associateChildTopic(long childTopicId, AssociationDefinition assocDef) { 474 return associateChildTopic(new TopicRoleModel(childTopicId, "dm4.core.child"), assocDef); 475 } 476 477 private Association associateChildTopic(String childTopicUri, AssociationDefinition assocDef) { 478 return associateChildTopic(new TopicRoleModel(childTopicUri, "dm4.core.child"), assocDef); 479 } 480 481 // --- 482 483 private Association associateChildTopic(TopicRoleModel child, AssociationDefinition assocDef) { 484 return dms.createAssociation(assocDef.getInstanceLevelAssocTypeUri(), 485 parent.getModel().createRoleModel("dm4.core.parent"), child); 486 } 487 488 private RelatedTopic createRelatedTopic(Topic topic, Association assoc) { 489 return new AttachedRelatedTopic(new RelatedTopicModel(topic.getModel(), assoc.getModel()), dms); 490 } 491 492 // --- ### end TODO 493 494 private void updateChildTopicOne(TopicModel newChildTopic, AssociationDefinition assocDef) { 495 AttachedTopic childTopic = _getTopic(assocDef.getChildTypeUri(), null); 496 if (childTopic != null && childTopic.getId() == newChildTopic.getId()) { 497 // update DB 498 childTopic._update(newChildTopic); 499 // Note: memory is already up-to-date. The child topic is updated in-place of parent. 500 } else { 501 throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " + 502 parent.className() + " " + parent.getId() + " according to " + assocDef); 503 } 504 } 505 506 private void updateChildTopicMany(TopicModel newChildTopic, AssociationDefinition assocDef) { 507 AttachedTopic childTopic = findChildTopic(newChildTopic.getId(), assocDef); 508 if (childTopic != null) { 509 // update DB 510 childTopic._update(newChildTopic); 511 // Note: memory is already up-to-date. The child topic is updated in-place of parent. 512 } else { 513 throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " + 514 parent.className() + " " + parent.getId() + " according to " + assocDef); 515 } 516 } 517 518 // --- 519 520 private void createChildTopicOne(TopicModel newChildTopic, AssociationDefinition assocDef) { 521 // update DB 522 Topic childTopic = dms.createTopic(newChildTopic); 523 dms.valueStorage.associateChildTopic(parent.getModel(), childTopic.getId(), assocDef); 524 // update memory 525 putInChildTopics(childTopic, assocDef); 526 } 527 528 private void createChildTopicMany(TopicModel newChildTopic, AssociationDefinition assocDef) { 529 // update DB 530 Topic childTopic = dms.createTopic(newChildTopic); 531 dms.valueStorage.associateChildTopic(parent.getModel(), childTopic.getId(), assocDef); 532 // update memory 533 addToChildTopics(childTopic, assocDef); 534 } 535 536 537 538 // === Attached Object Cache Initialization === 539 540 /** 541 * Initializes this attached object cache. For all childs contained in the underlying model attached topics are 542 * created and put in the attached object cache (recursively). 543 */ 544 private void initAttachedObjectCache() { 545 for (String childTypeUri : model) { 546 initAttachedObjectCache(childTypeUri); 547 } 548 } 549 550 /** 551 * Initializes this attached object cache selectively (and recursively). 552 */ 553 private void initAttachedObjectCache(String childTypeUri) { 554 Object value = model.get(childTypeUri); 555 // Note: topics just created have no child topics yet 556 if (value == null) { 557 return; 558 } 559 // Note: no direct recursion takes place here. Recursion is indirect: attached topics are created here, this 560 // implies creating further attached composite values, which in turn calls this method again but for the next 561 // child-level. Finally attached topics are created for all child-levels. 562 if (value instanceof TopicModel) { 563 TopicModel childTopic = (TopicModel) value; 564 childTopics.put(childTypeUri, createAttachedObject(childTopic)); 565 } else if (value instanceof List) { 566 List<Topic> topics = new ArrayList(); 567 childTopics.put(childTypeUri, topics); 568 for (TopicModel childTopic : (List<TopicModel>) value) { 569 topics.add(createAttachedObject(childTopic)); 570 } 571 } else { 572 throw new RuntimeException("Unexpected value in a ChildTopicsModel: " + value); 573 } 574 } 575 576 /** 577 * Creates an attached topic to be put in this attached object cache. 578 */ 579 private Topic createAttachedObject(TopicModel model) { 580 if (model instanceof RelatedTopicModel) { 581 // Note: composite value models obtained through *fetching* contain *related topic models*. 582 // We exploit the related topics when updating assignments (in conjunction with aggregations). 583 // See updateAggregationOne() and updateAggregationMany(). 584 return new AttachedRelatedTopic((RelatedTopicModel) model, dms); 585 } else { 586 // Note: composite value models for *new topics* to be created contain sole *topic models*. 587 return new AttachedTopic(model, dms); 588 } 589 } 590 591 592 593 // === Update === 594 595 // --- Update this attached object cache + underlying model --- 596 597 /** 598 * For single-valued childs 599 */ 600 private void putInChildTopics(Topic childTopic, AssociationDefinition assocDef) { 601 String childTypeUri = assocDef.getChildTypeUri(); 602 put(childTypeUri, childTopic); // attached object cache 603 getModel().put(childTypeUri, childTopic.getModel()); // underlying model 604 } 605 606 /** 607 * For multiple-valued childs 608 */ 609 private void addToChildTopics(Topic childTopic, AssociationDefinition assocDef) { 610 String childTypeUri = assocDef.getChildTypeUri(); 611 add(childTypeUri, childTopic); // attached object cache 612 getModel().add(childTypeUri, childTopic.getModel()); // underlying model 613 } 614 615 /** 616 * For multiple-valued childs 617 */ 618 private void removeFromChildTopics(Topic childTopic, AssociationDefinition assocDef) { 619 String childTypeUri = assocDef.getChildTypeUri(); 620 remove(childTypeUri, childTopic); // attached object cache 621 getModel().remove(childTypeUri, childTopic.getModel()); // underlying model 622 } 623 624 // --- Update this attached object cache --- 625 626 /** 627 * Puts a single-valued child. An existing value is overwritten. 628 */ 629 private void put(String childTypeUri, Topic topic) { 630 childTopics.put(childTypeUri, topic); 631 } 632 633 /** 634 * Adds a value to a multiple-valued child. 635 */ 636 private void add(String childTypeUri, Topic topic) { 637 List<Topic> topics = _getTopics(childTypeUri, null); // defaultValue=null 638 // Note: topics just created have no child topics yet 639 if (topics == null) { 640 topics = new ArrayList(); 641 childTopics.put(childTypeUri, topics); 642 } 643 topics.add(topic); 644 } 645 646 /** 647 * Removes a value from a multiple-valued child. 648 */ 649 private void remove(String childTypeUri, Topic topic) { 650 List<Topic> topics = _getTopics(childTypeUri, null); // defaultValue=null 651 if (topics != null) { 652 topics.remove(topic); 653 } 654 } 655 656 657 658 // === Helper === 659 660 private AttachedRelatedTopic findChildTopic(long childTopicId, AssociationDefinition assocDef) { 661 List<Topic> childTopics = _getTopics(assocDef.getChildTypeUri(), new ArrayList()); 662 for (Topic childTopic : childTopics) { 663 if (childTopic.getId() == childTopicId) { 664 return (AttachedRelatedTopic) childTopic; 665 } 666 } 667 return null; 668 } 669 670 /** 671 * Checks weather the given topic reference refers to any of the child topics. 672 * 673 * @param assocDef the child topics according to this association definition are considered. 674 */ 675 private boolean isReferingToAny(TopicReferenceModel topicRef, AssociationDefinition assocDef) { 676 return topicRef.isReferingToAny(_getTopics(assocDef.getChildTypeUri(), new ArrayList())); 677 } 678 679 private AssociationDefinition getAssocDef(String childTypeUri) { 680 // Note: doesn't work for facets 681 return parent.getType().getAssocDef(childTypeUri); 682 } 683 }