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}