001package de.deepamehta.core.impl;
002
003import de.deepamehta.core.DeepaMehtaObject;
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.TopicDeletionModel;
014import de.deepamehta.core.model.TopicReferenceModel;
015import de.deepamehta.core.model.TypeModel;
016import de.deepamehta.core.service.DeepaMehtaEvent;
017import de.deepamehta.core.service.Directive;
018import de.deepamehta.core.service.Directives;
019import de.deepamehta.core.util.JavaUtils;
020
021import org.codehaus.jettison.json.JSONObject;
022
023import java.util.ArrayList;
024import java.util.Iterator;
025import java.util.List;
026import java.util.logging.Logger;
027
028
029
030class DeepaMehtaObjectModelImpl implements DeepaMehtaObjectModel {
031
032    // ------------------------------------------------------------------------------------------------------- Constants
033
034    private static final String LABEL_CHILD_SEPARATOR = " ";
035    private static final String LABEL_TOPIC_SEPARATOR = ", ";
036
037    // ---------------------------------------------------------------------------------------------- Instance Variables
038
039    long id;                            // is -1 in models used for a create operation. ### FIXDOC
040                                        // is never -1 in models used for an update operation.
041    String uri;                         // is never null in models used for a create operation, may be empty. ### FIXDOC
042                                        // may be null in models used for an update operation.
043    String typeUri;                     // is never null in models used for a create operation. ### FIXDOC
044                                        // may be null in models used for an update operation.
045    SimpleValue value;                  // is never null in models used for a create operation, may be constructed
046                                        //                                                   on empty string. ### FIXDOC
047                                        // may be null in models used for an update operation.
048    ChildTopicsModelImpl childTopics;   // is never null, may be empty. ### FIXDOC
049
050    // ---
051
052    PersistenceLayer pl;
053    EventManager em;
054    ModelFactoryImpl mf;
055
056    Logger logger = Logger.getLogger(getClass().getName());
057
058    // ---------------------------------------------------------------------------------------------------- Constructors
059
060    DeepaMehtaObjectModelImpl(long id, String uri, String typeUri, SimpleValue value, ChildTopicsModelImpl childTopics,
061                                                                                      PersistenceLayer pl) {
062        this.id          = id;
063        this.uri         = uri;
064        this.typeUri     = typeUri;
065        this.value       = value;
066        this.childTopics = childTopics != null ? childTopics : pl.mf.newChildTopicsModel();
067        //
068        this.pl          = pl;
069        this.em          = pl.em;
070        this.mf          = pl.mf;
071    }
072
073    DeepaMehtaObjectModelImpl(DeepaMehtaObjectModelImpl object) {
074        this(object.getId(), object.getUri(), object.getTypeUri(), object.getSimpleValue(),
075            object.getChildTopicsModel(), object.pl);
076    }
077
078    // -------------------------------------------------------------------------------------------------- Public Methods
079
080    // --- ID ---
081
082    @Override
083    public long getId() {
084        return id;
085    }
086
087    @Override
088    public void setId(long id) {
089        this.id = id;
090    }
091
092    // --- URI ---
093
094    @Override
095    public String getUri() {
096        return uri;
097    }
098
099    @Override
100    public void setUri(String uri) {
101        this.uri = uri;
102    }
103
104    // --- Type URI ---
105
106    @Override
107    public String getTypeUri() {
108        return typeUri;
109    }
110
111    @Override
112    public void setTypeUri(String typeUri) {
113        this.typeUri = typeUri;
114    }
115
116    // --- Simple Value ---
117
118    @Override
119    public SimpleValue getSimpleValue() {
120        return value;
121    }
122
123    // ---
124
125    @Override
126    public void setSimpleValue(String value) {
127        setSimpleValue(new SimpleValue(value));
128    }
129
130    @Override
131    public void setSimpleValue(int value) {
132        setSimpleValue(new SimpleValue(value));
133    }
134
135    @Override
136    public void setSimpleValue(long value) {
137        setSimpleValue(new SimpleValue(value));
138    }
139
140    @Override
141    public void setSimpleValue(boolean value) {
142        setSimpleValue(new SimpleValue(value));
143    }
144
145    @Override
146    public void setSimpleValue(SimpleValue value) {
147        this.value = value;
148    }
149
150    // --- Child Topics ---
151
152    @Override
153    public ChildTopicsModelImpl getChildTopicsModel() {
154        return childTopics;
155    }
156
157    @Override
158    public void setChildTopicsModel(ChildTopicsModel childTopics) {
159        this.childTopics = (ChildTopicsModelImpl) childTopics;
160    }
161
162    // --- misc ---
163
164    @Override
165    public void set(DeepaMehtaObjectModel object) {
166        setId(object.getId());
167        setUri(object.getUri());
168        setTypeUri(object.getTypeUri());
169        setSimpleValue(object.getSimpleValue());
170        setChildTopicsModel(object.getChildTopicsModel());
171    }
172
173    // ---
174
175    @Override
176    public RoleModel createRoleModel(String roleTypeUri) {
177        throw new RuntimeException("Not implemented");  // only implemented in subclasses
178        // Note: technically this class is not abstract. It is instantiated by the ModelFactory.
179    }
180
181
182
183    // === Serialization ===
184
185    @Override
186    public JSONObject toJSON() {
187        try {
188            // Note: for models used for topic/association enrichment (e.g. timestamps, permissions)
189            // default values must be set in case they are not fully initialized.
190            setDefaults();
191            //
192            JSONObject o = new JSONObject();
193            o.put("id", id);
194            o.put("uri", uri);
195            o.put("type_uri", typeUri);
196            o.put("value", value.value());
197            o.put("childs", childTopics.toJSON());
198            return o;
199        } catch (Exception e) {
200            throw new RuntimeException("Serialization failed (" + this + ")", e);
201        }
202    }
203
204
205
206    // === Java API ===
207
208    @Override
209    public DeepaMehtaObjectModel clone() {
210        try {
211            DeepaMehtaObjectModel object = (DeepaMehtaObjectModel) super.clone();
212            object.setChildTopicsModel(childTopics.clone());
213            return object;
214        } catch (Exception e) {
215            throw new RuntimeException("Cloning a DeepaMehtaObjectModel failed", e);
216        }
217    }
218
219    @Override
220    public boolean equals(Object o) {
221        return ((DeepaMehtaObjectModel) o).getId() == id;
222    }
223
224    @Override
225    public int hashCode() {
226        return ((Long) id).hashCode();
227    }
228
229    @Override
230    public String toString() {
231        return "id=" + id + ", uri=\"" + uri + "\", typeUri=\"" + typeUri + "\", value=\"" + value +
232            "\", childTopics=" + childTopics;
233    }
234
235    // ----------------------------------------------------------------------------------------- Package Private Methods
236
237
238
239    // === Abstract Methods ===
240
241    // ### TODO: make this a real abstract class.
242    // Change the model factory in a way it never instantiates DeepaMehtaObjectModels.
243
244    String className() {
245        throw new UnsupportedOperationException();
246    }
247
248    DeepaMehtaObject instantiate() {
249        throw new UnsupportedOperationException();
250    }
251
252    DeepaMehtaObjectModelImpl createModelWithChildTopics(ChildTopicsModel childTopics) {
253        throw new UnsupportedOperationException();
254    }
255
256    // ---
257
258    TypeModelImpl getType() {
259        throw new UnsupportedOperationException();
260    }
261
262    List<AssociationModelImpl> getAssociations() {
263        throw new UnsupportedOperationException();
264    }
265
266    // ---
267
268    RelatedTopicModelImpl getRelatedTopic(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri,
269                                                                                     String othersTopicTypeUri) {
270        throw new UnsupportedOperationException();
271    }
272
273    List<RelatedTopicModelImpl> getRelatedTopics(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri,
274                                                                                            String othersTopicTypeUri) {
275        throw new UnsupportedOperationException();
276    }
277
278    List<RelatedTopicModelImpl> getRelatedTopics(List assocTypeUris, String myRoleTypeUri, String othersRoleTypeUri,
279                                                                                           String othersTopicTypeUri) {
280        throw new UnsupportedOperationException();
281    }
282
283    // ---
284
285    void storeUri() {
286        throw new UnsupportedOperationException();
287    }
288
289    void storeTypeUri() {
290        throw new UnsupportedOperationException();
291    }
292
293    /**
294     * Stores and indexes the simple value of the specified topic or association model.
295     * Determines the index key and index modes.
296     */
297    void storeSimpleValue() {
298        throw new UnsupportedOperationException();
299    }
300
301    /**
302     * Indexes the simple value of the given object model according to the given index mode.
303     * <p>
304     * Called to index existing topics/associations once an index mode has been added to a type definition.
305     */
306    void indexSimpleValue(IndexMode indexMode) {
307        throw new UnsupportedOperationException();
308    }
309
310    void storeProperty(String propUri, Object propValue, boolean addToIndex) {
311        throw new UnsupportedOperationException();
312    }
313
314    void removeProperty(String propUri) {
315        throw new UnsupportedOperationException();
316    }
317
318    // ---
319
320    void _delete() {
321        throw new UnsupportedOperationException();
322    }
323
324    // ---
325
326    /**
327     * @throws  AccessControlException
328     */
329    void checkReadAccess() {
330        throw new UnsupportedOperationException();
331    }
332
333    // ---
334
335    DeepaMehtaEvent getPreUpdateEvent() {
336        throw new UnsupportedOperationException();
337    }
338
339    DeepaMehtaEvent getPostUpdateEvent() {
340        throw new UnsupportedOperationException();
341    }
342
343    DeepaMehtaEvent getPreDeleteEvent() {
344        throw new UnsupportedOperationException();
345    }
346
347    DeepaMehtaEvent getPostDeleteEvent() {
348        throw new UnsupportedOperationException();
349    }
350
351    // ---
352
353    Directive getUpdateDirective() {
354        throw new UnsupportedOperationException();
355    }
356
357    Directive getDeleteDirective() {
358        throw new UnsupportedOperationException();
359    }
360
361
362
363    // === Core Internal Hooks ===
364
365    void preCreate() {
366    }
367
368    void postCreate() {
369    }
370
371    // ---
372
373    void preUpdate(DeepaMehtaObjectModel updateModel) {
374    }
375
376    void postUpdate(DeepaMehtaObjectModel updateModel, DeepaMehtaObjectModel oldObject) {
377    }
378
379    // ---
380
381    void preDelete() {
382    }
383
384    void postDelete() {
385    }
386
387
388
389    // === Update (memory + DB) ===
390
391    final void updateWithChildTopics(ChildTopicsModel childTopics) {
392        update(createModelWithChildTopics(childTopics));
393    }
394
395    /**
396     * @param   updateModel    The data to update.
397     *              If the URI is <code>null</code> it is not updated.
398     *              If the type URI is <code>null</code> it is not updated.
399     *              If the simple value is <code>null</code> it is not updated.
400     */
401    final void update(DeepaMehtaObjectModelImpl updateModel) {
402        try {
403            logger.info("Updating " + objectInfo() + " (typeUri=\"" + typeUri + "\")");
404            DeepaMehtaObjectModel oldObject = clone();
405            em.fireEvent(getPreUpdateEvent(), instantiate(), updateModel);
406            //
407            preUpdate(updateModel);
408            //
409            _updateUri(updateModel.getUri());
410            _updateTypeUri(updateModel.getTypeUri());
411            if (isSimple()) {
412                _updateSimpleValue(updateModel.getSimpleValue());
413            } else {
414                _updateChildTopics(updateModel.getChildTopicsModel());
415            }
416            //
417            postUpdate(updateModel, oldObject);
418            //
419            // Note: in case of a type topic the instantiate() call above creates a cloned model
420            // that doesn't reflect the update. Here we instantiate the now updated model.
421            DeepaMehtaObject object = instantiate();
422            Directives.get().add(getUpdateDirective(), object);
423            em.fireEvent(getPostUpdateEvent(), object, updateModel, oldObject);
424        } catch (Exception e) {
425            throw new RuntimeException("Updating " + objectInfo() + " failed (typeUri=\"" + typeUri + "\")", e);
426        }
427    }
428
429    // ---
430
431    final void updateUri(String uri) {
432        setUri(uri);            // update memory
433        storeUri();             // update DB, "abstract"
434    }
435
436    final void updateTypeUri(String typeUri) {
437        setTypeUri(typeUri);    // update memory
438        storeTypeUri();         // update DB, "abstract"
439    }
440
441    final void updateSimpleValue(SimpleValue value) {
442        if (value == null) {
443            throw new IllegalArgumentException("Tried to set a null SimpleValue (" + this + ")");
444        }
445        setSimpleValue(value);  // update memory
446        storeSimpleValue();     // update DB, "abstract"
447    }
448
449
450
451    // === Delete ===
452
453    /**
454     * Deletes 1) this DeepaMehta object's child topics (recursively) which have an underlying association definition of
455     * type "Composition Definition" and 2) deletes all the remaining direct associations of this DeepaMehta object.
456     * <p>
457     * Note: deletion of the object itself is up to the subclasses. ### FIXDOC
458     */
459    final void delete() {
460        try {
461            em.fireEvent(getPreDeleteEvent(), instantiate());
462            //
463            preDelete();
464            //
465            // delete child topics (recursively)
466            for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) {
467                if (assocDef.getTypeUri().equals("dm4.core.composition_def")) {
468                    for (TopicModelImpl childTopic : getRelatedTopics(assocDef.getInstanceLevelAssocTypeUri(),
469                            "dm4.core.parent", "dm4.core.child", assocDef.getChildTypeUri())) {
470                        childTopic.delete();
471                    }
472                }
473            }
474            // delete direct associations
475            for (AssociationModelImpl assoc : getAssociations()) {
476                assoc.delete();
477            }
478            // delete object itself
479            logger.info("Deleting " + objectInfo() + " (typeUri=\"" + typeUri + "\")");
480            _delete();
481            //
482            postDelete();
483            //
484            Directives.get().add(getDeleteDirective(), this);
485            em.fireEvent(getPostDeleteEvent(), this);
486        } catch (IllegalStateException e) {
487            // Note: getAssociations() might throw IllegalStateException and is no problem.
488            // This can happen when this object is an association which is already deleted.
489            //
490            // Consider this particular situation: let A1 and A2 be associations of this object and let A2 point to A1.
491            // If A1 gets deleted first (the association set order is non-deterministic), A2 is implicitely deleted
492            // with it (because it is a direct association of A1 as well). Then when the loop comes to A2
493            // "IllegalStateException: Node[1327] has been deleted in this tx" is thrown because A2 has been deleted
494            // already. (The Node appearing in the exception is the middle node of A2.) If, on the other hand, A2
495            // gets deleted first no error would occur.
496            //
497            // This particular situation exists when e.g. a topicmap is deleted while one of its mapcontext
498            // associations is also a part of the topicmap itself. This originates e.g. when the user reveals
499            // a topicmap's mapcontext association and then deletes the topicmap.
500            //
501            if (e.getMessage().equals("Node[" + id + "] has been deleted in this tx")) {
502                logger.info("### Association " + id + " has already been deleted in this transaction. This can " +
503                    "happen while deleting a topic with associations A1 and A2 while A2 points to A1 (" + this + ")");
504            } else {
505                throw e;
506            }
507        } catch (Exception e) {
508            throw new RuntimeException("Deleting " + objectInfo() + " failed (typeUri=\"" + typeUri + "\")", e);
509        }
510    }
511
512
513
514    // === Update Child Topics (memory + DB) ===
515
516    // ### TODO: make this private. See comment in DeepaMehtaObjectImpl.setChildTopics()
517    final void _updateChildTopics(ChildTopicsModelImpl updateModel) {
518        try {
519            for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) {
520                String assocDefUri    = assocDef.getAssocDefUri();
521                String cardinalityUri = assocDef.getChildCardinalityUri();
522                RelatedTopicModelImpl newChildTopic = null;             // only used for "one"
523                List<RelatedTopicModelImpl> newChildTopics = null;      // only used for "many"
524                if (cardinalityUri.equals("dm4.core.one")) {
525                    newChildTopic = updateModel.getTopicOrNull(assocDefUri);
526                    // skip if not contained in update request
527                    if (newChildTopic == null) {
528                        continue;
529                    }
530                } else if (cardinalityUri.equals("dm4.core.many")) {
531                    newChildTopics = updateModel.getTopicsOrNull(assocDefUri);
532                    // skip if not contained in update request
533                    if (newChildTopics == null) {
534                        continue;
535                    }
536                } else {
537                    throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI");
538                }
539                //
540                updateChildTopics(newChildTopic, newChildTopics, assocDef);
541            }
542            //
543            _calculateLabelAndUpdate();
544            //
545        } catch (Exception e) {
546            throw new RuntimeException("Updating the child topics of " + objectInfo() + " failed", e);
547        }
548    }
549
550    // Note: the given association definition must not necessarily originate from the parent object's type definition.
551    // It may originate from a facet definition as well.
552    // Called from DeepaMehtaObjectImpl.updateChildTopic() and DeepaMehtaObjectImpl.updateChildTopics().
553    // ### TODO: make this private? See comments in DeepaMehtaObjectImpl.
554    final void updateChildTopics(RelatedTopicModelImpl newChildTopic, List<RelatedTopicModelImpl> newChildTopics,
555                                                                      AssociationDefinitionModel assocDef) {
556        // Note: updating the child topics requires them to be loaded
557        loadChildTopics(assocDef);
558        //
559        String assocTypeUri = assocDef.getTypeUri();
560        boolean one = newChildTopic != null;
561        if (assocTypeUri.equals("dm4.core.composition_def")) {
562            if (one) {
563                updateCompositionOne(newChildTopic, assocDef);
564            } else {
565                updateCompositionMany(newChildTopics, assocDef);
566            }
567        } else if (assocTypeUri.equals("dm4.core.aggregation_def")) {
568            if (one) {
569                updateAggregationOne(newChildTopic, assocDef);
570            } else {
571                updateAggregationMany(newChildTopics, assocDef);
572            }
573        } else {
574            throw new RuntimeException("Association type \"" + assocTypeUri + "\" not supported");
575        }
576    }
577
578    // ---
579
580    /**
581     * Loads the child topics which are not loaded already.
582     */
583    final DeepaMehtaObjectModel loadChildTopics() {
584        for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) {
585            loadChildTopics(assocDef);
586        }
587        return this;
588    }
589
590    /**
591     * Loads the child topics for the given assoc def, provided they are not loaded already.
592     */
593    final DeepaMehtaObjectModel loadChildTopics(String assocDefUri) {
594        try {
595            return loadChildTopics(getAssocDef(assocDefUri));
596        } catch (Exception e) {
597            throw new RuntimeException("Loading \"" + assocDefUri + "\" child topics of " + objectInfo() + " failed",
598                e);
599        }
600    }
601
602    // ---
603
604    /**
605     * Calculates the simple value that is to be indexed for this object.
606     *
607     * HTML tags are stripped from HTML values. Non-HTML values are returned directly.
608     */
609    SimpleValue getIndexValue() {
610        SimpleValue value = getSimpleValue();
611        if (getType().getDataTypeUri().equals("dm4.core.html")) {
612            return new SimpleValue(JavaUtils.stripHTML(value.toString()));
613        } else {
614            return value;
615        }
616    }
617
618    boolean uriChange(String newUri, String compareUri) {
619        return newUri != null && !newUri.equals(compareUri);
620    }
621
622    boolean isSimple() {
623        return !getType().getDataTypeUri().equals("dm4.core.composite");
624    }
625
626
627
628    // ------------------------------------------------------------------------------------------------- Private Methods
629
630    // ### TODO: a principal copy exists in Neo4jStorage.
631    // Should this be package private? Should Neo4jStorage have access to the Core's impl package?
632    private void setDefaults() {
633        if (getUri() == null) {
634            setUri("");
635        }
636        if (getSimpleValue() == null) {
637            setSimpleValue("");
638        }
639    }
640
641    /**
642     * Recursively loads child topics (model) and updates this attached object cache accordingly. ### FIXDOC
643     * If the child topics are loaded already nothing is performed.
644     *
645     * @param   assocDef    the child topics according to this association definition are loaded.
646     *                      <p>
647     *                      Note: the association definition must not necessarily originate from the parent object's
648     *                      type definition. It may originate from a facet definition as well.
649     */
650    private DeepaMehtaObjectModel loadChildTopics(AssociationDefinitionModel assocDef) {
651        String assocDefUri = assocDef.getAssocDefUri();
652        if (!childTopics.has(assocDefUri)) {
653            logger.fine("### Lazy-loading \"" + assocDefUri + "\" child topic(s) of " + objectInfo());
654            pl.valueStorage.fetchChildTopics(this, assocDef);
655        }
656        return this;
657    }
658
659
660
661    // === Update (memory + DB) ===
662
663    private void _updateUri(String newUri) {
664        if (uriChange(newUri, uri)) {                               // abort if no update is requested
665            logger.info("### Changing URI of " + objectInfo() + " from \"" + uri + "\" -> \"" + newUri + "\"");
666            updateUri(newUri);
667        }
668    }
669
670    private void _updateTypeUri(String newTypeUri) {
671        if (newTypeUri != null && !newTypeUri.equals(typeUri)) {    // abort if no update is requested
672            logger.info("### Changing type URI of " + objectInfo() + " from \"" + typeUri + "\" -> \"" + newTypeUri +
673                "\"");
674            updateTypeUri(newTypeUri);
675        }
676    }
677
678    private void _updateSimpleValue(SimpleValue newValue) {
679        if (newValue != null && !newValue.equals(value)) {          // abort if no update is requested
680            logger.info("### Changing simple value of " + objectInfo() + " from \"" + value + "\" -> \"" + newValue +
681                "\"");
682            updateSimpleValue(newValue);
683        }
684    }
685
686
687
688    // === Update Child Topics (memory + DB) ===
689
690    // --- Composition ---
691
692    private void updateCompositionOne(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
693        RelatedTopicModelImpl childTopic = childTopics.getTopicOrNull(assocDef.getAssocDefUri());
694        // Note: for cardinality one the simple request format is sufficient. The child's topic ID is not required.
695        // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic.
696        if (newChildTopic instanceof TopicDeletionModel) {
697            deleteChildTopicOne(childTopic, assocDef, true);                                         // deleteChild=true
698        } else if (newChildTopic instanceof TopicReferenceModel) {
699            createAssignmentOne(childTopic, (TopicReferenceModelImpl) newChildTopic, assocDef, true);// deleteChild=true
700        } else if (childTopic != null) {
701            updateRelatedTopic(childTopic, newChildTopic);
702        } else {
703            createChildTopicOne(newChildTopic, assocDef);
704        }
705    }
706
707    private void updateCompositionMany(List<RelatedTopicModelImpl> newChildTopics,
708                                       AssociationDefinitionModel assocDef) {
709        for (RelatedTopicModelImpl newChildTopic : newChildTopics) {
710            long childTopicId = newChildTopic.getId();
711            if (newChildTopic instanceof TopicDeletionModel) {
712                deleteChildTopicMany(childTopicId, assocDef, true);                                 // deleteChild=true
713            } else if (newChildTopic instanceof TopicReferenceModel) {
714                createAssignmentMany((TopicReferenceModelImpl) newChildTopic, assocDef);
715            } else if (childTopicId != -1) {
716                updateChildTopicMany(newChildTopic, assocDef);
717            } else {
718                createChildTopicMany(newChildTopic, assocDef);
719            }
720        }
721    }
722
723    // --- Aggregation ---
724
725    private void updateAggregationOne(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
726        RelatedTopicModelImpl childTopic = childTopics.getTopicOrNull(assocDef.getAssocDefUri());
727        // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic.
728        if (newChildTopic instanceof TopicDeletionModel) {
729            deleteChildTopicOne(childTopic, assocDef, false);                                       // deleteChild=false
730        } else if (newChildTopic instanceof TopicReferenceModel) {
731            createAssignmentOne(childTopic, (TopicReferenceModelImpl) newChildTopic, assocDef, false);
732        } else if (newChildTopic.getId() != -1) {                                                   // deleteChild=false
733            updateChildTopicOne(newChildTopic, assocDef);
734        } else {
735            if (childTopic != null) {
736                childTopic.getRelatingAssociation().delete();
737            }
738            createChildTopicOne(newChildTopic, assocDef);
739        }
740    }
741
742    private void updateAggregationMany(List<RelatedTopicModelImpl> newChildTopics,
743                                       AssociationDefinitionModel assocDef) {
744        for (RelatedTopicModelImpl newChildTopic : newChildTopics) {
745            long childTopicId = newChildTopic.getId();
746            if (newChildTopic instanceof TopicDeletionModel) {
747                deleteChildTopicMany(childTopicId, assocDef, false);                                // deleteChild=false
748            } else if (newChildTopic instanceof TopicReferenceModel) {
749                createAssignmentMany((TopicReferenceModelImpl) newChildTopic, assocDef);
750            } else if (childTopicId != -1) {
751                updateChildTopicMany(newChildTopic, assocDef);
752            } else {
753                createChildTopicMany(newChildTopic, assocDef);
754            }
755        }
756    }
757
758    // --- Update ---
759
760    private void updateChildTopicOne(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
761        RelatedTopicModelImpl childTopic = childTopics.getTopicOrNull(assocDef.getAssocDefUri());
762        //
763        if (childTopic == null || childTopic.getId() != newChildTopic.getId()) {
764            throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " + objectInfo() +
765                " according to " + assocDef);
766        }
767        //
768        updateRelatedTopic(childTopic, newChildTopic);
769        // Note: memory is already up-to-date. The child topic is updated in-place of parent.
770    }
771
772    private void updateChildTopicMany(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
773        RelatedTopicModelImpl childTopic = childTopics.findChildTopicById(newChildTopic.getId(), assocDef);
774        //
775        if (childTopic == null) {
776            throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " + objectInfo() +
777                " according to " + assocDef);
778        }
779        //
780        updateRelatedTopic(childTopic, newChildTopic);
781        // Note: memory is already up-to-date. The child topic is updated in-place of parent.
782    }
783
784    // ---
785
786    private void updateRelatedTopic(RelatedTopicModelImpl childTopic, RelatedTopicModelImpl newChildTopic) {
787        // update topic
788        childTopic.update(newChildTopic);
789        // update association
790        updateRelatingAssociation(childTopic, newChildTopic);
791    }
792
793    private void updateRelatingAssociation(RelatedTopicModelImpl childTopic, RelatedTopicModelImpl newChildTopic) {
794        childTopic.getRelatingAssociation().update(newChildTopic.getRelatingAssociation());
795    }
796
797    // --- Create ---
798
799    private void createChildTopicOne(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
800        // update DB
801        createAndAssociateChildTopic(newChildTopic, assocDef);
802        // update memory
803        childTopics.putInChildTopics(newChildTopic, assocDef);
804    }
805
806    private void createChildTopicMany(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
807        // update DB
808        createAndAssociateChildTopic(newChildTopic, assocDef);
809        // update memory
810        childTopics.addToChildTopics(newChildTopic, assocDef);
811    }
812
813    // ---
814
815    private void createAndAssociateChildTopic(RelatedTopicModelImpl childTopic, AssociationDefinitionModel assocDef) {
816        pl.createTopic(childTopic);
817        associateChildTopic(childTopic, assocDef);
818    }
819
820    // --- Assignment ---
821
822    private void createAssignmentOne(RelatedTopicModelImpl childTopic, TopicReferenceModelImpl newChildTopic,
823                                     AssociationDefinitionModel assocDef, boolean deleteChildTopic) {
824        if (childTopic != null) {
825            if (newChildTopic.isReferingTo(childTopic)) {
826                updateRelatingAssociation(childTopic, newChildTopic);
827                // Note: memory is already up-to-date. The association is updated in-place of parent.
828                return;
829            }
830            if (deleteChildTopic) {
831                childTopic.delete();
832            } else {
833                childTopic.getRelatingAssociation().delete();
834            }
835        }
836        // update DB
837        resolveRefAndAssociateChildTopic(newChildTopic, assocDef);
838        // update memory
839        childTopics.putInChildTopics(newChildTopic, assocDef);
840    }
841
842    private void createAssignmentMany(TopicReferenceModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
843        RelatedTopicModelImpl childTopic = childTopics.findChildTopicByRef(newChildTopic, assocDef);
844        if (childTopic != null) {
845            // Note: "create assignment" is an idempotent operation. A create request for an assignment which
846            // exists already is not an error. Instead, nothing is performed.
847            updateRelatingAssociation(childTopic, newChildTopic);
848            // Note: memory is already up-to-date. The association is updated in-place of parent.
849            return;
850        }
851        // update DB
852        resolveRefAndAssociateChildTopic(newChildTopic, assocDef);
853        // update memory
854        childTopics.addToChildTopics(newChildTopic, assocDef);
855    }
856
857    // ---
858
859    /**
860     * Creates an association between our parent object ("Parent" role) and the referenced topic ("Child" role).
861     * The association type is taken from the given association definition.
862     *
863     * @return  the resolved child topic.
864     */
865    private void resolveRefAndAssociateChildTopic(TopicReferenceModel childTopicRef,
866                                                  AssociationDefinitionModel assocDef) {
867        pl.valueStorage.resolveReference(childTopicRef);
868        associateChildTopic(childTopicRef, assocDef);
869    }
870
871    private void associateChildTopic(RelatedTopicModel childTopic, AssociationDefinitionModel assocDef) {
872        pl.valueStorage.associateChildTopic(this, childTopic, assocDef);
873    }
874
875    // --- Delete ---
876
877    private void deleteChildTopicOne(RelatedTopicModelImpl childTopic, AssociationDefinitionModel assocDef,
878                                                                       boolean deleteChildTopic) {
879        if (childTopic == null) {
880            // Note: "delete child"/"delete assignment" is an idempotent operation. A delete request for a
881            // child/assignment which has been deleted already (resp. is non-existing) is not an error.
882            // Instead, nothing is performed.
883            return;
884        }
885        // update DB
886        if (deleteChildTopic) {
887            childTopic.delete();
888        } else {
889            childTopic.getRelatingAssociation().delete();
890        }
891        // update memory
892        childTopics.removeChildTopic(assocDef);
893    }
894
895    private void deleteChildTopicMany(long childTopicId, AssociationDefinitionModel assocDef,
896                                                         boolean deleteChildTopic) {
897        RelatedTopicModelImpl childTopic = childTopics.findChildTopicById(childTopicId, assocDef);
898        if (childTopic == null) {
899            // Note: "delete child"/"delete assignment" is an idempotent operation. A delete request for a
900            // child/assignment which has been deleted already (resp. is non-existing) is not an error.
901            // Instead, nothing is performed.
902            return;
903        }
904        // update DB
905        if (deleteChildTopic) {
906            childTopic.delete();
907        } else {
908            childTopic.getRelatingAssociation().delete();
909        }
910        // update memory
911        childTopics.removeFromChildTopics(childTopic, assocDef);
912    }
913
914
915
916    // === Label Calculation ===
917
918    private void _calculateLabelAndUpdate() {
919        List<String> labelAssocDefUris = null;
920        try {
921            // load required childs
922            labelAssocDefUris = getLabelAssocDefUris();
923            for (String assocDefUri : labelAssocDefUris) {
924                loadChildTopics(assocDefUri);
925            }
926            //
927            calculateLabelAndUpdate();
928        } catch (Exception e) {
929            throw new RuntimeException("Calculating and updating label of " + objectInfo() +
930                " failed (assoc defs involved: " + labelAssocDefUris + ")", e);
931        }
932    }
933
934    /**
935     * Calculates the label for this object model and updates it in both, memory (in-place), and DB.
936     * <p>
937     * Prerequisites:
938     * 1) this object model is a composite.
939     * 2) this object model contains all the child topic models involved in the label calculation.
940     *    Note: this method does not load any child topics from DB.
941     *
942     * ### TODO: make private
943     */
944    void calculateLabelAndUpdate() {
945        try {
946            updateSimpleValue(new SimpleValue(calculateLabel()));
947        } catch (Exception e) {
948            throw new RuntimeException("Calculating and updating label of " + objectInfo() + " failed", e);
949        }
950    }
951
952    /**
953     * Calculates the label for this object model recursively. Recursion ends at a simple object model.
954     * <p>
955     * Note: called from this class only but can't be private as called on a different object.
956     */
957    String calculateLabel() {
958        TypeModel type = getType();
959        if (type.getDataTypeUri().equals("dm4.core.composite")) {
960            StringBuilder builder = new StringBuilder();
961            for (String assocDefUri : getLabelAssocDefUris()) {
962                appendLabel(calculateChildLabel(assocDefUri), builder, LABEL_CHILD_SEPARATOR);
963            }
964            return builder.toString();
965        } else {
966            return getSimpleValue().toString();
967        }
968    }
969
970    private String calculateChildLabel(String assocDefUri) {
971        Object value = getChildTopicsModel().get(assocDefUri);
972        // Note: topics just created have no child topics yet
973        if (value == null) {
974            return "";
975        }
976        //
977        if (value instanceof TopicModel) {
978            // single value
979            return ((TopicModelImpl) value).calculateLabel();                               // recursion
980        } else if (value instanceof List) {
981            // multiple value
982            StringBuilder builder = new StringBuilder();
983            for (TopicModelImpl childTopic : (List<TopicModelImpl>) value) {
984                appendLabel(childTopic.calculateLabel(), builder, LABEL_TOPIC_SEPARATOR);   // recursion
985            }
986            return builder.toString();
987        } else {
988            throw new RuntimeException("Unexpected value in a ChildTopicsModel: " + value);
989        }
990    }
991
992    private void appendLabel(String label, StringBuilder builder, String separator) {
993        // add separator
994        if (builder.length() > 0 && label.length() > 0) {
995            builder.append(separator);
996        }
997        //
998        builder.append(label);
999    }
1000
1001    /**
1002     * Prerequisite: this is a composite model.
1003     */
1004    private List<String> getLabelAssocDefUris() {
1005        TypeModelImpl type = getType();
1006        List<String> labelConfig = type.getLabelConfig();
1007        if (labelConfig.size() > 0) {
1008            return labelConfig;
1009        } else {
1010            List<String> assocDefUris = new ArrayList();
1011            Iterator<? extends AssociationDefinitionModel> i = type.getAssocDefs().iterator();
1012            // Note: types just created might have no child types yet
1013            if (i.hasNext()) {
1014                assocDefUris.add(i.next().getAssocDefUri());
1015            }
1016            return assocDefUris;
1017        }
1018    }
1019
1020
1021
1022    // === Helper ===
1023
1024    private AssociationDefinitionModel getAssocDef(String assocDefUri) {
1025        // Note: doesn't work for facets
1026        return getType().getAssocDef(assocDefUri);
1027    }
1028
1029    private String objectInfo() {
1030        return className() + " " + id;
1031    }
1032}