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