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