001    package de.deepamehta.core.impl;
002    
003    import de.deepamehta.core.AssociationDefinition;
004    import de.deepamehta.core.ChildTopics;
005    import de.deepamehta.core.RelatedTopic;
006    import de.deepamehta.core.Topic;
007    import de.deepamehta.core.model.ChildTopicsModel;
008    import de.deepamehta.core.model.DeepaMehtaObjectModel;
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    
015    import java.util.ArrayList;
016    import java.util.HashMap;
017    import java.util.Iterator;
018    import java.util.List;
019    import java.util.Map;
020    import java.util.logging.Logger;
021    
022    
023    
024    /**
025     * A child topics 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: 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    }