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 }