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