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