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