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