001package de.deepamehta.core.impl;
002
003import de.deepamehta.core.AssociationDefinition;
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.RelatedTopicModel;
009import de.deepamehta.core.model.TopicDeletionModel;
010import de.deepamehta.core.model.TopicModel;
011import de.deepamehta.core.model.TopicRoleModel;
012import de.deepamehta.core.model.TypeModel;
013import de.deepamehta.core.model.ViewConfigurationModel;
014
015import org.codehaus.jettison.json.JSONException;
016import org.codehaus.jettison.json.JSONObject;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.List;
021import java.util.logging.Logger;
022
023
024
025/**
026 * Definition of an association between 2 topic types -- part of DeepaMehta's type system,
027 * like an association in a class diagram. Used to represent both, aggregations and compositions.
028 * ### FIXDOC: also assoc types have assoc defs
029 *
030 * @author <a href="mailto:jri@deepamehta.de">Jörg Richter</a>
031 */
032class AssociationDefinitionModelImpl extends AssociationModelImpl implements AssociationDefinitionModel {
033
034    // ---------------------------------------------------------------------------------------------- Instance Variables
035
036    private String parentCardinalityUri;
037    private String childCardinalityUri;
038
039    private ViewConfigurationModelImpl viewConfig;     // is never null
040
041    private Logger logger = Logger.getLogger(getClass().getName());
042
043    // ---------------------------------------------------------------------------------------------------- Constructors
044
045    /**
046     * ### TODO: add include-in-label parameter?
047     *
048     * @param   customAssocTypeUri      if null no custom association type will be set.
049     */
050    AssociationDefinitionModelImpl(AssociationModelImpl assoc, String parentCardinalityUri, String childCardinalityUri,
051                                                                                ViewConfigurationModelImpl viewConfig) {
052        super(assoc);
053        this.parentCardinalityUri = parentCardinalityUri;
054        this.childCardinalityUri  = childCardinalityUri;
055        this.viewConfig = viewConfig != null ? viewConfig : mf.newViewConfigurationModel();
056        // ### TODO: why null check? Compare to TypeModelImpl constructor
057    }
058
059    // -------------------------------------------------------------------------------------------------- Public Methods
060
061    @Override
062    public String getAssocDefUri() {
063        String customAssocTypeUri = getCustomAssocTypeUriOrNull();
064        return getChildTypeUri() + (customAssocTypeUri !=null ? "#" + customAssocTypeUri : "");
065    }
066
067    @Override
068    public String getCustomAssocTypeUri() {
069        TopicModel customAssocType = getCustomAssocType();
070        return customAssocType != null ? customAssocType.getUri() : null;
071    }
072
073    /**
074     * The type to be used to create an association instance based on this association definition.
075     */
076    @Override
077    public String getInstanceLevelAssocTypeUri() {
078        String customAssocTypeUri = getCustomAssocTypeUri();
079        return customAssocTypeUri !=null ? customAssocTypeUri : defaultInstanceLevelAssocTypeUri();
080    }
081
082    @Override
083    public String getParentTypeUri() {
084        return ((TopicRoleModel) getRoleModel("dm4.core.parent_type")).getTopicUri();
085    }
086
087    @Override
088    public String getChildTypeUri() {
089        return ((TopicRoleModel) getRoleModel("dm4.core.child_type")).getTopicUri();
090    }
091
092    @Override
093    public String getParentCardinalityUri() {
094        return parentCardinalityUri;
095    }
096
097    @Override
098    public String getChildCardinalityUri() {
099        return childCardinalityUri;
100    }
101
102    @Override
103    public ViewConfigurationModelImpl getViewConfigModel() {
104        return viewConfig;
105    }
106
107    // ---
108
109    @Override
110    public void setParentCardinalityUri(String parentCardinalityUri) {
111        this.parentCardinalityUri = parentCardinalityUri;
112    }
113
114    @Override
115    public void setChildCardinalityUri(String childCardinalityUri) {
116        this.childCardinalityUri = childCardinalityUri;
117    }
118
119    @Override
120    public void setViewConfigModel(ViewConfigurationModel viewConfig) {
121        this.viewConfig = (ViewConfigurationModelImpl) viewConfig;
122    }
123
124    // ---
125
126    @Override
127    public JSONObject toJSON() {
128        try {
129            return super.toJSON()
130                .put("parent_cardinality_uri", parentCardinalityUri)
131                .put("child_cardinality_uri", childCardinalityUri)
132                .put("view_config_topics", viewConfig.toJSONArray());
133        } catch (Exception e) {
134            throw new RuntimeException("Serialization failed (" + this + ")", e);
135        }
136    }
137
138    // ---
139
140    @Override
141    public String toString() {
142        return "\n    association definition (" + super.toString() +
143            ",\n        parent cardinality=\"" + parentCardinalityUri +
144            "\",\n        child cardinality=\"" + childCardinalityUri +
145            "\",\n        " + viewConfig + ")\n";
146    }
147
148    // ----------------------------------------------------------------------------------------- Package Private Methods
149
150
151
152    @Override
153    String className() {
154        return "association definition";
155    }
156
157    @Override
158    AssociationDefinition instantiate() {
159        return new AssociationDefinitionImpl(this, pl);
160    }
161
162
163
164    // === Core Internal Hooks ===
165
166    @Override
167    void postUpdate(DeepaMehtaObjectModel newModel, DeepaMehtaObjectModel oldModel) {
168        super.postUpdate(newModel, oldModel);
169        //
170        updateCardinality((AssociationDefinitionModel) newModel);
171        //
172        // rehash
173        boolean changeCustomAssocType = customAssocTypeChange((AssociationDefinitionModel) newModel,
174            (AssociationDefinitionModel) oldModel);
175        if (changeCustomAssocType) {
176            logger.info("### Changed custom association type URI from \"" +
177                ((AssociationDefinitionModelImpl) oldModel).getCustomAssocTypeUri() + "\" -> \"" +
178                ((AssociationDefinitionModelImpl) newModel).getCustomAssocTypeUriOrNull() + "\"");
179            getParentType().rehashAssocDef(newModel.getId());
180        }
181    }
182
183
184
185    // === Update (memory + DB) ===
186
187    void updateParentCardinalityUri(String parentCardinalityUri) {
188        setParentCardinalityUri(parentCardinalityUri);                      // update memory
189        pl.typeStorage.storeParentCardinalityUri(id, parentCardinalityUri); // update DB
190    }
191
192    void updateChildCardinalityUri(String childCardinalityUri) {
193        setChildCardinalityUri(childCardinalityUri);                        // update memory
194        pl.typeStorage.storeChildCardinalityUri(id, childCardinalityUri);   // update DB
195    }
196
197
198
199    // ===
200
201    /**
202     * ### TODO: make private
203     *
204     * @return  <code>null</code> if this assoc def's custom assoc type model is null or represents a deletion ref.
205     *          Otherwise returns the custom assoc type URI.
206     */
207    String getCustomAssocTypeUriOrNull() {
208        return getCustomAssocType() instanceof TopicDeletionModel ? null : getCustomAssocTypeUri();
209    }
210
211    // ---
212
213    TypeModelImpl getParentType() {
214        return pl.typeStorage.getType(getParentTypeUri());
215    }
216
217    // ------------------------------------------------------------------------------------------------- Private Methods
218
219
220
221    // === Update ===
222
223    private void updateCardinality(AssociationDefinitionModel newModel) {
224        updateParentCardinality(newModel.getParentCardinalityUri());
225        updateChildCardinality(newModel.getChildCardinalityUri());
226    }
227
228    // ---
229
230    private void updateParentCardinality(String newParentCardinalityUri) {
231        // abort if no update is requested
232        if (newParentCardinalityUri == null) {
233            return;
234        }
235        //
236        String parentCardinalityUri = getParentCardinalityUri();
237        if (!parentCardinalityUri.equals(newParentCardinalityUri)) {
238            logger.info("### Changing parent cardinality URI from \"" + parentCardinalityUri + "\" -> \"" +
239                newParentCardinalityUri + "\"");
240            updateParentCardinalityUri(newParentCardinalityUri);
241        }
242    }
243
244    private void updateChildCardinality(String newChildCardinalityUri) {
245        // abort if no update is requested
246        if (newChildCardinalityUri == null) {
247            return;
248        }
249        //
250        String childCardinalityUri = getChildCardinalityUri();
251        if (!childCardinalityUri.equals(newChildCardinalityUri)) {
252            logger.info("### Changing child cardinality URI from \"" + childCardinalityUri + "\" -> \"" +
253                newChildCardinalityUri + "\"");
254            updateChildCardinalityUri(newChildCardinalityUri);
255        }
256    }
257
258
259
260    // ====
261
262    private boolean customAssocTypeChange(AssociationDefinitionModel newModel, AssociationDefinitionModel oldModel) {
263        String oldUri = oldModel.getCustomAssocTypeUri();   // null if no assoc type is set
264        String newUri = ((AssociationDefinitionModelImpl) newModel).getCustomAssocTypeUriOrNull();  // null if del ref
265        if (newUri != null) {
266            // new value is neither a deletion ref nor null, compare it to old value (which may be null)
267            return !newUri.equals(oldUri);
268        } else {
269            // compare old value to null if new value is a deletion ref or null
270            // ### FIXME: must differentiate "no change requested" (= null) and "remove current assignment" (= del ref)?
271            return oldUri != null;
272        }
273    }
274
275    private RelatedTopicModel getCustomAssocType() {
276        RelatedTopicModel customAssocType = getChildTopicsModel().getTopicOrNull(
277            "dm4.core.assoc_type#dm4.core.custom_assoc_type");
278        // Note: we can't do this sanity check because a type model would not even deserialize.
279        // The type model JSON constructor repeatedly calls addAssocDef() which hashes by assoc def URI. ### still true?
280        /* if (customAssocType instanceof TopicDeletionModel) {
281            throw new RuntimeException("Tried to get an assoc def's custom assoc type when it is a deletion " +
282                "reference (" + this + ")");
283        } */
284        return customAssocType;
285    }
286
287    private String defaultInstanceLevelAssocTypeUri() {
288        if (typeUri.equals("dm4.core.aggregation_def")) {
289            return "dm4.core.aggregation";
290        } else if (typeUri.equals("dm4.core.composition_def")) {
291            return "dm4.core.composition";
292        } else {
293            throw new RuntimeException("Unexpected association type URI: \"" + typeUri + "\"");
294        }
295    }
296}