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