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