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