001package de.deepamehta.core.impl;
002
003import de.deepamehta.core.Topic;
004import de.deepamehta.core.model.AssociationDefinitionModel;
005import de.deepamehta.core.model.AssociationModel;
006import de.deepamehta.core.model.ChildTopicsModel;
007import de.deepamehta.core.model.DeepaMehtaObjectModel;
008import de.deepamehta.core.model.IndexMode;
009import de.deepamehta.core.model.RelatedTopicModel;
010import de.deepamehta.core.model.RoleModel;
011import de.deepamehta.core.model.SimpleValue;
012import de.deepamehta.core.model.TopicModel;
013import de.deepamehta.core.model.TypeModel;
014import de.deepamehta.core.model.ViewConfigurationModel;
015import de.deepamehta.core.util.DeepaMehtaUtils;
016
017import static java.util.Arrays.asList;
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.logging.Logger;
024
025
026
027/**
028 * Storage-impl agnostic support for fetching/storing type models.
029 */
030class TypeStorage {
031
032    // ------------------------------------------------------------------------------------------------------- Constants
033
034    // role types
035    private static final String PARENT_CARDINALITY = "dm4.core.parent_cardinality";
036    private static final String CHILD_CARDINALITY  = "dm4.core.child_cardinality";
037
038    // ---------------------------------------------------------------------------------------------- Instance Variables
039
040    private Map<String, TypeModelImpl> typeCache = new HashMap();   // type model cache
041
042    private EndlessRecursionDetection endlessRecursionDetection = new EndlessRecursionDetection();
043
044    private PersistenceLayer pl;
045    private ModelFactoryImpl mf;
046
047    private Logger logger = Logger.getLogger(getClass().getName());
048
049    // ---------------------------------------------------------------------------------------------------- Constructors
050
051    TypeStorage(PersistenceLayer pl) {
052        this.pl = pl;
053        this.mf = pl.mf;
054    }
055
056    // --------------------------------------------------------------------------------------------------------- Methods
057
058
059
060    // === Type Model Cache ===
061
062    TopicTypeModelImpl getTopicType(String topicTypeUri) {
063        TopicTypeModelImpl topicType = (TopicTypeModelImpl) getType(topicTypeUri);
064        return topicType != null ? topicType : fetchTopicType(topicTypeUri);
065    }
066
067    AssociationTypeModelImpl getAssociationType(String assocTypeUri) {
068        AssociationTypeModelImpl assocType = (AssociationTypeModelImpl) getType(assocTypeUri);
069        return assocType != null ? assocType : fetchAssociationType(assocTypeUri);
070    }
071
072    // ---
073
074    void putInTypeCache(TypeModelImpl type) {
075        typeCache.put(type.uri, type);
076    }
077
078    void removeFromTypeCache(String typeUri) {
079        logger.info("### Removing type \"" + typeUri + "\" from type cache");
080        if (typeCache.remove(typeUri) == null) {
081            throw new RuntimeException("Type \"" + typeUri + "\" not found in type cache");
082        }
083    }
084
085    // ---
086
087    // ### FIXME: make private
088    TypeModelImpl getType(String typeUri) {
089        return typeCache.get(typeUri);
090    }
091
092
093
094    // === Types ===
095
096    // --- Fetch ---
097
098    private TopicTypeModelImpl fetchTopicType(String topicTypeUri) {
099        try {
100            logger.info("Fetching topic type \"" + topicTypeUri + "\"");
101            endlessRecursionDetection.check(topicTypeUri);
102            //
103            // fetch generic topic
104            TopicModelImpl typeTopic = pl.fetchTopic("uri", new SimpleValue(topicTypeUri));
105            checkTopicType(topicTypeUri, typeTopic);
106            //
107            // fetch type-specific parts
108            String dataTypeUri = fetchDataTypeTopic(typeTopic.getId(), topicTypeUri, "topic type").getUri();
109            List<IndexMode> indexModes = fetchIndexModes(typeTopic.getId());
110            List<AssociationDefinitionModel> assocDefs = fetchAssociationDefinitions(typeTopic);
111            List<String> labelConfig = fetchLabelConfig(assocDefs);
112            ViewConfigurationModel viewConfig = fetchTypeViewConfig(typeTopic);
113            //
114            // create and cache type model
115            TopicTypeModelImpl topicType = mf.newTopicTypeModel(typeTopic, dataTypeUri, indexModes,
116                assocDefs, labelConfig, viewConfig);
117            putInTypeCache(topicType);
118            return topicType;
119        } catch (Exception e) {
120            throw new RuntimeException("Fetching topic type \"" + topicTypeUri + "\" failed", e);
121        } finally {
122            endlessRecursionDetection.reset(topicTypeUri);
123        }
124    }
125
126    private AssociationTypeModelImpl fetchAssociationType(String assocTypeUri) {
127        try {
128            logger.info("Fetching association type \"" + assocTypeUri + "\"");
129            endlessRecursionDetection.check(assocTypeUri);
130            //
131            // fetch generic topic
132            TopicModelImpl typeTopic = pl.fetchTopic("uri", new SimpleValue(assocTypeUri));
133            checkAssociationType(assocTypeUri, typeTopic);
134            //
135            // fetch type-specific parts
136            String dataTypeUri = fetchDataTypeTopic(typeTopic.getId(), assocTypeUri, "association type").getUri();
137            List<IndexMode> indexModes = fetchIndexModes(typeTopic.getId());
138            List<AssociationDefinitionModel> assocDefs = fetchAssociationDefinitions(typeTopic);
139            List<String> labelConfig = fetchLabelConfig(assocDefs);
140            ViewConfigurationModel viewConfig = fetchTypeViewConfig(typeTopic);
141            //
142            // create and cache type model
143            AssociationTypeModelImpl assocType = mf.newAssociationTypeModel(typeTopic, dataTypeUri, indexModes,
144                assocDefs, labelConfig, viewConfig);
145            putInTypeCache(assocType);
146            return assocType;
147        } catch (Exception e) {
148            throw new RuntimeException("Fetching association type \"" + assocTypeUri + "\" failed", e);
149        } finally {
150            endlessRecursionDetection.reset(assocTypeUri);
151        }
152    }
153
154    // ---
155
156    private void checkTopicType(String topicTypeUri, TopicModel typeTopic) {
157        if (typeTopic == null) {
158            throw new RuntimeException("Topic type \"" + topicTypeUri + "\" not found in DB");
159        } else if (!typeTopic.getTypeUri().equals("dm4.core.topic_type") &&
160                   !typeTopic.getTypeUri().equals("dm4.core.meta_type") &&
161                   !typeTopic.getTypeUri().equals("dm4.core.meta_meta_type")) {
162            throw new RuntimeException("URI \"" + topicTypeUri + "\" refers to a \"" + typeTopic.getTypeUri() +
163                "\" when the caller expects a \"dm4.core.topic_type\"");
164        }
165    }
166
167    private void checkAssociationType(String assocTypeUri, TopicModel typeTopic) {
168        if (typeTopic == null) {
169            throw new RuntimeException("Association type \"" + assocTypeUri + "\" not found in DB");
170        } else if (!typeTopic.getTypeUri().equals("dm4.core.assoc_type")) {
171            throw new RuntimeException("URI \"" + assocTypeUri + "\" refers to a \"" + typeTopic.getTypeUri() +
172                "\" when the caller expects a \"dm4.core.assoc_type\"");
173        }
174    }
175
176    // --- Store ---
177
178    /**
179     * Stores the type-specific parts of the given type model.
180     * Prerequisite: the generic topic parts are stored already.
181     * <p>
182     * Called to store a newly created topic type or association type.
183     */
184    void storeType(TypeModelImpl type) {
185        // 1) put in type model cache
186        // Note: an association type must be put in type model cache *before* storing its association definitions.
187        // Consider creation of association type "Composition Definition": it has a composition definition itself.
188        putInTypeCache(type);
189        //
190        // 2) store type-specific parts
191        storeDataType(type.getUri(), type.getDataTypeUri());
192        storeIndexModes(type.getUri(), type.getIndexModes());
193        storeAssocDefs(type.getId(), type.getAssocDefs());
194        storeLabelConfig(type.getLabelConfig(), type.getAssocDefs());
195        storeViewConfig(newTypeRole(type.getId()), type.getViewConfigModel());
196    }
197
198
199
200    // === Data Type ===
201
202    // --- Fetch ---
203
204    private RelatedTopicModel fetchDataTypeTopic(long typeId, String typeUri, String className) {
205        try {
206            RelatedTopicModel dataType = pl.fetchTopicRelatedTopic(typeId, "dm4.core.aggregation", "dm4.core.type",
207                "dm4.core.default", "dm4.core.data_type");
208            if (dataType == null) {
209                throw new RuntimeException("No data type topic is associated to " + className + " \"" + typeUri + "\"");
210            }
211            return dataType;
212        } catch (Exception e) {
213            throw new RuntimeException("Fetching the data type topic of " + className + " \"" + typeUri + "\" failed",
214                e);
215        }
216    }
217
218    // --- Store ---
219
220    // ### TODO: compare to low-level method CoreServiceImpl._associateDataType(). Remove structural similarity.
221    void storeDataType(String typeUri, String dataTypeUri) {
222        try {
223            pl.createAssociation("dm4.core.aggregation",
224                mf.newTopicRoleModel(typeUri,     "dm4.core.type"),
225                mf.newTopicRoleModel(dataTypeUri, "dm4.core.default")
226            );
227        } catch (Exception e) {
228            throw new RuntimeException("Associating type \"" + typeUri + "\" with data type \"" +
229                dataTypeUri + "\" failed", e);
230        }
231    }
232
233
234
235    // === Index Modes ===
236
237    // --- Fetch ---
238
239    private List<IndexMode> fetchIndexModes(long typeId) {
240        List<RelatedTopicModelImpl> indexModes = pl.fetchTopicRelatedTopics(typeId, "dm4.core.aggregation",
241            "dm4.core.type", "dm4.core.default", "dm4.core.index_mode");
242        return IndexMode.fromTopics(indexModes);
243    }
244
245    // --- Store ---
246
247    private void storeIndexModes(String typeUri, List<IndexMode> indexModes) {
248        for (IndexMode indexMode : indexModes) {
249            storeIndexMode(typeUri, indexMode);
250        }
251    }
252
253    void storeIndexMode(String typeUri, IndexMode indexMode) {
254        pl.createAssociation("dm4.core.aggregation",
255            mf.newTopicRoleModel(typeUri,           "dm4.core.type"),
256            mf.newTopicRoleModel(indexMode.toUri(), "dm4.core.default")
257        );
258    }
259
260
261
262    // === Association Definitions ===
263
264    // Note: if the underlying association was an association definition before it has cardinality
265    // assignments already. These assignments are restored. Otherwise "One" is used as default.
266    private String defaultCardinalityUri(AssociationModel assoc, String cardinalityRoleTypeUri) {
267        RelatedTopicModel cardinality = fetchCardinality(assoc.getId(), cardinalityRoleTypeUri);
268        if (cardinality != null) {
269            return cardinality.getUri();
270        } else {
271            return "dm4.core.one";
272        }
273    }
274
275    // --- Fetch ---
276
277    private List<AssociationDefinitionModel> fetchAssociationDefinitions(TopicModelImpl typeTopic) {
278        Map<Long, AssociationDefinitionModel> assocDefs = fetchAssociationDefinitionsUnsorted(typeTopic);
279        List<RelatedAssociationModelImpl> sequence = fetchSequence(typeTopic);
280        // error check
281        if (assocDefs.size() != sequence.size()) {
282            throw new RuntimeException("DB inconsistency: type \"" + typeTopic.getUri() + "\" has " +
283                assocDefs.size() + " association definitions but in sequence are " + sequence.size());
284        }
285        //
286        return sortAssocDefs(assocDefs, DeepaMehtaUtils.idList(sequence));
287    }
288
289    private Map<Long, AssociationDefinitionModel> fetchAssociationDefinitionsUnsorted(TopicModelImpl typeTopic) {
290        Map<Long, AssociationDefinitionModel> assocDefs = new HashMap();
291        //
292        // 1) fetch child topic types
293        // Note: we must set fetchRelatingComposite to false here. Fetching the composite of association type
294        // Composition Definition would cause an endless recursion. Composition Definition is defined through
295        // Composition Definition itself (child types "Include in Label", "Ordered"). ### FIXDOC: this is obsolete
296        // Note: the "othersTopicTypeUri" filter is not set here (null). We want match both "dm4.core.topic_type"
297        // and "dm4.core.meta_type" (the latter is required e.g. by dm4-mail). ### TODO: add a getRelatedTopics()
298        // method that takes a list of topic types.
299        List<RelatedTopicModelImpl> childTypes = typeTopic.getRelatedTopics(asList("dm4.core.aggregation_def",
300            "dm4.core.composition_def"), "dm4.core.parent_type", "dm4.core.child_type", null);
301            // othersTopicTypeUri=null
302        //
303        // 2) create association definitions
304        // Note: the returned map is an intermediate, hashed by ID. The actual type model is
305        // subsequently build from it by sorting the assoc def's according to the sequence IDs.
306        for (RelatedTopicModel childType : childTypes) {
307            AssociationDefinitionModel assocDef = fetchAssociationDefinition(childType.getRelatingAssociation(),
308                typeTopic.getUri(), childType.getUri());
309            assocDefs.put(assocDef.getId(), assocDef);
310        }
311        return assocDefs;
312    }
313
314    // ---
315
316    /**
317     * Creates an assoc def from an association which may or may not have been an assoc def before.
318     * This is needed when an association becomes an assoc def through retyping.
319     * <p>
320     * Note: the assoc is **not** required to identify its players by URI (by ID is OK)
321     */
322    AssociationDefinitionModelImpl newAssociationDefinition(AssociationModel assoc) {
323        // Note: we must not manipulate the assoc model in-place. The Webclient expects by-ID roles.
324        AssociationModel model = mf.newAssociationModel(assoc);
325        String parentTypeUri = fetchParentTypeTopic(assoc).getUri();
326        String childTypeUri = fetchChildTypeTopic(assoc).getUri();
327        prepareAssocModel(model, parentTypeUri, childTypeUri);
328        return mf.newAssociationDefinitionModel(model,
329            defaultCardinalityUri(assoc, PARENT_CARDINALITY),
330            defaultCardinalityUri(assoc, CHILD_CARDINALITY),
331            null   // viewConfigModel=null
332        );
333    }
334
335    // Note: the assoc is **not** required to identify its players by URI (by ID is OK)
336    private AssociationDefinitionModel fetchAssociationDefinition(AssociationModel assoc, String parentTypeUri,
337                                                                                          String childTypeUri) {
338        try {
339            prepareAssocModel(assoc, parentTypeUri, childTypeUri);
340            return mf.newAssociationDefinitionModel(assoc,
341                fetchCardinalityOrThrow(assoc.getId(), PARENT_CARDINALITY).getUri(),
342                fetchCardinalityOrThrow(assoc.getId(), CHILD_CARDINALITY).getUri(),
343                fetchAssocDefViewConfig(assoc)
344            );
345        } catch (Exception e) {
346            throw new RuntimeException("Fetching association definition failed (parentTypeUri=\"" + parentTypeUri +
347                "\", childTypeUri=" + childTypeUri + ", " + assoc + ")", e);
348        }
349    }
350
351    /**
352     * Prepares an assoc model for being used as the base for an assoc def model.
353     */
354    private void prepareAssocModel(AssociationModel assoc, String parentTypeUri, String childTypeUri) {
355        long assocDefId = assoc.getId();
356        assoc.setRoleModel1(mf.newTopicRoleModel(parentTypeUri, "dm4.core.parent_type"));
357        assoc.setRoleModel2(mf.newTopicRoleModel(childTypeUri,  "dm4.core.child_type"));
358        ChildTopicsModel childTopics = assoc.getChildTopicsModel();
359        RelatedTopicModel customAssocType = fetchCustomAssocType(assocDefId);
360        if (customAssocType != null) {
361            childTopics.put("dm4.core.assoc_type#dm4.core.custom_assoc_type", customAssocType);
362        }
363        RelatedTopicModel includeInLabel = fetchIncludeInLabel(assocDefId);
364        if (includeInLabel != null) {   // ### TODO: a includeInLabel topic should always exist
365            childTopics.put("dm4.core.include_in_label", includeInLabel);
366        }
367    }
368
369    private RelatedTopicModel fetchCustomAssocType(long assocDefId) {
370        // ### TODO: can we use type-driven retrieval?
371        return pl.fetchAssociationRelatedTopic(assocDefId, "dm4.core.custom_assoc_type", "dm4.core.parent",
372            "dm4.core.child", "dm4.core.assoc_type");
373    }
374
375    private RelatedTopicModel fetchIncludeInLabel(long assocDefId) {
376        // ### TODO: can we use type-driven retrieval?
377        return pl.fetchAssociationRelatedTopic(assocDefId, "dm4.core.composition", "dm4.core.parent",
378            "dm4.core.child", "dm4.core.include_in_label");
379    }
380
381    // ---
382
383    private List<AssociationDefinitionModel> sortAssocDefs(Map<Long, AssociationDefinitionModel> assocDefs,
384                                                           List<Long> sequence) {
385        List<AssociationDefinitionModel> sortedAssocDefs = new ArrayList();
386        for (long assocDefId : sequence) {
387            AssociationDefinitionModel assocDef = assocDefs.get(assocDefId);
388            // error check
389            if (assocDef == null) {
390                throw new RuntimeException("DB inconsistency: ID " + assocDefId +
391                    " is in sequence but not in the type's association definitions");
392            }
393            sortedAssocDefs.add(assocDef);
394        }
395        return sortedAssocDefs;
396    }
397
398    // --- Store ---
399
400    private void storeAssocDefs(long typeId, Collection<AssociationDefinitionModelImpl> assocDefs) {
401        for (AssociationDefinitionModelImpl assocDef : assocDefs) {
402            storeAssociationDefinition(assocDef);
403        }
404        storeSequence(typeId, assocDefs);
405    }
406
407    void storeAssociationDefinition(AssociationDefinitionModelImpl assocDef) {
408        try {
409            long assocDefId = assocDef.getId();
410            //
411            // 1) create association
412            // Note: if the association definition has been created interactively the underlying association
413            // exists already. We must not create it again. We detect this case by inspecting the ID.
414            if (assocDefId == -1) {
415                assocDefId = pl.createAssociation(assocDef).getId();
416            }
417            //
418            // 2) cardinality
419            // Note: if the underlying association was an association definition before it has cardinality
420            // assignments already. These must be removed before assigning new cardinality.
421            removeCardinalityAssignmentIfExists(assocDefId, PARENT_CARDINALITY);
422            removeCardinalityAssignmentIfExists(assocDefId, CHILD_CARDINALITY);
423            associateCardinality(assocDefId, PARENT_CARDINALITY, assocDef.getParentCardinalityUri());
424            associateCardinality(assocDefId, CHILD_CARDINALITY,  assocDef.getChildCardinalityUri());
425            //
426            // 3) view config
427            storeViewConfig(newAssocDefRole(assocDefId), assocDef.getViewConfigModel());
428        } catch (Exception e) {
429            throw new RuntimeException("Storing association definition \"" + assocDef.getAssocDefUri() +
430                "\" of type \"" + assocDef.getParentTypeUri() + "\" failed", e);
431        }
432    }
433
434
435
436    // === Parent Type / Child Type ===
437
438    // --- Fetch ---
439
440    /**
441     * @param   assoc   an association representing an association definition
442     *
443     * @return  the parent type topic.
444     *          A topic representing either a topic type or an association type.
445     */
446    private TopicModel fetchParentTypeTopic(AssociationModel assoc) {
447        TopicModel parentType = ((AssociationModelImpl) assoc).getTopic("dm4.core.parent_type");
448        // error check
449        if (parentType == null) {
450            throw new RuntimeException("DB inconsistency: topic role \"dm4.core.parent_type\" is missing in " + assoc);
451        }
452        //
453        return parentType;
454    }
455
456    /**
457     * @param   assoc   an association representing an association definition
458     *
459     * @return  the child type topic.
460     *          A topic representing a topic type.
461     */
462    private TopicModel fetchChildTypeTopic(AssociationModel assoc) {
463        TopicModel childType = ((AssociationModelImpl) assoc).getTopic("dm4.core.child_type");
464        // error check
465        if (childType == null) {
466            throw new RuntimeException("DB inconsistency: topic role \"dm4.core.child_type\" is missing in " + assoc);
467        }
468        //
469        return childType;
470    }
471
472    // ---
473
474    TypeModelImpl fetchParentType(AssociationModel assoc) {
475        TopicModel type = fetchParentTypeTopic(assoc);
476        String typeUri = type.getTypeUri();
477        if (typeUri.equals("dm4.core.topic_type")) {
478            return getTopicType(type.getUri());
479        } else if (typeUri.equals("dm4.core.assoc_type")) {
480            return getAssociationType(type.getUri());
481        } else {
482            throw new RuntimeException("DB inconsistency: the \"dm4.core.parent_type\" player is not a type " +
483                "but of type \"" + typeUri + "\" in " + assoc);
484        }
485    }
486
487
488
489    // === Cardinality ===
490
491    // --- Fetch ---
492
493    private RelatedTopicModelImpl fetchCardinality(long assocDefId, String cardinalityRoleTypeUri) {
494        return pl.fetchAssociationRelatedTopic(assocDefId, "dm4.core.aggregation", "dm4.core.assoc_def",
495            cardinalityRoleTypeUri, "dm4.core.cardinality");
496    }
497
498    private RelatedTopicModelImpl fetchCardinalityOrThrow(long assocDefId, String cardinalityRoleTypeUri) {
499        RelatedTopicModelImpl cardinality = fetchCardinality(assocDefId, cardinalityRoleTypeUri);
500        // error check
501        if (cardinality == null) {
502            throw new RuntimeException("DB inconsistency: association definition " + assocDefId +
503                " is missing a cardinality (\"" + cardinalityRoleTypeUri + "\")");
504        }
505        //
506        return cardinality;
507    }
508
509    // --- Store ---
510
511    void storeParentCardinalityUri(long assocDefId, String parentCardinalityUri) {
512        storeCardinalityUri(assocDefId, PARENT_CARDINALITY, parentCardinalityUri);
513    }
514
515    void storeChildCardinalityUri(long assocDefId, String childCardinalityUri) {
516        storeCardinalityUri(assocDefId, CHILD_CARDINALITY, childCardinalityUri);
517    }
518
519    // ---
520
521    private void storeCardinalityUri(long assocDefId, String cardinalityRoleTypeUri, String cardinalityUri) {
522        // remove current assignment
523        RelatedTopicModelImpl cardinality = fetchCardinalityOrThrow(assocDefId, cardinalityRoleTypeUri);
524        removeCardinalityAssignment(cardinality);
525        // create new assignment
526        associateCardinality(assocDefId, cardinalityRoleTypeUri, cardinalityUri);
527    }
528
529    private void removeCardinalityAssignmentIfExists(long assocDefId, String cardinalityRoleTypeUri) {
530        RelatedTopicModelImpl cardinality = fetchCardinality(assocDefId, cardinalityRoleTypeUri);
531        if (cardinality != null) {
532            removeCardinalityAssignment(cardinality);
533        }
534    }
535
536    private void removeCardinalityAssignment(RelatedTopicModelImpl cardinalityAssignment) {
537        cardinalityAssignment.getRelatingAssociation().delete();
538    }
539
540    private void associateCardinality(long assocDefId, String cardinalityRoleTypeUri, String cardinalityUri) {
541        pl.createAssociation("dm4.core.aggregation",
542            mf.newTopicRoleModel(cardinalityUri, cardinalityRoleTypeUri),
543            mf.newAssociationRoleModel(assocDefId, "dm4.core.assoc_def")
544        );
545    }
546
547
548
549    // === Sequence ===
550
551    // --- Fetch ---
552
553    // Note: the sequence is fetched in 2 situations:
554    // 1) When fetching a type's association definitions.
555    //    In this situation we don't have a DeepaMehtaType object at hand but a sole type topic.
556    // 2) When deleting a sequence in order to rebuild it.
557    private List<RelatedAssociationModelImpl> fetchSequence(TopicModel typeTopic) {
558        try {
559            List<RelatedAssociationModelImpl> sequence = new ArrayList();
560            //
561            RelatedAssociationModelImpl assocDef = fetchSequenceStart(typeTopic.getId());
562            if (assocDef != null) {
563                sequence.add(assocDef);
564                while ((assocDef = fetchSuccessor(assocDef.getId())) != null) {
565                    sequence.add(assocDef);
566                }
567            }
568            //
569            return sequence;
570        } catch (Exception e) {
571            throw new RuntimeException("Fetching sequence for type \"" + typeTopic.getUri() + "\" failed", e);
572        }
573    }
574
575    // ---
576
577    private RelatedAssociationModelImpl fetchSequenceStart(long typeId) {
578        return pl.fetchTopicRelatedAssociation(typeId, "dm4.core.aggregation", "dm4.core.type",
579            "dm4.core.sequence_start", null);   // othersAssocTypeUri=null
580    }
581
582    private RelatedAssociationModelImpl fetchSuccessor(long assocDefId) {
583        return pl.fetchAssociationRelatedAssociation(assocDefId, "dm4.core.sequence", "dm4.core.predecessor",
584            "dm4.core.successor", null);        // othersAssocTypeUri=null
585    }
586
587    private RelatedAssociationModelImpl fetchPredecessor(long assocDefId) {
588        return pl.fetchAssociationRelatedAssociation(assocDefId, "dm4.core.sequence", "dm4.core.successor",
589            "dm4.core.predecessor", null);      // othersAssocTypeUri=null
590    }
591
592    // --- Store ---
593
594    private void storeSequence(long typeId, Collection<AssociationDefinitionModelImpl> assocDefs) {
595        logger.fine("### Storing " + assocDefs.size() + " sequence segments for type " + typeId);
596        long predAssocDefId = -1;
597        for (AssociationDefinitionModel assocDef : assocDefs) {
598            addAssocDefToSequence(typeId, assocDef.getId(), -1, -1, predAssocDefId);
599            predAssocDefId = assocDef.getId();
600        }
601    }
602
603    /**
604     * Adds an assoc def to the sequence. Depending on the last 3 arguments either appends it at end, inserts it at
605     * start, or inserts it in the middle.
606     *
607     * @param   beforeAssocDefId    the ID of the assoc def <i>before</i> the assoc def is added
608     *                              If <code>-1</code> the assoc def is <b>appended at end</b>.
609     *                              In this case <code>lastAssocDefId</code> must identify the end.
610     *                              (<code>firstAssocDefId</code> is not relevant in this case.)
611     * @param   firstAssocDefId     Identifies the first assoc def. If this equals the ID of the assoc def to add
612     *                              the assoc def is <b>inserted at start</b>.
613     */
614    void addAssocDefToSequence(long typeId, long assocDefId, long beforeAssocDefId, long firstAssocDefId,
615                                                                                    long lastAssocDefId) {
616        if (beforeAssocDefId == -1) {
617            // append at end
618            appendToSequence(typeId, assocDefId, lastAssocDefId);
619        } else if (firstAssocDefId == assocDefId) {
620            // insert at start
621            insertAtSequenceStart(typeId, assocDefId);
622        } else {
623            // insert in the middle
624            insertIntoSequence(assocDefId, beforeAssocDefId);
625        }
626    }
627
628    private void appendToSequence(long typeId, long assocDefId, long predAssocDefId) {
629        if (predAssocDefId == -1) {
630            storeSequenceStart(typeId, assocDefId);
631        } else {
632            storeSequenceSegment(predAssocDefId, assocDefId);
633        }
634    }
635
636    private void insertAtSequenceStart(long typeId, long assocDefId) {
637        // delete sequence start
638        RelatedAssociationModelImpl assocDef = fetchSequenceStart(typeId);
639        assocDef.getRelatingAssociation().delete();
640        // reconnect
641        storeSequenceStart(typeId, assocDefId);
642        storeSequenceSegment(assocDefId, assocDef.getId());
643    }
644
645    private void insertIntoSequence(long assocDefId, long beforeAssocDefId) {
646        // delete sequence segment
647        RelatedAssociationModelImpl assocDef = fetchPredecessor(beforeAssocDefId);
648        assocDef.getRelatingAssociation().delete();
649        // reconnect
650        storeSequenceSegment(assocDef.getId(), assocDefId);
651        storeSequenceSegment(assocDefId, beforeAssocDefId);
652    }
653
654    // ---
655
656    private void storeSequenceStart(long typeId, long assocDefId) {
657        pl.createAssociation("dm4.core.aggregation",
658            mf.newTopicRoleModel(typeId, "dm4.core.type"),
659            mf.newAssociationRoleModel(assocDefId, "dm4.core.sequence_start")
660        );
661    }
662
663    private void storeSequenceSegment(long predAssocDefId, long succAssocDefId) {
664        pl.createAssociation("dm4.core.sequence",
665            mf.newAssociationRoleModel(predAssocDefId, "dm4.core.predecessor"),
666            mf.newAssociationRoleModel(succAssocDefId, "dm4.core.successor")
667        );
668    }
669
670    // ---
671
672    void rebuildSequence(TypeModelImpl type) {
673        deleteSequence(type);
674        storeSequence(type.getId(), type.getAssocDefs());
675    }
676
677    private void deleteSequence(TopicModel typeTopic) {
678        List<RelatedAssociationModelImpl> sequence = fetchSequence(typeTopic);
679        logger.info("### Deleting " + sequence.size() + " sequence segments of type \"" + typeTopic.getUri() + "\"");
680        for (RelatedAssociationModelImpl assoc : sequence) {
681            assoc.getRelatingAssociation().delete();
682        }
683    }
684
685
686
687    // === Label Configuration ===
688
689    // --- Fetch ---
690
691    // ### TODO: to be dropped
692    private List<String> fetchLabelConfig(List<AssociationDefinitionModel> assocDefs) {
693        List<String> labelConfig = new ArrayList();
694        for (AssociationDefinitionModel assocDef : assocDefs) {
695            RelatedTopicModel includeInLabel = fetchIncludeInLabel(assocDef.getId());
696            if (includeInLabel != null && includeInLabel.getSimpleValue().booleanValue()) {
697                labelConfig.add(assocDef.getAssocDefUri());
698            }
699        }
700        return labelConfig;
701    }
702
703    // --- Store ---
704
705    /**
706     * Stores the label configuration of a <i>newly created</i> type.
707     */
708    private void storeLabelConfig(List<String> labelConfig, Collection<AssociationDefinitionModelImpl> assocDefs) {
709        for (AssociationDefinitionModel assocDef : assocDefs) {
710            boolean includeInLabel = labelConfig.contains(assocDef.getAssocDefUri());
711            // Note: we don't do the storage in a type-driven fashion here (as in new AssociationDefinitionImpl(
712            // assocDef, dm4).getChildTopics().set(...)). A POST_UPDATE_ASSOCIATION event would be fired for the
713            // assoc def and the Type Editor plugin would react and try to access the assoc def's parent type.
714            // This means retrieving a type that is in-mid its storage process. Strange errors would occur.
715            // As a workaround we create the child topic manually.
716            Topic topic = pl.createTopic(mf.newTopicModel("dm4.core.include_in_label",
717                new SimpleValue(includeInLabel)));
718            pl.createAssociation("dm4.core.composition",
719                mf.newAssociationRoleModel(assocDef.getId(), "dm4.core.parent"),
720                mf.newTopicRoleModel(topic.getId(), "dm4.core.child")
721            );
722        }
723    }
724
725    /**
726     * Updates the label configuration of an <i>existing</i> type.
727     */
728    void updateLabelConfig(List<String> labelConfig, Collection<AssociationDefinitionModelImpl> assocDefs) {
729        for (AssociationDefinitionModel assocDef : assocDefs) {
730            // Note: the Type Editor plugin must not react
731            TopicModel includeInLabel = fetchIncludeInLabel(assocDef.getId());
732            boolean value = labelConfig.contains(assocDef.getAssocDefUri());
733            pl.storeTopicValue(includeInLabel.getId(), new SimpleValue(value));
734        }
735    }
736
737
738
739    // === View Configurations ===
740
741    // --- Fetch ---
742
743    private ViewConfigurationModel fetchTypeViewConfig(TopicModel typeTopic) {
744        try {
745            // Note: othersTopicTypeUri=null, the view config's topic type is unknown (it is client-specific)
746            return viewConfigModel(pl.fetchTopicRelatedTopics(typeTopic.getId(), "dm4.core.aggregation",
747                "dm4.core.type", "dm4.core.view_config", null));
748        } catch (Exception e) {
749            throw new RuntimeException("Fetching view configuration for type \"" + typeTopic.getUri() +
750                "\" failed", e);
751        }
752    }
753
754    private ViewConfigurationModel fetchAssocDefViewConfig(AssociationModel assocDef) {
755        try {
756            // Note: othersTopicTypeUri=null, the view config's topic type is unknown (it is client-specific)
757            return viewConfigModel(pl.fetchAssociationRelatedTopics(assocDef.getId(), "dm4.core.aggregation",
758                "dm4.core.assoc_def", "dm4.core.view_config", null));
759        } catch (Exception e) {
760            throw new RuntimeException("Fetching view configuration for assoc def " + assocDef.getId() + " failed", e);
761        }
762    }
763
764    // ---
765
766    private ViewConfigurationModel viewConfigModel(Iterable<? extends TopicModel> configTopics) {
767        fetchChildTopics(configTopics);
768        return mf.newViewConfigurationModel(configTopics);
769    }
770
771    // --- Store ---
772
773    private void storeViewConfig(RoleModel configurable, ViewConfigurationModelImpl viewConfig) {
774        try {
775            for (TopicModelImpl configTopic : viewConfig.getConfigTopics()) {
776                storeViewConfigTopic(configurable, configTopic);
777            }
778        } catch (Exception e) {
779            throw new RuntimeException("Storing view configuration failed (configurable=" + configurable +
780                ", viewConfig=" + viewConfig + ")", e);
781        }
782    }
783
784    void storeViewConfigTopic(RoleModel configurable, TopicModelImpl configTopic) {
785        pl.createTopic(configTopic);
786        pl.createAssociation("dm4.core.aggregation", configurable, mf.newTopicRoleModel(configTopic.getId(),
787            "dm4.core.view_config"));
788    }
789
790    // --- Helper ---
791
792    private void fetchChildTopics(Iterable<? extends DeepaMehtaObjectModel> objects) {
793        for (DeepaMehtaObjectModel object : objects) {
794            pl.valueStorage.fetchChildTopics(object);
795        }
796    }
797
798    // ---
799
800    RoleModel newTypeRole(long typeId) {
801        return mf.newTopicRoleModel(typeId, "dm4.core.type");
802    }
803
804    RoleModel newAssocDefRole(long assocDefId) {
805        return mf.newAssociationRoleModel(assocDefId, "dm4.core.assoc_def");
806    }
807
808
809
810    // ------------------------------------------------------------------------------------------------- Private Classes
811
812    private static final class EndlessRecursionDetection {
813
814        private Map<String, Boolean> loadInProgress = new HashMap();
815
816        private void check(String typeUri) {
817            if (loadInProgress.get(typeUri) != null) {
818                throw new RuntimeException("Endless recursion detected while loading type \"" + typeUri + "\"");
819            }
820            loadInProgress.put(typeUri, true);
821        }
822
823        private void reset(String typeUri) {
824            loadInProgress.remove(typeUri);
825        }
826    }
827}