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