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