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}