001    package de.deepamehta.core.impl;
002    
003    import de.deepamehta.core.Association;
004    import de.deepamehta.core.AssociationType;
005    import de.deepamehta.core.AssociationDefinition;
006    import de.deepamehta.core.RelatedAssociation;
007    import de.deepamehta.core.RelatedTopic;
008    import de.deepamehta.core.Topic;
009    import de.deepamehta.core.TopicType;
010    import de.deepamehta.core.Type;
011    import de.deepamehta.core.model.AssociationDefinitionModel;
012    import de.deepamehta.core.model.AssociationModel;
013    import de.deepamehta.core.model.AssociationRoleModel;
014    import de.deepamehta.core.model.AssociationTypeModel;
015    import de.deepamehta.core.model.IndexMode;
016    import de.deepamehta.core.model.RelatedAssociationModel;
017    import de.deepamehta.core.model.RelatedTopicModel;
018    import de.deepamehta.core.model.RoleModel;
019    import de.deepamehta.core.model.SimpleValue;
020    import de.deepamehta.core.model.TopicModel;
021    import de.deepamehta.core.model.TopicRoleModel;
022    import de.deepamehta.core.model.TopicTypeModel;
023    import de.deepamehta.core.model.TypeModel;
024    import de.deepamehta.core.model.ViewConfigurationModel;
025    import de.deepamehta.core.service.Directives;
026    import de.deepamehta.core.service.ResultList;
027    import de.deepamehta.core.service.TypeStorage;
028    import de.deepamehta.core.util.DeepaMehtaUtils;
029    
030    import static java.util.Arrays.asList;
031    import java.util.ArrayList;
032    import java.util.Collection;
033    import java.util.HashMap;
034    import java.util.List;
035    import java.util.Map;
036    import java.util.logging.Logger;
037    
038    
039    
040    /**
041     * Storage-impl agnostic support for fetching/storing type models.
042     */
043    class TypeStorageImpl implements TypeStorage {
044    
045        // ---------------------------------------------------------------------------------------------- Instance Variables
046    
047        private Map<String, TypeModel> typeCache = new HashMap();
048    
049        private EmbeddedService dms;
050    
051        private Logger logger = Logger.getLogger(getClass().getName());
052    
053        // ---------------------------------------------------------------------------------------------------- Constructors
054    
055        TypeStorageImpl(EmbeddedService dms) {
056            this.dms = dms;
057        }
058    
059        // --------------------------------------------------------------------------------------------------------- Methods
060    
061    
062    
063        // === Type Model Cache ===
064    
065        private TypeModel getType(String typeUri) {
066            return typeCache.get(typeUri);
067        }
068    
069        private void putInTypeCache(TypeModel type) {
070            typeCache.put(type.getUri(), type);
071        }
072    
073        // ---
074    
075        TopicTypeModel getTopicType(String topicTypeUri) {
076            TopicTypeModel topicType = (TopicTypeModel) getType(topicTypeUri);
077            return topicType != null ? topicType : fetchTopicType(topicTypeUri);
078        }
079    
080        AssociationTypeModel getAssociationType(String assocTypeUri) {
081            AssociationTypeModel assocType = (AssociationTypeModel) getType(assocTypeUri);
082            return assocType != null ? assocType : fetchAssociationType(assocTypeUri);
083        }
084    
085    
086    
087        // === Types ===
088    
089        // --- Fetch ---
090    
091        // ### TODO: unify with next method
092        private TopicTypeModel fetchTopicType(String topicTypeUri) {
093            Topic typeTopic = dms.getTopic("uri", new SimpleValue(topicTypeUri), false);
094            checkTopicType(topicTypeUri, typeTopic);
095            //
096            // 1) fetch type components
097            String dataTypeUri = fetchDataTypeTopic(typeTopic.getId(), topicTypeUri, "topic type").getUri();
098            List<IndexMode> indexModes = fetchIndexModes(typeTopic.getId());
099            List<AssociationDefinitionModel> assocDefs = fetchAssociationDefinitions(typeTopic);
100            List<String> labelConfig = fetchLabelConfig(assocDefs);
101            ViewConfigurationModel viewConfig = fetchTypeViewConfig(typeTopic);
102            //
103            // 2) build type model
104            TopicTypeModel topicType = new TopicTypeModel(typeTopic.getModel(), dataTypeUri, indexModes,
105                assocDefs, labelConfig, viewConfig);
106            //
107            // 3) put in type cache
108            putInTypeCache(topicType);
109            //
110            return topicType;
111        }
112    
113        // ### TODO: unify with previous method
114        private AssociationTypeModel fetchAssociationType(String assocTypeUri) {
115            Topic typeTopic = dms.getTopic("uri", new SimpleValue(assocTypeUri), false);
116            checkAssociationType(assocTypeUri, typeTopic);
117            //
118            // 1) fetch type components
119            String dataTypeUri = fetchDataTypeTopic(typeTopic.getId(), assocTypeUri, "association type").getUri();
120            List<IndexMode> indexModes = fetchIndexModes(typeTopic.getId());
121            List<AssociationDefinitionModel> assocDefs = fetchAssociationDefinitions(typeTopic);
122            List<String> labelConfig = fetchLabelConfig(assocDefs);
123            ViewConfigurationModel viewConfig = fetchTypeViewConfig(typeTopic);
124            //
125            // 2) build type model
126            AssociationTypeModel assocType = new AssociationTypeModel(typeTopic.getModel(), dataTypeUri, indexModes,
127                assocDefs, labelConfig, viewConfig);
128            //
129            // 3) put in type cache
130            putInTypeCache(assocType);
131            //
132            return assocType;
133        }
134    
135        // ---
136    
137        private void checkTopicType(String topicTypeUri, Topic typeTopic) {
138            if (typeTopic == null) {
139                throw new RuntimeException("Topic type \"" + topicTypeUri + "\" not found in DB");
140            } else if (!typeTopic.getTypeUri().equals("dm4.core.topic_type") &&
141                       !typeTopic.getTypeUri().equals("dm4.core.meta_type") &&
142                       !typeTopic.getTypeUri().equals("dm4.core.meta_meta_type")) {
143                throw new RuntimeException("URI \"" + topicTypeUri + "\" refers to a \"" + typeTopic.getTypeUri() +
144                    "\" when the caller expects a \"dm4.core.topic_type\"");
145            }
146        }
147    
148        private void checkAssociationType(String assocTypeUri, Topic typeTopic) {
149            if (typeTopic == null) {
150                throw new RuntimeException("Association type \"" + assocTypeUri + "\" not found in DB");
151            } else if (!typeTopic.getTypeUri().equals("dm4.core.assoc_type")) {
152                throw new RuntimeException("URI \"" + assocTypeUri + "\" refers to a \"" + typeTopic.getTypeUri() +
153                    "\" when the caller expects a \"dm4.core.assoc_type\"");
154            }
155        }
156    
157        // --- Store ---
158    
159        /**
160         * Stores the type-specific parts of the given type model.
161         * Prerequisite: the generic topic parts are stored already.
162         * <p>
163         * Called to store a newly created topic type or association type.
164         */
165        void storeType(TypeModel type) {
166            // 1) put in type cache
167            // Note: an association type must be put in type cache *before* storing its association definitions.
168            // Consider creation of association type "Composition Definition": it has a composition definition itself.
169            putInTypeCache(type);
170            //
171            // 2) store type-specific parts
172            storeDataType(type.getUri(), type.getDataTypeUri());
173            storeIndexModes(type.getUri(), type.getIndexModes());
174            storeAssocDefs(type.getUri(), type.getAssocDefs());
175            storeLabelConfig(type.getLabelConfig(), type.getAssocDefs(), new Directives());
176            storeViewConfig(createConfigurableType(type.getId()), type.getViewConfigModel());
177        }
178    
179    
180    
181        // === Data Type ===
182    
183        // --- Fetch ---
184    
185        private RelatedTopicModel fetchDataTypeTopic(long typeId, String typeUri, String className) {
186            try {
187                RelatedTopicModel dataType = dms.storageDecorator.fetchTopicRelatedTopic(typeId, "dm4.core.aggregation",
188                    "dm4.core.type", "dm4.core.default", "dm4.core.data_type");
189                if (dataType == null) {
190                    throw new RuntimeException("No data type topic is associated to " + className + " \"" + typeUri + "\"");
191                }
192                return dataType;
193            } catch (Exception e) {
194                throw new RuntimeException("Fetching the data type topic of " + className + " \"" + typeUri + "\" failed",
195                    e);
196            }
197        }
198    
199        // --- Store ---
200    
201        // ### TODO: compare to low-level method EmbeddedService._associateDataType(). Remove structural similarity.
202        void storeDataType(String typeUri, String dataTypeUri) {
203            try {
204                dms.createAssociation("dm4.core.aggregation",
205                    new TopicRoleModel(typeUri,     "dm4.core.type"),
206                    new TopicRoleModel(dataTypeUri, "dm4.core.default"));
207            } catch (Exception e) {
208                throw new RuntimeException("Associating type \"" + typeUri + "\" with data type \"" +
209                    dataTypeUri + "\" failed", e);
210            }
211        }
212    
213    
214    
215        // === Index Modes ===
216    
217        // --- Fetch ---
218    
219        private List<IndexMode> fetchIndexModes(long typeId) {
220            ResultList<RelatedTopicModel> indexModes = dms.storageDecorator.fetchTopicRelatedTopics(typeId,
221                "dm4.core.aggregation", "dm4.core.type", "dm4.core.default", "dm4.core.index_mode", 0);
222            return IndexMode.fromTopics(indexModes.getItems());
223        }
224    
225        // --- Store ---
226    
227        private void storeIndexModes(String typeUri, List<IndexMode> indexModes) {
228            for (IndexMode indexMode : indexModes) {
229                storeIndexMode(typeUri, indexMode);
230            }
231        }
232    
233        void storeIndexMode(String typeUri, IndexMode indexMode) {
234            dms.createAssociation("dm4.core.aggregation",
235                new TopicRoleModel(typeUri,           "dm4.core.type"),
236                new TopicRoleModel(indexMode.toUri(), "dm4.core.default"));
237        }
238    
239    
240    
241        // === Association Definitions ===
242    
243        @Override
244        public void removeAssociationDefinitionFromMemoryAndRebuildSequence(Type type, String childTypeUri) {
245            ((AttachedType) type).removeAssocDefFromMemoryAndRebuildSequence(childTypeUri);
246        }
247    
248        // --- Fetch ---
249    
250        private List<AssociationDefinitionModel> fetchAssociationDefinitions(Topic typeTopic) {
251            Map<Long, AssociationDefinitionModel> assocDefs = fetchAssociationDefinitionsUnsorted(typeTopic);
252            List<RelatedAssociationModel> sequence = fetchSequence(typeTopic);
253            // error check
254            if (assocDefs.size() != sequence.size()) {
255                throw new RuntimeException("DB inconsistency: type \"" + typeTopic.getUri() + "\" has " +
256                    assocDefs.size() + " association definitions but in sequence are " + sequence.size());
257            }
258            //
259            return sortAssocDefs(assocDefs, DeepaMehtaUtils.idList(sequence));
260        }
261    
262        private Map<Long, AssociationDefinitionModel> fetchAssociationDefinitionsUnsorted(Topic typeTopic) {
263            Map<Long, AssociationDefinitionModel> assocDefs = new HashMap();
264            //
265            // 1) fetch child topic types
266            // Note: we must set fetchRelatingComposite to false here. Fetching the composite of association type
267            // Composition Definition would cause an endless recursion. Composition Definition is defined through
268            // Composition Definition itself (child types "Include in Label", "Ordered").
269            // Note: "othersTopicTypeUri" is set to null. We want consider "dm4.core.topic_type" and "dm4.core.meta_type"
270            // as well (the latter required e.g. by dm4-mail) ### TODO: add a getRelatedTopics() method that takes a list
271            // of topic types.
272            ResultList<RelatedTopic> childTypes = typeTopic.getRelatedTopics(asList("dm4.core.aggregation_def",
273                "dm4.core.composition_def"), "dm4.core.parent_type", "dm4.core.child_type", null, false, false, 0);
274                // othersTopicTypeUri=null, fetchComposite=false, fetchRelatingComposite=false, clientState=null
275            //
276            // 2) create association definitions
277            // Note: the returned map is an intermediate, hashed by ID. The actual type model is
278            // subsequently build from it by sorting the assoc def's according to the sequence IDs.
279            for (RelatedTopic childType : childTypes) {
280                AssociationDefinitionModel assocDef = fetchAssociationDefinition(childType.getRelatingAssociation(),
281                    typeTopic.getUri(), childType.getUri());
282                assocDefs.put(assocDef.getId(), assocDef);
283            }
284            return assocDefs;
285        }
286    
287        // ---
288    
289        @Override
290        public AssociationDefinitionModel fetchAssociationDefinition(Association assoc) {
291            return fetchAssociationDefinition(assoc, fetchParentType(assoc).getUri(), fetchChildType(assoc).getUri());
292        }
293    
294        private AssociationDefinitionModel fetchAssociationDefinition(Association assoc, String parentTypeUri,
295                                                                                         String childTypeUri) {
296            try {
297                long assocId = assoc.getId();
298                return new AssociationDefinitionModel(
299                    assocId, assoc.getUri(), assoc.getTypeUri(),
300                    parentTypeUri, childTypeUri,
301                    fetchParentCardinality(assocId).getUri(), fetchChildCardinality(assocId).getUri(),
302                    fetchAssocDefViewConfig(assoc)
303                );
304            } catch (Exception e) {
305                throw new RuntimeException("Fetching association definition failed (parentTypeUri=\"" + parentTypeUri +
306                    "\", childTypeUri=" + childTypeUri + ", " + assoc + ")", e);
307            }
308        }
309    
310        // ---
311    
312        private List<AssociationDefinitionModel> sortAssocDefs(Map<Long, AssociationDefinitionModel> assocDefs,
313                                                               List<Long> sequence) {
314            List<AssociationDefinitionModel> sortedAssocDefs = new ArrayList();
315            for (long assocDefId : sequence) {
316                AssociationDefinitionModel assocDef = assocDefs.get(assocDefId);
317                // error check
318                if (assocDef == null) {
319                    throw new RuntimeException("DB inconsistency: ID " + assocDefId +
320                        " is in sequence but not in the type's association definitions");
321                }
322                sortedAssocDefs.add(assocDef);
323            }
324            return sortedAssocDefs;
325        }
326    
327        // --- Store ---
328    
329        private void storeAssocDefs(String typeUri, Collection<AssociationDefinitionModel> assocDefs) {
330            for (AssociationDefinitionModel assocDef : assocDefs) {
331                storeAssociationDefinition(assocDef);
332            }
333            storeSequence(typeUri, assocDefs);
334        }
335    
336        void storeAssociationDefinition(AssociationDefinitionModel assocDef) {
337            try {
338                // Note: creating the underlying association is conditional. It exists already for
339                // an interactively created association definition. Its ID is already set.
340                if (assocDef.getId() == -1) {
341                    dms.createAssociation(assocDef, null);      // clientState=null
342                }
343                // Note: the assoc def ID is known only after creating the association
344                long assocDefId = assocDef.getId();
345                // cardinality
346                associateParentCardinality(assocDefId, assocDef.getParentCardinalityUri());
347                associateChildCardinality(assocDefId, assocDef.getChildCardinalityUri());
348                //
349                storeViewConfig(createConfigurableAssocDef(assocDefId), assocDef.getViewConfigModel());
350            } catch (Exception e) {
351                throw new RuntimeException("Storing association definition \"" + assocDef.getChildTypeUri() +
352                    "\" of type \"" + assocDef.getParentTypeUri() + "\" failed", e);
353            }
354        }
355    
356    
357    
358        // === Parent Type / Child Type ===
359    
360        // --- Fetch ---
361    
362        @Override
363        public Topic fetchParentType(Association assoc) {
364            Topic parentTypeTopic = assoc.getTopic("dm4.core.parent_type");
365            // error check
366            if (parentTypeTopic == null) {
367                throw new RuntimeException("Invalid association definition: topic role dm4.core.parent_type " +
368                    "is missing in " + assoc);
369            }
370            //
371            return parentTypeTopic;
372        }
373    
374        @Override
375        public Topic fetchChildType(Association assoc) {
376            Topic childTypeTopic = assoc.getTopic("dm4.core.child_type");
377            // error check
378            if (childTypeTopic == null) {
379                throw new RuntimeException("Invalid association definition: topic role dm4.core.child_type " +
380                    "is missing in " + assoc);
381            }
382            //
383            return childTypeTopic;
384        }
385    
386    
387    
388        // === Cardinality ===
389    
390        // --- Fetch ---
391    
392        // ### TODO: pass Association instead ID?
393        private RelatedTopicModel fetchParentCardinality(long assocDefId) {
394            RelatedTopicModel parentCard = dms.storageDecorator.fetchAssociationRelatedTopic(assocDefId,
395                "dm4.core.aggregation", "dm4.core.assoc_def", "dm4.core.parent_cardinality", "dm4.core.cardinality");
396            // error check
397            if (parentCard == null) {
398                throw new RuntimeException("Invalid association definition: parent cardinality is missing (assocDefId=" +
399                    assocDefId + ")");
400            }
401            //
402            return parentCard;
403        }
404    
405        // ### TODO: pass Association instead ID?
406        private RelatedTopicModel fetchChildCardinality(long assocDefId) {
407            RelatedTopicModel childCard = dms.storageDecorator.fetchAssociationRelatedTopic(assocDefId,
408                "dm4.core.aggregation", "dm4.core.assoc_def", "dm4.core.child_cardinality", "dm4.core.cardinality");
409            // error check
410            if (childCard == null) {
411                throw new RuntimeException("Invalid association definition: child cardinality is missing (assocDefId=" +
412                    assocDefId + ")");
413            }
414            //
415            return childCard;
416        }
417    
418        // --- Store ---
419    
420        void storeParentCardinalityUri(long assocDefId, String parentCardinalityUri) {
421            // remove current assignment
422            long assocId = fetchParentCardinality(assocDefId).getRelatingAssociation().getId();
423            dms.deleteAssociation(assocId);
424            // create new assignment
425            associateParentCardinality(assocDefId, parentCardinalityUri);
426        }
427    
428        void storeChildCardinalityUri(long assocDefId, String childCardinalityUri) {
429            // remove current assignment
430            long assocId = fetchChildCardinality(assocDefId).getRelatingAssociation().getId();
431            dms.deleteAssociation(assocId);
432            // create new assignment
433            associateChildCardinality(assocDefId, childCardinalityUri);
434        }
435    
436        // ---
437    
438        private void associateParentCardinality(long assocDefId, String parentCardinalityUri) {
439            dms.createAssociation("dm4.core.aggregation",
440                new TopicRoleModel(parentCardinalityUri, "dm4.core.parent_cardinality"),
441                new AssociationRoleModel(assocDefId, "dm4.core.assoc_def"));
442        }
443    
444        private void associateChildCardinality(long assocDefId, String childCardinalityUri) {
445            dms.createAssociation("dm4.core.aggregation",
446                new TopicRoleModel(childCardinalityUri, "dm4.core.child_cardinality"),
447                new AssociationRoleModel(assocDefId, "dm4.core.assoc_def"));
448        }
449    
450    
451    
452        // === Sequence ===
453    
454        // --- Fetch ---
455    
456        // Note: the sequence is fetched in 2 situations:
457        // 1) When fetching a type's association definitions.
458        //    In this situation we don't have a Type object at hand but a sole type topic.
459        // 2) When deleting a sequence in order to rebuild it.
460        private List<RelatedAssociationModel> fetchSequence(Topic typeTopic) {
461            try {
462                List<RelatedAssociationModel> sequence = new ArrayList();
463                // find sequence start
464                RelatedAssociation assocDef = typeTopic.getRelatedAssociation("dm4.core.aggregation", "dm4.core.type",
465                    "dm4.core.sequence_start", null, false, false);     // othersAssocTypeUri=null
466                // fetch sequence segments
467                if (assocDef != null) {
468                    sequence.add(assocDef.getModel());
469                    while ((assocDef = assocDef.getRelatedAssociation("dm4.core.sequence", "dm4.core.predecessor",
470                        "dm4.core.successor")) != null) {
471                        //
472                        sequence.add(assocDef.getModel());
473                    }
474                }
475                //
476                return sequence;
477            } catch (Exception e) {
478                throw new RuntimeException("Fetching sequence for type \"" + typeTopic.getUri() + "\" failed", e);
479            }
480        }
481    
482        // --- Store ---
483    
484        private void storeSequence(String typeUri, Collection<AssociationDefinitionModel> assocDefs) {
485            logger.info("### Storing " + assocDefs.size() + " sequence segments for type \"" + typeUri + "\"");
486            AssociationDefinitionModel predecessor = null;
487            for (AssociationDefinitionModel assocDef : assocDefs) {
488                appendToSequence(typeUri, assocDef, predecessor);
489                predecessor = assocDef;
490            }
491        }
492    
493        void appendToSequence(String typeUri, AssociationDefinitionModel assocDef, AssociationDefinitionModel predecessor) {
494            if (predecessor == null) {
495                storeSequenceStart(typeUri, assocDef.getId());
496            } else {
497                storeSequenceSegment(predecessor.getId(), assocDef.getId());
498            }
499        }
500    
501        private void storeSequenceStart(String typeUri, long assocDefId) {
502            dms.createAssociation("dm4.core.aggregation",
503                new TopicRoleModel(typeUri, "dm4.core.type"),
504                new AssociationRoleModel(assocDefId, "dm4.core.sequence_start"));
505        }
506    
507        private void storeSequenceSegment(long predAssocDefId, long succAssocDefId) {
508            dms.createAssociation("dm4.core.sequence",
509                new AssociationRoleModel(predAssocDefId, "dm4.core.predecessor"),
510                new AssociationRoleModel(succAssocDefId, "dm4.core.successor"));
511        }
512    
513        // ---
514    
515        void rebuildSequence(Type type) {
516            deleteSequence(type);
517            storeSequence(type.getUri(), type.getModel().getAssocDefs());
518        }
519    
520        private void deleteSequence(Topic typeTopic) {
521            List<RelatedAssociationModel> sequence = fetchSequence(typeTopic);
522            logger.info("### Deleting " + sequence.size() + " sequence segments of type \"" + typeTopic.getUri() + "\"");
523            for (RelatedAssociationModel assoc : sequence) {
524                long assocId = assoc.getRelatingAssociation().getId();
525                dms.deleteAssociation(assocId);
526            }
527        }
528    
529    
530    
531        // === Label Configuration ===
532    
533        // --- Fetch ---
534    
535        private List<String> fetchLabelConfig(List<AssociationDefinitionModel> assocDefs) {
536            List<String> labelConfig = new ArrayList();
537            for (AssociationDefinitionModel assocDef : assocDefs) {
538                RelatedTopicModel includeInLabel = fetchLabelConfigTopic(assocDef.getId());
539                if (includeInLabel != null && includeInLabel.getSimpleValue().booleanValue()) {
540                    labelConfig.add(assocDef.getChildTypeUri());
541                }
542            }
543            return labelConfig;
544        }
545    
546        private RelatedTopicModel fetchLabelConfigTopic(long assocDefId) {
547            return dms.storageDecorator.fetchAssociationRelatedTopic(assocDefId, "dm4.core.composition",
548                "dm4.core.parent", "dm4.core.child", "dm4.core.include_in_label");
549        }
550    
551        // --- Store ---
552    
553        void storeLabelConfig(List<String> labelConfig, Collection<AssociationDefinitionModel> assocDefs,
554                                                                                    Directives directives) {
555            for (AssociationDefinitionModel assocDef : assocDefs) {
556                boolean includeInLabel = labelConfig.contains(assocDef.getChildTypeUri());
557                new AttachedAssociationDefinition(assocDef, dms).getCompositeValue()
558                    .set("dm4.core.include_in_label", includeInLabel, null, directives);    // clientState=null
559            }
560        }
561    
562    
563    
564        // === View Configurations ===
565    
566        // --- Fetch ---
567    
568        private ViewConfigurationModel fetchTypeViewConfig(Topic typeTopic) {
569            try {
570                // Note: othersTopicTypeUri=null, the view config's topic type is unknown (it is client-specific)
571                ResultList<RelatedTopic> configTopics = typeTopic.getRelatedTopics("dm4.core.aggregation",
572                    "dm4.core.type", "dm4.core.view_config", null, true, false, 0);
573                return new ViewConfigurationModel(DeepaMehtaUtils.toTopicModels(configTopics.getItems()));
574            } catch (Exception e) {
575                throw new RuntimeException("Fetching view configuration for type \"" + typeTopic.getUri() +
576                    "\" failed", e);
577            }
578        }
579    
580        private ViewConfigurationModel fetchAssocDefViewConfig(Association assocDef) {
581            try {
582                // Note: othersTopicTypeUri=null, the view config's topic type is unknown (it is client-specific)
583                ResultList<RelatedTopic> configTopics = assocDef.getRelatedTopics("dm4.core.aggregation",
584                    "dm4.core.assoc_def", "dm4.core.view_config", null, true, false, 0);
585                return new ViewConfigurationModel(DeepaMehtaUtils.toTopicModels(configTopics.getItems()));
586            } catch (Exception e) {
587                throw new RuntimeException("Fetching view configuration for association definition " + assocDef.getId() +
588                    " failed", e);
589            }
590        }
591    
592        // ---
593    
594        private RelatedTopicModel fetchTypeViewConfigTopic(long typeId, String configTypeUri) {
595            // Note: the composite is not fetched as it is not needed
596            return dms.storageDecorator.fetchTopicRelatedTopic(typeId, "dm4.core.aggregation",
597                "dm4.core.type", "dm4.core.view_config", configTypeUri);
598        }
599    
600        private RelatedTopicModel fetchAssocDefViewConfigTopic(long assocDefId, String configTypeUri) {
601            // Note: the composite is not fetched as it is not needed
602            return dms.storageDecorator.fetchAssociationRelatedTopic(assocDefId, "dm4.core.aggregation",
603                "dm4.core.assoc_def", "dm4.core.view_config", configTypeUri);
604        }
605    
606        // ---
607    
608        private TopicModel fetchViewConfigTopic(RoleModel configurable, String configTypeUri) {
609            if (configurable instanceof TopicRoleModel) {
610                long typeId = configurable.getPlayerId();
611                return fetchTypeViewConfigTopic(typeId, configTypeUri);
612            } else if (configurable instanceof AssociationRoleModel) {
613                long assocDefId = configurable.getPlayerId();
614                return fetchAssocDefViewConfigTopic(assocDefId, configTypeUri);
615            } else {
616                throw new RuntimeException("Unexpected configurable: " + configurable);
617            }
618        }
619    
620        // --- Store ---
621    
622        private void storeViewConfig(RoleModel configurable, ViewConfigurationModel viewConfig) {
623            try {
624                for (TopicModel configTopic : viewConfig.getConfigTopics()) {
625                    storeViewConfigTopic(configurable, configTopic);
626                }
627            } catch (Exception e) {
628                throw new RuntimeException("Storing view configuration failed (configurable=" + configurable + ")", e);
629            }
630        }
631    
632        void storeViewConfigTopic(RoleModel configurable, TopicModel configTopic) {
633            // Note: null is passed as clientState. Called only (indirectly) from a migration ### FIXME: is this true?
634            // and in a migration we have no clientState anyway.
635            Topic topic = dms.createTopic(configTopic, null);   // clientState=null
636            dms.createAssociation("dm4.core.aggregation", configurable,
637                new TopicRoleModel(topic.getId(), "dm4.core.view_config"));
638        }
639    
640        // ---
641    
642        /**
643         * Prerequisite: for the configurable a config topic of type configTypeUri exists in the DB.
644         */
645        void storeViewConfigSetting(RoleModel configurable, String configTypeUri, String settingUri, Object value) {
646            try {
647                TopicModel configTopic = fetchViewConfigTopic(configurable, configTypeUri);
648                // ### TODO: do not create an attached topic here. Can we use the value storage?
649                new AttachedTopic(configTopic, dms).getCompositeValue().set(settingUri, value, null, new Directives());
650            } catch (Exception e) {
651                throw new RuntimeException("Storing view configuration setting failed (configurable=" + configurable +
652                    ", configTypeUri=\"" + configTypeUri + "\", settingUri=\"" + settingUri + "\", value=\"" + value +
653                    "\")", e);
654            }
655        }
656    
657        // --- Helper ---
658    
659        RoleModel createConfigurableType(long typeId) {
660            return new TopicRoleModel(typeId, "dm4.core.type");
661        }
662    
663        RoleModel createConfigurableAssocDef(long assocDefId) {
664            return new AssociationRoleModel(assocDefId, "dm4.core.assoc_def");
665        }
666    }