001package de.deepamehta.core.impl;
002
003import de.deepamehta.core.Topic;
004import de.deepamehta.core.model.AssociationDefinitionModel;
005import de.deepamehta.core.model.AssociationModel;
006import de.deepamehta.core.model.ChildTopicsModel;
007import de.deepamehta.core.model.DeepaMehtaObjectModel;
008import de.deepamehta.core.model.IndexMode;
009import de.deepamehta.core.model.RelatedTopicModel;
010import de.deepamehta.core.model.SimpleValue;
011import de.deepamehta.core.model.TopicModel;
012import de.deepamehta.core.model.TopicReferenceModel;
013import de.deepamehta.core.model.TypeModel;
014import de.deepamehta.core.service.ResultList;
015import de.deepamehta.core.util.JavaUtils;
016
017import java.util.ArrayList;
018import java.util.Iterator;
019import java.util.List;
020import java.util.logging.Logger;
021
022
023
024/**
025 * Helper for storing/fetching simple values and composite value models.
026 */
027class 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}