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    // ---
253
254    TypeModel getType() {
255        throw new UnsupportedOperationException();
256    }
257
258    List<AssociationModelImpl> getAssociations() {
259        throw new UnsupportedOperationException();
260    }
261
262    // ---
263
264    RelatedTopicModelImpl getRelatedTopic(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri,
265                                                                                     String othersTopicTypeUri) {
266        throw new UnsupportedOperationException();
267    }
268
269    List<RelatedTopicModelImpl> getRelatedTopics(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri,
270                                                                                            String othersTopicTypeUri) {
271        throw new UnsupportedOperationException();
272    }
273
274    List<RelatedTopicModelImpl> getRelatedTopics(List assocTypeUris, String myRoleTypeUri, String othersRoleTypeUri,
275                                                                                           String othersTopicTypeUri) {
276        throw new UnsupportedOperationException();
277    }
278
279    // ---
280
281    void storeUri() {
282        throw new UnsupportedOperationException();
283    }
284
285    void storeTypeUri() {
286        throw new UnsupportedOperationException();
287    }
288
289    /**
290     * Stores and indexes the simple value of the specified topic or association model.
291     * Determines the index key and index modes.
292     */
293    void storeSimpleValue() {
294        throw new UnsupportedOperationException();
295    }
296
297    /**
298     * Indexes the simple value of the given object model according to the given index mode.
299     * <p>
300     * Called to index existing topics/associations once an index mode has been added to a type definition.
301     */
302    void indexSimpleValue(IndexMode indexMode) {
303        throw new UnsupportedOperationException();
304    }
305
306    // ---
307
308    void updateChildTopics(ChildTopicsModel childTopics) {
309        throw new UnsupportedOperationException();
310    }
311
312    void _delete() {
313        throw new UnsupportedOperationException();
314    }
315
316    // ---
317
318    DeepaMehtaEvent getReadAccessEvent() {
319        throw new UnsupportedOperationException();
320    }
321
322    DeepaMehtaEvent getPreUpdateEvent() {
323        throw new UnsupportedOperationException();
324    }
325
326    DeepaMehtaEvent getPostUpdateEvent() {
327        throw new UnsupportedOperationException();
328    }
329
330    DeepaMehtaEvent getPreDeleteEvent() {
331        throw new UnsupportedOperationException();
332    }
333
334    DeepaMehtaEvent getPostDeleteEvent() {
335        throw new UnsupportedOperationException();
336    }
337
338    // ---
339
340    Directive getUpdateDirective() {
341        throw new UnsupportedOperationException();
342    }
343
344    Directive getDeleteDirective() {
345        throw new UnsupportedOperationException();
346    }
347
348
349
350    // === Core Internal Hooks ===
351
352    void preCreate() {
353    }
354
355    void postCreate() {
356    }
357
358    // ---
359
360    void preUpdate(DeepaMehtaObjectModel newModel) {
361    }
362
363    void postUpdate(DeepaMehtaObjectModel newModel, DeepaMehtaObjectModel oldModel) {
364    }
365
366    // ---
367
368    void preDelete() {
369    }
370
371    void postDelete() {
372    }
373
374
375
376    // === Update (memory + DB) ===
377
378    /**
379     * @param   newModel    The data to update.
380     *              If the URI is <code>null</code> it is not updated.
381     *              If the type URI is <code>null</code> it is not updated.
382     *              If the simple value is <code>null</code> it is not updated.
383     */
384    final void update(DeepaMehtaObjectModelImpl newModel) {
385        try {
386            logger.info("Updating " + objectInfo() + " (typeUri=\"" + typeUri + "\")");
387            DeepaMehtaObjectModel oldModel = clone();
388            em.fireEvent(getPreUpdateEvent(), instantiate(), newModel);
389            //
390            preUpdate(newModel);
391            //
392            _updateUri(newModel.getUri());
393            _updateTypeUri(newModel.getTypeUri());
394            if (isSimple()) {
395                _updateSimpleValue(newModel.getSimpleValue());
396            } else {
397                _updateChildTopics(newModel.getChildTopicsModel());
398            }
399            //
400            postUpdate(newModel, oldModel);
401            //
402            // Note: in case of a type topic the instantiate() call above creates a cloned model
403            // that doesn't reflect the update. Here we instantiate the now updated model.
404            DeepaMehtaObject object = instantiate();
405            Directives.get().add(getUpdateDirective(), object);
406            em.fireEvent(getPostUpdateEvent(), object, newModel, oldModel);
407        } catch (Exception e) {
408            throw new RuntimeException("Updating " + objectInfo() + " failed (typeUri=\"" + typeUri + "\")", e);
409        }
410    }
411
412    // ---
413
414    final void updateUri(String uri) {
415        setUri(uri);            // update memory
416        storeUri();             // update DB, "abstract"
417    }
418
419    final void updateTypeUri(String typeUri) {
420        setTypeUri(typeUri);    // update memory
421        storeTypeUri();         // update DB, "abstract"
422    }
423
424    final void updateSimpleValue(SimpleValue value) {
425        if (value == null) {
426            throw new IllegalArgumentException("Tried to set a null SimpleValue (" + this + ")");
427        }
428        setSimpleValue(value);  // update memory
429        storeSimpleValue();     // update DB, "abstract"
430    }
431
432
433
434    // === Delete ===
435
436    /**
437     * Deletes 1) this DeepaMehta object's child topics (recursively) which have an underlying association definition of
438     * type "Composition Definition" and 2) deletes all the remaining direct associations of this DeepaMehta object.
439     * <p>
440     * Note: deletion of the object itself is up to the subclasses. ### FIXDOC
441     */
442    final void delete() {
443        try {
444            em.fireEvent(getPreDeleteEvent(), instantiate());
445            //
446            preDelete();
447            //
448            // delete child topics (recursively)
449            for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) {
450                if (assocDef.getTypeUri().equals("dm4.core.composition_def")) {
451                    for (TopicModelImpl childTopic : getRelatedTopics(assocDef.getInstanceLevelAssocTypeUri(),
452                            "dm4.core.parent", "dm4.core.child", assocDef.getChildTypeUri())) {
453                        childTopic.delete();
454                    }
455                }
456            }
457            // delete direct associations
458            for (AssociationModelImpl assoc : getAssociations()) {
459                assoc.delete();
460            }
461            // delete object itself
462            logger.info("Deleting " + objectInfo() + " (typeUri=\"" + typeUri + "\")");
463            _delete();
464            //
465            postDelete();
466            //
467            Directives.get().add(getDeleteDirective(), this);
468            em.fireEvent(getPostDeleteEvent(), this);
469        } catch (IllegalStateException e) {
470            // Note: getAssociations() might throw IllegalStateException and is no problem.
471            // This can happen when this object is an association which is already deleted.
472            //
473            // Consider this particular situation: let A1 and A2 be associations of this object and let A2 point to A1.
474            // If A1 gets deleted first (the association set order is non-deterministic), A2 is implicitely deleted
475            // with it (because it is a direct association of A1 as well). Then when the loop comes to A2
476            // "IllegalStateException: Node[1327] has been deleted in this tx" is thrown because A2 has been deleted
477            // already. (The Node appearing in the exception is the middle node of A2.) If, on the other hand, A2
478            // gets deleted first no error would occur.
479            //
480            // This particular situation exists when e.g. a topicmap is deleted while one of its mapcontext
481            // associations is also a part of the topicmap itself. This originates e.g. when the user reveals
482            // a topicmap's mapcontext association and then deletes the topicmap.
483            //
484            if (e.getMessage().equals("Node[" + id + "] has been deleted in this tx")) {
485                logger.info("### Association " + id + " has already been deleted in this transaction. This can " +
486                    "happen while deleting a topic with associations A1 and A2 while A2 points to A1 (" + this + ")");
487            } else {
488                throw e;
489            }
490        } catch (Exception e) {
491            throw new RuntimeException("Deleting " + objectInfo() + " failed (typeUri=\"" + typeUri + "\")", e);
492        }
493    }
494
495
496
497    // === Update Child Topics (memory + DB) ===
498
499    // ### TODO: make this private. See comment in DeepaMehtaObjectImpl.setChildTopics()
500    final void _updateChildTopics(ChildTopicsModelImpl newModel) {
501        try {
502            for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) {
503                String assocDefUri    = assocDef.getAssocDefUri();
504                String cardinalityUri = assocDef.getChildCardinalityUri();
505                RelatedTopicModelImpl newChildTopic = null;             // only used for "one"
506                List<RelatedTopicModelImpl> newChildTopics = null;      // only used for "many"
507                if (cardinalityUri.equals("dm4.core.one")) {
508                    newChildTopic = newModel.getTopicOrNull(assocDefUri);
509                    // skip if not contained in update request
510                    if (newChildTopic == null) {
511                        continue;
512                    }
513                } else if (cardinalityUri.equals("dm4.core.many")) {
514                    newChildTopics = newModel.getTopicsOrNull(assocDefUri);
515                    // skip if not contained in update request
516                    if (newChildTopics == null) {
517                        continue;
518                    }
519                } else {
520                    throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI");
521                }
522                //
523                updateChildTopics(newChildTopic, newChildTopics, assocDef);
524            }
525            //
526            _calculateLabelAndUpdate();
527            //
528        } catch (Exception e) {
529            throw new RuntimeException("Updating the child topics of " + objectInfo() + " failed", e);
530        }
531    }
532
533    // Note: the given association definition must not necessarily originate from the parent object's type definition.
534    // It may originate from a facet definition as well.
535    // Called from DeepaMehtaObjectImpl.updateChildTopic() and DeepaMehtaObjectImpl.updateChildTopics().
536    // ### TODO: make this private? See comments in DeepaMehtaObjectImpl.
537    final void updateChildTopics(RelatedTopicModelImpl newChildTopic, List<RelatedTopicModelImpl> newChildTopics,
538                                                                      AssociationDefinitionModel assocDef) {
539        // Note: updating the child topics requires them to be loaded
540        loadChildTopics(assocDef);
541        //
542        String assocTypeUri = assocDef.getTypeUri();
543        boolean one = newChildTopic != null;
544        if (assocTypeUri.equals("dm4.core.composition_def")) {
545            if (one) {
546                updateCompositionOne(newChildTopic, assocDef);
547            } else {
548                updateCompositionMany(newChildTopics, assocDef);
549            }
550        } else if (assocTypeUri.equals("dm4.core.aggregation_def")) {
551            if (one) {
552                updateAggregationOne(newChildTopic, assocDef);
553            } else {
554                updateAggregationMany(newChildTopics, assocDef);
555            }
556        } else {
557            throw new RuntimeException("Association type \"" + assocTypeUri + "\" not supported");
558        }
559    }
560
561    // ---
562
563    /**
564     * Loads the child topics which are not loaded already.
565     */
566    final DeepaMehtaObjectModel loadChildTopics() {
567        for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) {
568            loadChildTopics(assocDef);
569        }
570        return this;
571    }
572
573    /**
574     * Loads the child topics for the given assoc def, provided they are not loaded already.
575     */
576    final DeepaMehtaObjectModel loadChildTopics(String assocDefUri) {
577        try {
578            return loadChildTopics(getAssocDef(assocDefUri));
579        } catch (Exception e) {
580            throw new RuntimeException("Loading \"" + assocDefUri + "\" child topics of " + objectInfo() + " failed",
581                e);
582        }
583    }
584
585    // ---
586
587    /**
588     * Calculates the simple value that is to be indexed for this object.
589     *
590     * HTML tags are stripped from HTML values. Non-HTML values are returned directly.
591     */
592    SimpleValue getIndexValue() {
593        SimpleValue value = getSimpleValue();
594        if (getType().getDataTypeUri().equals("dm4.core.html")) {
595            return new SimpleValue(JavaUtils.stripHTML(value.toString()));
596        } else {
597            return value;
598        }
599    }
600
601    boolean uriChange(String newUri, String compareUri) {
602        return newUri != null && !newUri.equals(compareUri);
603    }
604
605    boolean isSimple() {
606        return !getType().getDataTypeUri().equals("dm4.core.composite");
607    }
608
609
610
611    // ------------------------------------------------------------------------------------------------- Private Methods
612
613    // ### TODO: a principal copy exists in Neo4jStorage.
614    // Should this be package private? Should Neo4jStorage have access to the Core's impl package?
615    private void setDefaults() {
616        if (getUri() == null) {
617            setUri("");
618        }
619        if (getSimpleValue() == null) {
620            setSimpleValue("");
621        }
622    }
623
624    /**
625     * Recursively loads child topics (model) and updates this attached object cache accordingly. ### FIXDOC
626     * If the child topics are loaded already nothing is performed.
627     *
628     * @param   assocDef    the child topics according to this association definition are loaded.
629     *                      <p>
630     *                      Note: the association definition must not necessarily originate from the parent object's
631     *                      type definition. It may originate from a facet definition as well.
632     */
633    private DeepaMehtaObjectModel loadChildTopics(AssociationDefinitionModel assocDef) {
634        String assocDefUri = assocDef.getAssocDefUri();
635        if (!childTopics.has(assocDefUri)) {
636            logger.fine("### Lazy-loading \"" + assocDefUri + "\" child topic(s) of " + objectInfo());
637            pl.valueStorage.fetchChildTopics(this, assocDef);
638        }
639        return this;
640    }
641
642
643
644    // === Update (memory + DB) ===
645
646    private void _updateUri(String newUri) {
647        if (uriChange(newUri, uri)) {                               // abort if no update is requested
648            logger.info("### Changing URI of " + objectInfo() + " from \"" + uri + "\" -> \"" + newUri + "\"");
649            updateUri(newUri);
650        }
651    }
652
653    private void _updateTypeUri(String newTypeUri) {
654        if (newTypeUri != null && !newTypeUri.equals(typeUri)) {    // abort if no update is requested
655            logger.info("### Changing type URI of " + objectInfo() + " from \"" + typeUri + "\" -> \"" + newTypeUri +
656                "\"");
657            updateTypeUri(newTypeUri);
658        }
659    }
660
661    private void _updateSimpleValue(SimpleValue newValue) {
662        if (newValue != null && !newValue.equals(value)) {          // abort if no update is requested
663            logger.info("### Changing simple value of " + objectInfo() + " from \"" + value + "\" -> \"" + newValue +
664                "\"");
665            updateSimpleValue(newValue);
666        }
667    }
668
669
670
671    // === Update Child Topics (memory + DB) ===
672
673    // --- Composition ---
674
675    private void updateCompositionOne(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
676        RelatedTopicModelImpl childTopic = childTopics.getTopicOrNull(assocDef.getAssocDefUri());
677        // Note: for cardinality one the simple request format is sufficient. The child's topic ID is not required.
678        // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic.
679        if (newChildTopic instanceof TopicDeletionModel) {
680            deleteChildTopicOne(childTopic, assocDef, true);                                         // deleteChild=true
681        } else if (newChildTopic instanceof TopicReferenceModel) {
682            createAssignmentOne(childTopic, (TopicReferenceModelImpl) newChildTopic, assocDef, true);// deleteChild=true
683        } else if (childTopic != null) {
684            updateRelatedTopic(childTopic, newChildTopic);
685        } else {
686            createChildTopicOne(newChildTopic, assocDef);
687        }
688    }
689
690    private void updateCompositionMany(List<RelatedTopicModelImpl> newChildTopics,
691                                       AssociationDefinitionModel assocDef) {
692        for (RelatedTopicModelImpl newChildTopic : newChildTopics) {
693            long childTopicId = newChildTopic.getId();
694            if (newChildTopic instanceof TopicDeletionModel) {
695                deleteChildTopicMany(childTopicId, assocDef, true);                                 // deleteChild=true
696            } else if (newChildTopic instanceof TopicReferenceModel) {
697                createAssignmentMany((TopicReferenceModelImpl) newChildTopic, assocDef);
698            } else if (childTopicId != -1) {
699                updateChildTopicMany(newChildTopic, assocDef);
700            } else {
701                createChildTopicMany(newChildTopic, assocDef);
702            }
703        }
704    }
705
706    // --- Aggregation ---
707
708    private void updateAggregationOne(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
709        RelatedTopicModelImpl childTopic = childTopics.getTopicOrNull(assocDef.getAssocDefUri());
710        // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic.
711        if (newChildTopic instanceof TopicDeletionModel) {
712            deleteChildTopicOne(childTopic, assocDef, false);                                       // deleteChild=false
713        } else if (newChildTopic instanceof TopicReferenceModel) {
714            createAssignmentOne(childTopic, (TopicReferenceModelImpl) newChildTopic, assocDef, false);
715        } else if (newChildTopic.getId() != -1) {                                                   // deleteChild=false
716            updateChildTopicOne(newChildTopic, assocDef);
717        } else {
718            if (childTopic != null) {
719                childTopic.getRelatingAssociation().delete();
720            }
721            createChildTopicOne(newChildTopic, assocDef);
722        }
723    }
724
725    private void updateAggregationMany(List<RelatedTopicModelImpl> newChildTopics,
726                                       AssociationDefinitionModel assocDef) {
727        for (RelatedTopicModelImpl newChildTopic : newChildTopics) {
728            long childTopicId = newChildTopic.getId();
729            if (newChildTopic instanceof TopicDeletionModel) {
730                deleteChildTopicMany(childTopicId, assocDef, false);                                // deleteChild=false
731            } else if (newChildTopic instanceof TopicReferenceModel) {
732                createAssignmentMany((TopicReferenceModelImpl) newChildTopic, assocDef);
733            } else if (childTopicId != -1) {
734                updateChildTopicMany(newChildTopic, assocDef);
735            } else {
736                createChildTopicMany(newChildTopic, assocDef);
737            }
738        }
739    }
740
741    // --- Update ---
742
743    private void updateChildTopicOne(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
744        RelatedTopicModelImpl childTopic = childTopics.getTopicOrNull(assocDef.getAssocDefUri());
745        //
746        if (childTopic == null || childTopic.getId() != newChildTopic.getId()) {
747            throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " + objectInfo() +
748                " according to " + assocDef);
749        }
750        //
751        updateRelatedTopic(childTopic, newChildTopic);
752        // Note: memory is already up-to-date. The child topic is updated in-place of parent.
753    }
754
755    private void updateChildTopicMany(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
756        RelatedTopicModelImpl childTopic = childTopics.findChildTopicById(newChildTopic.getId(), assocDef);
757        //
758        if (childTopic == null) {
759            throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " + objectInfo() +
760                " according to " + assocDef);
761        }
762        //
763        updateRelatedTopic(childTopic, newChildTopic);
764        // Note: memory is already up-to-date. The child topic is updated in-place of parent.
765    }
766
767    // ---
768
769    private void updateRelatedTopic(RelatedTopicModelImpl childTopic, RelatedTopicModelImpl newChildTopic) {
770        // update topic
771        childTopic.update(newChildTopic);
772        // update association
773        updateRelatingAssociation(childTopic, newChildTopic);
774    }
775
776    private void updateRelatingAssociation(RelatedTopicModelImpl childTopic, RelatedTopicModelImpl newChildTopic) {
777        childTopic.getRelatingAssociation().update(newChildTopic.getRelatingAssociation());
778    }
779
780    // --- Create ---
781
782    private void createChildTopicOne(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
783        // update DB
784        createAndAssociateChildTopic(newChildTopic, assocDef);
785        // update memory
786        childTopics.putInChildTopics(newChildTopic, assocDef);
787    }
788
789    private void createChildTopicMany(RelatedTopicModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
790        // update DB
791        createAndAssociateChildTopic(newChildTopic, assocDef);
792        // update memory
793        childTopics.addToChildTopics(newChildTopic, assocDef);
794    }
795
796    // ---
797
798    private void createAndAssociateChildTopic(RelatedTopicModelImpl childTopic, AssociationDefinitionModel assocDef) {
799        pl.createTopic(childTopic);
800        associateChildTopic(childTopic, assocDef);
801    }
802
803    // --- Assignment ---
804
805    private void createAssignmentOne(RelatedTopicModelImpl childTopic, TopicReferenceModelImpl newChildTopic,
806                                     AssociationDefinitionModel assocDef, boolean deleteChildTopic) {
807        if (childTopic != null) {
808            if (newChildTopic.isReferingTo(childTopic)) {
809                updateRelatingAssociation(childTopic, newChildTopic);
810                // Note: memory is already up-to-date. The association is updated in-place of parent.
811                return;
812            }
813            if (deleteChildTopic) {
814                childTopic.delete();
815            } else {
816                childTopic.getRelatingAssociation().delete();
817            }
818        }
819        // update DB
820        resolveRefAndAssociateChildTopic(newChildTopic, assocDef);
821        // update memory
822        childTopics.putInChildTopics(newChildTopic, assocDef);
823    }
824
825    private void createAssignmentMany(TopicReferenceModelImpl newChildTopic, AssociationDefinitionModel assocDef) {
826        RelatedTopicModelImpl childTopic = childTopics.findChildTopicByRef(newChildTopic, assocDef);
827        if (childTopic != null) {
828            // Note: "create assignment" is an idempotent operation. A create request for an assignment which
829            // exists already is not an error. Instead, nothing is performed.
830            updateRelatingAssociation(childTopic, newChildTopic);
831            // Note: memory is already up-to-date. The association is updated in-place of parent.
832            return;
833        }
834        // update DB
835        resolveRefAndAssociateChildTopic(newChildTopic, assocDef);
836        // update memory
837        childTopics.addToChildTopics(newChildTopic, assocDef);
838    }
839
840    // ---
841
842    /**
843     * Creates an association between our parent object ("Parent" role) and the referenced topic ("Child" role).
844     * The association type is taken from the given association definition.
845     *
846     * @return  the resolved child topic.
847     */
848    private void resolveRefAndAssociateChildTopic(TopicReferenceModel childTopicRef,
849                                                  AssociationDefinitionModel assocDef) {
850        pl.valueStorage.resolveReference(childTopicRef);
851        associateChildTopic(childTopicRef, assocDef);
852    }
853
854    private void associateChildTopic(RelatedTopicModel childTopic, AssociationDefinitionModel assocDef) {
855        pl.valueStorage.associateChildTopic(this, childTopic, assocDef);
856    }
857
858    // --- Delete ---
859
860    private void deleteChildTopicOne(RelatedTopicModelImpl childTopic, AssociationDefinitionModel assocDef,
861                                                                       boolean deleteChildTopic) {
862        if (childTopic == null) {
863            // Note: "delete child"/"delete assignment" is an idempotent operation. A delete request for a
864            // child/assignment which has been deleted already (resp. is non-existing) is not an error.
865            // Instead, nothing is performed.
866            return;
867        }
868        // update DB
869        if (deleteChildTopic) {
870            childTopic.delete();
871        } else {
872            childTopic.getRelatingAssociation().delete();
873        }
874        // update memory
875        childTopics.removeChildTopic(assocDef);
876    }
877
878    private void deleteChildTopicMany(long childTopicId, AssociationDefinitionModel assocDef,
879                                                         boolean deleteChildTopic) {
880        RelatedTopicModelImpl childTopic = childTopics.findChildTopicById(childTopicId, assocDef);
881        if (childTopic == null) {
882            // Note: "delete child"/"delete assignment" is an idempotent operation. A delete request for a
883            // child/assignment which has been deleted already (resp. is non-existing) is not an error.
884            // Instead, nothing is performed.
885            return;
886        }
887        // update DB
888        if (deleteChildTopic) {
889            childTopic.delete();
890        } else {
891            childTopic.getRelatingAssociation().delete();
892        }
893        // update memory
894        childTopics.removeFromChildTopics(childTopic, assocDef);
895    }
896
897
898
899    // === Label Calculation ===
900
901    private void _calculateLabelAndUpdate() {
902        List<String> labelAssocDefUris = null;
903        try {
904            // load required childs
905            labelAssocDefUris = getLabelAssocDefUris();
906            for (String assocDefUri : labelAssocDefUris) {
907                loadChildTopics(assocDefUri);
908            }
909            //
910            calculateLabelAndUpdate();
911        } catch (Exception e) {
912            throw new RuntimeException("Calculating and updating label of " + objectInfo() +
913                " failed (assoc defs involved: " + labelAssocDefUris + ")", e);
914        }
915    }
916
917    /**
918     * Calculates the label for this object model and updates it in both, memory (in-place), and DB.
919     * <p>
920     * Prerequisites:
921     * 1) this object model is a composite.
922     * 2) this object model contains all the child topic models involved in the label calculation.
923     *    Note: this method does not load any child topics from DB.
924     *
925     * ### TODO: make private
926     */
927    void calculateLabelAndUpdate() {
928        try {
929            updateSimpleValue(new SimpleValue(calculateLabel()));
930        } catch (Exception e) {
931            throw new RuntimeException("Calculating and updating label of " + objectInfo() + " failed", e);
932        }
933    }
934
935    /**
936     * Calculates the label for this object model recursively. Recursion ends at a simple object model.
937     * <p>
938     * Note: called from this class only but can't be private as called on a different object.
939     */
940    String calculateLabel() {
941        TypeModel type = getType();
942        if (type.getDataTypeUri().equals("dm4.core.composite")) {
943            StringBuilder builder = new StringBuilder();
944            for (String assocDefUri : getLabelAssocDefUris()) {
945                appendLabel(calculateChildLabel(assocDefUri), builder, LABEL_CHILD_SEPARATOR);
946            }
947            return builder.toString();
948        } else {
949            return getSimpleValue().toString();
950        }
951    }
952
953    private String calculateChildLabel(String assocDefUri) {
954        Object value = getChildTopicsModel().get(assocDefUri);
955        // Note: topics just created have no child topics yet
956        if (value == null) {
957            return "";
958        }
959        //
960        if (value instanceof TopicModel) {
961            // single value
962            return ((TopicModelImpl) value).calculateLabel();                               // recursion
963        } else if (value instanceof List) {
964            // multiple value
965            StringBuilder builder = new StringBuilder();
966            for (TopicModelImpl childTopic : (List<TopicModelImpl>) value) {
967                appendLabel(childTopic.calculateLabel(), builder, LABEL_TOPIC_SEPARATOR);   // recursion
968            }
969            return builder.toString();
970        } else {
971            throw new RuntimeException("Unexpected value in a ChildTopicsModel: " + value);
972        }
973    }
974
975    private void appendLabel(String label, StringBuilder builder, String separator) {
976        // add separator
977        if (builder.length() > 0 && label.length() > 0) {
978            builder.append(separator);
979        }
980        //
981        builder.append(label);
982    }
983
984    /**
985     * Prerequisite: this is a composite model.
986     */
987    private List<String> getLabelAssocDefUris() {
988        TypeModel type = getType();
989        List<String> labelConfig = type.getLabelConfig();
990        if (labelConfig.size() > 0) {
991            return labelConfig;
992        } else {
993            List<String> assocDefUris = new ArrayList();
994            Iterator<? extends AssociationDefinitionModel> i = type.getAssocDefs().iterator();
995            // Note: types just created might have no child types yet
996            if (i.hasNext()) {
997                assocDefUris.add(i.next().getAssocDefUri());
998            }
999            return assocDefUris;
1000        }
1001    }
1002
1003
1004
1005    // === Helper ===
1006
1007    private AssociationDefinitionModel getAssocDef(String assocDefUri) {
1008        // Note: doesn't work for facets
1009        return getType().getAssocDef(assocDefUri);
1010    }
1011
1012    private String objectInfo() {
1013        return className() + " " + id;
1014    }
1015}