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 RoleModelImpl 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 boolean hasSameRoleTypeUris() {
089        return roleModel1.getRoleTypeUri().equals(roleModel2.getRoleTypeUri());
090    }
091
092    @Override
093    public boolean matches(String roleTypeUri1, long playerId1, String roleTypeUri2, long playerId2) {
094        if (roleTypeUri1.equals(roleTypeUri2)) {
095            throw new IllegalArgumentException("matches() was called with 2 identical role type URIs (\"" +
096                roleTypeUri1 + "\")");
097        }
098        if (!hasSameRoleTypeUris()) {
099            RoleModel r1 = getRoleModel(roleTypeUri1);
100            RoleModel r2 = getRoleModel(roleTypeUri2);
101            if (r1 != null && r1.getPlayerId() == playerId1 &&
102                r2 != null && r2.getPlayerId() == playerId2) {
103                return true;
104            }
105        }
106        return false;
107    }
108
109    @Override
110    public long getOtherPlayerId(long id) {
111        long id1 = roleModel1.getPlayerId();
112        long id2 = roleModel2.getPlayerId();
113        if (id1 == id) {
114            return id2;
115        } else if (id2 == id) {
116            return id1;
117        } else {
118            throw new IllegalArgumentException("ID " + id + " doesn't refer to a player in " + this);
119        }
120    }
121
122
123
124    // === Implementation of the abstract methods ===
125
126    @Override
127    public RoleModel createRoleModel(String roleTypeUri) {
128        return mf.newAssociationRoleModel(id, roleTypeUri);
129    }
130
131
132
133    // === Serialization ===
134
135    @Override
136    public JSONObject toJSON() {
137        try {
138            return super.toJSON()
139                .put("role_1", roleModel1.toJSON())
140                .put("role_2", roleModel2.toJSON());
141        } catch (Exception e) {
142            throw new RuntimeException("Serialization failed (" + this + ")", e);
143        }
144    }
145
146
147
148    // === Java API ===
149
150    @Override
151    public AssociationModel clone() {
152        try {
153            AssociationModel model = (AssociationModel) super.clone();
154            model.setRoleModel1(roleModel1.clone());
155            model.setRoleModel2(roleModel2.clone());
156            return model;
157        } catch (Exception e) {
158            throw new RuntimeException("Cloning an AssociationModel failed", e);
159        }
160    }
161
162    @Override
163    public String toString() {
164        return "association (" + super.toString() + ", " + roleModel1 + ", " + roleModel2 + ")";
165    }
166
167
168
169    // ----------------------------------------------------------------------------------------- Package Private Methods
170
171    @Override
172    String className() {
173        return "association";
174    }
175
176    @Override
177    AssociationImpl instantiate() {
178        return new AssociationImpl(this, pl);
179    }
180
181    @Override
182    AssociationModelImpl createModelWithChildTopics(ChildTopicsModel childTopics) {
183        return mf.newAssociationModel(childTopics);
184    }
185
186    // ---
187
188    @Override
189    final AssociationTypeModelImpl getType() {
190        return pl.typeStorage.getAssociationType(typeUri);
191    }
192
193    @Override
194    final List<AssociationModelImpl> getAssociations() {
195        return pl.fetchAssociationAssociations(id);
196    }
197
198    // ---
199
200    @Override
201    final RelatedTopicModelImpl getRelatedTopic(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri,
202                                                                                           String othersTopicTypeUri) {
203        return pl.fetchAssociationRelatedTopic(id, assocTypeUri, myRoleTypeUri, othersRoleTypeUri,
204            othersTopicTypeUri);
205    }
206
207    @Override
208    final List<RelatedTopicModelImpl> getRelatedTopics(String assocTypeUri, String myRoleTypeUri,
209                                                                                           String othersRoleTypeUri,
210                                                                                           String othersTopicTypeUri) {
211        return pl.fetchAssociationRelatedTopics(id, assocTypeUri, myRoleTypeUri, othersRoleTypeUri,
212            othersTopicTypeUri);
213    }
214
215    @Override
216    final List<RelatedTopicModelImpl> getRelatedTopics(List assocTypeUris, String myRoleTypeUri,
217                                                                                           String othersRoleTypeUri,
218                                                                                           String othersTopicTypeUri) {
219        return pl.fetchAssociationRelatedTopics(id, assocTypeUris, myRoleTypeUri, othersRoleTypeUri,
220            othersTopicTypeUri);
221    }
222
223    // ---
224
225    @Override
226    final void storeUri() {
227        pl.storeAssociationUri(id, uri);
228    }
229
230    @Override
231    final void storeTypeUri() {
232        reassignInstantiation();
233        pl.storeAssociationTypeUri(id, typeUri);
234    }
235
236    @Override
237    final void storeSimpleValue() {
238        TypeModel type = getType();
239        pl.storeAssociationValue(id, value, type.getIndexModes(), type.getUri(), getIndexValue());
240    }
241
242    @Override
243    final void indexSimpleValue(IndexMode indexMode) {
244        pl.indexAssociationValue(id, indexMode, typeUri, getIndexValue());
245    }
246
247    @Override
248    final void storeProperty(String propUri, Object propValue, boolean addToIndex) {
249        pl.storeAssociationProperty(id, propUri, propValue, addToIndex);
250    }
251
252    @Override
253    final void removeProperty(String propUri) {
254        pl.removeAssociationProperty(id, propUri);
255    }
256
257    // ---
258
259    @Override
260    final void _delete() {
261        pl._deleteAssociation(id);
262    }
263
264    // ---
265
266    @Override
267    final void checkReadAccess() {
268        pl.checkAssociationReadAccess(id);
269    }
270
271    // ---
272
273    @Override
274    final DeepaMehtaEvent getPreUpdateEvent() {
275        return CoreEvent.PRE_UPDATE_ASSOCIATION;
276    }
277
278    @Override
279    final DeepaMehtaEvent getPostUpdateEvent() {
280        return CoreEvent.POST_UPDATE_ASSOCIATION;
281    }
282
283    @Override
284    final DeepaMehtaEvent getPreDeleteEvent() {
285        return CoreEvent.PRE_DELETE_ASSOCIATION;
286    }
287
288    @Override
289    final DeepaMehtaEvent getPostDeleteEvent() {
290        return CoreEvent.POST_DELETE_ASSOCIATION;
291    }
292
293    // ---
294
295    @Override
296    final Directive getUpdateDirective() {
297        return Directive.UPDATE_ASSOCIATION;
298    }
299
300    @Override
301    final Directive getDeleteDirective() {
302        return Directive.DELETE_ASSOCIATION;
303    }
304
305
306
307    // === Core Internal Hooks ===
308
309    @Override
310    void preCreate() {
311        duplicateCheck();
312    }
313
314    @Override
315    void postUpdate(DeepaMehtaObjectModel updateModel, DeepaMehtaObjectModel oldObject) {
316        // update association specific parts: the 2 roles
317        updateRoles((AssociationModel) updateModel);
318        //
319        duplicateCheck();
320        //
321        // Type Editor Support
322        if (isAssocDef(this)) {
323            if (isAssocDef((AssociationModel) oldObject)) {
324                updateAssocDef((AssociationModel) oldObject);
325            } else {
326                createAssocDef();
327            }
328        } else if (isAssocDef((AssociationModel) oldObject)) {
329            removeAssocDef();
330        }
331    }
332
333    @Override
334    void preDelete() {
335        // Type Editor Support
336        if (isAssocDef(this)) {
337            // Note: we listen to the PRE event here, not the POST event. At POST time the assocdef sequence might be
338            // interrupted, which would result in a corrupted sequence once rebuild. (Due to the interruption, while
339            // rebuilding not all segments would be catched for deletion and recreated redundantly -> ambiguity.)
340            // ### FIXDOC
341            removeAssocDef();
342        }
343    }
344
345
346
347    // ===
348
349    /**
350     * @teturn  this association's topic which plays the given role.
351     *          If there is no such topic, null is returned.
352     *          <p>
353     *          If there are 2 such topics an exception is thrown.
354     */
355    TopicModelImpl getTopic(String roleTypeUri) {
356        RoleModel role = getRoleModel(roleTypeUri);
357        return role instanceof TopicRoleModel ? ((TopicRoleModelImpl) role).getPlayer() : null;
358    }
359
360    TopicModelImpl getTopicByType(String topicTypeUri) {
361        TopicModelImpl topic1 = filterTopic(roleModel1, topicTypeUri);
362        TopicModelImpl topic2 = filterTopic(roleModel2, topicTypeUri);
363        if (topic1 != null && topic2 != null) {
364            throw new RuntimeException("Ambiguous getTopicByType() call: both topics are of type \"" + topicTypeUri +
365                "\" (" + this + ")");
366        }
367        return topic1 != null ? topic1 : topic2 != null ? topic2 : null;
368    }
369
370    // ---
371
372    void updateRoleTypeUri(RoleModelImpl role, String roleTypeUri) {
373        role.setRoleTypeUri(roleTypeUri);                           // update memory
374        pl.storeRoleTypeUri(id, role.playerId, role.roleTypeUri);   // update DB
375    }
376
377    // ------------------------------------------------------------------------------------------------- Private Methods
378
379
380
381    private void duplicateCheck() {
382        // ### FIXME: the duplicate check is supported only for topic players, and if they are identified by-ID.
383        // Note: we can't call roleModel.getPlayer() as this would build an entire object model, but its "value"
384        // is not yet available in case this association is part of the player's composite structure.
385        // Compare to DeepaMehtaUtils.associationAutoTyping()
386        if (!(roleModel1 instanceof TopicRoleModel) || ((TopicRoleModel) roleModel1).topicIdentifiedByUri() ||
387            !(roleModel2 instanceof TopicRoleModel) || ((TopicRoleModel) roleModel2).topicIdentifiedByUri()) {
388            return;
389        }
390        // Note: only readable assocs (access control) are considered
391        for (AssociationModelImpl assoc : pl._getAssociations(typeUri, roleModel1.playerId, roleModel2.playerId,
392               roleModel1.roleTypeUri, roleModel2.roleTypeUri)) {
393            if (assoc.id != id && assoc.value.equals(value)) {
394                throw new RuntimeException("Duplicate: such an association exists already (ID=" + assoc.id +
395                    ", typeUri=\"" + typeUri + "\", value=\"" + value + "\")");
396            }
397        }
398    }
399
400
401
402    // === Update (memory + DB) ===
403
404    /**
405     * @param   updateModel     The data to update.
406     *                          If role 1 is <code>null</code> it is not updated.
407     *                          If role 2 is <code>null</code> it is not updated.
408     */
409    private void updateRoles(AssociationModel updateModel) {
410        updateRole(updateModel.getRoleModel1(), 1);
411        updateRole(updateModel.getRoleModel2(), 2);
412    }
413
414    /**
415     * @param   nr      used only for logging
416     */
417    private void updateRole(RoleModel updateModel, int nr) {
418        if (updateModel != null) {     // abort if no update is requested
419            // Note: We must lookup the roles individually.
420            // The role order (getRole1(), getRole2()) is undeterministic and not fix.
421            RoleModelImpl role = getRole(updateModel);
422            String newRoleTypeUri = updateModel.getRoleTypeUri();   // new value
423            String roleTypeUri = role.getRoleTypeUri();             // current value
424            if (!roleTypeUri.equals(newRoleTypeUri)) {              // has changed?
425                logger.info("### Changing role type " + nr + " from \"" + roleTypeUri + "\" -> \"" + newRoleTypeUri +
426                    "\"");
427                updateRoleTypeUri(role, newRoleTypeUri);
428            }
429        }
430    }
431
432
433
434    // === Roles (memory access) ===
435
436    /**
437     * Returns this association's role which refers to the same object as the given role model.
438     * The role returned is found by comparing topic IDs, topic URIs, or association IDs.
439     * The role types are <i>not</i> compared.
440     * <p>
441     * If the object refered by the given role model is not a player in this association an exception is thrown.
442     */
443    private RoleModelImpl getRole(RoleModel roleModel) {
444        if (roleModel1.refsSameObject(roleModel)) {
445            return roleModel1;
446        } else if (roleModel2.refsSameObject(roleModel)) {
447            return roleModel2;
448        }
449        throw new RuntimeException("Role is not part of association (role=" + roleModel + ", association=" + this);
450    }
451
452    // ---
453
454    private TopicModelImpl filterTopic(RoleModelImpl role, String topicTypeUri) {
455        if (role instanceof TopicRoleModel) {
456            TopicModelImpl topic = ((TopicRoleModelImpl) role).getPlayer();
457            if (topic.getTypeUri().equals(topicTypeUri)) {
458                return topic;
459            }
460        }
461        return null;
462    }
463
464
465
466    // === Store (DB only) ===
467
468    private void reassignInstantiation() {
469        // remove current assignment
470        fetchInstantiation().delete();
471        // create new assignment
472        pl.createAssociationInstantiation(id, typeUri);
473    }
474
475    private AssociationModelImpl fetchInstantiation() {
476        RelatedTopicModelImpl assocType = getRelatedTopic("dm4.core.instantiation", "dm4.core.instance",
477            "dm4.core.type", "dm4.core.assoc_type");
478        //
479        if (assocType == null) {
480            throw new RuntimeException("Association " + id + " is not associated to an association type");
481        }
482        //
483        return assocType.getRelatingAssociation();
484    }
485
486
487
488    // === Type Editor Support ===
489
490    // ### TODO: explain
491
492    private void createAssocDef() {
493        TypeModelImpl parentType = fetchParentType();
494        logger.info("##### Adding association definition " + id + " to type \"" + parentType.getUri() + "\"");
495        //
496        parentType._addAssocDef(this);
497    }
498
499    private void updateAssocDef(AssociationModel oldAssoc) {
500        TypeModelImpl parentType = fetchParentType();
501        logger.info("##### Updating association definition " + id + " of type \"" + parentType.getUri() + "\"");
502        //
503        parentType._updateAssocDef(this, oldAssoc);
504    }
505
506    private void removeAssocDef() {
507        TypeModelImpl parentType = fetchParentType();
508        logger.info("##### Removing association definition " + id + " from type \"" + parentType.getUri() + "\"");
509        //
510        parentType._removeAssocDefFromMemoryAndRebuildSequence(this);
511    }
512
513    // ---
514
515    private boolean isAssocDef(AssociationModel assoc) {
516        String typeUri = assoc.getTypeUri();
517        if (!typeUri.equals("dm4.core.aggregation_def") &&
518            !typeUri.equals("dm4.core.composition_def")) {
519            return false;
520        }
521        //
522        if (assoc.hasSameRoleTypeUris()) {
523            return false;
524        }
525        if (assoc.getRoleModel("dm4.core.parent_type") == null ||
526            assoc.getRoleModel("dm4.core.child_type") == null)  {
527            return false;
528        }
529        //
530        return true;
531    }
532
533    private TypeModelImpl fetchParentType() {
534        return pl.typeStorage.fetchParentType(this);
535    }
536}