001    package de.deepamehta.core.impl;
002    
003    import de.deepamehta.core.AssociationDefinition;
004    import de.deepamehta.core.ChildTopics;
005    import de.deepamehta.core.DeepaMehtaObject;
006    import de.deepamehta.core.RelatedTopic;
007    import de.deepamehta.core.Topic;
008    import de.deepamehta.core.model.ChildTopicsModel;
009    import de.deepamehta.core.model.RelatedTopicModel;
010    import de.deepamehta.core.model.SimpleValue;
011    import de.deepamehta.core.model.TopicDeletionModel;
012    import de.deepamehta.core.model.TopicModel;
013    import de.deepamehta.core.model.TopicReferenceModel;
014    
015    import java.util.ArrayList;
016    import java.util.HashMap;
017    import java.util.List;
018    import java.util.Map;
019    import java.util.logging.Logger;
020    
021    
022    
023    /**
024     * A composite value model that is attached to the DB.
025     */
026    class AttachedChildTopics implements ChildTopics {
027    
028        // ---------------------------------------------------------------------------------------------- Instance Variables
029    
030        private ChildTopicsModel model;                             // underlying model
031    
032        private AttachedDeepaMehtaObject parent;                    // attached object cache
033    
034        /**
035         * Attached object cache.
036         * Key: child type URI (String), value: AttachedTopic or List<AttachedTopic>
037         */
038        private Map<String, Object> childTopics = new HashMap();    // attached object cache
039    
040        private EmbeddedService dms;
041    
042        private Logger logger = Logger.getLogger(getClass().getName());
043    
044        // ---------------------------------------------------------------------------------------------------- Constructors
045    
046        AttachedChildTopics(ChildTopicsModel model, AttachedDeepaMehtaObject parent, EmbeddedService dms) {
047            this.model = model;
048            this.parent = parent;
049            this.dms = dms;
050            initAttachedObjectCache();
051        }
052    
053        // -------------------------------------------------------------------------------------------------- Public Methods
054    
055    
056    
057        // **********************************
058        // *** ChildTopics Implementation ***
059        // **********************************
060    
061    
062    
063        // === Accessors ===
064    
065        @Override
066        public Topic getTopic(String childTypeUri) {
067            loadChildTopics(childTypeUri);
068            return _getTopic(childTypeUri);
069        }
070    
071        @Override
072        public List<Topic> getTopics(String childTypeUri) {
073            loadChildTopics(childTypeUri);
074            return _getTopics(childTypeUri);
075        }
076    
077        // ---
078    
079        @Override
080        public Object get(String childTypeUri) {
081            return childTopics.get(childTypeUri);
082        }
083    
084        @Override
085        public boolean has(String childTypeUri) {
086            return childTopics.containsKey(childTypeUri);
087        }
088    
089        @Override
090        public Iterable<String> childTypeUris() {
091            return childTopics.keySet();
092        }
093    
094        @Override
095        public int size() {
096            return childTopics.size();
097        }
098    
099        // ---
100    
101        @Override
102        public ChildTopicsModel getModel() {
103            return model;
104        }
105    
106    
107    
108        // === Convenience Accessors ===
109    
110        @Override
111        public String getString(String childTypeUri) {
112            return getTopic(childTypeUri).getSimpleValue().toString();
113        }
114    
115        @Override
116        public int getInt(String childTypeUri) {
117            return getTopic(childTypeUri).getSimpleValue().intValue();
118        }
119    
120        @Override
121        public long getLong(String childTypeUri) {
122            return getTopic(childTypeUri).getSimpleValue().longValue();
123        }
124    
125        @Override
126        public double getDouble(String childTypeUri) {
127            return getTopic(childTypeUri).getSimpleValue().doubleValue();
128        }
129    
130        @Override
131        public boolean getBoolean(String childTypeUri) {
132            return getTopic(childTypeUri).getSimpleValue().booleanValue();
133        }
134    
135        @Override
136        public Object getObject(String childTypeUri) {
137            return getTopic(childTypeUri).getSimpleValue().value();
138        }
139    
140        // ---
141    
142        @Override
143        public ChildTopics getChildTopics(String childTypeUri) {
144            return getTopic(childTypeUri).getChildTopics();
145        }
146    
147        // Note: there are no convenience accessors for a multiple-valued child.
148    
149    
150    
151        // === Manipulators ===
152    
153        @Override
154        public ChildTopics set(String childTypeUri, TopicModel value) {
155            return _update(childTypeUri, value);
156        }
157    
158        @Override
159        public ChildTopics set(String childTypeUri, Object value) {
160            return _update(childTypeUri, new TopicModel(childTypeUri, new SimpleValue(value)));
161        }
162    
163        @Override
164        public ChildTopics set(String childTypeUri, ChildTopicsModel value) {
165            return _update(childTypeUri, new TopicModel(childTypeUri, value));
166        }
167    
168        // ---
169    
170        @Override
171        public ChildTopics setRef(String childTypeUri, long refTopicId) {
172            return _update(childTypeUri, new TopicReferenceModel(refTopicId));
173        }
174    
175        @Override
176        public ChildTopics setRef(String childTypeUri, String refTopicUri) {
177            return _update(childTypeUri, new TopicReferenceModel(refTopicUri));
178        }
179    
180        // ---
181    
182        @Override
183        public ChildTopics remove(String childTypeUri, long topicId) {
184            return _update(childTypeUri, new TopicDeletionModel(topicId));
185        }
186    
187    
188    
189        // ----------------------------------------------------------------------------------------- Package Private Methods
190    
191        void update(ChildTopicsModel newComp) {
192            try {
193                for (AssociationDefinition assocDef : parent.getType().getAssocDefs()) {
194                    String childTypeUri   = assocDef.getChildTypeUri();
195                    String cardinalityUri = assocDef.getChildCardinalityUri();
196                    TopicModel newChildTopic        = null;     // only used for "one"
197                    List<TopicModel> newChildTopics = null;     // only used for "many"
198                    if (cardinalityUri.equals("dm4.core.one")) {
199                        newChildTopic = newComp.getTopic(childTypeUri, null);        // defaultValue=null
200                        // skip if not contained in update request
201                        if (newChildTopic == null) {
202                            continue;
203                        }
204                    } else if (cardinalityUri.equals("dm4.core.many")) {
205                        newChildTopics = newComp.getTopics(childTypeUri, null);      // defaultValue=null
206                        // skip if not contained in update request
207                        if (newChildTopics == null) {
208                            continue;
209                        }
210                    } else {
211                        throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI");
212                    }
213                    //
214                    updateChildTopics(newChildTopic, newChildTopics, assocDef);
215                }
216                //
217                dms.valueStorage.refreshLabel(parent.getModel());
218                //
219            } catch (Exception e) {
220                throw new RuntimeException("Updating the child topics of " + parent.className() + " " + parent.getId() +
221                    " failed (newComp=" + newComp + ")", e);
222            }
223        }
224    
225        void updateChildTopics(TopicModel newChildTopic, List<TopicModel> newChildTopics, AssociationDefinition assocDef) {
226            // Note: updating the child topics requires them to be loaded
227            loadChildTopics(assocDef);
228            //
229            String assocTypeUri = assocDef.getTypeUri();
230            boolean one = newChildTopic != null;
231            if (assocTypeUri.equals("dm4.core.composition_def")) {
232                if (one) {
233                    updateCompositionOne(newChildTopic, assocDef);
234                } else {
235                    updateCompositionMany(newChildTopics, assocDef);
236                }
237            } else if (assocTypeUri.equals("dm4.core.aggregation_def")) {
238                if (one) {
239                    updateAggregationOne(newChildTopic, assocDef);
240                } else {
241                    updateAggregationMany(newChildTopics, assocDef);
242                }
243            } else {
244                throw new RuntimeException("Association type \"" + assocTypeUri + "\" not supported");
245            }
246        }
247    
248        // ---
249    
250        void loadChildTopics() {
251            dms.valueStorage.fetchChildTopics(parent.getModel());
252            initAttachedObjectCache();
253        }
254    
255        void loadChildTopics(String childTypeUri) {
256            loadChildTopics(getAssocDef(childTypeUri));
257        }
258    
259        // ------------------------------------------------------------------------------------------------- Private Methods
260    
261        /**
262         * Recursively loads child topics (model) and updates this attached object cache accordingly.
263         * If the child topics are loaded already nothing is performed.
264         *
265         * @param   assocDef    the child topics according to this association definition are loaded.
266         *                      <p>
267         *                      Note: the association definition must not necessarily originate from this object's
268         *                      type definition. It may originate from a facet definition as well.
269         */
270        private void loadChildTopics(AssociationDefinition assocDef) {
271            String childTypeUri = assocDef.getChildTypeUri();
272            if (!has(childTypeUri)) {
273                logger.fine("### Lazy-loading \"" + childTypeUri + "\" child topic(s) of " + parent.className() + " " +
274                    parent.getId());
275                dms.valueStorage.fetchChildTopics(parent.getModel(), assocDef);
276                initAttachedObjectCache(childTypeUri);
277            }
278        }
279    
280        // --- Access this attached object cache ---
281    
282        private Topic _getTopic(String childTypeUri) {
283            Topic topic = (Topic) childTopics.get(childTypeUri);
284            // error check
285            if (topic == null) {
286                throw new RuntimeException("Child topic of type \"" + childTypeUri + "\" not found in " + childTopics);
287            }
288            //
289            return topic;
290        }
291    
292        private AttachedTopic _getTopic(String childTypeUri, AttachedTopic defaultTopic) {
293            AttachedTopic topic = (AttachedTopic) childTopics.get(childTypeUri);
294            return topic != null ? topic : defaultTopic;
295        }
296    
297        // ---
298    
299        private List<Topic> _getTopics(String childTypeUri) {
300            try {
301                List<Topic> topics = (List<Topic>) childTopics.get(childTypeUri);
302                // error check
303                if (topics == null) {
304                    throw new RuntimeException("Child topics of type \"" + childTypeUri + "\" not found in " + childTopics);
305                }
306                //
307                return topics;
308            } catch (ClassCastException e) {
309                getModel().throwInvalidAccess(childTypeUri, e);
310                return null;    // never reached
311            }
312        }
313    
314        private List<Topic> _getTopics(String childTypeUri, List<Topic> defaultValue) {
315            try {
316                List<Topic> topics = (List<Topic>) childTopics.get(childTypeUri);
317                return topics != null ? topics : defaultValue;
318            } catch (ClassCastException e) {
319                getModel().throwInvalidAccess(childTypeUri, e);
320                return null;    // never reached
321            }
322        }
323    
324        // ---
325    
326        private ChildTopics _update(String childTypeUri, TopicModel newChildTopic) {
327            // regard parent object as updated
328            parent.addUpdateDirective();
329            //
330            updateChildTopics(newChildTopic, null, getAssocDef(childTypeUri));  // newChildTopics=null
331            dms.valueStorage.refreshLabel(parent.getModel());
332            return this;
333        }
334    
335        // --- Composition ---
336    
337        private void updateCompositionOne(TopicModel newChildTopic, AssociationDefinition assocDef) {
338            AttachedTopic childTopic = _getTopic(assocDef.getChildTypeUri(), null);
339            // Note: for cardinality one the simple request format is sufficient. The child's topic ID is not required.
340            // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic.
341            if (childTopic != null) {
342                // == update child ==
343                // update DB
344                childTopic._update(newChildTopic);
345                // Note: memory is already up-to-date. The child topic is updated in-place of parent.
346            } else {
347                // == create child ==
348                createChildTopicOne(newChildTopic, assocDef);
349            }
350        }
351    
352        private void updateCompositionMany(List<TopicModel> newChildTopics, AssociationDefinition assocDef) {
353            for (TopicModel newChildTopic : newChildTopics) {
354                long childTopicId = newChildTopic.getId();
355                if (newChildTopic instanceof TopicDeletionModel) {
356                    Topic childTopic = findChildTopic(childTopicId, assocDef);
357                    if (childTopic == null) {
358                        // Note: "delete child" is an idempotent operation. A delete request for an child which has been
359                        // deleted already (resp. is non-existing) is not an error. Instead, nothing is performed.
360                        continue;
361                    }
362                    // == delete child ==
363                    // update DB
364                    childTopic.delete();
365                    // update memory
366                    removeFromChildTopics(childTopic, assocDef);
367                } else if (childTopicId != -1) {
368                    // == update child ==
369                    updateChildTopicMany(newChildTopic, assocDef);
370                } else {
371                    // == create child ==
372                    createChildTopicMany(newChildTopic, assocDef);
373                }
374            }
375        }
376    
377        // --- Aggregation ---
378    
379        private void updateAggregationOne(TopicModel newChildTopic, AssociationDefinition assocDef) {
380            RelatedTopic childTopic = (RelatedTopic) _getTopic(assocDef.getChildTypeUri(), null);
381            if (newChildTopic instanceof TopicReferenceModel) {
382                if (childTopic != null) {
383                    if (((TopicReferenceModel) newChildTopic).isReferingTo(childTopic)) {
384                        return;
385                    }
386                    // == update assignment ==
387                    // update DB
388                    childTopic.getRelatingAssociation().delete();
389                } else {
390                    // == create assignment ==
391                }
392                // update DB
393                Topic topic = dms.valueStorage.associateReferencedChildTopic(parent.getModel(),
394                    (TopicReferenceModel) newChildTopic, assocDef);
395                // update memory
396                putInChildTopics(topic, assocDef);
397            } else if (newChildTopic.getId() != -1) {
398                // == update child ==
399                updateChildTopicOne(newChildTopic, assocDef);
400            } else {
401                // == create child ==
402                // update DB
403                if (childTopic != null) {
404                    childTopic.getRelatingAssociation().delete();
405                }
406                createChildTopicOne(newChildTopic, assocDef);
407            }
408        }
409    
410        private void updateAggregationMany(List<TopicModel> newChildTopics, AssociationDefinition assocDef) {
411            for (TopicModel newChildTopic : newChildTopics) {
412                long childTopicId = newChildTopic.getId();
413                if (newChildTopic instanceof TopicDeletionModel) {
414                    RelatedTopic childTopic = findChildTopic(childTopicId, assocDef);
415                    if (childTopic == null) {
416                        // Note: "delete assignment" is an idempotent operation. A delete request for an assignment which
417                        // has been deleted already (resp. is non-existing) is not an error. Instead, nothing is performed.
418                        continue;
419                    }
420                    // == delete assignment ==
421                    // update DB
422                    childTopic.getRelatingAssociation().delete();
423                    // update memory
424                    removeFromChildTopics(childTopic, assocDef);
425                } else if (newChildTopic instanceof TopicReferenceModel) {
426                    if (isReferingToAny((TopicReferenceModel) newChildTopic, assocDef)) {
427                        // Note: "create assignment" is an idempotent operation. A create request for an assignment which
428                        // exists already is not an error. Instead, nothing is performed.
429                        continue;
430                    }
431                    // == create assignment ==
432                    // update DB
433                    Topic topic = dms.valueStorage.associateReferencedChildTopic(parent.getModel(),
434                        (TopicReferenceModel) newChildTopic, assocDef);
435                    // update memory
436                    addToChildTopics(topic, assocDef);
437                } else if (childTopicId != -1) {
438                    // == update child ==
439                    updateChildTopicMany(newChildTopic, assocDef);
440                } else {
441                    // == create child ==
442                    createChildTopicMany(newChildTopic, assocDef);
443                }
444            }
445        }
446    
447        // ---
448    
449        private void updateChildTopicOne(TopicModel newChildTopic, AssociationDefinition assocDef) {
450            AttachedTopic childTopic = _getTopic(assocDef.getChildTypeUri(), null);
451            if (childTopic != null && childTopic.getId() == newChildTopic.getId()) {
452                // update DB
453                childTopic._update(newChildTopic);
454                // Note: memory is already up-to-date. The child topic is updated in-place of parent.
455            } else {
456                throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " +
457                    parent.className() + " " + parent.getId() + " according to " + assocDef);
458            }
459        }
460    
461        private void updateChildTopicMany(TopicModel newChildTopic, AssociationDefinition assocDef) {
462            AttachedTopic childTopic = findChildTopic(newChildTopic.getId(), assocDef);
463            if (childTopic != null) {
464                // update DB
465                childTopic._update(newChildTopic);
466                // Note: memory is already up-to-date. The child topic is updated in-place of parent.
467            } else {
468                throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " +
469                    parent.className() + " " + parent.getId() + " according to " + assocDef);
470            }
471        }
472    
473        // ---
474    
475        private void createChildTopicOne(TopicModel newChildTopic, AssociationDefinition assocDef) {
476            // update DB
477            Topic childTopic = dms.createTopic(newChildTopic);
478            dms.valueStorage.associateChildTopic(parent.getModel(), childTopic.getId(), assocDef);
479            // update memory
480            putInChildTopics(childTopic, assocDef);
481        }
482    
483        private void createChildTopicMany(TopicModel newChildTopic, AssociationDefinition assocDef) {
484            // update DB
485            Topic childTopic = dms.createTopic(newChildTopic);
486            dms.valueStorage.associateChildTopic(parent.getModel(), childTopic.getId(), assocDef);
487            // update memory
488            addToChildTopics(childTopic, assocDef);
489        }
490    
491    
492    
493        // === Attached Object Cache Initialization ===
494    
495        /**
496         * Initializes this attached object cache. For all childs contained in the underlying model attached topics are
497         * created and put in the attached object cache (recursively).
498         */
499        private void initAttachedObjectCache() {
500            for (String childTypeUri : model) {
501                initAttachedObjectCache(childTypeUri);
502            }
503        }
504    
505        /**
506         * Initializes this attached object cache selectively (and recursively).
507         */
508        private void initAttachedObjectCache(String childTypeUri) {
509            Object value = model.get(childTypeUri);
510            // Note: topics just created have no child topics yet
511            if (value == null) {
512                return;
513            }
514            // Note: no direct recursion takes place here. Recursion is indirect: attached topics are created here, this
515            // implies creating further attached composite values, which in turn calls this method again but for the next
516            // child-level. Finally attached topics are created for all child-levels.
517            if (value instanceof TopicModel) {
518                TopicModel childTopic = (TopicModel) value;
519                childTopics.put(childTypeUri, createAttachedObject(childTopic));
520            } else if (value instanceof List) {
521                List<Topic> topics = new ArrayList();
522                childTopics.put(childTypeUri, topics);
523                for (TopicModel childTopic : (List<TopicModel>) value) {
524                    topics.add(createAttachedObject(childTopic));
525                }
526            } else {
527                throw new RuntimeException("Unexpected value in a ChildTopicsModel: " + value);
528            }
529        }
530    
531        /**
532         * Creates an attached topic to be put in this attached object cache.
533         */
534        private Topic createAttachedObject(TopicModel model) {
535            if (model instanceof RelatedTopicModel) {
536                // Note: composite value models obtained through *fetching* contain *related topic models*.
537                // We exploit the related topics when updating assignments (in conjunction with aggregations).
538                // See updateAggregationOne() and updateAggregationMany().
539                return new AttachedRelatedTopic((RelatedTopicModel) model, dms);
540            } else {
541                // Note: composite value models for *new topics* to be created contain sole *topic models*.
542                return new AttachedTopic(model, dms);
543            }
544        }
545    
546    
547    
548        // === Update ===
549    
550        // --- Update this attached object cache + underlying model ---
551    
552        /**
553         * For single-valued childs
554         */
555        private void putInChildTopics(Topic childTopic, AssociationDefinition assocDef) {
556            String childTypeUri = assocDef.getChildTypeUri();
557            put(childTypeUri, childTopic);                              // attached object cache
558            getModel().put(childTypeUri, childTopic.getModel());        // underlying model
559        }
560    
561        /**
562         * For multiple-valued childs
563         */
564        private void addToChildTopics(Topic childTopic, AssociationDefinition assocDef) {
565            String childTypeUri = assocDef.getChildTypeUri();
566            add(childTypeUri, childTopic);                              // attached object cache
567            getModel().add(childTypeUri, childTopic.getModel());        // underlying model
568        }
569    
570        /**
571         * For multiple-valued childs
572         */
573        private void removeFromChildTopics(Topic childTopic, AssociationDefinition assocDef) {
574            String childTypeUri = assocDef.getChildTypeUri();
575            remove(childTypeUri, childTopic);                           // attached object cache
576            getModel().remove(childTypeUri, childTopic.getModel());     // underlying model
577        }
578    
579        // --- Update this attached object cache ---
580    
581        /**
582         * Puts a single-valued child. An existing value is overwritten.
583         */
584        private void put(String childTypeUri, Topic topic) {
585            childTopics.put(childTypeUri, topic);
586        }
587    
588        /**
589         * Adds a value to a multiple-valued child.
590         */
591        private void add(String childTypeUri, Topic topic) {
592            List<Topic> topics = _getTopics(childTypeUri, null);        // defaultValue=null
593            // Note: topics just created have no child topics yet
594            if (topics == null) {
595                topics = new ArrayList();
596                childTopics.put(childTypeUri, topics);
597            }
598            topics.add(topic);
599        }
600    
601        /**
602         * Removes a value from a multiple-valued child.
603         */
604        private void remove(String childTypeUri, Topic topic) {
605            List<Topic> topics = _getTopics(childTypeUri, null);        // defaultValue=null
606            if (topics != null) {
607                topics.remove(topic);
608            }
609        }
610    
611    
612    
613        // === Helper ===
614    
615        private AttachedRelatedTopic findChildTopic(long childTopicId, AssociationDefinition assocDef) {
616            List<Topic> childTopics = _getTopics(assocDef.getChildTypeUri(), new ArrayList());
617            for (Topic childTopic : childTopics) {
618                if (childTopic.getId() == childTopicId) {
619                    return (AttachedRelatedTopic) childTopic;
620                }
621            }
622            return null;
623        }
624    
625        /**
626         * Checks weather the given topic reference refers to any of the child topics.
627         *
628         * @param   assocDef    the child topics according to this association definition are considered.
629         */
630        private boolean isReferingToAny(TopicReferenceModel topicRef, AssociationDefinition assocDef) {
631            return topicRef.isReferingToAny(_getTopics(assocDef.getChildTypeUri(), new ArrayList()));
632        }
633    
634        private AssociationDefinition getAssocDef(String childTypeUri) {
635            // Note: doesn't work for facets
636            return parent.getType().getAssocDef(childTypeUri);
637        }
638    }