001 package de.deepamehta.core.impl; 002 003 import de.deepamehta.core.AssociationDefinition; 004 import de.deepamehta.core.CompositeValue; 005 import de.deepamehta.core.DeepaMehtaObject; 006 import de.deepamehta.core.RelatedTopic; 007 import de.deepamehta.core.Topic; 008 import de.deepamehta.core.model.CompositeValueModel; 009 import de.deepamehta.core.model.RelatedTopicModel; 010 import de.deepamehta.core.model.SimpleValue; 011 import de.deepamehta.core.model.TopicDeletionModel; 012 import de.deepamehta.core.model.TopicModel; 013 import de.deepamehta.core.model.TopicReferenceModel; 014 import de.deepamehta.core.service.ClientState; 015 import de.deepamehta.core.service.Directives; 016 017 import java.util.ArrayList; 018 import java.util.HashMap; 019 import java.util.List; 020 import java.util.Map; 021 import java.util.logging.Logger; 022 023 024 025 /** 026 * A composite value model that is attached to the DB. 027 */ 028 class AttachedCompositeValue implements CompositeValue { 029 030 // ---------------------------------------------------------------------------------------------- Instance Variables 031 032 private CompositeValueModel model; // underlying model 033 034 private AttachedDeepaMehtaObject parent; // attached object cache 035 036 /** 037 * Attached object cache. 038 * Key: child type URI (String), value: AttachedTopic or List<AttachedTopic> 039 */ 040 private Map<String, Object> childTopics = new HashMap(); // attached object cache 041 042 private EmbeddedService dms; 043 044 private Logger logger = Logger.getLogger(getClass().getName()); 045 046 // ---------------------------------------------------------------------------------------------------- Constructors 047 048 AttachedCompositeValue(CompositeValueModel model, AttachedDeepaMehtaObject parent, EmbeddedService dms) { 049 this.model = model; 050 this.parent = parent; 051 this.dms = dms; 052 initAttachedObjectCache(); 053 } 054 055 // -------------------------------------------------------------------------------------------------- Public Methods 056 057 058 059 // ************************************* 060 // *** CompositeValue Implementation *** 061 // ************************************* 062 063 064 065 // === Accessors === 066 067 @Override 068 public Topic getTopic(String childTypeUri) { 069 loadChildTopics(childTypeUri); 070 return _getTopic(childTypeUri); 071 } 072 073 @Override 074 public Topic getTopic(String childTypeUri, Topic defaultTopic) { 075 loadChildTopics(childTypeUri); 076 return _getTopic(childTypeUri, (AttachedTopic) defaultTopic); 077 } 078 079 // --- 080 081 @Override 082 public List<Topic> getTopics(String childTypeUri) { 083 loadChildTopics(childTypeUri); 084 return _getTopics(childTypeUri); 085 } 086 087 @Override 088 public List<Topic> getTopics(String childTypeUri, List<Topic> defaultValue) { 089 loadChildTopics(childTypeUri); 090 return _getTopics(childTypeUri, defaultValue); 091 } 092 093 // --- 094 095 @Override 096 public Object get(String childTypeUri) { 097 return childTopics.get(childTypeUri); 098 } 099 100 @Override 101 public boolean has(String childTypeUri) { 102 return childTopics.containsKey(childTypeUri); 103 } 104 105 @Override 106 public Iterable<String> childTypeUris() { 107 return childTopics.keySet(); 108 } 109 110 @Override 111 public int size() { 112 return childTopics.size(); 113 } 114 115 // --- 116 117 @Override 118 public CompositeValueModel getModel() { 119 return model; 120 } 121 122 123 124 // === Convenience Accessors === 125 126 @Override 127 public String getString(String childTypeUri) { 128 return getModel().getString(childTypeUri); 129 } 130 131 @Override 132 public String getString(String childTypeUri, String defaultValue) { 133 return getModel().getString(childTypeUri, defaultValue); 134 } 135 136 // --- 137 138 @Override 139 public int getInt(String childTypeUri) { 140 return getModel().getInt(childTypeUri); 141 } 142 143 @Override 144 public int getInt(String childTypeUri, int defaultValue) { 145 return getModel().getInt(childTypeUri, defaultValue); 146 } 147 148 // --- 149 150 @Override 151 public long getLong(String childTypeUri) { 152 return getModel().getLong(childTypeUri); 153 } 154 155 @Override 156 public long getLong(String childTypeUri, long defaultValue) { 157 return getModel().getLong(childTypeUri, defaultValue); 158 } 159 160 // --- 161 162 @Override 163 public double getDouble(String childTypeUri) { 164 return getModel().getDouble(childTypeUri); 165 } 166 167 @Override 168 public double getDouble(String childTypeUri, double defaultValue) { 169 return getModel().getDouble(childTypeUri, defaultValue); 170 } 171 172 // --- 173 174 @Override 175 public boolean getBoolean(String childTypeUri) { 176 return getModel().getBoolean(childTypeUri); 177 } 178 179 @Override 180 public boolean getBoolean(String childTypeUri, boolean defaultValue) { 181 return getModel().getBoolean(childTypeUri, defaultValue); 182 } 183 184 // --- 185 186 @Override 187 public Object getObject(String childTypeUri) { 188 return getModel().getObject(childTypeUri); 189 } 190 191 @Override 192 public Object getObject(String childTypeUri, Object defaultValue) { 193 return getModel().getObject(childTypeUri, defaultValue); 194 } 195 196 // --- 197 198 @Override 199 public CompositeValue getCompositeValue(String childTypeUri) { 200 return getTopic(childTypeUri).getCompositeValue(); 201 } 202 203 @Override 204 public CompositeValue getCompositeValue(String childTypeUri, CompositeValue defaultValue) { 205 Topic topic = getTopic(childTypeUri, null); 206 return topic != null ? topic.getCompositeValue() : defaultValue; 207 } 208 209 // Note: there are no convenience accessors for a multiple-valued child. 210 211 212 213 // === Manipulators === 214 215 @Override 216 public CompositeValue set(String childTypeUri, TopicModel value, ClientState clientState, Directives directives) { 217 return _update(childTypeUri, value, clientState, directives); 218 } 219 220 @Override 221 public CompositeValue set(String childTypeUri, Object value, ClientState clientState, Directives directives) { 222 return _update(childTypeUri, new TopicModel(childTypeUri, new SimpleValue(value)), clientState, directives); 223 } 224 225 @Override 226 public CompositeValue set(String childTypeUri, CompositeValueModel value, ClientState clientState, 227 Directives directives) { 228 return _update(childTypeUri, new TopicModel(childTypeUri, value), clientState, directives); 229 } 230 231 // --- 232 233 @Override 234 public CompositeValue setRef(String childTypeUri, long refTopicId, ClientState clientState, Directives directives) { 235 return _update(childTypeUri, new TopicReferenceModel(refTopicId), clientState, directives); 236 } 237 238 @Override 239 public CompositeValue setRef(String childTypeUri, String refTopicUri, ClientState clientState, 240 Directives directives) { 241 return _update(childTypeUri, new TopicReferenceModel(refTopicUri), clientState, directives); 242 } 243 244 // --- 245 246 @Override 247 public CompositeValue remove(String childTypeUri, long topicId, ClientState clientState, Directives directives) { 248 return _update(childTypeUri, new TopicDeletionModel(topicId), clientState, directives); 249 } 250 251 252 253 // ----------------------------------------------------------------------------------------- Package Private Methods 254 255 void update(CompositeValueModel newComp, ClientState clientState, Directives directives) { 256 try { 257 for (AssociationDefinition assocDef : parent.getType().getAssocDefs()) { 258 String childTypeUri = assocDef.getChildTypeUri(); 259 String cardinalityUri = assocDef.getChildCardinalityUri(); 260 TopicModel newChildTopic = null; // only used for "one" 261 List<TopicModel> newChildTopics = null; // only used for "many" 262 if (cardinalityUri.equals("dm4.core.one")) { 263 newChildTopic = newComp.getTopic(childTypeUri, null); // defaultValue=null 264 // skip if not contained in update request 265 if (newChildTopic == null) { 266 continue; 267 } 268 } else if (cardinalityUri.equals("dm4.core.many")) { 269 newChildTopics = newComp.getTopics(childTypeUri, null); // defaultValue=null 270 // skip if not contained in update request 271 if (newChildTopics == null) { 272 continue; 273 } 274 } else { 275 throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI"); 276 } 277 // 278 updateChildTopics(newChildTopic, newChildTopics, assocDef, clientState, directives); 279 } 280 // 281 dms.valueStorage.refreshLabel(parent.getModel()); 282 // 283 } catch (Exception e) { 284 throw new RuntimeException("Updating composite value of " + parent.className() + " " + parent.getId() + 285 " failed (newComp=" + newComp + ")", e); 286 } 287 } 288 289 void updateChildTopics(TopicModel newChildTopic, List<TopicModel> newChildTopics, AssociationDefinition assocDef, 290 ClientState clientState, Directives directives) { 291 // Note: updating the child topics requires them to be loaded 292 loadChildTopics(assocDef); 293 // 294 String assocTypeUri = assocDef.getTypeUri(); 295 boolean one = newChildTopic != null; 296 if (assocTypeUri.equals("dm4.core.composition_def")) { 297 if (one) { 298 updateCompositionOne(newChildTopic, assocDef, clientState, directives); 299 } else { 300 updateCompositionMany(newChildTopics, assocDef, clientState, directives); 301 } 302 } else if (assocTypeUri.equals("dm4.core.aggregation_def")) { 303 if (one) { 304 updateAggregationOne(newChildTopic, assocDef, clientState, directives); 305 } else { 306 updateAggregationMany(newChildTopics, assocDef, clientState, directives); 307 } 308 } else { 309 throw new RuntimeException("Association type \"" + assocTypeUri + "\" not supported"); 310 } 311 } 312 313 // --- 314 315 void loadChildTopics(String childTypeUri) { 316 loadChildTopics(getAssocDef(childTypeUri)); 317 } 318 319 // ------------------------------------------------------------------------------------------------- Private Methods 320 321 // --- Access this attached object cache --- 322 323 private Topic _getTopic(String childTypeUri) { 324 Topic topic = (Topic) childTopics.get(childTypeUri); 325 // error check 326 if (topic == null) { 327 throw new RuntimeException("Child topic of type \"" + childTypeUri + "\" not found in " + childTopics); 328 } 329 // 330 return topic; 331 } 332 333 private AttachedTopic _getTopic(String childTypeUri, AttachedTopic defaultTopic) { 334 AttachedTopic topic = (AttachedTopic) childTopics.get(childTypeUri); 335 return topic != null ? topic : defaultTopic; 336 } 337 338 // --- 339 340 private List<Topic> _getTopics(String childTypeUri) { 341 try { 342 List<Topic> topics = (List<Topic>) childTopics.get(childTypeUri); 343 // error check 344 if (topics == null) { 345 throw new RuntimeException("Child topics of type \"" + childTypeUri + "\" not found in " + childTopics); 346 } 347 // 348 return topics; 349 } catch (ClassCastException e) { 350 getModel().throwInvalidAccess(childTypeUri, e); 351 return null; // never reached 352 } 353 } 354 355 private List<Topic> _getTopics(String childTypeUri, List<Topic> defaultValue) { 356 try { 357 List<Topic> topics = (List<Topic>) childTopics.get(childTypeUri); 358 return topics != null ? topics : defaultValue; 359 } catch (ClassCastException e) { 360 getModel().throwInvalidAccess(childTypeUri, e); 361 return null; // never reached 362 } 363 } 364 365 // --- 366 367 private CompositeValue _update(String childTypeUri, TopicModel newChildTopic, ClientState clientState, 368 Directives directives) { 369 // regard parent object as updated 370 parent.addUpdateDirective(directives); 371 // 372 updateChildTopics(newChildTopic, null, getAssocDef(childTypeUri), clientState, directives); 373 dms.valueStorage.refreshLabel(parent.getModel()); // newChildTopics=null 374 return this; 375 } 376 377 // --- Composition --- 378 379 private void updateCompositionOne(TopicModel newChildTopic, AssociationDefinition assocDef, 380 ClientState clientState, Directives directives) { 381 AttachedTopic childTopic = _getTopic(assocDef.getChildTypeUri(), null); 382 // Note: for cardinality one the simple request format is sufficient. The child's topic ID is not required. 383 // ### TODO: possibly sanity check: if child's topic ID *is* provided it must match with the fetched topic. 384 if (childTopic != null) { 385 // == update child == 386 // update DB 387 childTopic._update(newChildTopic, clientState, directives); 388 // Note: memory is already up-to-date. The child topic is updated in-place of parent. 389 } else { 390 // == create child == 391 createChildTopicOne(newChildTopic, assocDef, clientState, directives); 392 } 393 } 394 395 private void updateCompositionMany(List<TopicModel> newChildTopics, AssociationDefinition assocDef, 396 ClientState clientState, Directives directives) { 397 for (TopicModel newChildTopic : newChildTopics) { 398 long childTopicId = newChildTopic.getId(); 399 if (newChildTopic instanceof TopicDeletionModel) { 400 Topic childTopic = findChildTopic(childTopicId, assocDef); 401 if (childTopic == null) { 402 // Note: "delete child" is an idempotent operation. A delete request for an child which has been 403 // deleted already (resp. is non-existing) is not an error. Instead, nothing is performed. 404 continue; 405 } 406 // == delete child == 407 // update DB 408 childTopic.delete(directives); 409 // update memory 410 removeFromCompositeValue(childTopic, assocDef); 411 } else if (childTopicId != -1) { 412 // == update child == 413 updateChildTopicMany(newChildTopic, assocDef, clientState, directives); 414 } else { 415 // == create child == 416 createChildTopicMany(newChildTopic, assocDef, clientState, directives); 417 } 418 } 419 } 420 421 // --- Aggregation --- 422 423 private void updateAggregationOne(TopicModel newChildTopic, AssociationDefinition assocDef, 424 ClientState clientState, Directives directives) { 425 RelatedTopic childTopic = (RelatedTopic) _getTopic(assocDef.getChildTypeUri(), null); 426 if (newChildTopic instanceof TopicReferenceModel) { 427 if (childTopic != null) { 428 if (((TopicReferenceModel) newChildTopic).isReferingTo(childTopic)) { 429 return; 430 } 431 // == update assignment == 432 // update DB 433 childTopic.getRelatingAssociation().delete(directives); 434 } else { 435 // == create assignment == 436 } 437 // update DB 438 Topic topic = dms.valueStorage.associateReferencedChildTopic(parent.getModel(), 439 (TopicReferenceModel) newChildTopic, assocDef, clientState); 440 // update memory 441 putInCompositeValue(topic, assocDef); 442 } else if (newChildTopic.getId() != -1) { 443 // == update child == 444 updateChildTopicOne(newChildTopic, assocDef, clientState, directives); 445 } else { 446 // == create child == 447 // update DB 448 if (childTopic != null) { 449 childTopic.getRelatingAssociation().delete(directives); 450 } 451 createChildTopicOne(newChildTopic, assocDef, clientState, directives); 452 } 453 } 454 455 private void updateAggregationMany(List<TopicModel> newChildTopics, AssociationDefinition assocDef, 456 ClientState clientState, Directives directives) { 457 for (TopicModel newChildTopic : newChildTopics) { 458 long childTopicId = newChildTopic.getId(); 459 if (newChildTopic instanceof TopicDeletionModel) { 460 RelatedTopic childTopic = findChildTopic(childTopicId, assocDef); 461 if (childTopic == null) { 462 // Note: "delete assignment" is an idempotent operation. A delete request for an assignment which 463 // has been deleted already (resp. is non-existing) is not an error. Instead, nothing is performed. 464 continue; 465 } 466 // == delete assignment == 467 // update DB 468 childTopic.getRelatingAssociation().delete(directives); 469 // update memory 470 removeFromCompositeValue(childTopic, assocDef); 471 } else if (newChildTopic instanceof TopicReferenceModel) { 472 if (isReferingToAny((TopicReferenceModel) newChildTopic, assocDef)) { 473 // Note: "create assignment" is an idempotent operation. A create request for an assignment which 474 // exists already is not an error. Instead, nothing is performed. 475 continue; 476 } 477 // == create assignment == 478 // update DB 479 Topic topic = dms.valueStorage.associateReferencedChildTopic(parent.getModel(), 480 (TopicReferenceModel) newChildTopic, assocDef, clientState); 481 // update memory 482 addToCompositeValue(topic, assocDef); 483 } else if (childTopicId != -1) { 484 // == update child == 485 updateChildTopicMany(newChildTopic, assocDef, clientState, directives); 486 } else { 487 // == create child == 488 createChildTopicMany(newChildTopic, assocDef, clientState, directives); 489 } 490 } 491 } 492 493 // --- 494 495 private void updateChildTopicOne(TopicModel newChildTopic, AssociationDefinition assocDef, ClientState clientState, 496 Directives directives) { 497 AttachedTopic childTopic = _getTopic(assocDef.getChildTypeUri(), null); 498 if (childTopic != null && childTopic.getId() == newChildTopic.getId()) { 499 // update DB 500 childTopic._update(newChildTopic, clientState, directives); 501 // Note: memory is already up-to-date. The child topic is updated in-place of parent. 502 } else { 503 throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " + 504 parent.className() + " " + parent.getId() + " according to " + assocDef); 505 } 506 } 507 508 private void updateChildTopicMany(TopicModel newChildTopic, AssociationDefinition assocDef, ClientState clientState, 509 Directives directives) { 510 AttachedTopic childTopic = findChildTopic(newChildTopic.getId(), assocDef); 511 if (childTopic != null) { 512 // update DB 513 childTopic._update(newChildTopic, clientState, directives); 514 // Note: memory is already up-to-date. The child topic is updated in-place of parent. 515 } else { 516 throw new RuntimeException("Topic " + newChildTopic.getId() + " is not a child of " + 517 parent.className() + " " + parent.getId() + " according to " + assocDef); 518 } 519 } 520 521 // --- 522 523 private void createChildTopicOne(TopicModel newChildTopic, AssociationDefinition assocDef, ClientState clientState, 524 Directives directives) { 525 // update DB 526 Topic childTopic = dms.createTopic(newChildTopic, clientState); 527 dms.valueStorage.associateChildTopic(parent.getModel(), childTopic.getId(), assocDef, clientState); 528 // update memory 529 putInCompositeValue(childTopic, assocDef); 530 } 531 532 private void createChildTopicMany(TopicModel newChildTopic, AssociationDefinition assocDef, ClientState clientState, 533 Directives directives) { 534 // update DB 535 Topic childTopic = dms.createTopic(newChildTopic, clientState); 536 dms.valueStorage.associateChildTopic(parent.getModel(), childTopic.getId(), assocDef, clientState); 537 // update memory 538 addToCompositeValue(childTopic, assocDef); 539 } 540 541 542 543 // === 544 545 void loadChildTopics() { 546 dms.valueStorage.fetchCompositeValue(parent.getModel()); 547 initAttachedObjectCache(); 548 } 549 550 /** 551 * Recursively loads child topics (model) and updates this attached object cache accordingly. 552 * If the child topics are loaded already nothing is performed. 553 * 554 * @param assocDef the child topics according to this association definition are loaded. 555 * <p> 556 * Note: the association definition must not necessarily originate from this object's 557 * type definition. It may originate from a facet definition as well. 558 */ 559 private void loadChildTopics(AssociationDefinition assocDef) { 560 String childTypeUri = assocDef.getChildTypeUri(); 561 if (!has(childTypeUri)) { 562 logger.fine("### Lazy-loading \"" + childTypeUri + "\" child topic(s) of " + parent.className() + " " + 563 parent.getId()); 564 dms.valueStorage.fetchChildTopics(parent.getModel(), assocDef); 565 initAttachedObjectCache(childTypeUri); 566 } 567 } 568 569 570 571 // === Attached Object Cache Initialization === 572 573 /** 574 * Initializes this attached object cache. For all childs contained in the underlying model attached topics are 575 * created and put in the attached object cache (recursively). 576 */ 577 private void initAttachedObjectCache() { 578 for (String childTypeUri : model) { 579 initAttachedObjectCache(childTypeUri); 580 } 581 } 582 583 /** 584 * Initializes this attached object cache selectively (and recursively). 585 */ 586 private void initAttachedObjectCache(String childTypeUri) { 587 Object value = model.get(childTypeUri); 588 // Note: topics just created have no child topics yet 589 if (value == null) { 590 return; 591 } 592 // Note: no direct recursion takes place here. Recursion is indirect: attached topics are created here, this 593 // implies creating further attached composite values, which in turn calls this method again but for the next 594 // child-level. Finally attached topics are created for all child-levels. 595 if (value instanceof TopicModel) { 596 TopicModel childTopic = (TopicModel) value; 597 childTopics.put(childTypeUri, createAttachedObject(childTopic)); 598 } else if (value instanceof List) { 599 List<Topic> topics = new ArrayList(); 600 childTopics.put(childTypeUri, topics); 601 for (TopicModel childTopic : (List<TopicModel>) value) { 602 topics.add(createAttachedObject(childTopic)); 603 } 604 } else { 605 throw new RuntimeException("Unexpected value in a CompositeValueModel: " + value); 606 } 607 } 608 609 /** 610 * Creates an attached topic to be put in this attached object cache. 611 */ 612 private Topic createAttachedObject(TopicModel model) { 613 if (model instanceof RelatedTopicModel) { 614 // Note: composite value models obtained through *fetching* contain *related topic models*. 615 // We exploit the related topics when updating assignments (in conjunction with aggregations). 616 // See updateAggregationOne() and updateAggregationMany(). 617 return new AttachedRelatedTopic((RelatedTopicModel) model, dms); 618 } else { 619 // Note: composite value models for *new topics* to be created contain sole *topic models*. 620 return new AttachedTopic(model, dms); 621 } 622 } 623 624 625 626 // === Update === 627 628 // --- Update this attached object cache + underlying model --- 629 630 /** 631 * For single-valued childs 632 */ 633 private void putInCompositeValue(Topic childTopic, AssociationDefinition assocDef) { 634 String childTypeUri = assocDef.getChildTypeUri(); 635 put(childTypeUri, childTopic); // attached object cache 636 getModel().put(childTypeUri, childTopic.getModel()); // underlying model 637 } 638 639 /** 640 * For multiple-valued childs 641 */ 642 private void addToCompositeValue(Topic childTopic, AssociationDefinition assocDef) { 643 String childTypeUri = assocDef.getChildTypeUri(); 644 add(childTypeUri, childTopic); // attached object cache 645 getModel().add(childTypeUri, childTopic.getModel()); // underlying model 646 } 647 648 /** 649 * For multiple-valued childs 650 */ 651 private void removeFromCompositeValue(Topic childTopic, AssociationDefinition assocDef) { 652 String childTypeUri = assocDef.getChildTypeUri(); 653 remove(childTypeUri, childTopic); // attached object cache 654 getModel().remove(childTypeUri, childTopic.getModel()); // underlying model 655 } 656 657 // --- Update this attached object cache --- 658 659 /** 660 * Puts a single-valued child. An existing value is overwritten. 661 */ 662 private void put(String childTypeUri, Topic topic) { 663 childTopics.put(childTypeUri, topic); 664 } 665 666 /** 667 * Adds a value to a multiple-valued child. 668 */ 669 private void add(String childTypeUri, Topic topic) { 670 List<Topic> topics = _getTopics(childTypeUri, null); // defaultValue=null 671 // Note: topics just created have no child topics yet 672 if (topics == null) { 673 topics = new ArrayList(); 674 childTopics.put(childTypeUri, topics); 675 } 676 topics.add(topic); 677 } 678 679 /** 680 * Removes a value from a multiple-valued child. 681 */ 682 private void remove(String childTypeUri, Topic topic) { 683 List<Topic> topics = _getTopics(childTypeUri, null); // defaultValue=null 684 if (topics != null) { 685 topics.remove(topic); 686 } 687 } 688 689 690 691 // === Helper === 692 693 private AttachedRelatedTopic findChildTopic(long childTopicId, AssociationDefinition assocDef) { 694 List<Topic> childTopics = _getTopics(assocDef.getChildTypeUri(), new ArrayList()); 695 for (Topic childTopic : childTopics) { 696 if (childTopic.getId() == childTopicId) { 697 return (AttachedRelatedTopic) childTopic; 698 } 699 } 700 return null; 701 } 702 703 /** 704 * Checks weather the given topic reference refers to any of the child topics. 705 * 706 * @param assocDef the child topics according to this association definition are considered. 707 */ 708 private boolean isReferingToAny(TopicReferenceModel topicRef, AssociationDefinition assocDef) { 709 return topicRef.isReferingToAny(_getTopics(assocDef.getChildTypeUri(), new ArrayList())); 710 } 711 712 private AssociationDefinition getAssocDef(String childTypeUri) { 713 // Note: doesn't work for facets 714 return parent.getType().getAssocDef(childTypeUri); 715 } 716 }