001package de.deepamehta.core.impl;
002
003import de.deepamehta.core.model.AssociationDefinitionModel;
004import de.deepamehta.core.model.AssociationModel;
005import de.deepamehta.core.model.ChildTopicsModel;
006import de.deepamehta.core.model.DeepaMehtaObjectModel;
007import de.deepamehta.core.model.RelatedTopicModel;
008import de.deepamehta.core.model.SimpleValue;
009import de.deepamehta.core.model.TopicModel;
010import de.deepamehta.core.model.TopicReferenceModel;
011import de.deepamehta.core.model.TypeModel;
012import de.deepamehta.core.service.Directives;
013
014import java.util.ArrayList;
015import java.util.Iterator;
016import java.util.List;
017import java.util.logging.Logger;
018
019
020
021/**
022 * Helper for storing/fetching simple values and composite value models.
023 */
024class ValueStorage {
025
026    // ------------------------------------------------------------------------------------------------------- Constants
027
028    private static final String LABEL_CHILD_SEPARATOR = " ";
029    private static final String LABEL_TOPIC_SEPARATOR = ", ";
030
031    // ---------------------------------------------------------------------------------------------- Instance Variables
032
033    private PersistenceLayer pl;
034
035    private Logger logger = Logger.getLogger(getClass().getName());
036
037    // ---------------------------------------------------------------------------------------------------- Constructors
038
039    ValueStorage(PersistenceLayer pl) {
040        this.pl = pl;
041    }
042
043    // ----------------------------------------------------------------------------------------- Package Private Methods
044
045    /**
046     * Fetches the child topic models (recursively) of the given parent object model and updates it in-place.
047     * ### TODO: recursion is required in some cases (e.g. when fetching a topic through REST API) but is possibly
048     * overhead in others (e.g. when updating composite structures).
049     */
050    void fetchChildTopics(DeepaMehtaObjectModel parent) {
051        for (AssociationDefinitionModel assocDef : ((DeepaMehtaObjectModelImpl) parent).getType().getAssocDefs()) {
052            fetchChildTopics(parent, assocDef);
053        }
054    }
055
056    /**
057     * Fetches the child topic models (recursively) of the given parent object model and updates it in-place.
058     * ### TODO: recursion is required in some cases (e.g. when fetching a topic through REST API) but is possibly
059     * overhead in others (e.g. when updating composite structures).
060     * <p>
061     * Works for both, "one" and "many" association definitions.
062     *
063     * @param   assocDef    The child topic models according to this association definition are fetched.
064     */
065    void fetchChildTopics(DeepaMehtaObjectModel parent, AssociationDefinitionModel assocDef) {
066        try {
067            ChildTopicsModel childTopics = parent.getChildTopicsModel();
068            String cardinalityUri = assocDef.getChildCardinalityUri();
069            String assocDefUri    = assocDef.getAssocDefUri();
070            if (cardinalityUri.equals("dm4.core.one")) {
071                RelatedTopicModel childTopic = fetchChildTopic(parent.getId(), assocDef);
072                // Note: topics just created have no child topics yet
073                if (childTopic != null) {
074                    childTopics.put(assocDefUri, childTopic);
075                    fetchChildTopics(childTopic);    // recursion
076                }
077            } else if (cardinalityUri.equals("dm4.core.many")) {
078                for (RelatedTopicModel childTopic : fetchChildTopics(parent.getId(), assocDef)) {
079                    childTopics.add(assocDefUri, childTopic);
080                    fetchChildTopics(childTopic);    // recursion
081                }
082            } else {
083                throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI");
084            }
085        } catch (Exception e) {
086            throw new RuntimeException("Fetching the \"" + assocDef.getAssocDefUri() + "\" child topics of object " +
087                parent.getId() + " failed", e);
088        }
089    }
090
091    // ---
092
093    /**
094     * Stores and indexes the specified model's value, either a simple value or a composite value (child topics).
095     * Depending on the model type's data type dispatches either to storeSimpleValue() or to storeChildTopics().
096     * <p>
097     * Called to store the initial value of a newly created topic/association.
098     */
099    void storeValue(DeepaMehtaObjectModelImpl model) {
100        if (model.getType().getDataTypeUri().equals("dm4.core.composite")) {
101            storeChildTopics(model);
102            recalculateLabel(model);
103        } else {
104            model.storeSimpleValue();
105        }
106    }
107
108    /**
109     * Recalculates the label of the given parent object model and updates it in-place.
110     * Note: no child topics are loaded from the DB. The given parent object model is expected to contain all the
111     * child topic models required for the label calculation.
112     *
113     * @param   parent  The object model the label is calculated for. This is expected to be a composite model.
114     */
115    void recalculateLabel(DeepaMehtaObjectModelImpl parent) {
116        try {
117            String label = calculateLabel(parent);
118            parent.updateSimpleValue(new SimpleValue(label));
119        } catch (Exception e) {
120            throw new RuntimeException("Recalculating label of object " + parent.getId() + " failed (" + parent + ")",
121                e);
122        }
123    }
124
125    // ------------------------------------------------------------------------------------------------- Private Methods
126
127    /**
128     * Stores the composite value (child topics) of the specified topic or association model.
129     * Called to store the initial value of a newly created topic/association.
130     * <p>
131     * Note: the given model can contain childs not defined in the type definition.
132     * Only the childs defined in the type definition are stored.
133     */
134    private void storeChildTopics(DeepaMehtaObjectModelImpl parent) {
135        ChildTopicsModel model = null;
136        try {
137            model = parent.getChildTopicsModel();
138            for (AssociationDefinitionModel assocDef : parent.getType().getAssocDefs()) {
139                String assocDefUri    = assocDef.getAssocDefUri();
140                String cardinalityUri = assocDef.getChildCardinalityUri();
141                if (cardinalityUri.equals("dm4.core.one")) {
142                    RelatedTopicModel childTopic = model.getTopicOrNull(assocDefUri);
143                    if (childTopic != null) {   // skip if not contained in create request
144                        storeChildTopic(childTopic, parent, assocDef);
145                    }
146                } else if (cardinalityUri.equals("dm4.core.many")) {
147                    List<? extends RelatedTopicModel> childTopics = model.getTopicsOrNull(assocDefUri);
148                    if (childTopics != null) {  // skip if not contained in create request
149                        for (RelatedTopicModel childTopic : childTopics) {
150                            storeChildTopic(childTopic, parent, assocDef);
151                        }
152                    }
153                } else {
154                    throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI");
155                }
156            }
157        } catch (Exception e) {
158            throw new RuntimeException("Storing the child topics of object " + parent.getId() + " failed (" +
159                model + ")", e);
160        }
161    }
162
163    private void storeChildTopic(RelatedTopicModel childTopic, DeepaMehtaObjectModel parent,
164                                                               AssociationDefinitionModel assocDef) {
165        if (childTopic instanceof TopicReferenceModel) {
166            resolveReference((TopicReferenceModel) childTopic);
167        } else {
168            pl.createTopic(childTopic);
169        }
170        associateChildTopic(parent, childTopic, assocDef);
171    }
172
173    // ---
174
175    /**
176     * Replaces a reference with the real thing.
177     */
178    void resolveReference(TopicReferenceModel topicRef) {
179        topicRef.set(fetchReferencedTopic(topicRef));
180    }
181
182    private TopicModel fetchReferencedTopic(TopicReferenceModel topicRef) {
183        // Note: the resolved topic must be fetched including its composite value.
184        // It might be required at client-side. ### TODO
185        if (topicRef.isReferenceById()) {
186            return pl.fetchTopic(topicRef.getId());                                // ### FIXME: had fetchComposite=true
187        } else if (topicRef.isReferenceByUri()) {
188            TopicModel topic = pl.fetchTopic("uri", new SimpleValue(topicRef.getUri())); // ### FIXME: had
189            if (topic == null) {                                                         //          fetchComposite=true
190                throw new RuntimeException("Topic with URI \"" + topicRef.getUri() + "\" not found");
191            }
192            return topic;
193        } else {
194            throw new RuntimeException("Invalid topic reference (" + topicRef + ")");
195        }
196    }
197
198    // ---
199
200    /**
201     * Creates an association between the given parent object ("Parent" role) and the child topic ("Child" role).
202     * The association type is taken from the given association definition.
203     */
204    void associateChildTopic(DeepaMehtaObjectModel parent, RelatedTopicModel childTopic,
205                                                           AssociationDefinitionModel assocDef) {
206        AssociationModel assoc = childTopic.getRelatingAssociation();
207        assoc.setTypeUri(assocDef.getInstanceLevelAssocTypeUri());
208        assoc.setRoleModel1(parent.createRoleModel("dm4.core.parent"));
209        assoc.setRoleModel2(childTopic.createRoleModel("dm4.core.child"));
210        pl.createAssociation((AssociationModelImpl) assoc);
211    }
212
213
214
215    // === Label ===
216
217    private String calculateLabel(DeepaMehtaObjectModelImpl model) {
218        TypeModel type = model.getType();
219        if (type.getDataTypeUri().equals("dm4.core.composite")) {
220            StringBuilder label = new StringBuilder();
221            for (String assocDefUri : getLabelAssocDefUris(model)) {
222                appendLabel(buildChildLabel(model, assocDefUri), label, LABEL_CHILD_SEPARATOR);
223            }
224            return label.toString();
225        } else {
226            return model.getSimpleValue().toString();
227        }
228    }
229
230    /**
231     * Prerequisite: parent is a composite model.
232     */
233    List<String> getLabelAssocDefUris(DeepaMehtaObjectModel parent) {
234        TypeModel type = ((DeepaMehtaObjectModelImpl) parent).getType();
235        List<String> labelConfig = type.getLabelConfig();
236        if (labelConfig.size() > 0) {
237            return labelConfig;
238        } else {
239            List<String> assocDefUris = new ArrayList();
240            Iterator<? extends AssociationDefinitionModel> i = type.getAssocDefs().iterator();
241            // Note: types just created might have no child types yet
242            if (i.hasNext()) {
243                assocDefUris.add(i.next().getAssocDefUri());
244            }
245            return assocDefUris;
246        }
247    }
248
249    private String buildChildLabel(DeepaMehtaObjectModel parent, String assocDefUri) {
250        Object value = parent.getChildTopicsModel().get(assocDefUri);
251        // Note: topics just created have no child topics yet
252        if (value == null) {
253            return "";
254        }
255        //
256        if (value instanceof TopicModel) {
257            TopicModelImpl childTopic = (TopicModelImpl) value;
258            return calculateLabel(childTopic);                                                          // recursion
259        } else if (value instanceof List) {
260            StringBuilder label = new StringBuilder();
261            for (TopicModel childTopic : (List<TopicModel>) value) {
262                appendLabel(calculateLabel((TopicModelImpl) childTopic), label, LABEL_TOPIC_SEPARATOR); // recursion
263            }
264            return label.toString();
265        } else {
266            throw new RuntimeException("Unexpected value in a ChildTopicsModel: " + value);
267        }
268    }
269
270    private void appendLabel(String label, StringBuilder builder, String separator) {
271        // add separator
272        if (builder.length() > 0 && label.length() > 0) {
273            builder.append(separator);
274        }
275        //
276        builder.append(label);
277    }
278
279
280
281    // === Helper ===
282
283    /**
284     * Fetches and returns a child topic or <code>null</code> if no such topic extists.
285     */
286    private RelatedTopicModel fetchChildTopic(long parentId, AssociationDefinitionModel assocDef) {
287        return pl.fetchRelatedTopic(
288            parentId,
289            assocDef.getInstanceLevelAssocTypeUri(),
290            "dm4.core.parent", "dm4.core.child",
291            assocDef.getChildTypeUri()
292        );
293    }
294
295    private List<RelatedTopicModelImpl> fetchChildTopics(long parentId, AssociationDefinitionModel assocDef) {
296        return pl.fetchRelatedTopics(
297            parentId,
298            assocDef.getInstanceLevelAssocTypeUri(),
299            "dm4.core.parent", "dm4.core.child",
300            assocDef.getChildTypeUri()
301        );
302    }
303}