001package de.deepamehta.core.impl;
002
003import de.deepamehta.core.AssociationDefinition;
004import de.deepamehta.core.ChildTopics;
005import de.deepamehta.core.RelatedTopic;
006import de.deepamehta.core.Topic;
007import de.deepamehta.core.model.ChildTopicsModel;
008import de.deepamehta.core.model.DeepaMehtaObjectModel;
009import de.deepamehta.core.model.RelatedTopicModel;
010import de.deepamehta.core.model.SimpleValue;
011import de.deepamehta.core.model.TopicDeletionModel;
012import de.deepamehta.core.model.TopicModel;
013import de.deepamehta.core.model.TopicReferenceModel;
014
015import java.util.ArrayList;
016import java.util.HashMap;
017import java.util.Iterator;
018import java.util.List;
019import java.util.Map;
020import java.util.logging.Logger;
021
022
023
024/**
025 * A child topics model that is attached to the DB.
026 */
027class 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: RelatedTopic or List<RelatedTopic>
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 RelatedTopic getTopic(String childTypeUri) {
068        loadChildTopics(childTypeUri);
069        return _getTopic(childTypeUri);
070    }
071
072    @Override
073    public List<RelatedTopic> 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 int size() {
092        return childTopics.size();
093    }
094
095    // ---
096
097    @Override
098    public ChildTopicsModel getModel() {
099        return model;
100    }
101
102
103
104    // === Convenience Accessors ===
105
106    @Override
107    public String getString(String childTypeUri) {
108        return getTopic(childTypeUri).getSimpleValue().toString();
109    }
110
111    @Override
112    public int getInt(String childTypeUri) {
113        return getTopic(childTypeUri).getSimpleValue().intValue();
114    }
115
116    @Override
117    public long getLong(String childTypeUri) {
118        return getTopic(childTypeUri).getSimpleValue().longValue();
119    }
120
121    @Override
122    public double getDouble(String childTypeUri) {
123        return getTopic(childTypeUri).getSimpleValue().doubleValue();
124    }
125
126    @Override
127    public boolean getBoolean(String childTypeUri) {
128        return getTopic(childTypeUri).getSimpleValue().booleanValue();
129    }
130
131    @Override
132    public Object getObject(String childTypeUri) {
133        return getTopic(childTypeUri).getSimpleValue().value();
134    }
135
136    // ---
137
138    @Override
139    public ChildTopics getChildTopics(String childTypeUri) {
140        return getTopic(childTypeUri).getChildTopics();
141    }
142
143    // Note: there are no convenience accessors for a multiple-valued child.
144
145
146
147    // === Manipulators ===
148
149    // --- Single-valued Childs ---
150
151    @Override
152    public ChildTopics set(String childTypeUri, TopicModel value) {
153        return _updateOne(childTypeUri, new RelatedTopicModel(value));
154    }
155
156    // ---
157
158    @Override
159    public ChildTopics set(String childTypeUri, Object value) {
160        return _updateOne(childTypeUri, new RelatedTopicModel(childTypeUri, new SimpleValue(value)));
161    }
162
163    @Override
164    public ChildTopics set(String childTypeUri, ChildTopicsModel value) {
165        return _updateOne(childTypeUri, new RelatedTopicModel(childTypeUri, value));
166    }
167
168    // ---
169
170    @Override
171    public ChildTopics setRef(String childTypeUri, long refTopicId) {
172        return _updateOne(childTypeUri, new TopicReferenceModel(refTopicId));
173    }
174
175    @Override
176    public ChildTopics setRef(String childTypeUri, long refTopicId, ChildTopicsModel relatingAssocChildTopics) {
177        return _updateOne(childTypeUri, new TopicReferenceModel(refTopicId, relatingAssocChildTopics));
178    }
179
180    @Override
181    public ChildTopics setRef(String childTypeUri, String refTopicUri) {
182        return _updateOne(childTypeUri, new TopicReferenceModel(refTopicUri));
183    }
184
185    @Override
186    public ChildTopics setRef(String childTypeUri, String refTopicUri, ChildTopicsModel relatingAssocChildTopics) {
187        return _updateOne(childTypeUri, new TopicReferenceModel(refTopicUri, relatingAssocChildTopics));
188    }
189
190    // ---
191
192    @Override
193    public ChildTopics setDeletionRef(String childTypeUri, long refTopicId) {
194        return _updateOne(childTypeUri, new TopicDeletionModel(refTopicId));
195    }
196
197    @Override
198    public ChildTopics setDeletionRef(String childTypeUri, String refTopicUri) {
199        return _updateOne(childTypeUri, new TopicDeletionModel(refTopicUri));
200    }
201
202    // --- Multiple-valued Childs ---
203
204    @Override
205    public ChildTopics add(String childTypeUri, TopicModel value) {
206        return _updateMany(childTypeUri, new RelatedTopicModel(value));
207    }
208
209    // ---
210
211    @Override
212    public ChildTopics add(String childTypeUri, Object value) {
213        return _updateMany(childTypeUri, new RelatedTopicModel(childTypeUri, new SimpleValue(value)));
214    }
215
216    @Override
217    public ChildTopics add(String childTypeUri, ChildTopicsModel value) {
218        return _updateMany(childTypeUri, new RelatedTopicModel(childTypeUri, value));
219    }
220
221    // ---
222
223    @Override
224    public ChildTopics addRef(String childTypeUri, long refTopicId) {
225        return _updateMany(childTypeUri, new TopicReferenceModel(refTopicId));
226    }
227
228    @Override
229    public ChildTopics addRef(String childTypeUri, long refTopicId, ChildTopicsModel relatingAssocChildTopics) {
230        return _updateMany(childTypeUri, new TopicReferenceModel(refTopicId, relatingAssocChildTopics));
231    }
232
233    @Override
234    public ChildTopics addRef(String childTypeUri, String refTopicUri) {
235        return _updateMany(childTypeUri, new TopicReferenceModel(refTopicUri));
236    }
237
238    @Override
239    public ChildTopics addRef(String childTypeUri, String refTopicUri, ChildTopicsModel relatingAssocChildTopics) {
240        return _updateMany(childTypeUri, new TopicReferenceModel(refTopicUri, relatingAssocChildTopics));
241    }
242
243    // ---
244
245    @Override
246    public ChildTopics addDeletionRef(String childTypeUri, long refTopicId) {
247        return _updateMany(childTypeUri, new TopicDeletionModel(refTopicId));
248    }
249
250    @Override
251    public ChildTopics addDeletionRef(String childTypeUri, String refTopicUri) {
252        return _updateMany(childTypeUri, new TopicDeletionModel(refTopicUri));
253    }
254
255
256
257    // === Iterable Implementation ===
258
259    @Override
260    public Iterator<String> iterator() {
261        return childTopics.keySet().iterator();
262    }
263
264
265
266    // ----------------------------------------------------------------------------------------- Package Private Methods
267
268    void update(ChildTopicsModel newComp) {
269        try {
270            for (AssociationDefinition assocDef : parent.getType().getAssocDefs()) {
271                String childTypeUri   = assocDef.getChildTypeUri();
272                String cardinalityUri = assocDef.getChildCardinalityUri();
273                RelatedTopicModel newChildTopic        = null;  // only used for "one"
274                List<RelatedTopicModel> newChildTopics = null;  // only used for "many"
275                if (cardinalityUri.equals("dm4.core.one")) {
276                    newChildTopic = newComp.getTopic(childTypeUri, null);        // defaultValue=null
277                    // skip if not contained in update request
278                    if (newChildTopic == null) {
279                        continue;
280                    }
281                } else if (cardinalityUri.equals("dm4.core.many")) {
282                    newChildTopics = newComp.getTopics(childTypeUri, null);      // defaultValue=null
283                    // skip if not contained in update request
284                    if (newChildTopics == null) {
285                        continue;
286                    }
287                } else {
288                    throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI");
289                }
290                //
291                updateChildTopics(newChildTopic, newChildTopics, assocDef);
292            }
293            //
294            refreshParentLabel();
295            //
296        } catch (Exception e) {
297            throw new RuntimeException("Updating the child topics of " + parent.className() + " " + parent.getId() +
298                " failed (newComp=" + newComp + ")", e);
299        }
300    }
301
302    // Note: the given association definition must not necessarily originate from the parent object's type definition.
303    // It may originate from a facet definition as well.
304    // Called from AttachedDeepaMehtaObject.updateChildTopic() and AttachedDeepaMehtaObject.updateChildTopics().
305    void updateChildTopics(RelatedTopicModel newChildTopic, List<RelatedTopicModel> newChildTopics,
306                                                            AssociationDefinition assocDef) {
307        // Note: updating the child topics requires them to be loaded
308        loadChildTopics(assocDef);
309        //
310        String assocTypeUri = assocDef.getTypeUri();
311        boolean one = newChildTopic != null;
312        if (assocTypeUri.equals("dm4.core.composition_def")) {
313            if (one) {
314                updateCompositionOne(newChildTopic, assocDef);
315            } else {
316                updateCompositionMany(newChildTopics, assocDef);
317            }
318        } else if (assocTypeUri.equals("dm4.core.aggregation_def")) {
319            if (one) {
320                updateAggregationOne(newChildTopic, assocDef);
321            } else {
322                updateAggregationMany(newChildTopics, assocDef);
323            }
324        } else {
325            throw new RuntimeException("Association type \"" + assocTypeUri + "\" not supported");
326        }
327    }
328
329    // ---
330
331    void loadChildTopics() {
332        for (AssociationDefinition assocDef : parent.getType().getAssocDefs()) {
333            loadChildTopics(assocDef);
334        }
335    }
336
337    void loadChildTopics(String childTypeUri) {
338        loadChildTopics(getAssocDef(childTypeUri));
339    }
340
341
342
343    // ------------------------------------------------------------------------------------------------- Private Methods
344
345    /**
346     * Recursively loads child topics (model) and updates this attached object cache accordingly.
347     * If the child topics are loaded already nothing is performed.
348     *
349     * @param   assocDef    the child topics according to this association definition are loaded.
350     *                      <p>
351     *                      Note: the association definition must not necessarily originate from the parent object's
352     *                      type definition. It may originate from a facet definition as well.
353     */
354    private void loadChildTopics(AssociationDefinition assocDef) {
355        String childTypeUri = assocDef.getChildTypeUri();
356        if (!has(childTypeUri)) {
357            logger.fine("### Lazy-loading \"" + childTypeUri + "\" child topic(s) of " + parent.className() + " " +
358                parent.getId());
359            dms.valueStorage.fetchChildTopics(parent.getModel(), assocDef.getModel());
360            initAttachedObjectCache(childTypeUri);
361        }
362    }
363
364    private void refreshParentLabel() {
365        DeepaMehtaObjectModel parent = this.parent.getModel();
366        //
367        for (String childTypeUri : dms.valueStorage.getLabelChildTypeUris(parent)) {
368            loadChildTopics(childTypeUri);
369        }
370        //
371        dms.valueStorage.refreshLabel(parent);
372    }
373
374    // ---
375
376    // Note 1: we need to explicitly declare the arg as RelatedTopicModel. When declared as TopicModel instead the
377    // JVM would invoke the ChildTopicsModel's put()/add() which takes a TopicModel object even if at runtime a
378    // RelatedTopicModel or even a TopicReferenceModel is passed. This is because Java method overloading involves
379    // no dynamic dispatch. See the methodOverloading tests in JavaAPITest.java (in module dm4-test).
380
381    // Note 2: calling parent.update(..) would not work. The JVM would call the update() method of the base class
382    // (AttachedDeepaMehtaObject), not the subclass's update() method. This is related to Java's (missing) multiple
383    // dispatch. Note that 2 inheritance hierarchies are involved here: the DM object hierarchy and the DM model
384    // hierarchy. See the missingMultipleDispatch tests in JavaAPITest.java (in module dm4-test).
385
386    private ChildTopics _updateOne(String childTypeUri, RelatedTopicModel newChildTopic) {
387        parent.updateChildTopics(new ChildTopicsModel().put(childTypeUri, newChildTopic));
388        return this;
389    }
390
391    private ChildTopics _updateMany(String childTypeUri, RelatedTopicModel newChildTopic) {
392        parent.updateChildTopics(new ChildTopicsModel().add(childTypeUri, newChildTopic));
393        return this;
394    }
395
396
397
398    // === Update Child Topics ===
399
400    // --- Composition ---
401
402    private void updateCompositionOne(RelatedTopicModel newChildTopic, AssociationDefinition assocDef) {
403        RelatedTopic childTopic = _getTopic(assocDef.getChildTypeUri(), null);
404        // Note: for cardinality one the simple request format is sufficient. The child's topic ID is not required.
405        // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic.
406        if (newChildTopic instanceof TopicDeletionModel) {
407            deleteChildTopicOne(childTopic, assocDef, true);                                        // deleteChild=true
408        } else if (newChildTopic instanceof TopicReferenceModel) {
409            createAssignmentOne(childTopic, (TopicReferenceModel) newChildTopic, assocDef, true);   // deleteChild=true
410        } else if (childTopic != null) {
411            updateRelatedTopic(childTopic, newChildTopic);
412        } else {
413            createChildTopicOne(newChildTopic, assocDef);
414        }
415    }
416
417    private void updateCompositionMany(List<RelatedTopicModel> newChildTopics, AssociationDefinition assocDef) {
418        for (RelatedTopicModel newChildTopic : newChildTopics) {
419            long childTopicId = newChildTopic.getId();
420            if (newChildTopic instanceof TopicDeletionModel) {
421                deleteChildTopicMany(childTopicId, assocDef, true);                                 // deleteChild=true
422            } else if (newChildTopic instanceof TopicReferenceModel) {
423                createAssignmentMany((TopicReferenceModel) newChildTopic, assocDef);
424            } else if (childTopicId != -1) {
425                updateChildTopicMany(newChildTopic, assocDef);
426            } else {
427                createChildTopicMany(newChildTopic, assocDef);
428            }
429        }
430    }
431
432    // --- Aggregation ---
433
434    private void updateAggregationOne(RelatedTopicModel newChildTopic, AssociationDefinition assocDef) {
435        RelatedTopic childTopic = _getTopic(assocDef.getChildTypeUri(), null);
436        // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic.
437        if (newChildTopic instanceof TopicDeletionModel) {
438            deleteChildTopicOne(childTopic, assocDef, false);                                       // deleteChild=false
439        } else if (newChildTopic instanceof TopicReferenceModel) {
440            createAssignmentOne(childTopic, (TopicReferenceModel) newChildTopic, assocDef, false);  // deleteChild=false
441        } else if (newChildTopic.getId() != -1) {
442            updateChildTopicOne(newChildTopic, assocDef);
443        } else {
444            if (childTopic != null) {
445                childTopic.getRelatingAssociation().delete();
446            }
447            createChildTopicOne(newChildTopic, assocDef);
448        }
449    }
450
451    private void updateAggregationMany(List<RelatedTopicModel> newChildTopics, AssociationDefinition assocDef) {
452        for (RelatedTopicModel newChildTopic : newChildTopics) {
453            long childTopicId = newChildTopic.getId();
454            if (newChildTopic instanceof TopicDeletionModel) {
455                deleteChildTopicMany(childTopicId, assocDef, false);                                // deleteChild=false
456            } else if (newChildTopic instanceof TopicReferenceModel) {
457                createAssignmentMany((TopicReferenceModel) newChildTopic, assocDef);
458            } else if (childTopicId != -1) {
459                updateChildTopicMany(newChildTopic, assocDef);
460            } else {
461                createChildTopicMany(newChildTopic, assocDef);
462            }
463        }
464    }
465
466    // --- Update ---
467
468    private void updateChildTopicOne(RelatedTopicModel newChildTopic, AssociationDefinition assocDef) {
469        RelatedTopic childTopic = _getTopic(assocDef.getChildTypeUri(), null);
470        //
471        if (childTopic == null || childTopic.getId() != newChildTopic.getId()) {
472            throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " +
473                parent.className() + " " + parent.getId() + " according to " + assocDef);
474        }
475        //
476        updateRelatedTopic(childTopic, newChildTopic);
477        // Note: memory is already up-to-date. The child topic is updated in-place of parent.
478    }
479
480    private void updateChildTopicMany(RelatedTopicModel newChildTopic, AssociationDefinition assocDef) {
481        RelatedTopic childTopic = findChildTopicById(newChildTopic.getId(), assocDef);
482        //
483        if (childTopic == null) {
484            throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " +
485                parent.className() + " " + parent.getId() + " according to " + assocDef);
486        }
487        //
488        updateRelatedTopic(childTopic, newChildTopic);
489        // Note: memory is already up-to-date. The child topic is updated in-place of parent.
490    }
491
492    // ---
493
494    private void updateRelatedTopic(RelatedTopic childTopic, RelatedTopicModel newChildTopic) {
495        // update topic
496        ((AttachedTopic) childTopic)._update(newChildTopic);
497        // update association
498        updateRelatingAssociation(childTopic, newChildTopic);
499    }
500
501    private void updateRelatingAssociation(RelatedTopic childTopic, RelatedTopicModel newChildTopic) {
502        childTopic.getRelatingAssociation().update(newChildTopic.getRelatingAssociation());
503    }
504
505    // --- Create ---
506
507    private void createChildTopicOne(RelatedTopicModel newChildTopic, AssociationDefinition assocDef) {
508        // update DB
509        RelatedTopic childTopic = createAndAssociateChildTopic(newChildTopic, assocDef);
510        // update memory
511        putInChildTopics(childTopic, assocDef);
512    }
513
514    private void createChildTopicMany(RelatedTopicModel newChildTopic, AssociationDefinition assocDef) {
515        // update DB
516        RelatedTopic childTopic = createAndAssociateChildTopic(newChildTopic, assocDef);
517        // update memory
518        addToChildTopics(childTopic, assocDef);
519    }
520
521    // ---
522
523    private RelatedTopic createAndAssociateChildTopic(RelatedTopicModel childTopic, AssociationDefinition assocDef) {
524        dms.createTopic(childTopic);
525        return associateChildTopic(childTopic, assocDef);
526    }
527
528    // --- Assignment ---
529
530    private void createAssignmentOne(RelatedTopic childTopic, TopicReferenceModel newChildTopic,
531                                     AssociationDefinition assocDef, boolean deleteChildTopic) {
532        if (childTopic != null) {
533            if (newChildTopic.isReferingTo(childTopic)) {
534                updateRelatingAssociation(childTopic, newChildTopic);
535                // Note: memory is already up-to-date. The association is updated in-place of parent.
536                return;
537            }
538            if (deleteChildTopic) {
539                childTopic.delete();
540            } else {
541                childTopic.getRelatingAssociation().delete();
542            }
543        }
544        // update DB
545        RelatedTopic topic = resolveRefAndAssociateChildTopic(newChildTopic, assocDef);
546        // update memory
547        putInChildTopics(topic, assocDef);
548    }
549
550    private void createAssignmentMany(TopicReferenceModel newChildTopic, AssociationDefinition assocDef) {
551        RelatedTopic childTopic = findChildTopicByRef(newChildTopic, assocDef);
552        if (childTopic != null) {
553            // Note: "create assignment" is an idempotent operation. A create request for an assignment which
554            // exists already is not an error. Instead, nothing is performed.
555            updateRelatingAssociation(childTopic, newChildTopic);
556            // Note: memory is already up-to-date. The association is updated in-place of parent.
557            return;
558        }
559        // update DB
560        RelatedTopic topic = resolveRefAndAssociateChildTopic(newChildTopic, assocDef);
561        // update memory
562        addToChildTopics(topic, assocDef);
563    }
564
565    // ---
566
567    /**
568     * Creates an association between our parent object ("Parent" role) and the referenced topic ("Child" role).
569     * The association type is taken from the given association definition.
570     *
571     * @return  the resolved child topic.
572     */
573    RelatedTopic resolveRefAndAssociateChildTopic(TopicReferenceModel childTopicRef, AssociationDefinition assocDef) {
574        dms.valueStorage.resolveReference(childTopicRef);
575        return associateChildTopic(childTopicRef, assocDef);
576    }
577
578    private RelatedTopic associateChildTopic(RelatedTopicModel childTopic, AssociationDefinition assocDef) {
579        dms.valueStorage.associateChildTopic(parent.getModel(), childTopic, assocDef.getModel());
580        return instantiateRelatedTopic(childTopic);
581    }
582
583    // --- Delete ---
584
585    private void deleteChildTopicOne(RelatedTopic childTopic, AssociationDefinition assocDef,
586                                                              boolean deleteChildTopic) {
587        if (childTopic == null) {
588            // Note: "delete child"/"delete assignment" is an idempotent operation. A delete request for a
589            // child/assignment which has been deleted already (resp. is non-existing) is not an error.
590            // Instead, nothing is performed.
591            return;
592        }
593        // update DB
594        if (deleteChildTopic) {
595            childTopic.delete();
596        } else {
597            childTopic.getRelatingAssociation().delete();
598        }
599        // update memory
600        removeChildTopic(assocDef);
601    }
602
603    private void deleteChildTopicMany(long childTopicId, AssociationDefinition assocDef, boolean deleteChildTopic) {
604        RelatedTopic childTopic = findChildTopicById(childTopicId, assocDef);
605        if (childTopic == null) {
606            // Note: "delete child"/"delete assignment" is an idempotent operation. A delete request for a
607            // child/assignment which has been deleted already (resp. is non-existing) is not an error.
608            // Instead, nothing is performed.
609            return;
610        }
611        // update DB
612        if (deleteChildTopic) {
613            childTopic.delete();
614        } else {
615            childTopic.getRelatingAssociation().delete();
616        }
617        // update memory
618        removeFromChildTopics(childTopic, assocDef);
619    }
620
621
622
623    // === Attached Object Cache ===
624
625    // --- Access ---
626
627    private RelatedTopic _getTopic(String childTypeUri) {
628        RelatedTopic topic = (RelatedTopic) childTopics.get(childTypeUri);
629        // error check
630        if (topic == null) {
631            throw new RuntimeException("Child topic of type \"" + childTypeUri + "\" not found in " + childTopics);
632        }
633        //
634        return topic;
635    }
636
637    private RelatedTopic _getTopic(String childTypeUri, RelatedTopic defaultTopic) {
638        RelatedTopic topic = (RelatedTopic) childTopics.get(childTypeUri);
639        return topic != null ? topic : defaultTopic;
640    }
641
642    // ---
643
644    private List<RelatedTopic> _getTopics(String childTypeUri) {
645        try {
646            List<RelatedTopic> topics = (List<RelatedTopic>) childTopics.get(childTypeUri);
647            // error check
648            if (topics == null) {
649                throw new RuntimeException("Child topics of type \"" + childTypeUri + "\" not found in " + childTopics);
650            }
651            //
652            return topics;
653        } catch (ClassCastException e) {
654            getModel().throwInvalidAccess(childTypeUri, e);
655            return null;    // never reached
656        }
657    }
658
659    private List<RelatedTopic> _getTopics(String childTypeUri, List<RelatedTopic> defaultValue) {
660        try {
661            List<RelatedTopic> topics = (List<RelatedTopic>) childTopics.get(childTypeUri);
662            return topics != null ? topics : defaultValue;
663        } catch (ClassCastException e) {
664            getModel().throwInvalidAccess(childTypeUri, e);
665            return null;    // never reached
666        }
667    }
668
669    // ---
670
671    /**
672     * For multiple-valued childs: looks in the attached object cache for a child topic by ID.
673     */
674    private RelatedTopic findChildTopicById(long childTopicId, AssociationDefinition assocDef) {
675        List<RelatedTopic> childTopics = _getTopics(assocDef.getChildTypeUri(), new ArrayList());
676        for (RelatedTopic childTopic : childTopics) {
677            if (childTopic.getId() == childTopicId) {
678                return childTopic;
679            }
680        }
681        return null;
682    }
683
684    /**
685     * For multiple-valued childs: looks in the attached object cache for the child topic the given reference refers to.
686     *
687     * @param   assocDef    the child topics according to this association definition are considered.
688     */
689    private RelatedTopic findChildTopicByRef(TopicReferenceModel topicRef, AssociationDefinition assocDef) {
690        return topicRef.findReferencedTopic(_getTopics(assocDef.getChildTypeUri(), new ArrayList()));
691    }
692
693    // ---
694
695    private AssociationDefinition getAssocDef(String childTypeUri) {
696        // Note: doesn't work for facets
697        return parent.getType().getAssocDef(childTypeUri);
698    }
699
700    // --- Update attached object cache + underlying model ---
701
702    /**
703     * For single-valued childs
704     */
705    private void putInChildTopics(RelatedTopic childTopic, AssociationDefinition assocDef) {
706        String childTypeUri = assocDef.getChildTypeUri();
707        put(childTypeUri, childTopic);                              // attached object cache
708        getModel().put(childTypeUri, childTopic.getModel());        // underlying model
709    }
710
711    /**
712     * For single-valued childs
713     */
714    private void removeChildTopic(AssociationDefinition assocDef) {
715        String childTypeUri = assocDef.getChildTypeUri();
716        remove(childTypeUri);                                       // attached object cache
717        getModel().remove(childTypeUri);                            // underlying model
718    }
719
720    /**
721     * For multiple-valued childs
722     */
723    private void addToChildTopics(RelatedTopic childTopic, AssociationDefinition assocDef) {
724        String childTypeUri = assocDef.getChildTypeUri();
725        add(childTypeUri, childTopic);                              // attached object cache
726        getModel().add(childTypeUri, childTopic.getModel());        // underlying model
727    }
728
729    /**
730     * For multiple-valued childs
731     */
732    private void removeFromChildTopics(Topic childTopic, AssociationDefinition assocDef) {
733        String childTypeUri = assocDef.getChildTypeUri();
734        remove(childTypeUri, childTopic);                           // attached object cache
735        getModel().remove(childTypeUri, childTopic.getModel());     // underlying model
736    }
737
738    // --- Update attached object cache ---
739
740    /**
741     * Puts a single-valued child. An existing value is overwritten.
742     */
743    private void put(String childTypeUri, Topic topic) {
744        childTopics.put(childTypeUri, topic);
745    }
746
747    /**
748     * Removes a single-valued child.
749     */
750    private void remove(String childTypeUri) {
751        childTopics.remove(childTypeUri);
752    }
753
754    /**
755     * Adds a value to a multiple-valued child.
756     */
757    private void add(String childTypeUri, RelatedTopic topic) {
758        List<RelatedTopic> topics = _getTopics(childTypeUri, null);        // defaultValue=null
759        // Note: topics just created have no child topics yet
760        if (topics == null) {
761            topics = new ArrayList();
762            childTopics.put(childTypeUri, topics);
763        }
764        topics.add(topic);
765    }
766
767    /**
768     * Removes a value from a multiple-valued child.
769     */
770    private void remove(String childTypeUri, Topic topic) {
771        List<RelatedTopic> topics = _getTopics(childTypeUri, null);        // defaultValue=null
772        if (topics != null) {
773            topics.remove(topic);
774        }
775    }
776
777    // --- Initialization ---
778
779    /**
780     * Initializes this attached object cache. Creates a hierarchy of attached topics (recursively) that is isomorph
781     * to the underlying model.
782     */
783    private void initAttachedObjectCache() {
784        for (String childTypeUri : model) {
785            initAttachedObjectCache(childTypeUri);
786        }
787    }
788
789    /**
790     * Initializes this attached object cache selectively. Creates a hierarchy of attached topics (recursively) that is
791     * isomorph to the underlying model, starting at the given child sub-tree.
792     */
793    private void initAttachedObjectCache(String childTypeUri) {
794        Object value = model.get(childTypeUri);
795        // Note: topics just created have no child topics yet
796        if (value == null) {
797            return;
798        }
799        // Note: no direct recursion takes place here. Recursion is indirect: attached topics are created here, this
800        // implies creating further AttachedChildTopics objects, which in turn calls this method again but for the next
801        // child-level. Finally attached topics are created for all child-levels.
802        if (value instanceof RelatedTopicModel) {
803            RelatedTopicModel childTopic = (RelatedTopicModel) value;
804            childTopics.put(childTypeUri, instantiateRelatedTopic(childTopic));
805        } else if (value instanceof List) {
806            List<RelatedTopic> topics = new ArrayList();
807            childTopics.put(childTypeUri, topics);
808            for (RelatedTopicModel childTopic : (List<RelatedTopicModel>) value) {
809                topics.add(instantiateRelatedTopic(childTopic));
810            }
811        } else {
812            throw new RuntimeException("Unexpected value in a ChildTopicsModel: " + value);
813        }
814    }
815
816    /**
817     * Creates an attached topic to be put in this attached object cache.
818     */
819    private RelatedTopic instantiateRelatedTopic(RelatedTopicModel model) {
820        try {
821            return new AttachedRelatedTopic(model, dms);
822        } catch (Exception e) {
823            throw new RuntimeException("RelatedTopic instantiation failed (" + model + ")", e);
824        }
825    }
826}