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