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    AssociationDefinitionImpl 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    // === Access Control ===
200
201    boolean isReadable() {
202        try {
203            // 1) check assoc def
204            if (!pl.hasReadAccess(this)) {
205                logger.info("### Assoc def \"" + getAssocDefUri() + "\" not READable");
206                return false;
207            }
208            // Note: there is no need to explicitly check READability for the assoc def's child type.
209            // If the child type is not READable the entire assoc def is not READable as well.
210            //
211            // 2) check custom assoc type, if set
212            TopicModelImpl assocType = getCustomAssocType();
213            if (assocType != null && !pl.hasReadAccess(assocType)) {
214                logger.info("### Assoc def \"" + getAssocDefUri() + "\" not READable (custom assoc type not READable)");
215                return false;
216            }
217            //
218            return true;
219        } catch (Exception e) {
220            throw new RuntimeException("Checking assoc def READability failed (" + this + ")", e);
221        }
222    }
223
224
225
226    // ===
227
228    /**
229     * ### TODO: make private
230     *
231     * @return  <code>null</code> if this assoc def's custom assoc type model is null or represents a deletion ref.
232     *          Otherwise returns the custom assoc type URI.
233     */
234    String getCustomAssocTypeUriOrNull() {
235        return getCustomAssocType() instanceof TopicDeletionModel ? null : getCustomAssocTypeUri();
236    }
237
238    // ---
239
240    TypeModelImpl getParentType() {
241        return pl.typeStorage.getType(getParentTypeUri());
242    }
243
244    // ------------------------------------------------------------------------------------------------- Private Methods
245
246
247
248    // === Update ===
249
250    private void updateCardinality(AssociationDefinitionModel newModel) {
251        updateParentCardinality(newModel.getParentCardinalityUri());
252        updateChildCardinality(newModel.getChildCardinalityUri());
253    }
254
255    // ---
256
257    private void updateParentCardinality(String newParentCardinalityUri) {
258        // abort if no update is requested
259        if (newParentCardinalityUri == null) {
260            return;
261        }
262        //
263        String parentCardinalityUri = getParentCardinalityUri();
264        if (!parentCardinalityUri.equals(newParentCardinalityUri)) {
265            logger.info("### Changing parent cardinality URI from \"" + parentCardinalityUri + "\" -> \"" +
266                newParentCardinalityUri + "\"");
267            updateParentCardinalityUri(newParentCardinalityUri);
268        }
269    }
270
271    private void updateChildCardinality(String newChildCardinalityUri) {
272        // abort if no update is requested
273        if (newChildCardinalityUri == null) {
274            return;
275        }
276        //
277        String childCardinalityUri = getChildCardinalityUri();
278        if (!childCardinalityUri.equals(newChildCardinalityUri)) {
279            logger.info("### Changing child cardinality URI from \"" + childCardinalityUri + "\" -> \"" +
280                newChildCardinalityUri + "\"");
281            updateChildCardinalityUri(newChildCardinalityUri);
282        }
283    }
284
285
286
287    // ====
288
289    private boolean customAssocTypeChange(AssociationDefinitionModel newModel, AssociationDefinitionModel oldModel) {
290        String oldUri = oldModel.getCustomAssocTypeUri();   // null if no assoc type is set
291        String newUri = ((AssociationDefinitionModelImpl) newModel).getCustomAssocTypeUriOrNull();  // null if del ref
292        if (newUri != null) {
293            // new value is neither a deletion ref nor null, compare it to old value (which may be null)
294            return !newUri.equals(oldUri);
295        } else {
296            // compare old value to null if new value is a deletion ref or null
297            // ### FIXME: must differentiate "no change requested" (= null) and "remove current assignment" (= del ref)?
298            return oldUri != null;
299        }
300    }
301
302    private TopicModelImpl getCustomAssocType() {
303        TopicModelImpl customAssocType = getChildTopicsModel().getTopicOrNull(
304            "dm4.core.assoc_type#dm4.core.custom_assoc_type");
305        // Note: we can't do this sanity check because a type model would not even deserialize.
306        // The type model JSON constructor repeatedly calls addAssocDef() which hashes by assoc def URI. ### still true?
307        /* if (customAssocType instanceof TopicDeletionModel) {
308            throw new RuntimeException("Tried to get an assoc def's custom assoc type when it is a deletion " +
309                "reference (" + this + ")");
310        } */
311        return customAssocType;
312    }
313
314    private String defaultInstanceLevelAssocTypeUri() {
315        if (typeUri.equals("dm4.core.aggregation_def")) {
316            return "dm4.core.aggregation";
317        } else if (typeUri.equals("dm4.core.composition_def")) {
318            return "dm4.core.composition";
319        } else {
320            throw new RuntimeException("Unexpected association type URI: \"" + typeUri + "\"");
321        }
322    }
323}