001    package de.deepamehta.core.impl;
002    
003    import de.deepamehta.core.AssociationDefinition;
004    import de.deepamehta.core.Topic;
005    import de.deepamehta.core.Type;
006    import de.deepamehta.core.model.AssociationModel;
007    import de.deepamehta.core.model.CompositeValueModel;
008    import de.deepamehta.core.model.DeepaMehtaObjectModel;
009    import de.deepamehta.core.model.RelatedTopicModel;
010    import de.deepamehta.core.model.SimpleValue;
011    import de.deepamehta.core.model.TopicModel;
012    import de.deepamehta.core.model.TopicReferenceModel;
013    import de.deepamehta.core.model.TopicRoleModel;
014    import de.deepamehta.core.service.ClientState;
015    import de.deepamehta.core.service.Directives;
016    import de.deepamehta.core.service.ResultList;
017    import de.deepamehta.core.util.JavaUtils;
018    
019    import java.util.Iterator;
020    import java.util.List;
021    import java.util.logging.Logger;
022    
023    
024    
025    /**
026     * Helper for storing/fetching simple values and composite value models.
027     */
028    class ValueStorage {
029    
030        // ------------------------------------------------------------------------------------------------------- Constants
031    
032        private static final String LABEL_CHILD_SEPARATOR = " ";
033        private static final String LABEL_TOPIC_SEPARATOR = ", ";
034    
035        // ---------------------------------------------------------------------------------------------- Instance Variables
036    
037        private EmbeddedService dms;
038    
039        private Logger logger = Logger.getLogger(getClass().getName());
040    
041        // ---------------------------------------------------------------------------------------------------- Constructors
042    
043        ValueStorage(EmbeddedService dms) {
044            this.dms = dms;
045        }
046    
047        // ----------------------------------------------------------------------------------------- Package Private Methods
048    
049        /**
050         * Fetches the composite value (child topic models) of the specified parent object model and updates the parent
051         * object model.
052         */
053        void fetchCompositeValue(DeepaMehtaObjectModel parent) {
054            try {
055                Type type = getType(parent);
056                if (!type.getDataTypeUri().equals("dm4.core.composite")) {
057                    return;
058                }
059                //
060                for (AssociationDefinition assocDef : type.getAssocDefs()) {
061                    fetchChildTopics(parent, assocDef);
062                }
063            } catch (Exception e) {
064                throw new RuntimeException("Fetching composite value of object " + parent.getId() + " failed (" +
065                    parent + ")", e);
066            }
067        }
068    
069        /**
070         * Fetches the child topic models of the specified parent object model according to the specified
071         * association definition and updates the parent object model.
072         * <p>
073         * Works for both, "one" and "many" association definitions.
074         */
075        void fetchChildTopics(DeepaMehtaObjectModel parent, AssociationDefinition assocDef) {
076            CompositeValueModel comp = parent.getCompositeValueModel();
077            String cardinalityUri = assocDef.getChildCardinalityUri();
078            String childTypeUri   = assocDef.getChildTypeUri();
079            if (cardinalityUri.equals("dm4.core.one")) {
080                RelatedTopicModel childTopic = fetchChildTopic(parent.getId(), assocDef);
081                // Note: topics just created have no child topics yet
082                if (childTopic != null) {
083                    comp.put(childTypeUri, childTopic);
084                    fetchCompositeValue(childTopic);    // recursion
085                }
086            } else if (cardinalityUri.equals("dm4.core.many")) {
087                for (RelatedTopicModel childTopic : fetchChildTopics(parent.getId(), assocDef)) {
088                    comp.add(childTypeUri, childTopic);
089                    fetchCompositeValue(childTopic);    // recursion
090                }
091            } else {
092                throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI");
093            }
094        }
095    
096        // ---
097    
098        /**
099         * Stores and indexes the specified model's value, either a simple value or a composite value (child topics).
100         * Depending on the model type's data type dispatches either to storeSimpleValue() or to storeCompositeValue().
101         * <p>
102         * Called to store the initial value of a newly created topic/association.
103         */
104        void storeValue(DeepaMehtaObjectModel model, ClientState clientState, Directives directives) {
105            if (getType(model).getDataTypeUri().equals("dm4.core.composite")) {
106                storeCompositeValue(model, clientState, directives);
107                refreshLabel(model);
108            } else {
109                storeSimpleValue(model);
110            }
111        }
112    
113        // ---
114    
115        /**
116         * Prerequisite: this is a composite object.
117         */
118        void refreshLabel(DeepaMehtaObjectModel model) {
119            try {
120                String label = buildLabel(model);
121                setSimpleValue(model, new SimpleValue(label));
122            } catch (Exception e) {
123                throw new RuntimeException("Refreshing label of object " + model.getId() + " failed (" + model + ")", e);
124            }
125        }
126    
127        void setSimpleValue(DeepaMehtaObjectModel model, SimpleValue value) {
128            if (value == null) {
129                throw new IllegalArgumentException("Tried to set a null SimpleValue (" + this + ")");
130            }
131            // update memory
132            model.setSimpleValue(value);
133            // update DB
134            storeSimpleValue(model);
135        }
136    
137    
138    
139        // === Helper ===
140    
141        /**
142         * Creates an association between the given parent object ("Parent" role) and the referenced topic ("Child" role).
143         * The association type is taken from the given association definition.
144         */
145        Topic associateChildTopic(TopicReferenceModel childTopicRef, DeepaMehtaObjectModel parent,
146                                                                  AssociationDefinition assocDef, ClientState clientState) {
147            if (childTopicRef.isReferenceById()) {
148                associateChildTopic(childTopicRef.getId(), parent, assocDef, clientState);
149                return dms.getTopic(childTopicRef.getId(), false);                           // fetchComposite=false
150                // ### FIXME: fetchComposite? Would storage.fetchTopic() be sufficient?
151            } else if (childTopicRef.isReferenceByUri()) {
152                associateChildTopic(childTopicRef.getUri(), parent, assocDef, clientState);
153                return dms.getTopic("uri", new SimpleValue(childTopicRef.getUri()), false);  // fetchComposite=false
154                // ### FIXME: fetchComposite? Would storage.fetchTopic() be sufficient?
155            } else {
156                throw new RuntimeException("Invalid topic reference (" + childTopicRef + ")");
157            }
158        }
159    
160        void associateChildTopic(long childTopicId, DeepaMehtaObjectModel parent, AssociationDefinition assocDef,
161                                                                                  ClientState clientState) {
162            dms.createAssociation(assocDef.getInstanceLevelAssocTypeUri(),
163                parent.createRoleModel("dm4.core.parent"),
164                new TopicRoleModel(childTopicId, "dm4.core.child"), clientState
165            );
166        }
167    
168        void associateChildTopic(String childTopicUri, DeepaMehtaObjectModel parent, AssociationDefinition assocDef,
169                                                                                     ClientState clientState) {
170            dms.createAssociation(assocDef.getInstanceLevelAssocTypeUri(),
171                parent.createRoleModel("dm4.core.parent"),
172                new TopicRoleModel(childTopicUri, "dm4.core.child"), clientState
173            );
174        }
175    
176        // ---
177    
178        /**
179         * Convenience method to get the (attached) type of a DeepaMehta object model.
180         * The type is obtained from the core service's type cache.
181         */
182        Type getType(DeepaMehtaObjectModel model) {
183            if (model instanceof TopicModel) {
184                return dms.getTopicType(model.getTypeUri());
185            } else if (model instanceof AssociationModel) {
186                return dms.getAssociationType(model.getTypeUri());
187            }
188            throw new RuntimeException("Unexpected model: " + model);
189        }
190    
191    
192    
193        // ------------------------------------------------------------------------------------------------- Private Methods
194    
195        /**
196         * Stores and indexes the simple value of the specified topic or association model.
197         * Determines the index key and index modes.
198         */
199        private void storeSimpleValue(DeepaMehtaObjectModel model) {
200            Type type = getType(model);
201            if (model instanceof TopicModel) {
202                dms.storageDecorator.storeTopicValue(
203                    model.getId(),
204                    model.getSimpleValue(),
205                    type.getIndexModes(),
206                    type.getUri(),
207                    getIndexValue(model)
208                );
209            } else if (model instanceof AssociationModel) {
210                dms.storageDecorator.storeAssociationValue(
211                    model.getId(),
212                    model.getSimpleValue(),
213                    type.getIndexModes(),
214                    type.getUri(),
215                    getIndexValue(model)
216                );
217            }
218        }
219    
220        /**
221         * Called to store the initial value of a newly created topic/association.
222         * Just prepares the arguments and calls storeChildTopics() repetitively.
223         * <p>
224         * Note: the given model can contain childs not defined in the type definition.
225         * Only the childs defined in the type definition are stored.
226         */
227        private void storeCompositeValue(DeepaMehtaObjectModel parent, ClientState clientState, Directives directives) {
228            CompositeValueModel model = null;
229            try {
230                model = parent.getCompositeValueModel();
231                for (AssociationDefinition assocDef : getType(parent).getAssocDefs()) {
232                    String childTypeUri   = assocDef.getChildTypeUri();
233                    String cardinalityUri = assocDef.getChildCardinalityUri();
234                    TopicModel childTopic        = null;     // only used for "one"
235                    List<TopicModel> childTopics = null;     // only used for "many"
236                    if (cardinalityUri.equals("dm4.core.one")) {
237                        childTopic = model.getTopic(childTypeUri, null);        // defaultValue=null
238                        // skip if not contained in create request
239                        if (childTopic == null) {
240                            continue;
241                        }
242                    } else if (cardinalityUri.equals("dm4.core.many")) {
243                        childTopics = model.getTopics(childTypeUri, null);      // defaultValue=null
244                        // skip if not contained in create request
245                        if (childTopics == null) {
246                            continue;
247                        }
248                    } else {
249                        throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI");
250                    }
251                    //
252                    storeChildTopics(childTopic, childTopics, parent, assocDef, clientState, directives);
253                }
254            } catch (Exception e) {
255                throw new RuntimeException("Storing composite value of object " + parent.getId() + " failed (" +
256                    model + ")", e);
257            }
258        }
259    
260        // ---
261    
262        private void storeChildTopics(TopicModel childTopic, List<TopicModel> childTopics, DeepaMehtaObjectModel parent,
263                                           AssociationDefinition assocDef, ClientState clientState, Directives directives) {
264            String assocTypeUri = assocDef.getTypeUri();
265            boolean one = childTopic != null;
266            if (assocTypeUri.equals("dm4.core.composition_def")) {
267                if (one) {
268                    storeCompositionOne(childTopic, parent, assocDef, clientState, directives);
269                } else {
270                    storeCompositionMany(childTopics, parent, assocDef, clientState, directives);
271                }
272            } else if (assocTypeUri.equals("dm4.core.aggregation_def")) {
273                if (one) {
274                    storeAggregationOne(childTopic, parent, assocDef, clientState, directives);
275                } else {
276                    storeAggregationMany(childTopics, parent, assocDef, clientState, directives);
277                }
278            } else {
279                throw new RuntimeException("Association type \"" + assocTypeUri + "\" not supported");
280            }
281        }
282    
283        // --- Composition ---
284    
285        private void storeCompositionOne(TopicModel model, DeepaMehtaObjectModel parent,
286                                         AssociationDefinition assocDef, ClientState clientState, Directives directives) {
287            // == create child ==
288            // update DB
289            Topic childTopic = dms.createTopic(model, clientState);
290            associateChildTopic(childTopic.getId(), parent, assocDef, clientState);
291            // Note: memory is already up-to-date. The child topic ID is updated in-place.
292        }
293    
294        private void storeCompositionMany(List<TopicModel> models, DeepaMehtaObjectModel parent,
295                                          AssociationDefinition assocDef, ClientState clientState, Directives directives) {
296            for (TopicModel model : models) {
297                // == create child ==
298                // update DB
299                Topic childTopic = dms.createTopic(model, clientState);
300                associateChildTopic(childTopic.getId(), parent, assocDef, clientState);
301                // Note: memory is already up-to-date. The child topic ID is updated in-place.
302            }
303        }
304    
305        // --- Aggregation ---
306    
307        private void storeAggregationOne(TopicModel model, DeepaMehtaObjectModel parent,
308                                         AssociationDefinition assocDef, ClientState clientState, Directives directives) {
309            if (model instanceof TopicReferenceModel) {
310                // == create assignment ==
311                // update DB
312                Topic childTopic = associateChildTopic((TopicReferenceModel) model, parent, assocDef, clientState);
313                // update memory
314                putInCompositeValue(childTopic, parent, assocDef);
315            } else {
316                // == create child ==
317                // update DB
318                Topic childTopic = dms.createTopic(model, clientState);
319                associateChildTopic(childTopic.getId(), parent, assocDef, clientState);
320                // Note: memory is already up-to-date. The child topic ID is updated in-place.
321            }
322        }
323    
324        private void storeAggregationMany(List<TopicModel> models, DeepaMehtaObjectModel parent,
325                                          AssociationDefinition assocDef, ClientState clientState, Directives directives) {
326            for (TopicModel model : models) {
327                if (model instanceof TopicReferenceModel) {
328                    // == create assignment ==
329                    // update DB
330                    Topic childTopic = associateChildTopic((TopicReferenceModel) model, parent, assocDef, clientState);
331                    // update memory
332                    replaceReference(model, childTopic);
333                } else {
334                    // == create child ==
335                    // update DB
336                    Topic childTopic = dms.createTopic(model, clientState);
337                    associateChildTopic(childTopic.getId(), parent, assocDef, clientState);
338                    // Note: memory is already up-to-date. The child topic ID is updated in-place.
339                }
340            }
341        }
342    
343        // ---
344    
345        /**
346         * For single-valued childs
347         */
348        private void putInCompositeValue(Topic childTopic, DeepaMehtaObjectModel parent, AssociationDefinition assocDef) {
349            String childTypeUri = assocDef.getChildTypeUri();
350            parent.getCompositeValueModel().put(childTypeUri, childTopic.getModel());
351        }
352    
353        /**
354         * Replaces a topic reference with the real thing.
355         *
356         * Used for multiple-valued childs.
357         */
358        private void replaceReference(TopicModel topicRef, Topic topic) {
359            // Note: we must update the topic reference in-place.
360            // Replacing the entire topic in the list of child topics would cause ConcurrentModificationException.
361            topicRef.set(topic.getModel());
362        }
363    
364    
365    
366        // === Label ===
367    
368        private String buildLabel(DeepaMehtaObjectModel model) {
369            Type type = getType(model);
370            if (type.getDataTypeUri().equals("dm4.core.composite")) {
371                List<String> labelConfig = type.getLabelConfig();
372                if (labelConfig.size() > 0) {
373                    return buildLabelFromConfig(model, labelConfig);
374                } else {
375                    return buildDefaultLabel(model);
376                }
377            } else {
378                return model.getSimpleValue().toString();
379            }
380        }
381    
382        /**
383         * Builds the specified object model's label according to a label configuration.
384         */
385        private String buildLabelFromConfig(DeepaMehtaObjectModel model, List<String> labelConfig) {
386            StringBuilder builder = new StringBuilder();
387            for (String childTypeUri : labelConfig) {
388                appendLabel(buildChildLabel(model, childTypeUri), builder, LABEL_CHILD_SEPARATOR);
389            }
390            return builder.toString();
391        }
392    
393        private String buildDefaultLabel(DeepaMehtaObjectModel model) {
394            Iterator<AssociationDefinition> i = getType(model).getAssocDefs().iterator();
395            // Note: types just created might have no child types yet
396            if (!i.hasNext()) {
397                return "";
398            }
399            //
400            String childTypeUri = i.next().getChildTypeUri();
401            return buildChildLabel(model, childTypeUri);
402        }
403    
404        // ---
405    
406        private String buildChildLabel(DeepaMehtaObjectModel parent, String childTypeUri) {
407            Object value = parent.getCompositeValueModel().get(childTypeUri);
408            // Note: topics just created have no child topics yet
409            if (value == null) {
410                return "";
411            }
412            //
413            if (value instanceof TopicModel) {
414                TopicModel childTopic = (TopicModel) value;
415                return buildLabel(childTopic);                                          // recursion
416            } else if (value instanceof List) {
417                StringBuilder builder = new StringBuilder();
418                for (TopicModel childTopic : (List<TopicModel>) value) {
419                    appendLabel(buildLabel(childTopic), builder, LABEL_TOPIC_SEPARATOR);  // recursion
420                }
421                return builder.toString();
422            } else {
423                throw new RuntimeException("Unexpected value in a CompositeValueModel: " + value);
424            }
425        }
426    
427        private void appendLabel(String label, StringBuilder builder, String separator) {
428            // add separator
429            if (builder.length() > 0 && label.length() > 0) {
430                builder.append(separator);
431            }
432            //
433            builder.append(label);
434        }
435    
436    
437    
438        // === Helper ===
439    
440        /**
441         * Fetches and returns a child topic or <code>null</code> if no such topic extists.
442         */
443        private RelatedTopicModel fetchChildTopic(long parentId, AssociationDefinition assocDef) {
444            return dms.storageDecorator.fetchRelatedTopic(
445                parentId,
446                assocDef.getInstanceLevelAssocTypeUri(),
447                "dm4.core.parent", "dm4.core.child",
448                assocDef.getChildTypeUri()
449            );
450        }
451    
452        private ResultList<RelatedTopicModel> fetchChildTopics(long parentId, AssociationDefinition assocDef) {
453            return dms.storageDecorator.fetchRelatedTopics(
454                parentId,
455                assocDef.getInstanceLevelAssocTypeUri(),
456                "dm4.core.parent", "dm4.core.child",
457                assocDef.getChildTypeUri()
458            );
459        }
460    
461        // ---
462    
463        /**
464         * Calculates the simple value that is to be indexed for this object.
465         *
466         * HTML tags are stripped from HTML values. Non-HTML values are returned directly.
467         */
468        private SimpleValue getIndexValue(DeepaMehtaObjectModel model) {
469            SimpleValue value = model.getSimpleValue();
470            if (getType(model).getDataTypeUri().equals("dm4.core.html")) {
471                return new SimpleValue(JavaUtils.stripHTML(value.toString()));
472            } else {
473                return value;
474            }
475        }
476    }