001package systems.dmx.core.impl; 002 003import systems.dmx.core.DMXObject; 004import systems.dmx.core.model.AssociationDefinitionModel; 005import systems.dmx.core.model.AssociationModel; 006import systems.dmx.core.model.ChildTopicsModel; 007import systems.dmx.core.model.DMXObjectModel; 008import systems.dmx.core.model.IndexMode; 009import systems.dmx.core.model.RelatedTopicModel; 010import systems.dmx.core.model.RoleModel; 011import systems.dmx.core.model.SimpleValue; 012import systems.dmx.core.model.TopicModel; 013import systems.dmx.core.model.TopicDeletionModel; 014import systems.dmx.core.model.TopicReferenceModel; 015import systems.dmx.core.model.TypeModel; 016import systems.dmx.core.service.DMXEvent; 017import systems.dmx.core.service.Directive; 018import systems.dmx.core.service.Directives; 019import systems.dmx.core.util.JavaUtils; 020 021import org.codehaus.jettison.json.JSONObject; 022 023import java.util.ArrayList; 024import java.util.Iterator; 025import java.util.List; 026import java.util.logging.Logger; 027 028 029 030class DMXObjectModelImpl implements DMXObjectModel { 031 032 // ------------------------------------------------------------------------------------------------------- Constants 033 034 private static final String LABEL_CHILD_SEPARATOR = " "; 035 private static final String LABEL_TOPIC_SEPARATOR = ", "; 036 037 // ---------------------------------------------------------------------------------------------- Instance Variables 038 039 long id; // is -1 in models used for a create operation. ### FIXDOC 040 // is never -1 in models used for an update operation. 041 String uri; // is never null in models used for a create operation, may be empty. ### FIXDOC 042 // may be null in models used for an update operation. 043 String typeUri; // is never null in models used for a create operation. ### FIXDOC 044 // may be null in models used for an update operation. 045 SimpleValue value; // is never null in models used for a create operation, may be constructed 046 // on empty string. ### FIXDOC 047 // may be null in models used for an update operation. 048 ChildTopicsModelImpl childTopics; // is never null, may be empty. ### FIXDOC 049 050 // --- 051 052 PersistenceLayer pl; 053 EventManager em; 054 ModelFactoryImpl mf; 055 056 Logger logger = Logger.getLogger(getClass().getName()); 057 058 // ---------------------------------------------------------------------------------------------------- Constructors 059 060 DMXObjectModelImpl(long id, String uri, String typeUri, SimpleValue value, ChildTopicsModelImpl childTopics, 061 PersistenceLayer pl) { 062 this.id = id; 063 this.uri = uri; 064 this.typeUri = typeUri; 065 this.value = value; 066 this.childTopics = childTopics != null ? childTopics : pl.mf.newChildTopicsModel(); 067 // 068 this.pl = pl; 069 this.em = pl.em; 070 this.mf = pl.mf; 071 } 072 073 DMXObjectModelImpl(DMXObjectModelImpl object) { 074 this(object.getId(), object.getUri(), object.getTypeUri(), object.getSimpleValue(), 075 object.getChildTopicsModel(), object.pl); 076 } 077 078 // -------------------------------------------------------------------------------------------------- Public Methods 079 080 // --- ID --- 081 082 @Override 083 public long getId() { 084 return id; 085 } 086 087 @Override 088 public void setId(long id) { 089 this.id = id; 090 } 091 092 // --- URI --- 093 094 @Override 095 public String getUri() { 096 return uri; 097 } 098 099 @Override 100 public void setUri(String uri) { 101 this.uri = uri; 102 } 103 104 // --- Type URI --- 105 106 @Override 107 public String getTypeUri() { 108 return typeUri; 109 } 110 111 @Override 112 public void setTypeUri(String typeUri) { 113 this.typeUri = typeUri; 114 } 115 116 // --- Simple Value --- 117 118 @Override 119 public SimpleValue getSimpleValue() { 120 return value; 121 } 122 123 // --- 124 125 @Override 126 public void setSimpleValue(String value) { 127 setSimpleValue(new SimpleValue(value)); 128 } 129 130 @Override 131 public void setSimpleValue(int value) { 132 setSimpleValue(new SimpleValue(value)); 133 } 134 135 @Override 136 public void setSimpleValue(long value) { 137 setSimpleValue(new SimpleValue(value)); 138 } 139 140 @Override 141 public void setSimpleValue(boolean value) { 142 setSimpleValue(new SimpleValue(value)); 143 } 144 145 @Override 146 public void setSimpleValue(SimpleValue value) { 147 this.value = value; 148 } 149 150 // --- Child Topics --- 151 152 @Override 153 public ChildTopicsModelImpl getChildTopicsModel() { 154 return childTopics; 155 } 156 157 @Override 158 public void setChildTopicsModel(ChildTopicsModel childTopics) { 159 this.childTopics = (ChildTopicsModelImpl) childTopics; 160 } 161 162 // --- misc --- 163 164 @Override 165 public void set(DMXObjectModel object) { 166 setId(object.getId()); 167 setUri(object.getUri()); 168 setTypeUri(object.getTypeUri()); 169 setSimpleValue(object.getSimpleValue()); 170 setChildTopicsModel(object.getChildTopicsModel()); 171 } 172 173 // --- 174 175 @Override 176 public RoleModel createRoleModel(String roleTypeUri) { 177 throw new RuntimeException("Not implemented"); // only implemented in subclasses 178 // Note: technically this class is not abstract. It is instantiated by the ModelFactory. 179 } 180 181 182 183 // === Serialization === 184 185 @Override 186 public JSONObject toJSON() { 187 try { 188 // Note: for models used for topic/association enrichment (e.g. timestamps, permissions) 189 // default values must be set in case they are not fully initialized. 190 setDefaults(); 191 // 192 return new JSONObject() 193 .put("id", id) 194 .put("uri", uri) 195 .put("typeUri", typeUri) 196 .put("value", value.value()) 197 .put("childs", childTopics.toJSON()); 198 } catch (Exception e) { 199 throw new RuntimeException("Serialization failed", e); 200 } 201 } 202 203 204 205 // === Java API === 206 207 @Override 208 public DMXObjectModel clone() { 209 try { 210 DMXObjectModel object = (DMXObjectModel) super.clone(); 211 object.setChildTopicsModel(childTopics.clone()); 212 return object; 213 } catch (Exception e) { 214 throw new RuntimeException("Cloning a DMXObjectModel failed", e); 215 } 216 } 217 218 @Override 219 public boolean equals(Object o) { 220 return ((DMXObjectModel) o).getId() == id; 221 } 222 223 @Override 224 public int hashCode() { 225 return ((Long) id).hashCode(); 226 } 227 228 @Override 229 public String toString() { 230 try { 231 return getClass().getSimpleName() + " " + toJSON().toString(4); 232 } catch (Exception e) { 233 throw new RuntimeException("Prettyprinting failed", e); 234 } 235 } 236 237 // ----------------------------------------------------------------------------------------- Package Private Methods 238 239 240 241 // === Abstract Methods === 242 243 // ### TODO: make this a real abstract class. 244 // Change the model factory in a way it never instantiates DMXObjectModels. 245 246 String className() { 247 throw new UnsupportedOperationException(); 248 } 249 250 DMXObject instantiate() { 251 throw new UnsupportedOperationException(); 252 } 253 254 DMXObjectModelImpl createModelWithChildTopics(ChildTopicsModel childTopics) { 255 throw new UnsupportedOperationException(); 256 } 257 258 // --- 259 260 TypeModelImpl getType() { 261 throw new UnsupportedOperationException(); 262 } 263 264 List<AssociationModelImpl> getAssociations() { 265 throw new UnsupportedOperationException(); 266 } 267 268 // --- 269 270 RelatedTopicModelImpl getRelatedTopic(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri, 271 String othersTopicTypeUri) { 272 throw new UnsupportedOperationException(); 273 } 274 275 List<RelatedTopicModelImpl> getRelatedTopics(String assocTypeUri, String myRoleTypeUri, String othersRoleTypeUri, 276 String othersTopicTypeUri) { 277 throw new UnsupportedOperationException(); 278 } 279 280 List<RelatedTopicModelImpl> getRelatedTopics(List assocTypeUris, String myRoleTypeUri, String othersRoleTypeUri, 281 String othersTopicTypeUri) { 282 throw new UnsupportedOperationException(); 283 } 284 285 // --- 286 287 void storeUri() { 288 throw new UnsupportedOperationException(); 289 } 290 291 void storeTypeUri() { 292 throw new UnsupportedOperationException(); 293 } 294 295 /** 296 * Stores and indexes the simple value of this object model. 297 * Determines the index key and index modes. 298 */ 299 void storeSimpleValue() { 300 throw new UnsupportedOperationException(); 301 } 302 303 /** 304 * Indexes the simple value of the given object model according to the given index mode. 305 * <p> 306 * Called to index existing topics/associations once an index mode has been added to a type definition. 307 */ 308 void indexSimpleValue(IndexMode indexMode) { 309 throw new UnsupportedOperationException(); 310 } 311 312 void storeProperty(String propUri, Object propValue, boolean addToIndex) { 313 throw new UnsupportedOperationException(); 314 } 315 316 void removeProperty(String propUri) { 317 throw new UnsupportedOperationException(); 318 } 319 320 // --- 321 322 void _delete() { 323 throw new UnsupportedOperationException(); 324 } 325 326 // --- 327 328 /** 329 * @throws AccessControlException 330 */ 331 void checkReadAccess() { 332 throw new UnsupportedOperationException(); 333 } 334 335 /** 336 * @throws AccessControlException 337 */ 338 void checkWriteAccess() { 339 throw new UnsupportedOperationException(); 340 } 341 342 // --- 343 344 DMXEvent getPreUpdateEvent() { 345 throw new UnsupportedOperationException(); 346 } 347 348 DMXEvent getPostUpdateEvent() { 349 throw new UnsupportedOperationException(); 350 } 351 352 DMXEvent getPreDeleteEvent() { 353 throw new UnsupportedOperationException(); 354 } 355 356 DMXEvent getPostDeleteEvent() { 357 throw new UnsupportedOperationException(); 358 } 359 360 // --- 361 362 Directive getUpdateDirective() { 363 throw new UnsupportedOperationException(); 364 } 365 366 Directive getDeleteDirective() { 367 throw new UnsupportedOperationException(); 368 } 369 370 371 372 // === Core Internal Hooks === 373 374 void preCreate() { 375 } 376 377 void postCreate() { 378 } 379 380 // --- 381 382 void preUpdate(DMXObjectModel updateModel) { 383 } 384 385 void postUpdate(DMXObjectModel updateModel, DMXObjectModel oldObject) { 386 } 387 388 // --- 389 390 void preDelete() { 391 } 392 393 void postDelete() { 394 } 395 396 397 398 // === Update (memory + DB) === 399 400 final void updateChildTopics(ChildTopicsModel childTopics) { 401 update(createModelWithChildTopics(childTopics)); 402 } 403 404 final void updateChildTopics(ChildTopicsModel updateModel, AssociationDefinitionModel assocDef) { 405 // ### TODO: think about: no directives are added, no events are fired, no core internal hooks are invoked. 406 // Possibly this is not wanted for facet updates. This method is solely used for facet updates. 407 // Compare to update() method. 408 new ValueIntegrator(pl).integrate(createModelWithChildTopics(updateModel), this, assocDef); 409 } 410 411 // --- 412 413 /** 414 * @param updateModel The data to update. 415 * If the URI is <code>null</code> it is not updated. 416 * If the type URI is <code>null</code> it is not updated. 417 * If the simple value is <code>null</code> it is not updated. 418 */ 419 final void update(DMXObjectModelImpl updateModel) { 420 try { 421 logger.info("Updating " + objectInfo() + " (typeUri=\"" + typeUri + "\")"); 422 DMXObjectModel oldObject = clone(); 423 em.fireEvent(getPreUpdateEvent(), instantiate(), updateModel); 424 // 425 preUpdate(updateModel); 426 // 427 _updateUri(updateModel.getUri()); 428 _updateTypeUri(updateModel.getTypeUri()); 429 new ValueIntegrator(pl).integrate(updateModel, this, null); // TODO: handle return value 430 // TODO: rethink semantics of 1) events, 2) core internal hooks, and 3) directives in the face 431 // of DM5 update logic (= unification). Note that update() is not called recursively anmore. 432 // 433 postUpdate(updateModel, oldObject); 434 // 435 // Note: in case of a type topic the instantiate() call above creates a cloned model 436 // that doesn't reflect the update. Here we instantiate the now updated model. 437 DMXObject object = instantiate(); 438 Directives.get().add(getUpdateDirective(), object); 439 em.fireEvent(getPostUpdateEvent(), object, updateModel, oldObject); 440 } catch (Exception e) { 441 throw new RuntimeException("Updating " + objectInfo() + " failed (typeUri=\"" + typeUri + "\")", e); 442 } 443 } 444 445 // --- 446 447 final void updateUri(String uri) { 448 setUri(uri); // update memory 449 storeUri(); // update DB, "abstract" 450 } 451 452 final void updateTypeUri(String typeUri) { 453 setTypeUri(typeUri); // update memory 454 storeTypeUri(); // update DB, "abstract" 455 } 456 457 final void updateSimpleValue(SimpleValue value) { 458 if (value == null) { 459 throw new IllegalArgumentException("Tried to set a null SimpleValue (" + this + ")"); 460 } 461 setSimpleValue(value); // update memory 462 storeSimpleValue(); // update DB, "abstract" 463 } 464 465 466 467 // === Delete === 468 469 /** 470 * Deletes this object's direct associations, and the object itself. 471 */ 472 final void delete() { 473 try { 474 em.fireEvent(getPreDeleteEvent(), instantiate()); 475 // 476 preDelete(); 477 // 478 // delete direct associations 479 for (AssociationModelImpl assoc : getAssociations()) { 480 assoc.delete(); 481 } 482 // delete object itself 483 logger.info("Deleting " + objectInfo() + " (typeUri=\"" + typeUri + "\")"); 484 _delete(); 485 // 486 postDelete(); 487 // 488 Directives.get().add(getDeleteDirective(), this); 489 em.fireEvent(getPostDeleteEvent(), this); 490 } catch (IllegalStateException e) { 491 // Note: getAssociations() might throw IllegalStateException and is no problem. 492 // This can happen when this object is an association which is already deleted. 493 // 494 // Consider this particular situation: let A1 and A2 be associations of this object and let A2 point to A1. 495 // If A1 gets deleted first (the association set order is non-deterministic), A2 is implicitely deleted 496 // with it (because it is a direct association of A1 as well). Then when the loop comes to A2 497 // "IllegalStateException: Node[1327] has been deleted in this tx" is thrown because A2 has been deleted 498 // already. (The Node appearing in the exception is the middle node of A2.) If, on the other hand, A2 499 // gets deleted first no error would occur. 500 // 501 // This particular situation exists when e.g. a topicmap is deleted while one of its mapcontext 502 // associations is also a part of the topicmap itself. This originates e.g. when the user reveals 503 // a topicmap's mapcontext association and then deletes the topicmap. 504 // 505 // Compare to PersistenceLayer.deleteAssociation() 506 // TODO: introduce storage-vendor neutral DM exception. 507 // 508 if (e.getMessage().equals("Node[" + id + "] has been deleted in this tx")) { 509 logger.info("### Association " + id + " has already been deleted in this transaction. This can " + 510 "happen while deleting a topic with associations A1 and A2 while A2 points to A1 (" + this + ")"); 511 } else { 512 throw e; 513 } 514 } catch (Exception e) { 515 throw new RuntimeException("Deleting " + objectInfo() + " failed (typeUri=\"" + typeUri + "\")", e); 516 } 517 } 518 519 520 521 // === Load Child Topics === 522 523 // All 3 loadChildTopics() methods use this object itself as a cache. 524 // Child topics are fetched from DB only when not fetched already. 525 // Caching is done on a per assoc def basis. 526 527 /** 528 * Recursively loads this object's child topics which are not loaded already. 529 */ 530 final DMXObjectModel loadChildTopics(boolean deep) { 531 for (AssociationDefinitionModel assocDef : getType().getAssocDefs()) { 532 loadChildTopics(assocDef, deep); 533 } 534 return this; 535 } 536 537 /** 538 * Recursively loads this object's child topics for the given assoc def, provided they are not loaded already. 539 * If the child topics are loaded already nothing is performed. 540 * <p> 541 * Implemented on top of {@link #loadChildTopics(AssociationDefinitionModel, boolean)}. 542 * The assoc def is get from this object's type definition. 543 * As a consequence this method can <i>not</i> be used to load facet values. 544 * To load facet values use {@link #loadChildTopics(AssociationDefinitionModel, boolean)} and pass the facet type's 545 * assoc def. 546 */ 547 final DMXObjectModel loadChildTopics(String assocDefUri, boolean deep) { 548 try { 549 return loadChildTopics(getAssocDef(assocDefUri), deep); 550 } catch (Exception e) { 551 throw new RuntimeException("Loading \"" + assocDefUri + "\" child topics of " + objectInfo() + " failed", 552 e); 553 } 554 } 555 556 /** 557 * Recursively loads this object's child topics for the given assoc def, provided they are not loaded already. 558 * If the child topics are loaded already nothing is performed. 559 * <p> 560 * Can be used to load facet values. 561 * 562 * @param assocDef the child topics according to this association definition are loaded. 563 * <p> 564 * Note: the association definition must not necessarily originate from this object's 565 * type definition. It may originate from a facet type as well. 566 */ 567 final DMXObjectModel loadChildTopics(AssociationDefinitionModel assocDef, boolean deep) { 568 String assocDefUri = assocDef.getAssocDefUri(); 569 if (!childTopics.has(assocDefUri)) { 570 logger.fine("### Loading \"" + assocDefUri + "\" child topics of " + objectInfo()); 571 new ChildTopicsFetcher(pl).fetch(this, assocDef, deep); 572 } 573 return this; 574 } 575 576 577 578 // === 579 580 /** 581 * Calculates the simple value that is to be indexed for this object. 582 * 583 * HTML tags are stripped from HTML values. Non-HTML values are returned directly. ### FIXDOC 584 */ 585 SimpleValue getIndexValue() { 586 // TODO: rethink HTML indexing. 587 // DM5's value updater needs the exact index also for HTML values. 588 return value; 589 /* SimpleValue value = getSimpleValue(); 590 if (getType().getDataTypeUri().equals("dmx.core.html")) { 591 return new SimpleValue(JavaUtils.stripHTML(value.toString())); 592 } else { 593 return value; 594 } */ 595 } 596 597 boolean uriChange(String newUri, String compareUri) { 598 return newUri != null && !newUri.equals(compareUri); 599 } 600 601 boolean isSimple() { 602 // TODO: add isSimple() to type model 603 String dataTypeUri = getType().getDataTypeUri(); 604 return dataTypeUri.equals("dmx.core.text") 605 || dataTypeUri.equals("dmx.core.html") 606 || dataTypeUri.equals("dmx.core.number") 607 || dataTypeUri.equals("dmx.core.boolean"); 608 } 609 610 611 612 // ------------------------------------------------------------------------------------------------- Private Methods 613 614 // ### TODO: a principal copy exists in Neo4jStorage. 615 // Should this be package private? Should Neo4jStorage have access to the Core's impl package? 616 private void setDefaults() { 617 if (getUri() == null) { 618 setUri(""); 619 } 620 if (getSimpleValue() == null) { 621 setSimpleValue(""); 622 } 623 } 624 625 626 627 // === Update (memory + DB) === 628 629 private void _updateUri(String newUri) { 630 if (uriChange(newUri, uri)) { // abort if no update is requested 631 logger.info("### Changing URI of " + objectInfo() + ": \"" + uri + "\" -> \"" + newUri + "\""); 632 updateUri(newUri); 633 } 634 } 635 636 private void _updateTypeUri(String newTypeUri) { 637 if (newTypeUri != null && !newTypeUri.equals(typeUri)) { // abort if no update is requested 638 logger.info("### Changing type URI of " + objectInfo() + ": \"" + typeUri + "\" -> \"" + newTypeUri + 639 "\""); 640 updateTypeUri(newTypeUri); 641 } 642 } 643 644 final void _updateSimpleValue(SimpleValue newValue) { 645 if (newValue != null && !newValue.equals(value)) { // abort if no update is requested 646 logger.info("### Changing simple value of " + objectInfo() + ": \"" + value + "\" -> \"" + newValue + 647 "\""); 648 updateSimpleValue(newValue); 649 } 650 } 651 652 653 654 // === Helper === 655 656 // Note: doesn't work for facets 657 private AssociationDefinitionModel getAssocDef(String assocDefUri) { 658 return getType().getAssocDef(assocDefUri); 659 } 660 661 // ### TODO: drop it? 662 String objectInfo() { 663 return className() + " " + id; 664 } 665}