001    package de.deepamehta.core.impl;
002    
003    import de.deepamehta.core.Topic;
004    import de.deepamehta.core.model.AssociationDefinitionModel;
005    import de.deepamehta.core.model.AssociationModel;
006    import de.deepamehta.core.model.ChildTopicsModel;
007    import de.deepamehta.core.model.DeepaMehtaObjectModel;
008    import de.deepamehta.core.model.IndexMode;
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.TypeModel;
014    import de.deepamehta.core.service.ResultList;
015    import de.deepamehta.core.util.JavaUtils;
016    
017    import java.util.ArrayList;
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         * Fetches the child topic models (recursively) of the given parent object model and updates it in-place.
050         * ### TODO: recursion is required in some cases (e.g. when fetching a topic through REST API) but is possibly
051         * overhead in others (e.g. when updating composite structures).
052         */
053        private void fetchChildTopics(DeepaMehtaObjectModel parent) {
054            for (AssociationDefinitionModel assocDef : getType(parent).getAssocDefs()) {
055                fetchChildTopics(parent, assocDef);
056            }
057        }
058    
059        /**
060         * Fetches the child topic models (recursively) of the given parent object model and updates it in-place.
061         * ### TODO: recursion is required in some cases (e.g. when fetching a topic through REST API) but is possibly
062         * overhead in others (e.g. when updating composite structures).
063         * <p>
064         * Works for both, "one" and "many" association definitions.
065         *
066         * @param   assocDef    The child topic models according to this association definition are fetched.
067         */
068        void fetchChildTopics(DeepaMehtaObjectModel parent, AssociationDefinitionModel assocDef) {
069            try {
070                ChildTopicsModel childTopics = parent.getChildTopicsModel();
071                String cardinalityUri = assocDef.getChildCardinalityUri();
072                String childTypeUri   = assocDef.getChildTypeUri();
073                if (cardinalityUri.equals("dm4.core.one")) {
074                    RelatedTopicModel childTopic = fetchChildTopic(parent.getId(), assocDef);
075                    // Note: topics just created have no child topics yet
076                    if (childTopic != null) {
077                        childTopics.put(childTypeUri, childTopic);
078                        fetchChildTopics(childTopic);    // recursion
079                    }
080                } else if (cardinalityUri.equals("dm4.core.many")) {
081                    for (RelatedTopicModel childTopic : fetchChildTopics(parent.getId(), assocDef)) {
082                        childTopics.add(childTypeUri, childTopic);
083                        fetchChildTopics(childTopic);    // recursion
084                    }
085                } else {
086                    throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI");
087                }
088            } catch (Exception e) {
089                throw new RuntimeException("Fetching the \"" + assocDef.getChildTypeUri() + "\" child topics of object " +
090                    parent.getId() + " failed", e);
091            }        
092        }
093    
094        // ---
095    
096        /**
097         * Stores and indexes the specified model's value, either a simple value or a composite value (child topics).
098         * Depending on the model type's data type dispatches either to storeSimpleValue() or to storeChildTopics().
099         * <p>
100         * Called to store the initial value of a newly created topic/association.
101         */
102        void storeValue(DeepaMehtaObjectModel model) {
103            if (getType(model).getDataTypeUri().equals("dm4.core.composite")) {
104                storeChildTopics(model);
105                refreshLabel(model);
106            } else {
107                storeSimpleValue(model);
108            }
109        }
110    
111        /**
112         * Indexes the simple value of the given object model according to the given index mode.
113         * <p>
114         * Called to index existing topics/associations once an index mode has been added to a type definition.
115         */
116        void indexSimpleValue(DeepaMehtaObjectModel model, IndexMode indexMode) {
117            if (model instanceof TopicModel) {
118                dms.storageDecorator.indexTopicValue(
119                    model.getId(),
120                    indexMode,
121                    model.getTypeUri(),
122                    getIndexValue(model)
123                );
124            } else if (model instanceof AssociationModel) {
125                dms.storageDecorator.indexAssociationValue(
126                    model.getId(),
127                    indexMode,
128                    model.getTypeUri(),
129                    getIndexValue(model)
130                );
131            }
132        }
133    
134        // ---
135    
136        /**
137         * Recalculates the label of the given parent object model and updates it in-place.
138         * Note: no child topics are loaded from the DB. The given parent object model is expected to contain all the
139         * child topic models required for the label calculation.
140         *
141         * @param   parent  The object model the label is calculated for. This is expected to be a composite model.
142         */
143        void refreshLabel(DeepaMehtaObjectModel parent) {
144            try {
145                String label = buildLabel(parent);
146                setSimpleValue(parent, new SimpleValue(label));
147            } catch (Exception e) {
148                throw new RuntimeException("Refreshing label of object " + parent.getId() + " failed (" + parent + ")", 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        // ------------------------------------------------------------------------------------------------- Private Methods
165    
166        /**
167         * Stores and indexes the simple value of the specified topic or association model.
168         * Determines the index key and index modes.
169         */
170        private void storeSimpleValue(DeepaMehtaObjectModel model) {
171            TypeModel type = getType(model);
172            if (model instanceof TopicModel) {
173                dms.storageDecorator.storeTopicValue(
174                    model.getId(),
175                    model.getSimpleValue(),
176                    type.getIndexModes(),
177                    type.getUri(),
178                    getIndexValue(model)
179                );
180            } else if (model instanceof AssociationModel) {
181                dms.storageDecorator.storeAssociationValue(
182                    model.getId(),
183                    model.getSimpleValue(),
184                    type.getIndexModes(),
185                    type.getUri(),
186                    getIndexValue(model)
187                );
188            }
189        }
190    
191        // ---
192    
193        /**
194         * Stores the composite value (child topics) of the specified topic or association model.
195         * Called to store the initial value of a newly created topic/association.
196         * <p>
197         * Note: the given model can contain childs not defined in the type definition.
198         * Only the childs defined in the type definition are stored.
199         */
200        private void storeChildTopics(DeepaMehtaObjectModel parent) {
201            ChildTopicsModel model = null;
202            try {
203                model = parent.getChildTopicsModel();
204                for (AssociationDefinitionModel assocDef : getType(parent).getAssocDefs()) {
205                    String childTypeUri   = assocDef.getChildTypeUri();
206                    String cardinalityUri = assocDef.getChildCardinalityUri();
207                    if (cardinalityUri.equals("dm4.core.one")) {
208                        RelatedTopicModel childTopic = model.getTopic(childTypeUri, null);          // defaultValue=null
209                        if (childTopic != null) {   // skip if not contained in create request
210                            storeChildTopic(childTopic, parent, assocDef);
211                        }
212                    } else if (cardinalityUri.equals("dm4.core.many")) {
213                        List<RelatedTopicModel> childTopics = model.getTopics(childTypeUri, null);  // defaultValue=null
214                        if (childTopics != null) {  // skip if not contained in create request
215                            for (RelatedTopicModel childTopic : childTopics) {
216                                storeChildTopic(childTopic, parent, assocDef);
217                            }
218                        }
219                    } else {
220                        throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI");
221                    }
222                }
223            } catch (Exception e) {
224                throw new RuntimeException("Storing the child topics of object " + parent.getId() + " failed (" +
225                    model + ")", e);
226            }
227        }
228    
229        private void storeChildTopic(RelatedTopicModel childTopic, DeepaMehtaObjectModel parent,
230                                                                   AssociationDefinitionModel assocDef) {
231            if (childTopic instanceof TopicReferenceModel) {
232                resolveReference((TopicReferenceModel) childTopic);
233            } else {
234                dms.createTopic(childTopic);
235            }
236            associateChildTopic(parent, childTopic, assocDef);
237        }
238    
239        // ---
240    
241        /**
242         * Replaces a reference with the real thing.
243         */
244        void resolveReference(TopicReferenceModel topicRef) {
245            topicRef.set(fetchReferencedTopic(topicRef).getModel());
246        }
247    
248        private Topic fetchReferencedTopic(TopicReferenceModel topicRef) {
249            // Note: the resolved topic must be fetched including its composite value.
250            // It might be required at client-side. ### TODO
251            if (topicRef.isReferenceById()) {
252                return dms.getTopic(topicRef.getId());                          // ### FIXME: had fetchComposite=true
253            } else if (topicRef.isReferenceByUri()) {
254                return dms.getTopic("uri", new SimpleValue(topicRef.getUri())); // ### FIXME: had fetchComposite=true
255            } else {
256                throw new RuntimeException("Invalid topic reference (" + topicRef + ")");
257            }
258        }
259    
260        // ---
261    
262        /**
263         * Creates an association between the given parent object ("Parent" role) and the child topic ("Child" role).
264         * The association type is taken from the given association definition.
265         */
266        void associateChildTopic(DeepaMehtaObjectModel parent, RelatedTopicModel childTopic,
267                                                               AssociationDefinitionModel assocDef) {
268            AssociationModel assoc = childTopic.getRelatingAssociation();
269            assoc.setTypeUri(assocDef.getInstanceLevelAssocTypeUri());
270            assoc.setRoleModel1(parent.createRoleModel("dm4.core.parent"));
271            assoc.setRoleModel2(childTopic.createRoleModel("dm4.core.child"));
272            dms.createAssociation(assoc);
273        }
274    
275    
276    
277        // === Label ===
278    
279        private String buildLabel(DeepaMehtaObjectModel model) {
280            TypeModel type = getType(model);
281            if (type.getDataTypeUri().equals("dm4.core.composite")) {
282                StringBuilder label = new StringBuilder();
283                for (String childTypeUri : getLabelChildTypeUris(model)) {
284                    appendLabel(buildChildLabel(model, childTypeUri), label, LABEL_CHILD_SEPARATOR);
285                }
286                return label.toString();
287            } else {
288                return model.getSimpleValue().toString();
289            }
290        }
291    
292        /**
293         * Prerequisite: parent is a composite model.
294         */
295        List<String> getLabelChildTypeUris(DeepaMehtaObjectModel parent) {
296            TypeModel type = getType(parent);
297            List<String> labelConfig = type.getLabelConfig();
298            if (labelConfig.size() > 0) {
299                return labelConfig;
300            } else {
301                List<String> childTypeUris = new ArrayList();
302                Iterator<AssociationDefinitionModel> i = type.getAssocDefs().iterator();
303                // Note: types just created might have no child types yet
304                if (i.hasNext()) {
305                    childTypeUris.add(i.next().getChildTypeUri());
306                }
307                return childTypeUris;
308            }
309        }
310    
311        private String buildChildLabel(DeepaMehtaObjectModel parent, String childTypeUri) {
312            Object value = parent.getChildTopicsModel().get(childTypeUri);
313            // Note: topics just created have no child topics yet
314            if (value == null) {
315                return "";
316            }
317            //
318            if (value instanceof TopicModel) {
319                TopicModel childTopic = (TopicModel) value;
320                return buildLabel(childTopic);                                          // recursion
321            } else if (value instanceof List) {
322                StringBuilder label = new StringBuilder();
323                for (TopicModel childTopic : (List<TopicModel>) value) {
324                    appendLabel(buildLabel(childTopic), label, LABEL_TOPIC_SEPARATOR);  // recursion
325                }
326                return label.toString();
327            } else {
328                throw new RuntimeException("Unexpected value in a ChildTopicsModel: " + value);
329            }
330        }
331    
332        private void appendLabel(String label, StringBuilder builder, String separator) {
333            // add separator
334            if (builder.length() > 0 && label.length() > 0) {
335                builder.append(separator);
336            }
337            //
338            builder.append(label);
339        }
340    
341    
342    
343        // === Helper ===
344    
345        /**
346         * Fetches and returns a child topic or <code>null</code> if no such topic extists.
347         */
348        private RelatedTopicModel fetchChildTopic(long parentId, AssociationDefinitionModel assocDef) {
349            return dms.storageDecorator.fetchRelatedTopic(
350                parentId,
351                assocDef.getInstanceLevelAssocTypeUri(),
352                "dm4.core.parent", "dm4.core.child",
353                assocDef.getChildTypeUri()
354            );
355        }
356    
357        private ResultList<RelatedTopicModel> fetchChildTopics(long parentId, AssociationDefinitionModel assocDef) {
358            return dms.storageDecorator.fetchRelatedTopics(
359                parentId,
360                assocDef.getInstanceLevelAssocTypeUri(),
361                "dm4.core.parent", "dm4.core.child",
362                assocDef.getChildTypeUri()
363            );
364        }
365    
366        // ---
367    
368        /**
369         * Calculates the simple value that is to be indexed for this object.
370         *
371         * HTML tags are stripped from HTML values. Non-HTML values are returned directly.
372         */
373        private SimpleValue getIndexValue(DeepaMehtaObjectModel model) {
374            SimpleValue value = model.getSimpleValue();
375            if (getType(model).getDataTypeUri().equals("dm4.core.html")) {
376                return new SimpleValue(JavaUtils.stripHTML(value.toString()));
377            } else {
378                return value;
379            }
380        }
381    
382        /**
383         * Returns the type model of a DeepaMehta object model.
384         * The type is obtained from the type storage.
385         */
386        private TypeModel getType(DeepaMehtaObjectModel model) {
387            if (model instanceof TopicModel) {
388                return dms.typeStorage.getTopicType(model.getTypeUri());
389            } else if (model instanceof AssociationModel) {
390                return dms.typeStorage.getAssociationType(model.getTypeUri());
391            }
392            throw new RuntimeException("Unexpected model: " + model);
393        }
394    }