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