001package systems.dmx.core.impl; 002 003import systems.dmx.core.Topic; 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.RelatedTopicModel; 009import systems.dmx.core.model.SimpleValue; 010import systems.dmx.core.model.TopicDeletionModel; 011import systems.dmx.core.model.TopicModel; 012import systems.dmx.core.model.TopicReferenceModel; 013import systems.dmx.core.model.TypeModel; 014import systems.dmx.core.util.DMXUtils; 015 016import static java.util.Arrays.asList; 017import java.util.ArrayList; 018import java.util.HashMap; 019import java.util.Iterator; 020import java.util.List; 021import java.util.Map; 022import java.util.logging.Logger; 023 024 025 026/** 027 * Integrates new values into the DB. 028 * 029 * Note: this class is not thread-safe. A ValueIntegrator instance must not be shared between threads. 030 */ 031class ValueIntegrator { 032 033 // ---------------------------------------------------------------------------------------------- Instance Variables 034 035 private DMXObjectModelImpl newValues; 036 private DMXObjectModelImpl targetObject; // may null 037 private AssociationDefinitionModel assocDef; // may null 038 private TypeModelImpl type; 039 private boolean isAssoc; 040 private boolean isType; 041 private boolean isFacetUpdate; 042 043 // For composites: assoc def URIs of empty child topics. 044 // Evaluated when deleting child-assignments, see updateAssignments(). 045 // Not having null entries in the unified child topics simplifies candidate determination. 046 // ### TODO: to be dropped? 047 private List<String> emptyValues = new ArrayList(); 048 049 private PersistenceLayer pl; 050 private ModelFactoryImpl mf; 051 052 private Logger logger = Logger.getLogger(getClass().getName()); 053 054 // ---------------------------------------------------------------------------------------------------- Constructors 055 056 ValueIntegrator(PersistenceLayer pl) { 057 this.pl = pl; 058 this.mf = pl.mf; 059 } 060 061 // ----------------------------------------------------------------------------------------- Package Private Methods 062 063 /** 064 * Integrates new values into the DB and returns the unified value. 065 * 066 * @return the unified value; never null; its "value" field is null if there was nothing to integrate. 067 */ 068 <M extends DMXObjectModelImpl> UnifiedValue<M> integrate(M newValues, M targetObject, 069 AssociationDefinitionModel assocDef) { 070 try { 071 this.newValues = newValues; 072 this.targetObject = targetObject; 073 this.assocDef = assocDef; 074 this.isAssoc = newValues instanceof AssociationModel; 075 this.isType = newValues instanceof TypeModel; 076 this.isFacetUpdate = assocDef != null; 077 // 078 // process refs 079 if (newValues instanceof TopicReferenceModel) { 080 return unifyRef(); 081 } 082 if (newValues instanceof TopicDeletionModel) { 083 return new UnifiedValue(null); 084 } 085 // argument check 086 if (newValues.getTypeUri() == null) { 087 throw new IllegalArgumentException("Tried to integrate values whose typeUri is not set, newValues=" + 088 newValues + ", targetObject=" + targetObject); 089 } 090 // Note: we must get type *after* processing refs. Refs might have no type set. 091 this.type = newValues.getType(); 092 // 093 // value integration 094 // Note: because a facet type is composite by definition a facet update is always a composite operation, 095 // even if the faceted object is a simple one. 096 DMXObjectModelImpl _value = !isFacetUpdate && newValues.isSimple() ? 097 integrateSimple() : 098 integrateComposite(); 099 // Note: UnifiedValue instantiation saves the new value's ID *before* it is overwritten 100 UnifiedValue value = new UnifiedValue(_value); 101 // 102 idTransfer(_value); 103 // 104 return value; 105 } catch (Exception e) { 106 throw new RuntimeException("Value integration failed, newValues=" + newValues + ", targetObject=" + 107 targetObject + ", assocDef=" + assocDef, e); 108 } 109 } 110 111 // ------------------------------------------------------------------------------------------------- Private Methods 112 113 private UnifiedValue unifyRef() { 114 TopicReferenceModelImpl ref = (TopicReferenceModelImpl) newValues; 115 if (!ref.isEmptyRef()) { 116 DMXObjectModelImpl object = ref.resolve(); 117 logger.fine("Referencing " + object); 118 return new UnifiedValue(object); 119 } else { 120 return new UnifiedValue(null); 121 } 122 } 123 124 // Note: this is a side effect, but we keep it for pragmatic reasons. 125 // 126 // In DM4 the create topic/assoc methods have the side effect of setting the generated ID into the update model. 127 // In DM5 the update model is not passed directly to the storage layer, but a new model object is created (see 128 // createSimpleTopic()). The ID transfer is here to emulate the DM4 behavior. 129 // 130 // Without this side effect e.g. managing view configs would be more hard. When creating a type through a migration 131 // (e.g. while bootstrap) the read type model is put in the type cache as is. Without the side effect the view 132 // config topic models would have no ID. Updating view config values later on would fail. 133 private void idTransfer(DMXObjectModelImpl value) { 134 if (value != null) { 135 if (value.id == -1) { 136 throw new RuntimeException("ID of unification result is not initialized: " + value); 137 } 138 newValues.id = value.id; 139 } 140 } 141 142 // Simple 143 144 /** 145 * Integrates a simple value into the DB and returns the unified simple value. 146 * 147 * Preconditions: 148 * - this.newValues is not null 149 * - this.newValues is simple 150 * 151 * @return the unified value, or null if there was nothing to integrate. 152 * The latter is the case if this.newValues is the empty string. 153 */ 154 private DMXObjectModelImpl integrateSimple() { 155 try { 156 if (isAssoc || isType) { 157 // Note 1: an assoc's simple value is not unified. In contrast to a topic an assoc can't be unified with 158 // another assoc. (Even if 2 assocs have the same type and value they are not the same as they still have 159 // different players.) An assoc's simple value is updated in-place. 160 // Note 2: a type's simple value is not unified. A type is updated in-place. 161 return storeAssocSimpleValue(); 162 } else if (newValues.getSimpleValue().toString().isEmpty()) { 163 return null; 164 } else { 165 return unifySimple(); 166 } 167 } catch (Exception e) { 168 throw new RuntimeException("Simple value integration failed, newValues=" + newValues, e); 169 } 170 } 171 172 /** 173 * Preconditions: 174 * - this.newValues is an assoc model. 175 */ 176 private DMXObjectModelImpl storeAssocSimpleValue() { 177 if (targetObject != null) { 178 // update 179 targetObject._updateSimpleValue(newValues.getSimpleValue()); 180 return targetObject; 181 } else { 182 // create 183 newValues.storeSimpleValue(); 184 return newValues; 185 } 186 } 187 188 /** 189 * Preconditions: 190 * - this.newValues is simple 191 * - this.newValues is not empty 192 * 193 * @return the unified value. Is never null. 194 */ 195 private TopicModelImpl unifySimple() { 196 SimpleValue newValue = newValues.getSimpleValue(); 197 // FIXME: HTML values must be tag-stripped before lookup, complementary to indexing 198 TopicImpl _topic = pl.getTopicByValue(type.getUri(), newValue); // TODO: let pl return models 199 TopicModelImpl topic = _topic != null ? _topic.getModel() : null; // TODO: drop 200 if (topic != null) { 201 logger.info("Reusing simple value " + topic.id + " \"" + newValue + "\" (typeUri=\"" + type.uri + "\")"); 202 } else { 203 topic = createSimpleTopic(); 204 logger.info("### Creating simple value " + topic.id + " \"" + newValue + "\" (typeUri=\"" + type.uri + 205 "\")"); 206 } 207 return topic; 208 } 209 210 // Composite 211 212 /** 213 * Integrates a composite value into the DB and returns the unified composite value. 214 * 215 * Preconditions: 216 * - this.newValues is composite 217 * 218 * @return the unified value, or null if there was nothing to integrate. 219 */ 220 private DMXObjectModelImpl integrateComposite() { 221 try { 222 Map<String, Object> childTopics = new HashMap(); // value: UnifiedValue or List<UnifiedValue> 223 ChildTopicsModel _childTopics = newValues.getChildTopicsModel(); 224 // Iterate through type, not through newValues. 225 // newValues might contain childs not contained in the type def, e.g. "dmx.time.modified". 226 for (String assocDefUri : assocDefUris()) { 227 Object newChildValue; // RelatedTopicModelImpl or List<RelatedTopicModelImpl> 228 if (isOne(assocDefUri)) { 229 newChildValue = _childTopics.getTopicOrNull(assocDefUri); 230 } else { 231 // TODO: if empty? 232 newChildValue = _childTopics.getTopicsOrNull(assocDefUri); 233 } 234 // skip if not contained in update request 235 if (newChildValue == null) { 236 continue; 237 } 238 // 239 Object childTopic = integrateChildValue(newChildValue, assocDefUri); 240 if (isOne(assocDefUri) && ((UnifiedValue) childTopic).value == null) { 241 emptyValues.add(assocDefUri); 242 } else { 243 childTopics.put(assocDefUri, childTopic); 244 } 245 } 246 DMXObjectModelImpl value = unifyComposite(childTopics); 247 // 248 // label calculation 249 if (!isFacetUpdate) { 250 if (value != null) { 251 new LabelCalculation(value).calculate(); 252 } else if (isAssoc) { 253 storeAssocSimpleValue(); 254 } 255 } 256 // 257 return value; 258 } catch (Exception e) { 259 throw new RuntimeException("Composite value integration failed, newValues=" + newValues, e); 260 } 261 } 262 263 private Iterable<String> assocDefUris() { 264 return isFacetUpdate ? asList(assocDef.getAssocDefUri()) : type; 265 } 266 267 /** 268 * Integrates a child value into the DB and returns the unified value. 269 * 270 * @param childValue RelatedTopicModelImpl or List<RelatedTopicModelImpl> 271 * 272 * @return UnifiedValue or List<UnifiedValue>; never null; 273 */ 274 private Object integrateChildValue(Object childValue, String assocDefUri) { 275 if (isOne(assocDefUri)) { 276 return new ValueIntegrator(pl).integrate((RelatedTopicModelImpl) childValue, null, null); 277 } else { 278 List<UnifiedValue> values = new ArrayList(); 279 for (RelatedTopicModelImpl value : (List<RelatedTopicModelImpl>) childValue) { 280 values.add(new ValueIntegrator(pl).integrate(value, null, null)); 281 } 282 return values; 283 } 284 } 285 286 /** 287 * Preconditions: 288 * - this.newValues is composite 289 * - assocDef's parent type is this.type 290 * - childTopic's type is assocDef's child type 291 * 292 * @param childTopics value: UnifiedValue or List<UnifiedValue> 293 */ 294 private DMXObjectModelImpl unifyComposite(Map<String, Object> childTopics) { 295 // Note: because a facet does not contribute to the value of a value object 296 // a facet update is always an in-place modification 297 if (!isFacetUpdate && isValueType()) { 298 return !childTopics.isEmpty() ? unifyChildTopics(childTopics, type) : null; 299 } else { 300 return updateAssignments(identifyParent(childTopics), childTopics); 301 } 302 } 303 304 /** 305 * @param childTopics value: UnifiedValue or List<UnifiedValue> 306 */ 307 private DMXObjectModelImpl identifyParent(Map<String, Object> childTopics) { 308 // TODO: 1st check identity attrs THEN target object?? => NO! 309 if (targetObject != null) { 310 return targetObject; 311 } else if (isAssoc) { 312 if (newValues.id == -1) { 313 throw new RuntimeException("newValues has no ID set"); 314 } 315 return mf.newAssociationModel(newValues.id, null, newValues.typeUri, null, null); 316 } else { 317 List<String> identityAssocDefUris = type.getIdentityAttrs(); 318 if (identityAssocDefUris.size() > 0) { 319 return unifyChildTopics(identityChildTopics(childTopics, identityAssocDefUris), identityAssocDefUris); 320 } else { 321 DMXObjectModelImpl parent = createSimpleTopic(); 322 logger.info("### Creating composite (w/o identity attrs) " + parent.id + " (typeUri=\"" + type.uri + 323 "\")"); 324 return parent; 325 } 326 } 327 } 328 329 /** 330 * @param childTopics value: UnifiedValue or List<UnifiedValue> 331 * 332 * @return value: UnifiedValue or List<UnifiedValue> 333 */ 334 private Map<String, Object> identityChildTopics(Map<String, Object> childTopics, 335 List<String> identityAssocDefUris) { 336 Map<String, Object> identityChildTopics = new HashMap(); 337 for (String assocDefUri : identityAssocDefUris) { 338 UnifiedValue childTopic; 339 if (isOne(assocDefUri)) { 340 childTopic = (UnifiedValue) childTopics.get(assocDefUri); 341 } else { 342 throw new RuntimeException("Cardinality \"many\" identity attributes not supported"); 343 } 344 // FIXME: only throw if NO identity child topic is given. 345 // If at least ONE is given it is sufficient. 346 if (childTopic.value == null) { 347 throw new RuntimeException("Identity child topic \"" + assocDefUri + "\" is missing in " + 348 childTopics.keySet()); 349 } 350 identityChildTopics.put(assocDefUri, childTopic); 351 } 352 // logger.info("### type=\"" + type.uri + "\" ### identityChildTopics=" + identityChildTopics); 353 return identityChildTopics; 354 } 355 356 /** 357 * Updates a parent's child assignments in-place. 358 * 359 * Preconditions: 360 * - this.newValues is composite 361 * - this.type is an identity type 362 * - parent's type is this.type 363 * - assocDef's parent type is this.type 364 * - newChildTopic's type is assocDef's child type 365 * 366 * @param unifiedChilds value: UnifiedValue or List<UnifiedValue> 367 */ 368 private DMXObjectModelImpl updateAssignments(DMXObjectModelImpl parent, Map<String, Object> unifiedChilds) { 369 // sanity check 370 if (!parent.getTypeUri().equals(type.getUri())) { 371 throw new RuntimeException("Type mismatch: newValues type=\"" + type.getUri() + "\", parent type=\"" + 372 parent.getTypeUri() + "\""); 373 } 374 // 375 for (String assocDefUri : assocDefUris()) { 376 // TODO: possible optimization: load only ONE child level here (deep=false). Later on, when updating the 377 // assignments, load the remaining levels only IF the assignment did not change. In contrast if the 378 // assignment changes, a new subtree is attached. The subtree is fully constructed already (through all 379 // levels) as it is build bottom-up (starting from the simple values at the leaves). 380 parent.loadChildTopics(assocDef(assocDefUri), true); // deep=true 381 Object unifiedChild = unifiedChilds.get(assocDefUri); 382 if (isOne(assocDefUri)) { 383 TopicModel _unifiedChild = (TopicModel) (unifiedChild != null ? ((UnifiedValue) unifiedChild).value : 384 null); 385 updateAssignmentsOne(parent, _unifiedChild, assocDefUri); 386 } else { 387 // Note: for partial create/update requests unifiedChild might be null 388 if (unifiedChild != null) { 389 updateAssignmentsMany(parent, (List<UnifiedValue>) unifiedChild, assocDefUri); 390 } 391 } 392 } 393 return parent; 394 } 395 396 /** 397 * @param unifiedChild may be null 398 */ 399 private void updateAssignmentsOne(DMXObjectModelImpl parent, TopicModel unifiedChild, String assocDefUri) { 400 ChildTopicsModelImpl childTopics = parent.getChildTopicsModel(); 401 RelatedTopicModelImpl oldValue = childTopics.getTopicOrNull(assocDefUri); // may be null 402 boolean newValueIsEmpty = isEmptyValue(assocDefUri); 403 // 404 // 1) delete assignment if exists AND value has changed or emptied 405 // 406 boolean deleted = false; 407 if (oldValue != null && (newValueIsEmpty || unifiedChild != null && !oldValue.equals(unifiedChild))) { 408 // update DB 409 oldValue.getRelatingAssociation().delete(); 410 // update memory 411 if (newValueIsEmpty) { 412 logger.info("### Deleting assignment (assocDefUri=\"" + assocDefUri + "\") from composite " + 413 parent.id + " (typeUri=\"" + type.uri + "\")"); 414 childTopics.remove(assocDefUri); 415 } 416 deleted = true; 417 } 418 // 2) create assignment if not exists OR value has changed 419 // a new value must be present 420 // 421 AssociationModelImpl assoc = null; 422 if (unifiedChild != null && (oldValue == null || !oldValue.equals(unifiedChild))) { 423 // update DB 424 assoc = createChildAssociation(parent, unifiedChild, assocDefUri, deleted); 425 // update memory 426 childTopics.put(assocDefUri, mf.newRelatedTopicModel(unifiedChild, assoc)); 427 } 428 // 3) update relating assoc 429 // 430 // Note: don't update an assoc's relating assoc 431 // TODO: condition needed? => yes, try remove child topic from rel assoc (e.g. "Phone Label") 432 if (!isAssoc) { 433 // take the old assoc if no new one is created, there is an old one, and it has not been deleted 434 if (assoc == null && oldValue != null && !deleted) { 435 assoc = oldValue.getRelatingAssociation(); 436 } 437 if (assoc != null) { 438 RelatedTopicModelImpl newChildValue = newValues.getChildTopicsModel().getTopicOrNull(assocDefUri); 439 updateRelatingAssociation(assoc, assocDefUri, newChildValue); 440 } 441 } 442 } 443 444 /** 445 * @param unifiedChilds never null; a UnifiedValue's "value" field may be null 446 */ 447 private void updateAssignmentsMany(DMXObjectModelImpl parent, List<UnifiedValue> unifiedChilds, 448 String assocDefUri) { 449 ChildTopicsModelImpl childTopics = parent.getChildTopicsModel(); 450 List<RelatedTopicModelImpl> oldValues = childTopics.getTopicsOrNull(assocDefUri); // may be null 451 // logger.info("### assocDefUri=\"" + assocDefUri + "\", oldValues=" + oldValues); 452 for (UnifiedValue _unifiedChild : unifiedChilds) { 453 TopicModel unifiedChild = (TopicModel) _unifiedChild.value; 454 long originalId = _unifiedChild.originalId; 455 long newId = unifiedChild != null ? unifiedChild.getId() : -1; 456 RelatedTopicModelImpl oldValue = null; 457 if (originalId != -1) { 458 oldValue = findTopic(oldValues, originalId); 459 } 460 // 461 // 1) delete assignment if exists AND value has changed or emptied 462 // 463 boolean deleted = false; 464 if (originalId != -1 && (newId == -1 || originalId != newId)) { 465 if (newId == -1) { 466 logger.info("### Deleting assignment (assocDefUri=\"" + assocDefUri + "\") from composite " + 467 parent.id + " (typeUri=\"" + type.uri + "\")"); 468 } 469 deleted = true; 470 // update DB 471 oldValue.getRelatingAssociation().delete(); 472 // update memory 473 removeTopic(oldValues, originalId); 474 } 475 // 2) create assignment if not exists OR value has changed 476 // a new value must be present 477 // 478 AssociationModelImpl assoc = null; 479 if (newId != -1 && (originalId == -1 || originalId != newId)) { 480 // update DB 481 assoc = createChildAssociation(parent, unifiedChild, assocDefUri, deleted); 482 // update memory 483 childTopics.add(assocDefUri, mf.newRelatedTopicModel(unifiedChild, assoc)); 484 } 485 // 3) update relating assoc 486 // 487 // Note: don't update an assoc's relating assoc 488 // TODO: condition needed? => yes, try remove child topic from rel assoc (e.g. "Phone Label") 489 if (!isAssoc) { 490 // take the old assoc if no new one is created, there is an old one, and it has not been deleted 491 if (assoc == null && oldValue != null && !deleted) { 492 assoc = oldValue.getRelatingAssociation(); 493 } 494 if (assoc != null) { 495 RelatedTopicModelImpl newValues = (RelatedTopicModelImpl) _unifiedChild._newValues; 496 updateRelatingAssociation(assoc, assocDefUri, newValues); 497 } 498 } 499 } 500 } 501 502 private void updateRelatingAssociation(AssociationModelImpl assoc, String assocDefUri, 503 RelatedTopicModelImpl newValues) { 504 try { 505 // Note: for partial create/update requests newValues might be null 506 if (newValues != null) { 507 AssociationModelImpl _newValues = newValues.getRelatingAssociation(); 508 // Note: the roles must be suppressed from being updated. Update would fail if a new child has 509 // been assigned (step 2) because the player is another one then. Here we are only interested 510 // in updating the assoc value. 511 _newValues.setRoleModel1(null); 512 _newValues.setRoleModel2(null); 513 // Note: if no relating assocs are contained in a create/update request the model factory 514 // creates assocs anyways, but these are completely uninitialized. ### TODO: Refactor 515 // TODO: is condition needed? => yes, try create new topic 516 if (_newValues.typeUri != null) { 517 assoc.update(_newValues); 518 // TODO: access control? Note: currently the child assocs of a workspace have no workspace 519 // assignments. With strict access control, updating a workspace topic would fail. 520 // pl.updateAssociation(assoc, _newValues); 521 } 522 } 523 } catch (Exception e) { 524 throw new RuntimeException("Updating relating assoc " + assoc.id + " (assocDefUri=\"" + assocDefUri + 525 "\") failed, assoc=" + assoc, e); 526 } 527 } 528 529 // --- 530 531 /** 532 * Preconditions: 533 * - this.newValues is composite 534 * - assocDef's parent type is this.type 535 * - childTopic's type is assocDef's child type 536 * - childTopics map is not empty 537 * 538 * @param childTopics value: UnifiedValue or List<UnifiedValue> 539 */ 540 private DMXObjectModelImpl unifyChildTopics(Map<String, Object> childTopics, Iterable<String> assocDefUris) { 541 List<RelatedTopicModelImpl> candidates = parentCandidates(childTopics); 542 // logger.info("### candidates (" + candidates.size() + "): " + DMXUtils.idList(candidates)); 543 for (String assocDefUri : assocDefUris) { 544 UnifiedValue value = (UnifiedValue) childTopics.get(assocDefUri); 545 eliminateParentCandidates(candidates, value != null ? value.value : null, assocDefUri); 546 if (candidates.isEmpty()) { 547 break; 548 } 549 } 550 switch (candidates.size()) { 551 case 0: 552 // logger.info("### no composite found, childTopics=" + childTopics); 553 return createCompositeTopic(childTopics); 554 case 1: 555 DMXObjectModelImpl comp = candidates.get(0); 556 logger.info("Reusing composite " + comp.getId() + " (typeUri=\"" + type.uri + "\")"); 557 return comp; 558 default: 559 throw new RuntimeException("ValueIntegrator ambiguity: there are " + candidates.size() + 560 " parents (typeUri=\"" + type.uri + "\", " + DMXUtils.idList(candidates) + 561 ") which have the same " + childTopics.values().size() + " child topics " + childTopics.values()); 562 } 563 } 564 565 /** 566 * Preconditions: 567 * - this.newValues is composite 568 * - assocDef's parent type is this.type 569 * - childTopic's type is assocDef's child type 570 * - childTopics map is not empty 571 * 572 * @param childTopics value: UnifiedValue or List<UnifiedValue> 573 */ 574 private List<RelatedTopicModelImpl> parentCandidates(Map<String, Object> childTopics) { 575 String assocDefUri = childTopics.keySet().iterator().next(); 576 // logger.info("### assocDefUri=\"" + assocDefUri + "\", childTopics=" + childTopics); 577 // sanity check 578 if (!type.getUri().equals(assocDef(assocDefUri).getParentTypeUri())) { 579 throw new RuntimeException("Type mismatch: type=\"" + type.getUri() + "\", assoc def's parent type=\"" + 580 assocDef(assocDefUri).getParentTypeUri() + "\""); 581 } 582 // 583 DMXObjectModel childTopic; 584 if (isOne(assocDefUri)) { 585 childTopic = ((UnifiedValue) childTopics.get(assocDefUri)).value; 586 } else { 587 throw new RuntimeException("Unification of cardinality \"many\" values not yet implemented"); 588 } 589 return pl.getTopicRelatedTopics(childTopic.getId(), assocDef(assocDefUri).getInstanceLevelAssocTypeUri(), 590 "dmx.core.child", "dmx.core.parent", type.getUri()); 591 } 592 593 /** 594 * @param childTopic may be null 595 */ 596 private void eliminateParentCandidates(List<RelatedTopicModelImpl> candidates, DMXObjectModel childTopic, 597 String assocDefUri) { 598 AssociationDefinitionModel assocDef = assocDef(assocDefUri); 599 Iterator<RelatedTopicModelImpl> i = candidates.iterator(); 600 while (i.hasNext()) { 601 long parentId = i.next().getId(); 602 String assocTypeUri = assocDef.getInstanceLevelAssocTypeUri(); 603 if (childTopic != null) { 604 // TODO: assoc parents? 605 if (pl.getAssociation(assocTypeUri, parentId, childTopic.getId(), "dmx.core.parent", "dmx.core.child") 606 == null) { 607 // logger.info("### eliminate (assoc doesn't exist)"); 608 i.remove(); 609 } 610 } else { 611 // TODO: assoc parents? 612 if (!pl.getTopicRelatedTopics(parentId, assocTypeUri, "dmx.core.parent", "dmx.core.child", 613 assocDef.getChildTypeUri()).isEmpty()) { 614 // logger.info("### eliminate (childs exist)"); 615 i.remove(); 616 } 617 } 618 } 619 } 620 621 // --- DB Access --- 622 623 /** 624 * Preconditions: 625 * - this.newValues is a topic model. 626 */ 627 private TopicModelImpl createSimpleTopic() { 628 // sanity check 629 if (isAssoc) { 630 throw new RuntimeException("Tried to create a topic from an assoc model"); 631 } 632 // 633 return pl._createTopic(mf.newTopicModel(newValues.uri, newValues.typeUri, newValues.value)).getModel(); 634 } 635 636 /** 637 * @param childTopics value: UnifiedValue or List<UnifiedValue> 638 */ 639 private TopicModelImpl createCompositeTopic(Map<String, Object> childTopics) { 640 // FIXME: construct the composite model first, then create topic as a whole. => NO! Endless recursion? 641 // Otherwise the POST_CREATE_TOPIC event is fired too early, and e.g. Address topics get no geo coordinates. 642 // logger.info("### childTopics=" + childTopics); 643 TopicModelImpl topic = createSimpleTopic(); 644 logger.info("### Creating composite " + topic.id + " (typeUri=\"" + type.uri + "\")"); 645 for (String assocDefUri : childTopics.keySet()) { 646 if (isOne(assocDefUri)) { 647 DMXObjectModel childTopic = ((UnifiedValue) childTopics.get(assocDefUri)).value; 648 createChildAssociation(topic, childTopic, assocDefUri); 649 } else { 650 for (UnifiedValue value : (List<UnifiedValue>) childTopics.get(assocDefUri)) { 651 createChildAssociation(topic, value.value, assocDefUri); 652 } 653 } 654 } 655 return topic; 656 } 657 658 private AssociationModelImpl createChildAssociation(DMXObjectModel parent, DMXObjectModel child, 659 String assocDefUri) { 660 return createChildAssociation(parent, child, assocDefUri, false); 661 } 662 663 private AssociationModelImpl createChildAssociation(DMXObjectModel parent, DMXObjectModel child, 664 String assocDefUri, boolean deleted) { 665 logger.info("### " + (deleted ? "Reassigning" : "Assigning") + " child " + child.getId() + " (assocDefUri=\"" + 666 assocDefUri + "\") to composite " + parent.getId() + " (typeUri=\"" + type.uri + "\")"); 667 return pl.createAssociation(assocDef(assocDefUri).getInstanceLevelAssocTypeUri(), 668 parent.createRoleModel("dmx.core.parent"), 669 child.createRoleModel("dmx.core.child") 670 ).getModel(); 671 } 672 673 // --- Memory Access --- 674 675 // TODO: make generic utility 676 private RelatedTopicModelImpl findTopic(List<RelatedTopicModelImpl> topics, long topicId) { 677 for (RelatedTopicModelImpl topic : topics) { 678 if (topic.id == topicId) { 679 return topic; 680 } 681 } 682 throw new RuntimeException("Topic " + topicId + " not found in " + topics); 683 } 684 685 private void removeTopic(List<RelatedTopicModelImpl> topics, long topicId) { 686 Iterator<RelatedTopicModelImpl> i = topics.iterator(); 687 while (i.hasNext()) { 688 RelatedTopicModelImpl topic = i.next(); 689 if (topic.id == topicId) { 690 i.remove(); 691 return; 692 } 693 } 694 throw new RuntimeException("Topic " + topicId + " not found in " + topics); 695 } 696 697 // --- 698 699 private AssociationDefinitionModel assocDef(String assocDefUri) { 700 if (isFacetUpdate) { 701 if (!assocDefUri.equals(assocDef.getAssocDefUri())) { 702 throw new RuntimeException("URI mismatch: assocDefUri=\"" + assocDefUri + "\", facet assocDefUri=\"" + 703 assocDef.getAssocDefUri() + "\""); 704 } 705 return assocDef; 706 } else { 707 return type.getAssocDef(assocDefUri); 708 } 709 } 710 711 private boolean isOne(String assocDefUri) { 712 return assocDef(assocDefUri).getChildCardinalityUri().equals("dmx.core.one"); 713 } 714 715 private boolean isValueType() { 716 return type.getDataTypeUri().equals("dmx.core.value"); 717 } 718 719 private boolean isEmptyValue(String assocDefUri) { 720 return emptyValues.contains(assocDefUri); 721 } 722 723 // -------------------------------------------------------------------------------------------------- Nested Classes 724 725 class UnifiedValue<M extends DMXObjectModelImpl> { 726 727 M value; // the resulting unified value 728 DMXObjectModelImpl _newValues; // the original new values 729 long originalId; // the original ID, saved here cause it is overwritten (see integrate()) 730 731 /** 732 * @param value may be null 733 */ 734 private UnifiedValue(M value) { 735 this.value = value; 736 this._newValues = newValues; 737 this.originalId = newValues.id; 738 } 739 } 740}