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    import de.deepamehta.core.service.Directives;
015    
016    import java.util.ArrayList;
017    import java.util.HashMap;
018    import java.util.List;
019    import java.util.Map;
020    import java.util.logging.Logger;
021    
022    
023    
024    /**
025     * A composite value model that is attached to the DB.
026     */
027    class AttachedChildTopics implements ChildTopics {
028    
029        // ---------------------------------------------------------------------------------------------- Instance Variables
030    
031        private ChildTopicsModel model;                             // underlying model
032    
033        private AttachedDeepaMehtaObject parent;                    // attached object cache
034    
035        /**
036         * Attached object cache.
037         * Key: child type URI (String), value: AttachedTopic or List<AttachedTopic>
038         */
039        private Map<String, Object> childTopics = new HashMap();    // attached object cache
040    
041        private EmbeddedService dms;
042    
043        private Logger logger = Logger.getLogger(getClass().getName());
044    
045        // ---------------------------------------------------------------------------------------------------- Constructors
046    
047        AttachedChildTopics(ChildTopicsModel model, AttachedDeepaMehtaObject parent, EmbeddedService dms) {
048            this.model = model;
049            this.parent = parent;
050            this.dms = dms;
051            initAttachedObjectCache();
052        }
053    
054        // -------------------------------------------------------------------------------------------------- Public Methods
055    
056    
057    
058        // **********************************
059        // *** ChildTopics Implementation ***
060        // **********************************
061    
062    
063    
064        // === Accessors ===
065    
066        @Override
067        public Topic getTopic(String childTypeUri) {
068            loadChildTopics(childTypeUri);
069            return _getTopic(childTypeUri);
070        }
071    
072        @Override
073        public List<Topic> getTopics(String childTypeUri) {
074            loadChildTopics(childTypeUri);
075            return _getTopics(childTypeUri);
076        }
077    
078        // ---
079    
080        @Override
081        public Object get(String childTypeUri) {
082            return childTopics.get(childTypeUri);
083        }
084    
085        @Override
086        public boolean has(String childTypeUri) {
087            return childTopics.containsKey(childTypeUri);
088        }
089    
090        @Override
091        public Iterable<String> childTypeUris() {
092            return childTopics.keySet();
093        }
094    
095        @Override
096        public int size() {
097            return childTopics.size();
098        }
099    
100        // ---
101    
102        @Override
103        public ChildTopicsModel getModel() {
104            return model;
105        }
106    
107    
108    
109        // === Convenience Accessors ===
110    
111        @Override
112        public String getString(String childTypeUri) {
113            return getTopic(childTypeUri).getSimpleValue().toString();
114        }
115    
116        @Override
117        public int getInt(String childTypeUri) {
118            return getTopic(childTypeUri).getSimpleValue().intValue();
119        }
120    
121        @Override
122        public long getLong(String childTypeUri) {
123            return getTopic(childTypeUri).getSimpleValue().longValue();
124        }
125    
126        @Override
127        public double getDouble(String childTypeUri) {
128            return getTopic(childTypeUri).getSimpleValue().doubleValue();
129        }
130    
131        @Override
132        public boolean getBoolean(String childTypeUri) {
133            return getTopic(childTypeUri).getSimpleValue().booleanValue();
134        }
135    
136        @Override
137        public Object getObject(String childTypeUri) {
138            return getTopic(childTypeUri).getSimpleValue().value();
139        }
140    
141        // ---
142    
143        @Override
144        public ChildTopics getChildTopics(String childTypeUri) {
145            return getTopic(childTypeUri).getChildTopics();
146        }
147    
148        // Note: there are no convenience accessors for a multiple-valued child.
149    
150    
151    
152        // === Manipulators ===
153    
154        @Override
155        public ChildTopics set(String childTypeUri, TopicModel value) {
156            return _update(childTypeUri, value);
157        }
158    
159        @Override
160        public ChildTopics set(String childTypeUri, Object value) {
161            return _update(childTypeUri, new TopicModel(childTypeUri, new SimpleValue(value)));
162        }
163    
164        @Override
165        public ChildTopics set(String childTypeUri, ChildTopicsModel value) {
166            return _update(childTypeUri, new TopicModel(childTypeUri, value));
167        }
168    
169        // ---
170    
171        @Override
172        public ChildTopics setRef(String childTypeUri, long refTopicId) {
173            return _update(childTypeUri, new TopicReferenceModel(refTopicId));
174        }
175    
176        @Override
177        public ChildTopics setRef(String childTypeUri, String refTopicUri) {
178            return _update(childTypeUri, new TopicReferenceModel(refTopicUri));
179        }
180    
181        // ---
182    
183        @Override
184        public ChildTopics remove(String childTypeUri, long topicId) {
185            return _update(childTypeUri, new TopicDeletionModel(topicId));
186        }
187    
188    
189    
190        // ----------------------------------------------------------------------------------------- Package Private Methods
191    
192        void update(ChildTopicsModel newComp) {
193            try {
194                for (AssociationDefinition assocDef : parent.getType().getAssocDefs()) {
195                    String childTypeUri   = assocDef.getChildTypeUri();
196                    String cardinalityUri = assocDef.getChildCardinalityUri();
197                    TopicModel newChildTopic        = null;     // only used for "one"
198                    List<TopicModel> newChildTopics = null;     // only used for "many"
199                    if (cardinalityUri.equals("dm4.core.one")) {
200                        newChildTopic = newComp.getTopic(childTypeUri, null);        // defaultValue=null
201                        // skip if not contained in update request
202                        if (newChildTopic == null) {
203                            continue;
204                        }
205                    } else if (cardinalityUri.equals("dm4.core.many")) {
206                        newChildTopics = newComp.getTopics(childTypeUri, null);      // defaultValue=null
207                        // skip if not contained in update request
208                        if (newChildTopics == null) {
209                            continue;
210                        }
211                    } else {
212                        throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI");
213                    }
214                    //
215                    updateChildTopics(newChildTopic, newChildTopics, assocDef);
216                }
217                //
218                dms.valueStorage.refreshLabel(parent.getModel());
219                //
220            } catch (Exception e) {
221                throw new RuntimeException("Updating the child topics of " + parent.className() + " " + parent.getId() +
222                    " failed (newComp=" + newComp + ")", e);
223            }
224        }
225    
226        // Note: the given association definition must not necessarily originate from the parent object's type definition.
227        // It may originate from a facet definition as well.
228        // Called from AttachedDeepaMehtaObject.updateChildTopic() and AttachedDeepaMehtaObject.updateChildTopics().
229        void updateChildTopics(TopicModel newChildTopic, List<TopicModel> newChildTopics, AssociationDefinition assocDef) {
230            // Note: updating the child topics requires them to be loaded
231            loadChildTopics(assocDef);
232            //
233            String assocTypeUri = assocDef.getTypeUri();
234            boolean one = newChildTopic != null;
235            if (assocTypeUri.equals("dm4.core.composition_def")) {
236                if (one) {
237                    updateCompositionOne(newChildTopic, assocDef);
238                } else {
239                    updateCompositionMany(newChildTopics, assocDef);
240                }
241            } else if (assocTypeUri.equals("dm4.core.aggregation_def")) {
242                if (one) {
243                    updateAggregationOne(newChildTopic, assocDef);
244                } else {
245                    updateAggregationMany(newChildTopics, assocDef);
246                }
247            } else {
248                throw new RuntimeException("Association type \"" + assocTypeUri + "\" not supported");
249            }
250        }
251    
252        // ---
253    
254        void loadChildTopics() {
255            dms.valueStorage.fetchChildTopics(parent.getModel());
256            initAttachedObjectCache();
257        }
258    
259        void loadChildTopics(String childTypeUri) {
260            loadChildTopics(getAssocDef(childTypeUri));
261        }
262    
263        // ------------------------------------------------------------------------------------------------- Private Methods
264    
265        /**
266         * Recursively loads child topics (model) and updates this attached object cache accordingly.
267         * If the child topics are loaded already nothing is performed.
268         *
269         * @param   assocDef    the child topics according to this association definition are loaded.
270         *                      <p>
271         *                      Note: the association definition must not necessarily originate from the parent object's
272         *                      type definition. It may originate from a facet definition as well.
273         */
274        private void loadChildTopics(AssociationDefinition assocDef) {
275            String childTypeUri = assocDef.getChildTypeUri();
276            if (!has(childTypeUri)) {
277                logger.fine("### Lazy-loading \"" + childTypeUri + "\" child topic(s) of " + parent.className() + " " +
278                    parent.getId());
279                dms.valueStorage.fetchChildTopics(parent.getModel(), assocDef);
280                initAttachedObjectCache(childTypeUri);
281            }
282        }
283    
284        // --- Access this attached object cache ---
285    
286        private Topic _getTopic(String childTypeUri) {
287            Topic topic = (Topic) childTopics.get(childTypeUri);
288            // error check
289            if (topic == null) {
290                throw new RuntimeException("Child topic of type \"" + childTypeUri + "\" not found in " + childTopics);
291            }
292            //
293            return topic;
294        }
295    
296        private AttachedTopic _getTopic(String childTypeUri, AttachedTopic defaultTopic) {
297            AttachedTopic topic = (AttachedTopic) childTopics.get(childTypeUri);
298            return topic != null ? topic : defaultTopic;
299        }
300    
301        // ---
302    
303        private List<Topic> _getTopics(String childTypeUri) {
304            try {
305                List<Topic> topics = (List<Topic>) childTopics.get(childTypeUri);
306                // error check
307                if (topics == null) {
308                    throw new RuntimeException("Child topics of type \"" + childTypeUri + "\" not found in " + childTopics);
309                }
310                //
311                return topics;
312            } catch (ClassCastException e) {
313                getModel().throwInvalidAccess(childTypeUri, e);
314                return null;    // never reached
315            }
316        }
317    
318        private List<Topic> _getTopics(String childTypeUri, List<Topic> defaultValue) {
319            try {
320                List<Topic> topics = (List<Topic>) childTopics.get(childTypeUri);
321                return topics != null ? topics : defaultValue;
322            } catch (ClassCastException e) {
323                getModel().throwInvalidAccess(childTypeUri, e);
324                return null;    // never reached
325            }
326        }
327    
328        // ---
329    
330        private ChildTopics _update(String childTypeUri, TopicModel newChildTopic) {
331            parent.update(new TopicModel(parent.getTypeUri(), new ChildTopicsModel().put(childTypeUri, newChildTopic)));
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    }