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 }