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}