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 import de.deepamehta.core.service.Directives; 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 = 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 }