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        void storeIndexModes(String typeUri, List<IndexMode> indexModes) {
228            for (IndexMode indexMode : indexModes) {
229                dms.createAssociation("dm4.core.aggregation",
230                    new TopicRoleModel(typeUri,           "dm4.core.type"),
231                    new TopicRoleModel(indexMode.toUri(), "dm4.core.default"));
232            }
233        }
234    
235    
236    
237        // === Association Definitions ===
238    
239        // --- Fetch ---
240    
241        private List<AssociationDefinitionModel> fetchAssociationDefinitions(Topic typeTopic) {
242            Map<Long, AssociationDefinitionModel> assocDefs = fetchAssociationDefinitionsUnsorted(typeTopic);
243            List<RelatedAssociationModel> sequence = fetchSequence(typeTopic);
244            // error check
245            if (assocDefs.size() != sequence.size()) {
246                throw new RuntimeException("DB inconsistency: type \"" + typeTopic.getUri() + "\" has " +
247                    assocDefs.size() + " association definitions but in sequence are " + sequence.size());
248            }
249            //
250            return sortAssocDefs(assocDefs, DeepaMehtaUtils.idList(sequence));
251        }
252    
253        private Map<Long, AssociationDefinitionModel> fetchAssociationDefinitionsUnsorted(Topic typeTopic) {
254            Map<Long, AssociationDefinitionModel> assocDefs = new HashMap();
255            //
256            // 1) fetch child topic types
257            // Note: we must set fetchRelatingComposite to false here. Fetching the composite of association type
258            // Composition Definition would cause an endless recursion. Composition Definition is defined through
259            // Composition Definition itself (child types "Include in Label", "Ordered").
260            // Note: "othersTopicTypeUri" is set to null. We want consider "dm4.core.topic_type" and "dm4.core.meta_type"
261            // as well (the latter required e.g. by dm4-mail) ### TODO: add a getRelatedTopics() method that takes a list
262            // of topic types.
263            ResultList<RelatedTopic> childTypes = typeTopic.getRelatedTopics(asList("dm4.core.aggregation_def",
264                "dm4.core.composition_def"), "dm4.core.parent_type", "dm4.core.child_type", null, false, false, 0);
265                // othersTopicTypeUri=null, fetchComposite=false, fetchRelatingComposite=false, clientState=null
266            //
267            // 2) create association definitions
268            // Note: the returned map is an intermediate, hashed by ID. The actual type model is
269            // subsequently build from it by sorting the assoc def's according to the sequence IDs.
270            for (RelatedTopic childType : childTypes) {
271                AssociationDefinitionModel assocDef = fetchAssociationDefinition(childType.getRelatingAssociation(),
272                    typeTopic.getUri(), childType.getUri());
273                assocDefs.put(assocDef.getId(), assocDef);
274            }
275            return assocDefs;
276        }
277    
278        // ---
279    
280        @Override
281        public AssociationDefinitionModel fetchAssociationDefinition(Association assoc) {
282            return fetchAssociationDefinition(assoc, fetchParentType(assoc).getUri(), fetchChildType(assoc).getUri());
283        }
284    
285        private AssociationDefinitionModel fetchAssociationDefinition(Association assoc, String parentTypeUri,
286                                                                                         String childTypeUri) {
287            try {
288                long assocId = assoc.getId();
289                return new AssociationDefinitionModel(
290                    assocId, assoc.getUri(), assoc.getTypeUri(),
291                    parentTypeUri, childTypeUri,
292                    fetchParentCardinality(assocId).getUri(), fetchChildCardinality(assocId).getUri(),
293                    fetchAssocDefViewConfig(assoc)
294                );
295            } catch (Exception e) {
296                throw new RuntimeException("Fetching association definition failed (parentTypeUri=\"" + parentTypeUri +
297                    "\", childTypeUri=" + childTypeUri + ", " + assoc + ")", e);
298            }
299        }
300    
301        // ---
302    
303        private List<AssociationDefinitionModel> sortAssocDefs(Map<Long, AssociationDefinitionModel> assocDefs,
304                                                               List<Long> sequence) {
305            List<AssociationDefinitionModel> sortedAssocDefs = new ArrayList();
306            for (long assocDefId : sequence) {
307                AssociationDefinitionModel assocDef = assocDefs.get(assocDefId);
308                // error check
309                if (assocDef == null) {
310                    throw new RuntimeException("DB inconsistency: ID " + assocDefId +
311                        " is in sequence but not in the type's association definitions");
312                }
313                sortedAssocDefs.add(assocDef);
314            }
315            return sortedAssocDefs;
316        }
317    
318        // --- Store ---
319    
320        private void storeAssocDefs(String typeUri, Collection<AssociationDefinitionModel> assocDefs) {
321            for (AssociationDefinitionModel assocDef : assocDefs) {
322                storeAssociationDefinition(assocDef);
323            }
324            storeSequence(typeUri, assocDefs);
325        }
326    
327        void storeAssociationDefinition(AssociationDefinitionModel assocDef) {
328            try {
329                // Note: creating the underlying association is conditional. It exists already for
330                // an interactively created association definition. Its ID is already set.
331                if (assocDef.getId() == -1) {
332                    dms.createAssociation(assocDef, null);      // clientState=null
333                }
334                // Note: the assoc def ID is known only after creating the association
335                long assocDefId = assocDef.getId();
336                // cardinality
337                associateParentCardinality(assocDefId, assocDef.getParentCardinalityUri());
338                associateChildCardinality(assocDefId, assocDef.getChildCardinalityUri());
339                //
340                storeViewConfig(createConfigurableAssocDef(assocDefId), assocDef.getViewConfigModel());
341            } catch (Exception e) {
342                throw new RuntimeException("Storing association definition \"" + assocDef.getChildTypeUri() +
343                    "\" of type \"" + assocDef.getParentTypeUri() + "\" failed", e);
344            }
345        }
346    
347    
348    
349        // === Parent Type / Child Type ===
350    
351        // --- Fetch ---
352    
353        @Override
354        public Topic fetchParentType(Association assoc) {
355            Topic parentTypeTopic = assoc.getTopic("dm4.core.parent_type");
356            // error check
357            if (parentTypeTopic == null) {
358                throw new RuntimeException("Invalid association definition: topic role dm4.core.parent_type " +
359                    "is missing in " + assoc);
360            }
361            //
362            return parentTypeTopic;
363        }
364    
365        @Override
366        public Topic fetchChildType(Association assoc) {
367            Topic childTypeTopic = assoc.getTopic("dm4.core.child_type");
368            // error check
369            if (childTypeTopic == null) {
370                throw new RuntimeException("Invalid association definition: topic role dm4.core.child_type " +
371                    "is missing in " + assoc);
372            }
373            //
374            return childTypeTopic;
375        }
376    
377    
378    
379        // === Cardinality ===
380    
381        // --- Fetch ---
382    
383        // ### TODO: pass Association instead ID?
384        private RelatedTopicModel fetchParentCardinality(long assocDefId) {
385            RelatedTopicModel parentCard = dms.storageDecorator.fetchAssociationRelatedTopic(assocDefId,
386                "dm4.core.aggregation", "dm4.core.assoc_def", "dm4.core.parent_cardinality", "dm4.core.cardinality");
387            // error check
388            if (parentCard == null) {
389                throw new RuntimeException("Invalid association definition: parent cardinality is missing (assocDefId=" +
390                    assocDefId + ")");
391            }
392            //
393            return parentCard;
394        }
395    
396        // ### TODO: pass Association instead ID?
397        private RelatedTopicModel fetchChildCardinality(long assocDefId) {
398            RelatedTopicModel childCard = dms.storageDecorator.fetchAssociationRelatedTopic(assocDefId,
399                "dm4.core.aggregation", "dm4.core.assoc_def", "dm4.core.child_cardinality", "dm4.core.cardinality");
400            // error check
401            if (childCard == null) {
402                throw new RuntimeException("Invalid association definition: child cardinality is missing (assocDefId=" +
403                    assocDefId + ")");
404            }
405            //
406            return childCard;
407        }
408    
409        // --- Store ---
410    
411        void storeParentCardinalityUri(long assocDefId, String parentCardinalityUri) {
412            // remove current assignment
413            long assocId = fetchParentCardinality(assocDefId).getRelatingAssociation().getId();
414            dms.deleteAssociation(assocId);
415            // create new assignment
416            associateParentCardinality(assocDefId, parentCardinalityUri);
417        }
418    
419        void storeChildCardinalityUri(long assocDefId, String childCardinalityUri) {
420            // remove current assignment
421            long assocId = fetchChildCardinality(assocDefId).getRelatingAssociation().getId();
422            dms.deleteAssociation(assocId);
423            // create new assignment
424            associateChildCardinality(assocDefId, childCardinalityUri);
425        }
426    
427        // ---
428    
429        private void associateParentCardinality(long assocDefId, String parentCardinalityUri) {
430            dms.createAssociation("dm4.core.aggregation",
431                new TopicRoleModel(parentCardinalityUri, "dm4.core.parent_cardinality"),
432                new AssociationRoleModel(assocDefId, "dm4.core.assoc_def"));
433        }
434    
435        private void associateChildCardinality(long assocDefId, String childCardinalityUri) {
436            dms.createAssociation("dm4.core.aggregation",
437                new TopicRoleModel(childCardinalityUri, "dm4.core.child_cardinality"),
438                new AssociationRoleModel(assocDefId, "dm4.core.assoc_def"));
439        }
440    
441    
442    
443        // === Sequence ===
444    
445        // --- Fetch ---
446    
447        // Note: the sequence is fetched in 2 situations:
448        // 1) When fetching a type's association definitions.
449        //    In this situation we don't have a Type object at hand but a sole type topic.
450        // 2) When deleting a sequence in order to rebuild it.
451        private List<RelatedAssociationModel> fetchSequence(Topic typeTopic) {
452            try {
453                List<RelatedAssociationModel> sequence = new ArrayList();
454                // find sequence start
455                RelatedAssociation assocDef = typeTopic.getRelatedAssociation("dm4.core.aggregation", "dm4.core.type",
456                    "dm4.core.sequence_start", null, false, false);     // othersAssocTypeUri=null
457                // fetch sequence segments
458                if (assocDef != null) {
459                    sequence.add(assocDef.getModel());
460                    while ((assocDef = assocDef.getRelatedAssociation("dm4.core.sequence", "dm4.core.predecessor",
461                        "dm4.core.successor")) != null) {
462                        //
463                        sequence.add(assocDef.getModel());
464                    }
465                }
466                //
467                return sequence;
468            } catch (Exception e) {
469                throw new RuntimeException("Fetching sequence for type \"" + typeTopic.getUri() + "\" failed", e);
470            }
471        }
472    
473        // --- Store ---
474    
475        private void storeSequence(String typeUri, Collection<AssociationDefinitionModel> assocDefs) {
476            logger.info("### Storing " + assocDefs.size() + " sequence segments for type \"" + typeUri + "\"");
477            AssociationDefinitionModel predecessor = null;
478            for (AssociationDefinitionModel assocDef : assocDefs) {
479                appendToSequence(typeUri, assocDef, predecessor);
480                predecessor = assocDef;
481            }
482        }
483    
484        void appendToSequence(String typeUri, AssociationDefinitionModel assocDef, AssociationDefinitionModel predecessor) {
485            if (predecessor == null) {
486                storeSequenceStart(typeUri, assocDef.getId());
487            } else {
488                storeSequenceSegment(predecessor.getId(), assocDef.getId());
489            }
490        }
491    
492        private void storeSequenceStart(String typeUri, long assocDefId) {
493            dms.createAssociation("dm4.core.aggregation",
494                new TopicRoleModel(typeUri, "dm4.core.type"),
495                new AssociationRoleModel(assocDefId, "dm4.core.sequence_start"));
496        }
497    
498        private void storeSequenceSegment(long predAssocDefId, long succAssocDefId) {
499            dms.createAssociation("dm4.core.sequence",
500                new AssociationRoleModel(predAssocDefId, "dm4.core.predecessor"),
501                new AssociationRoleModel(succAssocDefId, "dm4.core.successor"));
502        }
503    
504        // ---
505    
506        void rebuildSequence(Type type) {
507            deleteSequence(type);
508            storeSequence(type.getUri(), type.getModel().getAssocDefs());
509        }
510    
511        private void deleteSequence(Topic typeTopic) {
512            List<RelatedAssociationModel> sequence = fetchSequence(typeTopic);
513            logger.info("### Deleting " + sequence.size() + " sequence segments of type \"" + typeTopic.getUri() + "\"");
514            for (RelatedAssociationModel assoc : sequence) {
515                long assocId = assoc.getRelatingAssociation().getId();
516                dms.deleteAssociation(assocId);
517            }
518        }
519    
520    
521    
522        // === Label Configuration ===
523    
524        // --- Fetch ---
525    
526        private List<String> fetchLabelConfig(List<AssociationDefinitionModel> assocDefs) {
527            List<String> labelConfig = new ArrayList();
528            for (AssociationDefinitionModel assocDef : assocDefs) {
529                RelatedTopicModel includeInLabel = fetchLabelConfigTopic(assocDef.getId());
530                if (includeInLabel != null && includeInLabel.getSimpleValue().booleanValue()) {
531                    labelConfig.add(assocDef.getChildTypeUri());
532                }
533            }
534            return labelConfig;
535        }
536    
537        private RelatedTopicModel fetchLabelConfigTopic(long assocDefId) {
538            return dms.storageDecorator.fetchAssociationRelatedTopic(assocDefId, "dm4.core.composition",
539                "dm4.core.parent", "dm4.core.child", "dm4.core.include_in_label");
540        }
541    
542        // --- Store ---
543    
544        void storeLabelConfig(List<String> labelConfig, Collection<AssociationDefinitionModel> assocDefs,
545                                                                                    Directives directives) {
546            for (AssociationDefinitionModel assocDef : assocDefs) {
547                boolean includeInLabel = labelConfig.contains(assocDef.getChildTypeUri());
548                new AttachedAssociationDefinition(assocDef, dms).getCompositeValue()
549                    .set("dm4.core.include_in_label", includeInLabel, null, directives);    // clientState=null
550            }
551        }
552    
553    
554    
555        // === View Configurations ===
556    
557        // --- Fetch ---
558    
559        private ViewConfigurationModel fetchTypeViewConfig(Topic typeTopic) {
560            try {
561                // Note: othersTopicTypeUri=null, the view config's topic type is unknown (it is client-specific)
562                ResultList<RelatedTopic> configTopics = typeTopic.getRelatedTopics("dm4.core.aggregation",
563                    "dm4.core.type", "dm4.core.view_config", null, true, false, 0);
564                return new ViewConfigurationModel(DeepaMehtaUtils.toTopicModels(configTopics.getItems()));
565            } catch (Exception e) {
566                throw new RuntimeException("Fetching view configuration for type \"" + typeTopic.getUri() +
567                    "\" failed", e);
568            }
569        }
570    
571        private ViewConfigurationModel fetchAssocDefViewConfig(Association assocDef) {
572            try {
573                // Note: othersTopicTypeUri=null, the view config's topic type is unknown (it is client-specific)
574                ResultList<RelatedTopic> configTopics = assocDef.getRelatedTopics("dm4.core.aggregation",
575                    "dm4.core.assoc_def", "dm4.core.view_config", null, true, false, 0);
576                return new ViewConfigurationModel(DeepaMehtaUtils.toTopicModels(configTopics.getItems()));
577            } catch (Exception e) {
578                throw new RuntimeException("Fetching view configuration for association definition " + assocDef.getId() +
579                    " failed", e);
580            }
581        }
582    
583        // ---
584    
585        private RelatedTopicModel fetchTypeViewConfigTopic(long typeId, String configTypeUri) {
586            // Note: the composite is not fetched as it is not needed
587            return dms.storageDecorator.fetchTopicRelatedTopic(typeId, "dm4.core.aggregation",
588                "dm4.core.type", "dm4.core.view_config", configTypeUri);
589        }
590    
591        private RelatedTopicModel fetchAssocDefViewConfigTopic(long assocDefId, String configTypeUri) {
592            // Note: the composite is not fetched as it is not needed
593            return dms.storageDecorator.fetchAssociationRelatedTopic(assocDefId, "dm4.core.aggregation",
594                "dm4.core.assoc_def", "dm4.core.view_config", configTypeUri);
595        }
596    
597        // ---
598    
599        private TopicModel fetchViewConfigTopic(RoleModel configurable, String configTypeUri) {
600            if (configurable instanceof TopicRoleModel) {
601                long typeId = configurable.getPlayerId();
602                return fetchTypeViewConfigTopic(typeId, configTypeUri);
603            } else if (configurable instanceof AssociationRoleModel) {
604                long assocDefId = configurable.getPlayerId();
605                return fetchAssocDefViewConfigTopic(assocDefId, configTypeUri);
606            } else {
607                throw new RuntimeException("Unexpected configurable: " + configurable);
608            }
609        }
610    
611        // --- Store ---
612    
613        private void storeViewConfig(RoleModel configurable, ViewConfigurationModel viewConfig) {
614            try {
615                for (TopicModel configTopic : viewConfig.getConfigTopics()) {
616                    storeViewConfigTopic(configurable, configTopic);
617                }
618            } catch (Exception e) {
619                throw new RuntimeException("Storing view configuration failed (configurable=" + configurable + ")", e);
620            }
621        }
622    
623        void storeViewConfigTopic(RoleModel configurable, TopicModel configTopic) {
624            // Note: null is passed as clientState. Called only (indirectly) from a migration ### FIXME: is this true?
625            // and in a migration we have no clientState anyway.
626            Topic topic = dms.createTopic(configTopic, null);   // clientState=null
627            dms.createAssociation("dm4.core.aggregation", configurable,
628                new TopicRoleModel(topic.getId(), "dm4.core.view_config"));
629        }
630    
631        // ---
632    
633        /**
634         * Prerequisite: for the configurable a config topic of type configTypeUri exists in the DB.
635         */
636        void storeViewConfigSetting(RoleModel configurable, String configTypeUri, String settingUri, Object value) {
637            try {
638                TopicModel configTopic = fetchViewConfigTopic(configurable, configTypeUri);
639                // ### TODO: do not create an attached topic here. Can we use the value storage?
640                new AttachedTopic(configTopic, dms).getCompositeValue().set(settingUri, value, null, new Directives());
641            } catch (Exception e) {
642                throw new RuntimeException("Storing view configuration setting failed (configurable=" + configurable +
643                    ", configTypeUri=\"" + configTypeUri + "\", settingUri=\"" + settingUri + "\", value=\"" + value +
644                    "\")", e);
645            }
646        }
647    
648        // --- Helper ---
649    
650        RoleModel createConfigurableType(long typeId) {
651            return new TopicRoleModel(typeId, "dm4.core.type");
652        }
653    
654        RoleModel createConfigurableAssocDef(long assocDefId) {
655            return new AssociationRoleModel(assocDefId, "dm4.core.assoc_def");
656        }
657    }