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