001package systems.dmx.core.impl;
002
003import systems.dmx.core.Topic;
004import systems.dmx.core.model.AssociationDefinitionModel;
005import systems.dmx.core.model.AssociationModel;
006import systems.dmx.core.model.ChildTopicsModel;
007import systems.dmx.core.model.DMXObjectModel;
008import systems.dmx.core.model.RelatedTopicModel;
009import systems.dmx.core.model.SimpleValue;
010import systems.dmx.core.model.TopicDeletionModel;
011import systems.dmx.core.model.TopicModel;
012import systems.dmx.core.model.TopicReferenceModel;
013import systems.dmx.core.model.TypeModel;
014import systems.dmx.core.util.DMXUtils;
015
016import static java.util.Arrays.asList;
017import java.util.ArrayList;
018import java.util.HashMap;
019import java.util.Iterator;
020import java.util.List;
021import java.util.Map;
022import java.util.logging.Logger;
023
024
025
026/**
027 * Integrates new values into the DB.
028 *
029 * Note: this class is not thread-safe. A ValueIntegrator instance must not be shared between threads.
030 */
031class ValueIntegrator {
032
033    // ---------------------------------------------------------------------------------------------- Instance Variables
034
035    private DMXObjectModelImpl newValues;
036    private DMXObjectModelImpl targetObject;        // may null
037    private AssociationDefinitionModel assocDef;    // may null
038    private TypeModelImpl type;
039    private boolean isAssoc;
040    private boolean isType;
041    private boolean isFacetUpdate;
042
043    // For composites: assoc def URIs of empty child topics.
044    // Evaluated when deleting child-assignments, see updateAssignments().
045    // Not having null entries in the unified child topics simplifies candidate determination.
046    // ### TODO: to be dropped?
047    private List<String> emptyValues = new ArrayList();
048
049    private PersistenceLayer pl;
050    private ModelFactoryImpl mf;
051
052    private Logger logger = Logger.getLogger(getClass().getName());
053
054    // ---------------------------------------------------------------------------------------------------- Constructors
055
056    ValueIntegrator(PersistenceLayer pl) {
057        this.pl = pl;
058        this.mf = pl.mf;
059    }
060
061    // ----------------------------------------------------------------------------------------- Package Private Methods
062
063    /**
064     * Integrates new values into the DB and returns the unified value.
065     *
066     * @return  the unified value; never null; its "value" field is null if there was nothing to integrate.
067     */
068    <M extends DMXObjectModelImpl> UnifiedValue<M> integrate(M newValues, M targetObject,
069                                                             AssociationDefinitionModel assocDef) {
070        try {
071            this.newValues = newValues;
072            this.targetObject = targetObject;
073            this.assocDef = assocDef;
074            this.isAssoc = newValues instanceof AssociationModel;
075            this.isType  = newValues instanceof TypeModel;
076            this.isFacetUpdate = assocDef != null;
077            //
078            // process refs
079            if (newValues instanceof TopicReferenceModel) {
080                return unifyRef();
081            }
082            if (newValues instanceof TopicDeletionModel) {
083                return new UnifiedValue(null);
084            }
085            // argument check
086            if (newValues.getTypeUri() == null) {
087                throw new IllegalArgumentException("Tried to integrate values whose typeUri is not set, newValues=" +
088                    newValues + ", targetObject=" + targetObject);
089            }
090            // Note: we must get type *after* processing refs. Refs might have no type set.
091            this.type = newValues.getType();
092            //
093            // value integration
094            // Note: because a facet type is composite by definition a facet update is always a composite operation,
095            // even if the faceted object is a simple one.
096            DMXObjectModelImpl _value = !isFacetUpdate && newValues.isSimple() ?
097                integrateSimple() :
098                integrateComposite();
099            // Note: UnifiedValue instantiation saves the new value's ID *before* it is overwritten
100            UnifiedValue value = new UnifiedValue(_value);
101            //
102            idTransfer(_value);
103            //
104            return value;
105        } catch (Exception e) {
106            throw new RuntimeException("Value integration failed, newValues=" + newValues + ", targetObject=" +
107                targetObject + ", assocDef=" + assocDef, e);
108        }
109    }
110
111    // ------------------------------------------------------------------------------------------------- Private Methods
112
113    private UnifiedValue unifyRef() {
114        TopicReferenceModelImpl ref = (TopicReferenceModelImpl) newValues;
115        if (!ref.isEmptyRef()) {
116            DMXObjectModelImpl object = ref.resolve();
117            logger.fine("Referencing " + object);
118            return new UnifiedValue(object);
119        } else {
120            return new UnifiedValue(null);
121        }
122    }
123
124    // Note: this is a side effect, but we keep it for pragmatic reasons.
125    //
126    // In DM4 the create topic/assoc methods have the side effect of setting the generated ID into the update model.
127    // In DM5 the update model is not passed directly to the storage layer, but a new model object is created (see
128    // createSimpleTopic()). The ID transfer is here to emulate the DM4 behavior.
129    //
130    // Without this side effect e.g. managing view configs would be more hard. When creating a type through a migration
131    // (e.g. while bootstrap) the read type model is put in the type cache as is. Without the side effect the view
132    // config topic models would have no ID. Updating view config values later on would fail.
133    private void idTransfer(DMXObjectModelImpl value) {
134        if (value != null) {
135            if (value.id == -1) {
136                throw new RuntimeException("ID of unification result is not initialized: " + value);
137            }
138            newValues.id = value.id;
139        }
140    }
141
142    // Simple
143
144    /**
145     * Integrates a simple value into the DB and returns the unified simple value.
146     *
147     * Preconditions:
148     *   - this.newValues is not null
149     *   - this.newValues is simple
150     *
151     * @return  the unified value, or null if there was nothing to integrate.
152     *          The latter is the case if this.newValues is the empty string.
153     */
154    private DMXObjectModelImpl integrateSimple() {
155        try {
156            if (isAssoc || isType) {
157                // Note 1: an assoc's simple value is not unified. In contrast to a topic an assoc can't be unified with
158                // another assoc. (Even if 2 assocs have the same type and value they are not the same as they still have
159                // different players.) An assoc's simple value is updated in-place.
160                // Note 2: a type's simple value is not unified. A type is updated in-place.
161                return storeAssocSimpleValue();
162            } else if (newValues.getSimpleValue().toString().isEmpty()) {
163                return null;
164            } else {
165                return unifySimple();
166            }
167        } catch (Exception e) {
168            throw new RuntimeException("Simple value integration failed, newValues=" + newValues, e);
169        }
170    }
171
172    /**
173     * Preconditions:
174     *   - this.newValues is an assoc model.
175     */
176    private DMXObjectModelImpl storeAssocSimpleValue() {
177        if (targetObject != null) {
178            // update
179            targetObject._updateSimpleValue(newValues.getSimpleValue());
180            return targetObject;
181        } else {
182            // create
183            newValues.storeSimpleValue();
184            return newValues;
185        }
186    }
187
188    /**
189     * Preconditions:
190     *   - this.newValues is simple
191     *   - this.newValues is not empty
192     *
193     * @return  the unified value. Is never null.
194     */
195    private TopicModelImpl unifySimple() {
196        SimpleValue newValue = newValues.getSimpleValue();
197        // FIXME: HTML values must be tag-stripped before lookup, complementary to indexing
198        TopicImpl _topic = pl.getTopicByValue(type.getUri(), newValue);     // TODO: let pl return models
199        TopicModelImpl topic = _topic != null ? _topic.getModel() : null;   // TODO: drop
200        if (topic != null) {
201            logger.info("Reusing simple value " + topic.id + " \"" + newValue + "\" (typeUri=\"" + type.uri + "\")");
202        } else {
203            topic = createSimpleTopic();
204            logger.info("### Creating simple value " + topic.id + " \"" + newValue + "\" (typeUri=\"" + type.uri +
205                "\")");
206        }
207        return topic;
208    }
209
210    // Composite
211
212    /**
213     * Integrates a composite value into the DB and returns the unified composite value.
214     *
215     * Preconditions:
216     *   - this.newValues is composite
217     *
218     * @return  the unified value, or null if there was nothing to integrate.
219     */
220    private DMXObjectModelImpl integrateComposite() {
221        try {
222            Map<String, Object> childTopics = new HashMap();    // value: UnifiedValue or List<UnifiedValue>
223            ChildTopicsModel _childTopics = newValues.getChildTopicsModel();
224            // Iterate through type, not through newValues.
225            // newValues might contain childs not contained in the type def, e.g. "dmx.time.modified".
226            for (String assocDefUri : assocDefUris()) {
227                Object newChildValue;    // RelatedTopicModelImpl or List<RelatedTopicModelImpl>
228                if (isOne(assocDefUri)) {
229                    newChildValue = _childTopics.getTopicOrNull(assocDefUri);
230                } else {
231                    // TODO: if empty?
232                    newChildValue = _childTopics.getTopicsOrNull(assocDefUri);
233                }
234                // skip if not contained in update request
235                if (newChildValue == null) {
236                    continue;
237                }
238                //
239                Object childTopic = integrateChildValue(newChildValue, assocDefUri);
240                if (isOne(assocDefUri) && ((UnifiedValue) childTopic).value == null) {
241                    emptyValues.add(assocDefUri);
242                } else {
243                    childTopics.put(assocDefUri, childTopic);
244                }
245            }
246            DMXObjectModelImpl value = unifyComposite(childTopics);
247            //
248            // label calculation
249            if (!isFacetUpdate) {
250                if (value != null) {
251                    new LabelCalculation(value).calculate();
252                } else if (isAssoc) {
253                    storeAssocSimpleValue();
254                }
255            }
256            //
257            return value;
258        } catch (Exception e) {
259            throw new RuntimeException("Composite value integration failed, newValues=" + newValues, e);
260        }
261    }
262
263    private Iterable<String> assocDefUris() {
264        return isFacetUpdate ? asList(assocDef.getAssocDefUri()) : type;
265    }
266
267    /**
268     * Integrates a child value into the DB and returns the unified value.
269     *
270     * @param   childValue      RelatedTopicModelImpl or List<RelatedTopicModelImpl>
271     *
272     * @return  UnifiedValue or List<UnifiedValue>; never null;
273     */
274    private Object integrateChildValue(Object childValue, String assocDefUri) {
275        if (isOne(assocDefUri)) {
276            return new ValueIntegrator(pl).integrate((RelatedTopicModelImpl) childValue, null, null);
277        } else {
278            List<UnifiedValue> values = new ArrayList();
279            for (RelatedTopicModelImpl value : (List<RelatedTopicModelImpl>) childValue) {
280                values.add(new ValueIntegrator(pl).integrate(value, null, null));
281            }
282            return values;
283        }
284    }
285
286    /**
287     * Preconditions:
288     *   - this.newValues is composite
289     *   - assocDef's parent type is this.type
290     *   - childTopic's type is assocDef's child type
291     *
292     * @param   childTopics     value: UnifiedValue or List<UnifiedValue>
293     */
294    private DMXObjectModelImpl unifyComposite(Map<String, Object> childTopics) {
295        // Note: because a facet does not contribute to the value of a value object
296        // a facet update is always an in-place modification
297        if (!isFacetUpdate && isValueType()) {
298            return !childTopics.isEmpty() ? unifyChildTopics(childTopics, type) : null;
299        } else {
300            return updateAssignments(identifyParent(childTopics), childTopics);
301        }
302    }
303
304    /**
305     * @param   childTopics     value: UnifiedValue or List<UnifiedValue>
306     */
307    private DMXObjectModelImpl identifyParent(Map<String, Object> childTopics) {
308        // TODO: 1st check identity attrs THEN target object?? => NO!
309        if (targetObject != null) {
310            return targetObject;
311        } else if (isAssoc) {
312            if (newValues.id == -1) {
313                throw new RuntimeException("newValues has no ID set");
314            }
315            return mf.newAssociationModel(newValues.id, null, newValues.typeUri, null, null);
316        } else {
317            List<String> identityAssocDefUris = type.getIdentityAttrs();
318            if (identityAssocDefUris.size() > 0) {
319                return unifyChildTopics(identityChildTopics(childTopics, identityAssocDefUris), identityAssocDefUris);
320            } else {
321                DMXObjectModelImpl parent = createSimpleTopic();
322                logger.info("### Creating composite (w/o identity attrs) " + parent.id + " (typeUri=\"" + type.uri +
323                    "\")");
324                return parent;
325            }
326        }
327    }
328
329    /**
330     * @param   childTopics     value: UnifiedValue or List<UnifiedValue>
331     *
332     * @return  value: UnifiedValue or List<UnifiedValue>
333     */
334    private Map<String, Object> identityChildTopics(Map<String, Object> childTopics,
335                                                    List<String> identityAssocDefUris) {
336        Map<String, Object> identityChildTopics = new HashMap();
337        for (String assocDefUri : identityAssocDefUris) {
338            UnifiedValue childTopic;
339            if (isOne(assocDefUri)) {
340                childTopic = (UnifiedValue) childTopics.get(assocDefUri);
341            } else {
342                throw new RuntimeException("Cardinality \"many\" identity attributes not supported");
343            }
344            // FIXME: only throw if NO identity child topic is given.
345            // If at least ONE is given it is sufficient.
346            if (childTopic.value == null) {
347                throw new RuntimeException("Identity child topic \"" + assocDefUri + "\" is missing in " +
348                    childTopics.keySet());
349            }
350            identityChildTopics.put(assocDefUri, childTopic);
351        }
352        // logger.info("### type=\"" + type.uri + "\" ### identityChildTopics=" + identityChildTopics);
353        return identityChildTopics;
354    }
355
356    /**
357     * Updates a parent's child assignments in-place.
358     *
359     * Preconditions:
360     *   - this.newValues is composite
361     *   - this.type is an identity type
362     *   - parent's type is this.type
363     *   - assocDef's parent type is this.type
364     *   - newChildTopic's type is assocDef's child type
365     *
366     * @param   unifiedChilds     value: UnifiedValue or List<UnifiedValue>
367     */
368    private DMXObjectModelImpl updateAssignments(DMXObjectModelImpl parent, Map<String, Object> unifiedChilds) {
369        // sanity check
370        if (!parent.getTypeUri().equals(type.getUri())) {
371            throw new RuntimeException("Type mismatch: newValues type=\"" + type.getUri() + "\", parent type=\"" +
372                parent.getTypeUri() + "\"");
373        }
374        //
375        for (String assocDefUri : assocDefUris()) {
376            // TODO: possible optimization: load only ONE child level here (deep=false). Later on, when updating the
377            // assignments, load the remaining levels only IF the assignment did not change. In contrast if the
378            // assignment changes, a new subtree is attached. The subtree is fully constructed already (through all
379            // levels) as it is build bottom-up (starting from the simple values at the leaves).
380            parent.loadChildTopics(assocDef(assocDefUri), true);    // deep=true
381            Object unifiedChild = unifiedChilds.get(assocDefUri);
382            if (isOne(assocDefUri)) {
383                TopicModel _unifiedChild = (TopicModel) (unifiedChild != null ? ((UnifiedValue) unifiedChild).value :
384                    null);
385                updateAssignmentsOne(parent, _unifiedChild, assocDefUri);
386            } else {
387                // Note: for partial create/update requests unifiedChild might be null
388                if (unifiedChild != null) {
389                    updateAssignmentsMany(parent, (List<UnifiedValue>) unifiedChild, assocDefUri);
390                }
391            }
392        }
393        return parent;
394    }
395
396    /**
397     * @param   unifiedChild    may be null
398     */
399    private void updateAssignmentsOne(DMXObjectModelImpl parent, TopicModel unifiedChild, String assocDefUri) {
400        ChildTopicsModelImpl childTopics = parent.getChildTopicsModel();
401        RelatedTopicModelImpl oldValue = childTopics.getTopicOrNull(assocDefUri);   // may be null
402        boolean newValueIsEmpty = isEmptyValue(assocDefUri);
403        //
404        // 1) delete assignment if exists AND value has changed or emptied
405        //
406        boolean deleted = false;
407        if (oldValue != null && (newValueIsEmpty || unifiedChild != null && !oldValue.equals(unifiedChild))) {
408            // update DB
409            oldValue.getRelatingAssociation().delete();
410            // update memory
411            if (newValueIsEmpty) {
412                logger.info("### Deleting assignment (assocDefUri=\"" + assocDefUri + "\") from composite " +
413                    parent.id + " (typeUri=\"" + type.uri + "\")");
414                childTopics.remove(assocDefUri);
415            }
416            deleted = true;
417        }
418        // 2) create assignment if not exists OR value has changed
419        // a new value must be present
420        //
421        AssociationModelImpl assoc = null;
422        if (unifiedChild != null && (oldValue == null || !oldValue.equals(unifiedChild))) {
423            // update DB
424            assoc = createChildAssociation(parent, unifiedChild, assocDefUri, deleted);
425            // update memory
426            childTopics.put(assocDefUri, mf.newRelatedTopicModel(unifiedChild, assoc));
427        }
428        // 3) update relating assoc
429        //
430        // Note: don't update an assoc's relating assoc
431        // TODO: condition needed? => yes, try remove child topic from rel assoc (e.g. "Phone Label")
432        if (!isAssoc) {
433            // take the old assoc if no new one is created, there is an old one, and it has not been deleted
434            if (assoc == null && oldValue != null && !deleted) {
435                assoc = oldValue.getRelatingAssociation();
436            }
437            if (assoc != null) {
438                RelatedTopicModelImpl newChildValue = newValues.getChildTopicsModel().getTopicOrNull(assocDefUri);
439                updateRelatingAssociation(assoc, assocDefUri, newChildValue);
440            }
441        }
442    }
443
444    /**
445     * @param   unifiedChilds   never null; a UnifiedValue's "value" field may be null
446     */
447    private void updateAssignmentsMany(DMXObjectModelImpl parent, List<UnifiedValue> unifiedChilds,
448                                                                  String assocDefUri) {
449        ChildTopicsModelImpl childTopics = parent.getChildTopicsModel();
450        List<RelatedTopicModelImpl> oldValues = childTopics.getTopicsOrNull(assocDefUri);   // may be null
451        // logger.info("### assocDefUri=\"" + assocDefUri + "\", oldValues=" + oldValues);
452        for (UnifiedValue _unifiedChild : unifiedChilds) {
453            TopicModel unifiedChild = (TopicModel) _unifiedChild.value;
454            long originalId = _unifiedChild.originalId;
455            long newId = unifiedChild != null ? unifiedChild.getId() : -1;
456            RelatedTopicModelImpl oldValue = null;
457            if (originalId != -1) {
458                oldValue = findTopic(oldValues, originalId);
459            }
460            //
461            // 1) delete assignment if exists AND value has changed or emptied
462            //
463            boolean deleted = false;
464            if (originalId != -1 && (newId == -1 || originalId != newId)) {
465                if (newId == -1) {
466                    logger.info("### Deleting assignment (assocDefUri=\"" + assocDefUri + "\") from composite " +
467                        parent.id + " (typeUri=\"" + type.uri + "\")");
468                }
469                deleted = true;
470                // update DB
471                oldValue.getRelatingAssociation().delete();
472                // update memory
473                removeTopic(oldValues, originalId);
474            }
475            // 2) create assignment if not exists OR value has changed
476            // a new value must be present
477            //
478            AssociationModelImpl assoc = null;
479            if (newId != -1 && (originalId == -1 || originalId != newId)) {
480                // update DB
481                assoc = createChildAssociation(parent, unifiedChild, assocDefUri, deleted);
482                // update memory
483                childTopics.add(assocDefUri, mf.newRelatedTopicModel(unifiedChild, assoc));
484            }
485            // 3) update relating assoc
486            //
487            // Note: don't update an assoc's relating assoc
488            // TODO: condition needed? => yes, try remove child topic from rel assoc (e.g. "Phone Label")
489            if (!isAssoc) {
490                // take the old assoc if no new one is created, there is an old one, and it has not been deleted
491                if (assoc == null && oldValue != null && !deleted) {
492                    assoc = oldValue.getRelatingAssociation();
493                }
494                if (assoc != null) {
495                    RelatedTopicModelImpl newValues = (RelatedTopicModelImpl) _unifiedChild._newValues;
496                    updateRelatingAssociation(assoc, assocDefUri, newValues);
497                }
498            }
499        }
500    }
501
502    private void updateRelatingAssociation(AssociationModelImpl assoc, String assocDefUri,
503                                           RelatedTopicModelImpl newValues) {
504        try {
505            // Note: for partial create/update requests newValues might be null
506            if (newValues != null) {
507                AssociationModelImpl _newValues = newValues.getRelatingAssociation();
508                // Note: the roles must be suppressed from being updated. Update would fail if a new child has
509                // been assigned (step 2) because the player is another one then. Here we are only interested
510                // in updating the assoc value.
511                _newValues.setRoleModel1(null);
512                _newValues.setRoleModel2(null);
513                // Note: if no relating assocs are contained in a create/update request the model factory
514                // creates assocs anyways, but these are completely uninitialized. ### TODO: Refactor
515                // TODO: is condition needed? => yes, try create new topic
516                if (_newValues.typeUri != null) {
517                    assoc.update(_newValues);
518                    // TODO: access control? Note: currently the child assocs of a workspace have no workspace
519                    // assignments. With strict access control, updating a workspace topic would fail.
520                    // pl.updateAssociation(assoc, _newValues);
521                }
522            }
523        } catch (Exception e) {
524            throw new RuntimeException("Updating relating assoc " + assoc.id + " (assocDefUri=\"" + assocDefUri +
525                "\") failed, assoc=" + assoc, e);
526        }
527    }
528
529    // ---
530
531    /**
532     * Preconditions:
533     *   - this.newValues is composite
534     *   - assocDef's parent type is this.type
535     *   - childTopic's type is assocDef's child type
536     *   - childTopics map is not empty
537     *
538     * @param   childTopics     value: UnifiedValue or List<UnifiedValue>
539     */
540    private DMXObjectModelImpl unifyChildTopics(Map<String, Object> childTopics, Iterable<String> assocDefUris) {
541        List<RelatedTopicModelImpl> candidates = parentCandidates(childTopics);
542        // logger.info("### candidates (" + candidates.size() + "): " + DMXUtils.idList(candidates));
543        for (String assocDefUri : assocDefUris) {
544            UnifiedValue value = (UnifiedValue) childTopics.get(assocDefUri);
545            eliminateParentCandidates(candidates, value != null ? value.value : null, assocDefUri);
546            if (candidates.isEmpty()) {
547                break;
548            }
549        }
550        switch (candidates.size()) {
551        case 0:
552            // logger.info("### no composite found, childTopics=" + childTopics);
553            return createCompositeTopic(childTopics);
554        case 1:
555            DMXObjectModelImpl comp = candidates.get(0);
556            logger.info("Reusing composite " + comp.getId() + " (typeUri=\"" + type.uri + "\")");
557            return comp;
558        default:
559            throw new RuntimeException("ValueIntegrator ambiguity: there are " + candidates.size() +
560                " parents (typeUri=\"" + type.uri + "\", " + DMXUtils.idList(candidates) +
561                ") which have the same " + childTopics.values().size() + " child topics " + childTopics.values());
562        }
563    }
564
565    /**
566     * Preconditions:
567     *   - this.newValues is composite
568     *   - assocDef's parent type is this.type
569     *   - childTopic's type is assocDef's child type
570     *   - childTopics map is not empty
571     *
572     * @param   childTopics     value: UnifiedValue or List<UnifiedValue>
573     */
574    private List<RelatedTopicModelImpl> parentCandidates(Map<String, Object> childTopics) {
575        String assocDefUri = childTopics.keySet().iterator().next();
576        // logger.info("### assocDefUri=\"" + assocDefUri + "\", childTopics=" + childTopics);
577        // sanity check
578        if (!type.getUri().equals(assocDef(assocDefUri).getParentTypeUri())) {
579            throw new RuntimeException("Type mismatch: type=\"" + type.getUri() + "\", assoc def's parent type=\"" +
580                assocDef(assocDefUri).getParentTypeUri() + "\"");
581        }
582        //
583        DMXObjectModel childTopic;
584        if (isOne(assocDefUri)) {
585            childTopic = ((UnifiedValue) childTopics.get(assocDefUri)).value;
586        } else {
587            throw new RuntimeException("Unification of cardinality \"many\" values not yet implemented");
588        }
589        return pl.getTopicRelatedTopics(childTopic.getId(), assocDef(assocDefUri).getInstanceLevelAssocTypeUri(),
590            "dmx.core.child", "dmx.core.parent", type.getUri());
591    }
592
593    /**
594     * @param   childTopic      may be null
595     */
596    private void eliminateParentCandidates(List<RelatedTopicModelImpl> candidates, DMXObjectModel childTopic,
597                                                                                   String assocDefUri) {
598        AssociationDefinitionModel assocDef = assocDef(assocDefUri);
599        Iterator<RelatedTopicModelImpl> i = candidates.iterator();
600        while (i.hasNext()) {
601            long parentId = i.next().getId();
602            String assocTypeUri = assocDef.getInstanceLevelAssocTypeUri();
603            if (childTopic != null) {
604                // TODO: assoc parents?
605                if (pl.getAssociation(assocTypeUri, parentId, childTopic.getId(), "dmx.core.parent", "dmx.core.child")
606                        == null) {
607                    // logger.info("### eliminate (assoc doesn't exist)");
608                    i.remove();
609                }
610            } else {
611                // TODO: assoc parents?
612                if (!pl.getTopicRelatedTopics(parentId, assocTypeUri, "dmx.core.parent", "dmx.core.child",
613                        assocDef.getChildTypeUri()).isEmpty()) {
614                    // logger.info("### eliminate (childs exist)");
615                    i.remove();
616                }
617            }
618        }
619    }
620
621    // --- DB Access ---
622
623    /**
624     * Preconditions:
625     *   - this.newValues is a topic model.
626     */
627    private TopicModelImpl createSimpleTopic() {
628        // sanity check
629        if (isAssoc) {
630            throw new RuntimeException("Tried to create a topic from an assoc model");
631        }
632        //
633        return pl._createTopic(mf.newTopicModel(newValues.uri, newValues.typeUri, newValues.value)).getModel();
634    }
635
636    /**
637     * @param   childTopics     value: UnifiedValue or List<UnifiedValue>
638     */
639    private TopicModelImpl createCompositeTopic(Map<String, Object> childTopics) {
640        // FIXME: construct the composite model first, then create topic as a whole. => NO! Endless recursion?
641        // Otherwise the POST_CREATE_TOPIC event is fired too early, and e.g. Address topics get no geo coordinates.
642        // logger.info("### childTopics=" + childTopics);
643        TopicModelImpl topic = createSimpleTopic();
644        logger.info("### Creating composite " + topic.id + " (typeUri=\"" + type.uri + "\")");
645        for (String assocDefUri : childTopics.keySet()) {
646            if (isOne(assocDefUri)) {
647                DMXObjectModel childTopic = ((UnifiedValue) childTopics.get(assocDefUri)).value;
648                createChildAssociation(topic, childTopic, assocDefUri);
649            } else {
650                for (UnifiedValue value : (List<UnifiedValue>) childTopics.get(assocDefUri)) {
651                    createChildAssociation(topic, value.value, assocDefUri);
652                }
653            }
654        }
655        return topic;
656    }
657
658    private AssociationModelImpl createChildAssociation(DMXObjectModel parent, DMXObjectModel child,
659                                                                               String assocDefUri) {
660        return createChildAssociation(parent, child, assocDefUri, false);
661    }
662
663    private AssociationModelImpl createChildAssociation(DMXObjectModel parent, DMXObjectModel child,
664                                                                               String assocDefUri, boolean deleted) {
665        logger.info("### " + (deleted ? "Reassigning" : "Assigning") + " child " + child.getId() + " (assocDefUri=\"" +
666            assocDefUri + "\") to composite " + parent.getId() + " (typeUri=\"" + type.uri + "\")");
667        return pl.createAssociation(assocDef(assocDefUri).getInstanceLevelAssocTypeUri(),
668            parent.createRoleModel("dmx.core.parent"),
669            child.createRoleModel("dmx.core.child")
670        ).getModel();
671    }
672
673    // --- Memory Access ---
674
675    // TODO: make generic utility
676    private RelatedTopicModelImpl findTopic(List<RelatedTopicModelImpl> topics, long topicId) {
677        for (RelatedTopicModelImpl topic : topics) {
678            if (topic.id == topicId) {
679                return topic;
680            }
681        }
682        throw new RuntimeException("Topic " + topicId + " not found in " + topics);
683    }
684
685    private void removeTopic(List<RelatedTopicModelImpl> topics, long topicId) {
686        Iterator<RelatedTopicModelImpl> i = topics.iterator();
687        while (i.hasNext()) {
688            RelatedTopicModelImpl topic = i.next();
689            if (topic.id == topicId) {
690                i.remove();
691                return;
692            }
693        }
694        throw new RuntimeException("Topic " + topicId + " not found in " + topics);
695    }
696
697    // ---
698
699    private AssociationDefinitionModel assocDef(String assocDefUri) {
700        if (isFacetUpdate) {
701            if (!assocDefUri.equals(assocDef.getAssocDefUri())) {
702                throw new RuntimeException("URI mismatch: assocDefUri=\"" + assocDefUri + "\", facet assocDefUri=\"" +
703                    assocDef.getAssocDefUri() + "\"");
704            }
705            return assocDef;
706        } else {
707            return type.getAssocDef(assocDefUri);
708        }
709    }
710
711    private boolean isOne(String assocDefUri) {
712        return assocDef(assocDefUri).getChildCardinalityUri().equals("dmx.core.one");
713    }
714
715    private boolean isValueType() {
716        return type.getDataTypeUri().equals("dmx.core.value");
717    }
718
719    private boolean isEmptyValue(String assocDefUri) {
720        return emptyValues.contains(assocDefUri);
721    }
722
723    // -------------------------------------------------------------------------------------------------- Nested Classes
724
725    class UnifiedValue<M extends DMXObjectModelImpl> {
726
727        M value;                            // the resulting unified value
728        DMXObjectModelImpl _newValues;      // the original new values
729        long originalId;                    // the original ID, saved here cause it is overwritten (see integrate())
730
731        /**
732         * @param   value   may be null
733         */
734        private UnifiedValue(M value) {
735            this.value = value;
736            this._newValues = newValues;
737            this.originalId = newValues.id;
738        }
739    }
740}