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