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    AssociationImpl 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 preCreate() {
280        duplicateCheck();
281    }
282
283    @Override
284    void postUpdate(DeepaMehtaObjectModel newModel, DeepaMehtaObjectModel oldModel) {
285        // update association specific parts: the 2 roles
286        updateRoles((AssociationModel) newModel);
287        //
288        duplicateCheck();
289        //
290        // Type Editor Support
291        if (isAssocDef(this)) {
292            if (isAssocDef((AssociationModel) oldModel)) {
293                updateAssocDef();
294            } else {
295                createAssocDef();
296            }
297        } else if (isAssocDef((AssociationModel) oldModel)) {
298            removeAssocDef();
299        }
300    }
301
302    @Override
303    void preDelete() {
304        // Type Editor Support
305        if (isAssocDef(this)) {
306            // Note: we listen to the PRE event here, not the POST event. At POST time the assocdef sequence might be
307            // interrupted, which would result in a corrupted sequence once rebuild. (Due to the interruption, while
308            // rebuilding not all segments would be catched for deletion and recreated redundantly -> ambiguity.)
309            // ### FIXDOC
310            removeAssocDef();
311        }
312    }
313
314
315
316    // ===
317
318    /**
319     * @teturn  this association's topic which plays the given role.
320     *          If there is no such topic, null is returned.
321     *          <p>
322     *          If there are 2 such topics an exception is thrown.
323     */
324    TopicModelImpl getTopic(String roleTypeUri) {
325        RoleModel role = getRoleModel(roleTypeUri);
326        return role instanceof TopicRoleModel ? ((TopicRoleModelImpl) role).getPlayer() : null;
327    }
328
329    TopicModelImpl getTopicByType(String topicTypeUri) {
330        TopicModelImpl topic1 = filterTopic(roleModel1, topicTypeUri);
331        TopicModelImpl topic2 = filterTopic(roleModel2, topicTypeUri);
332        if (topic1 != null && topic2 != null) {
333            throw new RuntimeException("Ambiguous getTopicByType() call: both topics are of type \"" + topicTypeUri +
334                "\" (" + this + ")");
335        }
336        return topic1 != null ? topic1 : topic2 != null ? topic2 : null;
337    }
338
339    // ---
340
341    void updateRoleTypeUri(RoleModelImpl role, String roleTypeUri) {
342        role.setRoleTypeUri(roleTypeUri);                           // update memory
343        pl.storeRoleTypeUri(id, role.playerId, role.roleTypeUri);   // update DB
344    }
345
346    // ------------------------------------------------------------------------------------------------- Private Methods
347
348
349
350    private void duplicateCheck() {
351        // ### FIXME: the duplicate check is supported only for topic players, and if they are identified by-ID.
352        // Note: we can't call roleModel.getPlayer() as this would build an entire object model, but its "value"
353        // is not yet available in case this association is part of the player's composite structure.
354        // Compare to DeepaMehtaUtils.associationAutoTyping()
355        if (!(roleModel1 instanceof TopicRoleModel) || ((TopicRoleModel) roleModel1).topicIdentifiedByUri() ||
356            !(roleModel2 instanceof TopicRoleModel) || ((TopicRoleModel) roleModel2).topicIdentifiedByUri()) {
357            return;
358        }
359        // Note: only readable assocs (access control) are considered
360        for (AssociationModelImpl assoc : pl._getAssociations(typeUri, roleModel1.playerId, roleModel2.playerId,
361               roleModel1.roleTypeUri, roleModel2.roleTypeUri)) {
362            if (assoc.id != id) {
363                throw new RuntimeException("Duplicate: such an association exists already (ID=" + assoc.id +
364                    ", typeUri=\"" + typeUri + "\")");
365            }
366        }
367    }
368
369
370
371    // === Update (memory + DB) ===
372
373    /**
374     * @param   newModel    The data to update.
375     *                      If role 1 is <code>null</code> it is not updated.
376     *                      If role 2 is <code>null</code> it is not updated.
377     */
378    private void updateRoles(AssociationModel newModel) {
379        updateRole(newModel.getRoleModel1(), 1);
380        updateRole(newModel.getRoleModel2(), 2);
381    }
382
383    /**
384     * @param   nr      used only for logging
385     */
386    private void updateRole(RoleModel newModel, int nr) {
387        if (newModel != null) {     // abort if no update is requested
388            // Note: We must lookup the roles individually.
389            // The role order (getRole1(), getRole2()) is undeterministic and not fix.
390            RoleModelImpl role = getRole(newModel);
391            String newRoleTypeUri = newModel.getRoleTypeUri();  // new value
392            String roleTypeUri = role.getRoleTypeUri();         // current value
393            if (!roleTypeUri.equals(newRoleTypeUri)) {          // has changed?
394                logger.info("### Changing role type " + nr + " from \"" + roleTypeUri + "\" -> \"" + newRoleTypeUri +
395                    "\"");
396                updateRoleTypeUri(role, newRoleTypeUri);
397            }
398        }
399    }
400
401
402
403    // === Roles (memory access) ===
404
405    /**
406     * Returns this association's role which refers to the same object as the given role model.
407     * The role returned is found by comparing topic IDs, topic URIs, or association IDs.
408     * The role types are <i>not</i> compared.
409     * <p>
410     * If the object refered by the given role model is not a player in this association an exception is thrown.
411     */
412    private RoleModelImpl getRole(RoleModel roleModel) {
413        if (roleModel1.refsSameObject(roleModel)) {
414            return roleModel1;
415        } else if (roleModel2.refsSameObject(roleModel)) {
416            return roleModel2;
417        }
418        throw new RuntimeException("Role is not part of association (role=" + roleModel + ", association=" + this);
419    }
420
421    // ---
422
423    private TopicModelImpl filterTopic(RoleModelImpl role, String topicTypeUri) {
424        if (role instanceof TopicRoleModel) {
425            TopicModelImpl topic = ((TopicRoleModelImpl) role).getPlayer();
426            if (topic.getTypeUri().equals(topicTypeUri)) {
427                return topic;
428            }
429        }
430        return null;
431    }
432
433
434
435    // === Store (DB only) ===
436
437    private void reassignInstantiation() {
438        // remove current assignment
439        fetchInstantiation().delete();
440        // create new assignment
441        pl.createAssociationInstantiation(id, typeUri);
442    }
443
444    private AssociationModelImpl fetchInstantiation() {
445        RelatedTopicModelImpl assocType = getRelatedTopic("dm4.core.instantiation", "dm4.core.instance",
446            "dm4.core.type", "dm4.core.assoc_type");
447        //
448        if (assocType == null) {
449            throw new RuntimeException("Association " + id + " is not associated to an association type");
450        }
451        //
452        return assocType.getRelatingAssociation();
453    }
454
455
456
457    // === Type Editor Support ===
458
459    // ### TODO: explain
460
461    private void createAssocDef() {
462        TypeModelImpl parentType = fetchParentType();
463        logger.info("##### Adding association definition " + id + " to type \"" + parentType.getUri() + "\"");
464        //
465        parentType._addAssocDef(this);
466    }
467
468    private void updateAssocDef() {
469        TypeModelImpl parentType = fetchParentType();
470        logger.info("##### Updating association definition " + id + " of type \"" + parentType.getUri() + "\"");
471        //
472        parentType._updateAssocDef(this);
473    }
474
475    private void removeAssocDef() {
476        TypeModelImpl parentType = fetchParentType();
477        logger.info("##### Removing association definition " + id + " from type \"" + parentType.getUri() + "\"");
478        //
479        parentType._removeAssocDefFromMemoryAndRebuildSequence(this);
480    }
481
482    // ---
483
484    private boolean isAssocDef(AssociationModel assoc) {
485        String typeUri = assoc.getTypeUri();
486        if (!typeUri.equals("dm4.core.aggregation_def") &&
487            !typeUri.equals("dm4.core.composition_def")) {
488            return false;
489        }
490        //
491        if (assoc.hasSameRoleTypeUris()) {
492            return false;
493        }
494        if (assoc.getRoleModel("dm4.core.parent_type") == null ||
495            assoc.getRoleModel("dm4.core.child_type") == null)  {
496            return false;
497        }
498        //
499        return true;
500    }
501
502    private TypeModelImpl fetchParentType() {
503        return pl.typeStorage.fetchParentType(this);
504    }
505}