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