001package de.deepamehta.core.impl;
002
003import de.deepamehta.core.Association;
004import de.deepamehta.core.model.AssociationModel;
005import de.deepamehta.core.model.AssociationTypeModel;
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.RoleModel;
011import de.deepamehta.core.model.TopicModel;
012import de.deepamehta.core.model.TopicRoleModel;
013import de.deepamehta.core.model.TypeModel;
014import de.deepamehta.core.service.DeepaMehtaEvent;
015import de.deepamehta.core.service.Directive;
016import de.deepamehta.core.service.Directives;
017
018import org.codehaus.jettison.json.JSONObject;
019
020import java.util.List;
021
022
023
024/**
025 * Collection of the data that makes up an {@link Association}.
026 *
027 * @author <a href="mailto:jri@deepamehta.de">Jörg Richter</a>
028 */
029class AssociationModelImpl extends DeepaMehtaObjectModelImpl implements AssociationModel {
030
031    // ---------------------------------------------------------------------------------------------- Instance Variables
032
033    private RoleModelImpl roleModel1;   // may be null in models used for an update operation
034    private RoleModelImpl roleModel2;   // may be null in models used for an update operation
035
036    // ---------------------------------------------------------------------------------------------------- Constructors
037
038    AssociationModelImpl(DeepaMehtaObjectModelImpl object, RoleModelImpl roleModel1, RoleModelImpl roleModel2) {
039        super(object);
040        this.roleModel1 = roleModel1;
041        this.roleModel2 = roleModel2;
042    }
043
044    AssociationModelImpl(AssociationModelImpl assoc) {
045        super(assoc);
046        this.roleModel1 = assoc.roleModel1;
047        this.roleModel2 = assoc.roleModel2;
048    }
049
050    // -------------------------------------------------------------------------------------------------- Public Methods
051
052    @Override
053    public RoleModelImpl getRoleModel1() {
054        return roleModel1;
055    }
056
057    @Override
058    public RoleModelImpl getRoleModel2() {
059        return roleModel2;
060    }
061
062    // ---
063
064    @Override
065    public void setRoleModel1(RoleModel roleModel1) {
066        this.roleModel1 = (RoleModelImpl) roleModel1;
067    }
068
069    @Override
070    public void setRoleModel2(RoleModel roleModel2) {
071        this.roleModel2 = (RoleModelImpl) roleModel2;
072    }
073
074    // --- Convenience Methods ---
075
076    @Override
077    public RoleModel getRoleModel(String roleTypeUri) {
078        boolean rm1 = roleModel1.getRoleTypeUri().equals(roleTypeUri);
079        boolean rm2 = roleModel2.getRoleTypeUri().equals(roleTypeUri);
080        if (rm1 && rm2) {
081            throw new RuntimeException("Ambiguous getRoleModel() call: both players occupy role \"" + roleTypeUri +
082                "\" (" + this + ")");
083        }
084        return rm1 ? roleModel1 : rm2 ? roleModel2 : null;
085    }
086
087    @Override
088    public long getOtherPlayerId(long id) {
089        long id1 = roleModel1.getPlayerId();
090        long id2 = roleModel2.getPlayerId();
091        if (id1 == id) {
092            return id2;
093        } else if (id2 == id) {
094            return id1;
095        } else {
096            throw new IllegalArgumentException("ID " + id + " doesn't refer to a player in " + this);
097        }
098    }
099
100    @Override
101    public boolean hasSameRoleTypeUris() {
102        return roleModel1.getRoleTypeUri().equals(roleModel2.getRoleTypeUri());
103    }
104
105
106
107    // === Implementation of the abstract methods ===
108
109    @Override
110    public RoleModel createRoleModel(String roleTypeUri) {
111        return mf.newAssociationRoleModel(id, roleTypeUri);
112    }
113
114
115
116    // === Serialization ===
117
118    @Override
119    public JSONObject toJSON() {
120        try {
121            return super.toJSON()
122                .put("role_1", roleModel1.toJSON())
123                .put("role_2", roleModel2.toJSON());
124        } catch (Exception e) {
125            throw new RuntimeException("Serialization failed (" + this + ")", e);
126        }
127    }
128
129
130
131    // === Java API ===
132
133    @Override
134    public AssociationModel clone() {
135        try {
136            AssociationModel model = (AssociationModel) super.clone();
137            model.setRoleModel1(roleModel1.clone());
138            model.setRoleModel2(roleModel2.clone());
139            return model;
140        } catch (Exception e) {
141            throw new RuntimeException("Cloning an AssociationModel failed", e);
142        }
143    }
144
145    @Override
146    public String toString() {
147        return "association (" + super.toString() + ", " + roleModel1 + ", " + roleModel2 + ")";
148    }
149
150
151
152    // ----------------------------------------------------------------------------------------- Package Private Methods
153
154    @Override
155    String className() {
156        return "association";
157    }
158
159    @Override
160    Association instantiate() {
161        return new AssociationImpl(this, pl);
162    }
163
164    // ---
165
166    @Override
167    AssociationTypeModel getType() {
168        return pl.typeStorage.getAssociationType(typeUri);
169    }
170
171    @Override
172    List<AssociationModelImpl> getAssociations() {
173        return pl.fetchAssociationAssociations(id);
174    }
175
176    // ---
177
178    @Override
179    RelatedTopicModelImpl getRelatedTopic(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri,
180                                                                                     String othersTopicTypeUri) {
181        return pl.fetchAssociationRelatedTopic(id, assocTypeUri, myRoleTypeUri, othersRoleTypeUri,
182            othersTopicTypeUri);
183    }
184
185    @Override
186    List<RelatedTopicModelImpl> getRelatedTopics(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri,
187                                                                                            String othersTopicTypeUri) {
188        return pl.fetchAssociationRelatedTopics(id, assocTypeUri, myRoleTypeUri, othersRoleTypeUri,
189            othersTopicTypeUri);
190    }
191
192    @Override
193    List<RelatedTopicModelImpl> getRelatedTopics(List assocTypeUris, String myRoleTypeUri, String othersRoleTypeUri,
194                                                                                           String othersTopicTypeUri) {
195        return pl.fetchAssociationRelatedTopics(id, assocTypeUris, myRoleTypeUri, othersRoleTypeUri,
196            othersTopicTypeUri);
197    }
198
199    // ---
200
201    @Override
202    void storeUri() {
203        pl.storeAssociationUri(id, uri);
204    }
205
206    @Override
207    void storeTypeUri() {
208        reassignInstantiation();
209        pl.storeAssociationTypeUri(id, typeUri);
210    }
211
212    @Override
213    void storeSimpleValue() {
214        TypeModel type = getType();
215        pl.storeAssociationValue(id, value, type.getIndexModes(), type.getUri(), getIndexValue());
216    }
217
218    @Override
219    void indexSimpleValue(IndexMode indexMode) {
220        pl.indexAssociationValue(id, indexMode, typeUri, getIndexValue());
221    }
222
223    // ---
224
225    @Override
226    void updateChildTopics(ChildTopicsModel childTopics) {
227        update(mf.newAssociationModel(childTopics));
228    }
229
230    @Override
231    void _delete() {
232        pl._deleteAssociation(id);
233    }
234
235    // ---
236
237    @Override
238    DeepaMehtaEvent getReadAccessEvent() {
239        return CoreEvent.CHECK_ASSOCIATION_READ_ACCESS;
240    }
241
242    @Override
243    DeepaMehtaEvent getPreUpdateEvent() {
244        return CoreEvent.PRE_UPDATE_ASSOCIATION;
245    }
246
247    @Override
248    DeepaMehtaEvent getPostUpdateEvent() {
249        return CoreEvent.POST_UPDATE_ASSOCIATION;
250    }
251
252    @Override
253    DeepaMehtaEvent getPreDeleteEvent() {
254        return CoreEvent.PRE_DELETE_ASSOCIATION;
255    }
256
257    @Override
258    DeepaMehtaEvent getPostDeleteEvent() {
259        return CoreEvent.POST_DELETE_ASSOCIATION;
260    }
261
262    // ---
263
264    @Override
265    Directive getUpdateDirective() {
266        return Directive.UPDATE_ASSOCIATION;
267    }
268
269    @Override
270    Directive getDeleteDirective() {
271        return Directive.DELETE_ASSOCIATION;
272    }
273
274
275
276    // === Core Internal Hooks ===
277
278    @Override
279    void postUpdate(DeepaMehtaObjectModel newModel, DeepaMehtaObjectModel oldModel) {
280        super.postUpdate(newModel, oldModel);
281        //
282        updateRoles((AssociationModel) newModel);
283        //
284        // Type Editor Support
285        if (isAssocDef(this)) {
286            if (isAssocDef((AssociationModel) oldModel)) {
287                updateAssocDef();
288            } else {
289                createAssocDef();
290            }
291        } else if (isAssocDef((AssociationModel) oldModel)) {
292            removeAssocDef();
293        }
294    }
295
296    void preDelete() {
297        super.preDelete();
298        //
299        // Type Editor Support
300        if (isAssocDef(this)) {
301            // Note: we listen to the PRE event here, not the POST event. At POST time the assocdef sequence might be
302            // interrupted, which would result in a corrupted sequence once rebuild. (Due to the interruption, while
303            // rebuilding not all segments would be catched for deletion and recreated redundantly -> ambiguity.)
304            // ### FIXDOC
305            removeAssocDef();
306        }
307    }
308
309
310
311    // ===
312
313    /**
314     * @teturn  this association's topic which plays the given role.
315     *          If there is no such topic, null is returned.
316     *          <p>
317     *          If there are 2 such topics an exception is thrown.
318     */
319    TopicModelImpl getTopic(String roleTypeUri) {
320        RoleModel role = getRoleModel(roleTypeUri);
321        return role instanceof TopicRoleModel ? ((TopicRoleModelImpl) role).getPlayer() : null;
322    }
323
324    TopicModelImpl getTopicByType(String topicTypeUri) {
325        TopicModelImpl topic1 = filterTopic(roleModel1, topicTypeUri);
326        TopicModelImpl topic2 = filterTopic(roleModel2, topicTypeUri);
327        if (topic1 != null && topic2 != null) {
328            throw new RuntimeException("Ambiguous getTopicByType() call: both topics are of type \"" + topicTypeUri +
329                "\" (" + this + ")");
330        }
331        return topic1 != null ? topic1 : topic2 != null ? topic2 : null;
332    }
333
334    // ---
335
336    void updateRoleTypeUri(RoleModelImpl role, String roleTypeUri) {
337        role.setRoleTypeUri(roleTypeUri);                           // update memory
338        pl.storeRoleTypeUri(id, role.playerId, role.roleTypeUri);   // update DB
339    }
340
341    // ------------------------------------------------------------------------------------------------- Private Methods
342
343
344
345    // === Update (memory + DB) ===
346
347    /**
348     * @param   newModel    The data to update.
349     *                      If role 1 is <code>null</code> it is not updated.
350     *                      If role 2 is <code>null</code> it is not updated.
351     */
352    private void updateRoles(AssociationModel newModel) {
353        updateRole(newModel.getRoleModel1(), 1);
354        updateRole(newModel.getRoleModel2(), 2);
355    }
356
357    /**
358     * @param   nr      used only for logging
359     */
360    private void updateRole(RoleModel newModel, int nr) {
361        if (newModel != null) {     // abort if no update is requested
362            // Note: We must lookup the roles individually.
363            // The role order (getRole1(), getRole2()) is undeterministic and not fix.
364            RoleModelImpl role = getRole(newModel);
365            String newRoleTypeUri = newModel.getRoleTypeUri();  // new value
366            String roleTypeUri = role.getRoleTypeUri();         // current value
367            if (!roleTypeUri.equals(newRoleTypeUri)) {          // has changed?
368                logger.info("### Changing role type " + nr + " from \"" + roleTypeUri + "\" -> \"" + newRoleTypeUri +
369                    "\"");
370                updateRoleTypeUri(role, newRoleTypeUri);
371            }
372        }
373    }
374
375
376
377    // === Roles (memory access) ===
378
379    /**
380     * Returns this association's role which refers to the same object as the given role model.
381     * The role returned is found by comparing topic IDs, topic URIs, or association IDs.
382     * The role types are <i>not</i> compared.
383     * <p>
384     * If the object refered by the given role model is not a player in this association an exception is thrown.
385     */
386    private RoleModelImpl getRole(RoleModel roleModel) {
387        if (roleModel1.refsSameObject(roleModel)) {
388            return roleModel1;
389        } else if (roleModel2.refsSameObject(roleModel)) {
390            return roleModel2;
391        }
392        throw new RuntimeException("Role is not part of association (role=" + roleModel + ", association=" + this);
393    }
394
395    // ---
396
397    private TopicModelImpl filterTopic(RoleModelImpl role, String topicTypeUri) {
398        if (role instanceof TopicRoleModel) {
399            TopicModelImpl topic = ((TopicRoleModelImpl) role).getPlayer();
400            if (topic.getTypeUri().equals(topicTypeUri)) {
401                return topic;
402            }
403        }
404        return null;
405    }
406
407
408
409    // === Store (DB only) ===
410
411    private void reassignInstantiation() {
412        // remove current assignment
413        fetchInstantiation().delete();
414        // create new assignment
415        pl.createAssociationInstantiation(id, typeUri);
416    }
417
418    private AssociationModelImpl fetchInstantiation() {
419        RelatedTopicModelImpl assocType = getRelatedTopic("dm4.core.instantiation", "dm4.core.instance",
420            "dm4.core.type", "dm4.core.assoc_type");
421        //
422        if (assocType == null) {
423            throw new RuntimeException("Association " + id + " is not associated to an association type");
424        }
425        //
426        return assocType.getRelatingAssociation();
427    }
428
429
430
431    // === Type Editor Support ===
432
433    // ### TODO: explain
434
435    private void createAssocDef() {
436        TypeModelImpl parentType = fetchParentType();
437        logger.info("##### Adding association definition " + id + " to type \"" + parentType.getUri() + "\"");
438        //
439        parentType._addAssocDef(this);
440        //
441        addUpdateTypeDirective(parentType);
442    }
443
444    private void updateAssocDef() {
445        TypeModelImpl parentType = fetchParentType();
446        logger.info("##### Updating association definition " + id + " of type \"" + parentType.getUri() + "\"");
447        //
448        parentType._updateAssocDef(this);
449        //
450        addUpdateTypeDirective(parentType);
451    }
452
453    private void removeAssocDef() {
454        TypeModelImpl parentType = fetchParentType();
455        logger.info("##### Removing association definition " + id + " from type \"" + parentType.getUri() + "\"");
456        //
457        parentType._removeAssocDefFromMemoryAndRebuildSequence(this);
458        //
459        addUpdateTypeDirective(parentType);
460    }
461
462    // ---
463
464    private boolean isAssocDef(AssociationModel assoc) {
465        String typeUri = assoc.getTypeUri();
466        if (!typeUri.equals("dm4.core.aggregation_def") &&
467            !typeUri.equals("dm4.core.composition_def")) {
468            return false;
469        }
470        //
471        if (assoc.hasSameRoleTypeUris()) {
472            return false;
473        }
474        if (assoc.getRoleModel("dm4.core.parent_type") == null ||
475            assoc.getRoleModel("dm4.core.child_type") == null)  {
476            return false;
477        }
478        //
479        return true;
480    }
481
482    // ### TODO: adding the UPDATE directive should be the responsibility of a type. The DeepaMehtaType interface's
483    // ### addAssocDef(), updateAssocDef(), and removeAssocDef() methods should have a "directives" parameter ### FIXDOC
484    private void addUpdateTypeDirective(TypeModelImpl type) {
485        if (type.getTypeUri().equals("dm4.core.topic_type")) {
486            Directives.get().add(Directive.UPDATE_TOPIC_TYPE, type.instantiate());
487        } else if (type.getTypeUri().equals("dm4.core.assoc_type")) {
488            Directives.get().add(Directive.UPDATE_ASSOCIATION_TYPE, type.instantiate());
489        }
490        // Note: no else here as error check already performed in fetchParentType()
491    }
492
493    private TypeModelImpl fetchParentType() {
494        return pl.typeStorage.fetchParentType(this);
495    }
496}