001package org.hl7.fhir.r4.elementmodel; 002 003/*- 004 * #%L 005 * org.hl7.fhir.r4 006 * %% 007 * Copyright (C) 2014 - 2019 Health Level 7 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import java.util.ArrayList; 024import java.util.Collections; 025import java.util.Comparator; 026import java.util.HashMap; 027import java.util.HashSet; 028import java.util.List; 029import java.util.Map; 030import java.util.Set; 031 032import org.apache.commons.lang3.Validate; 033import org.hl7.fhir.exceptions.FHIRException; 034import org.hl7.fhir.r4.conformance.ProfileUtilities; 035import org.hl7.fhir.r4.model.Base; 036import org.hl7.fhir.r4.model.ElementDefinition; 037import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent; 038import org.hl7.fhir.r4.model.Enumerations.BindingStrength; 039import org.hl7.fhir.r4.model.ICoding; 040import org.hl7.fhir.r4.model.StringType; 041import org.hl7.fhir.r4.model.StructureDefinition; 042import org.hl7.fhir.r4.model.Type; 043import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent; 044import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 045import org.hl7.fhir.utilities.ElementDecoration; 046import org.hl7.fhir.utilities.ElementDecoration.DecorationType; 047import org.hl7.fhir.utilities.Utilities; 048import org.hl7.fhir.utilities.xhtml.XhtmlNode; 049 050/** 051 * This class represents the underlying reference model of FHIR 052 * 053 * A resource is nothing but a set of elements, where every element has a 054 * name, maybe a stated type, maybe an id, and either a value or child elements 055 * (one or the other, but not both or neither) 056 * 057 * @author Grahame Grieve 058 * 059 */ 060public class Element extends Base { 061 062 063 public enum SpecialElement { 064 CONTAINED, BUNDLE_ENTRY, BUNDLE_OUTCOME, PARAMETER; 065 066 public static SpecialElement fromProperty(Property property) { 067 if (property.getStructure().getIdElement().getIdPart().equals("Parameters")) 068 return PARAMETER; 069 if (property.getStructure().getIdElement().getIdPart().equals("Bundle") && property.getName().equals("resource")) 070 return BUNDLE_ENTRY; 071 if (property.getStructure().getIdElement().getIdPart().equals("Bundle") && property.getName().equals("outcome")) 072 return BUNDLE_OUTCOME; 073 if (property.getName().equals("contained")) 074 return CONTAINED; 075 throw new Error("Unknown resource containing a native resource: "+property.getDefinition().getId()); 076 } 077 } 078 079 private List<String> comments;// not relevant for production, but useful in documentation 080 private String name; 081 private String type; 082 private String value; 083 private int index = -1; 084 private List<Element> children; 085 private Property property; 086 private Property elementProperty; // this is used when special is set to true - it tracks the underlying element property which is used in a few places 087 private int line; 088 private int col; 089 private SpecialElement special; 090 private XhtmlNode xhtml; // if this is populated, then value will also hold the string representation 091 private String explicitType; // for xsi:type attribute 092 093 public Element(String name) { 094 super(); 095 this.name = name; 096 } 097 098 public Element(Element other) { 099 super(); 100 name = other.name; 101 type = other.type; 102 property = other.property; 103 elementProperty = other.elementProperty; 104 special = other.special; 105 } 106 107 public Element(String name, Property property) { 108 super(); 109 this.name = name; 110 this.property = property; 111 } 112 113 public Element(String name, Property property, String type, String value) { 114 super(); 115 this.name = name; 116 this.property = property; 117 this.type = type; 118 this.value = value; 119 } 120 121 public void updateProperty(Property property, SpecialElement special, Property elementProperty) { 122 this.property = property; 123 this.elementProperty = elementProperty; 124 this.special = special; 125 } 126 127 public SpecialElement getSpecial() { 128 return special; 129 } 130 131 public String getName() { 132 return name; 133 } 134 135 public String getType() { 136 if (type == null) 137 return property.getType(name); 138 else 139 return type; 140 } 141 142 public String getValue() { 143 return value; 144 } 145 146 public boolean hasChildren() { 147 return !(children == null || children.isEmpty()); 148 } 149 150 public List<Element> getChildren() { 151 if (children == null) 152 children = new ArrayList<Element>(); 153 return children; 154 } 155 156 public boolean hasComments() { 157 return !(comments == null || comments.isEmpty()); 158 } 159 160 public List<String> getComments() { 161 if (comments == null) 162 comments = new ArrayList<String>(); 163 return comments; 164 } 165 166 public Property getProperty() { 167 return property; 168 } 169 170 public void setValue(String value) { 171 this.value = value; 172 } 173 174 public void setType(String type) { 175 this.type = type; 176 177 } 178 179 public boolean hasValue() { 180 return value != null; 181 } 182 183 public List<Element> getChildrenByName(String name) { 184 List<Element> res = new ArrayList<Element>(); 185 if (hasChildren()) { 186 for (Element child : children) 187 if (name.equals(child.getName())) 188 res.add(child); 189 } 190 return res; 191 } 192 193 public void numberChildren() { 194 if (children == null) 195 return; 196 197 String last = ""; 198 int index = 0; 199 for (Element child : children) { 200 if (child.getProperty().isList()) { 201 if (last.equals(child.getName())) { 202 index++; 203 } else { 204 last = child.getName(); 205 index = 0; 206 } 207 child.index = index; 208 } else { 209 child.index = -1; 210 } 211 child.numberChildren(); 212 } 213 } 214 215 public int getIndex() { 216 return index; 217 } 218 219 public boolean hasIndex() { 220 return index > -1; 221 } 222 223 public void setIndex(int index) { 224 this.index = index; 225 } 226 227 public String getChildValue(String name) { 228 if (children == null) 229 return null; 230 for (Element child : children) { 231 if (name.equals(child.getName())) 232 return child.getValue(); 233 } 234 return null; 235 } 236 237 public void setChildValue(String name, String value) { 238 if (children == null) 239 children = new ArrayList<Element>(); 240 for (Element child : children) { 241 if (name.equals(child.getName())) { 242 if (!child.isPrimitive()) 243 throw new Error("Cannot set a value of a non-primitive type ("+name+" on "+this.getName()+")"); 244 child.setValue(value); 245 } 246 } 247 try { 248 setProperty(name.hashCode(), name, new StringType(value)); 249 } catch (FHIRException e) { 250 throw new Error(e); 251 } 252 } 253 254 public List<Element> getChildren(String name) { 255 List<Element> res = new ArrayList<Element>(); 256 if (children != null) 257 for (Element child : children) { 258 if (name.equals(child.getName())) 259 res.add(child); 260 } 261 return res; 262 } 263 264 public boolean hasType() { 265 if (type == null) 266 return property.hasType(name); 267 else 268 return true; 269 } 270 271 @Override 272 public String fhirType() { 273 return getType(); 274 } 275 276 @Override 277 public Base[] getProperty(int hash, String name, boolean checkValid) throws FHIRException { 278 if (isPrimitive() && (hash == "value".hashCode()) && !Utilities.noString(value)) { 279// String tn = getType(); 280// throw new Error(tn+" not done yet"); 281 Base[] b = new Base[1]; 282 b[0] = new StringType(value); 283 return b; 284 } 285 286 List<Base> result = new ArrayList<Base>(); 287 if (children != null) { 288 for (Element child : children) { 289 if (child.getName().equals(name)) 290 result.add(child); 291 if (child.getName().startsWith(name) && child.getProperty().isChoice() && child.getProperty().getName().equals(name+"[x]")) 292 result.add(child); 293 } 294 } 295 if (result.isEmpty() && checkValid) { 296// throw new FHIRException("not determined yet"); 297 } 298 return result.toArray(new Base[result.size()]); 299 } 300 301 @Override 302 protected void listChildren(List<org.hl7.fhir.r4.model.Property> childProps) { 303 if (children != null) { 304 Map<String, org.hl7.fhir.r4.model.Property> map = new HashMap<String, org.hl7.fhir.r4.model.Property>(); 305 for (Element c : children) { 306 org.hl7.fhir.r4.model.Property p = map.get(c.getName()); 307 if (p == null) { 308 p = new org.hl7.fhir.r4.model.Property(c.getName(), c.fhirType(), c.getProperty().getDefinition().getDefinition(), c.getProperty().getDefinition().getMin(), maxToInt(c.getProperty().getDefinition().getMax()), c); 309 childProps.add(p); 310 map.put(c.getName(), p); 311 312 } else 313 p.getValues().add(c); 314 } 315 } 316 } 317 318 @Override 319 public Base setProperty(int hash, String name, Base value) throws FHIRException { 320 if ("xhtml".equals(getType()) && (hash == "value".hashCode())) { 321 this.xhtml = castToXhtml(value); 322 this.value = castToXhtmlString(value); 323 return this; 324 } 325 if (isPrimitive() && (hash == "value".hashCode())) { 326 this.value = castToString(value).asStringValue(); 327 return this; 328 } 329 330 if (!value.isPrimitive() && !(value instanceof Element)) { 331 if (isDataType(value)) 332 value = convertToElement(property.getChild(name), value); 333 else 334 throw new FHIRException("Cannot set property "+name+" on "+this.name+" - value is not a primitive type ("+value.fhirType()+") or an ElementModel type"); 335 } 336 337 if (children == null) 338 children = new ArrayList<Element>(); 339 Element childForValue = null; 340 341 // look through existing children 342 for (Element child : children) { 343 if (child.getName().equals(name)) { 344 if (!child.isList()) { 345 childForValue = child; 346 break; 347 } else { 348 Element ne = new Element(child); 349 children.add(ne); 350 numberChildren(); 351 childForValue = ne; 352 break; 353 } 354 } 355 } 356 357 int i = 0; 358 if (childForValue == null) 359 for (Property p : property.getChildProperties(this.name, type)) { 360 int t = -1; 361 for (int c =0; c < children.size(); c++) { 362 Element e = children.get(c); 363 if (p.getName().equals(e.getName())) 364 t = c; 365 } 366 if (t > i) 367 i = t; 368 if (p.getName().equals(name) || p.getName().equals(name+"[x]")) { 369 Element ne = new Element(name, p); 370 children.add(i, ne); 371 childForValue = ne; 372 break; 373 } 374 } 375 376 if (childForValue == null) 377 throw new Error("Cannot set property "+name+" on "+this.name); 378 else if (value.isPrimitive()) { 379 if (childForValue.property.getName().endsWith("[x]")) 380 childForValue.name = name+Utilities.capitalize(value.fhirType()); 381 childForValue.setValue(value.primitiveValue()); 382 } else { 383 Element ve = (Element) value; 384 childForValue.type = ve.getType(); 385 if (childForValue.property.getName().endsWith("[x]")) 386 childForValue.name = name+Utilities.capitalize(childForValue.type); 387 else if (value.isResource()) { 388 if (childForValue.elementProperty == null) 389 childForValue.elementProperty = childForValue.property; 390 childForValue.property = ve.property; 391 childForValue.special = SpecialElement.BUNDLE_ENTRY; 392 } 393 if (ve.children != null) { 394 if (childForValue.children == null) 395 childForValue.children = new ArrayList<Element>(); 396 else 397 childForValue.children.clear(); 398 childForValue.children.addAll(ve.children); 399 } 400 } 401 return childForValue; 402 } 403 404 private Base convertToElement(Property prop, Base v) throws FHIRException { 405 return new ObjectConverter(property.getContext()).convert(prop, (Type) v); 406 } 407 408 private boolean isDataType(Base v) { 409 return v instanceof Type && property.getContext().getTypeNames().contains(v.fhirType()); 410 } 411 412 @Override 413 public Base makeProperty(int hash, String name) throws FHIRException { 414 if (isPrimitive() && (hash == "value".hashCode())) { 415 return new StringType(value); 416 } 417 418 if (children == null) 419 children = new ArrayList<Element>(); 420 421 // look through existing children 422 for (Element child : children) { 423 if (child.getName().equals(name)) { 424 if (!child.isList()) { 425 return child; 426 } else { 427 Element ne = new Element(child); 428 children.add(ne); 429 numberChildren(); 430 return ne; 431 } 432 } 433 } 434 435 for (Property p : property.getChildProperties(this.name, type)) { 436 if (p.getName().equals(name)) { 437 Element ne = new Element(name, p); 438 children.add(ne); 439 return ne; 440 } 441 } 442 443 throw new Error("Unrecognised name "+name+" on "+this.name); 444 } 445 446 private int maxToInt(String max) { 447 if (max.equals("*")) 448 return Integer.MAX_VALUE; 449 else 450 return Integer.parseInt(max); 451 } 452 453 @Override 454 public boolean isPrimitive() { 455 return type != null ? property.isPrimitive(type) : property.isPrimitive(property.getType(name)); 456 } 457 458 @Override 459 public boolean isBooleanPrimitive() { 460 return isPrimitive() && ("boolean".equals(type) || "boolean".equals(property.getType(name))); 461 } 462 463 @Override 464 public boolean isResource() { 465 return property.isResource(); 466 } 467 468 469 @Override 470 public boolean hasPrimitiveValue() { 471 return property.isPrimitiveName(name) || property.IsLogicalAndHasPrimitiveValue(name); 472 } 473 474 475 @Override 476 public String primitiveValue() { 477 if (isPrimitive()) 478 return value; 479 else { 480 if (hasPrimitiveValue() && children != null) { 481 for (Element c : children) { 482 if (c.getName().equals("value")) 483 return c.primitiveValue(); 484 } 485 } 486 return null; 487 } 488 } 489 490 // for the validator 491 public int line() { 492 return line; 493 } 494 495 public int col() { 496 return col; 497 } 498 499 public Element markLocation(int line, int col) { 500 this.line = line; 501 this.col = col; 502 return this; 503 } 504 505 public void clearDecorations() { 506 clearUserData("fhir.decorations"); 507 for (Element e : children) 508 e.clearDecorations(); 509 } 510 511 public void markValidation(StructureDefinition profile, ElementDefinition definition) { 512 @SuppressWarnings("unchecked") 513 List<ElementDecoration> decorations = (List<ElementDecoration>) getUserData("fhir.decorations"); 514 if (decorations == null) { 515 decorations = new ArrayList<>(); 516 setUserData("fhir.decorations", decorations); 517 } 518 decorations.add(new ElementDecoration(DecorationType.TYPE, profile.getUserString("path"), definition.getPath())); 519 if (definition.getId() != null && tail(definition.getId()).contains(":")) { 520 String[] details = tail(definition.getId()).split(":"); 521 decorations.add(new ElementDecoration(DecorationType.SLICE, null, details[1])); 522 } 523 } 524 525 private String tail(String id) { 526 return id.contains(".") ? id.substring(id.lastIndexOf(".")+1) : id; 527 } 528 529 public Element getNamedChild(String name) { 530 if (children == null) 531 return null; 532 Element result = null; 533 for (Element child : children) { 534 if (child.getName().equals(name)) { 535 if (result == null) 536 result = child; 537 else 538 throw new Error("Attempt to read a single element when there is more than one present ("+name+")"); 539 } 540 } 541 return result; 542 } 543 544 public void getNamedChildren(String name, List<Element> list) { 545 if (children != null) 546 for (Element child : children) 547 if (child.getName().equals(name)) 548 list.add(child); 549 } 550 551 public String getNamedChildValue(String name) { 552 Element child = getNamedChild(name); 553 return child == null ? null : child.value; 554 } 555 556 public void getNamedChildrenWithWildcard(String string, List<Element> values) { 557 Validate.isTrue(string.endsWith("[x]")); 558 559 String start = string.substring(0, string.length() - 3); 560 if (children != null) { 561 for (Element child : children) { 562 if (child.getName().startsWith(start)) { 563 values.add(child); 564 } 565 } 566 } 567 } 568 569 570 public XhtmlNode getXhtml() { 571 return xhtml; 572 } 573 574 public Element setXhtml(XhtmlNode xhtml) { 575 this.xhtml = xhtml; 576 return this; 577 } 578 579 @Override 580 public boolean isEmpty() { 581 // GG: this used to also test !"".equals(value). 582 // the condition where "" is empty and there are no children is an error, and so this really only manifested as an issue in corner cases technical testing of the validator / FHIRPath. 583 // it should not cause any problems in real life. 584 if (value != null) { 585 return false; 586 } 587 for (Element next : getChildren()) { 588 if (!next.isEmpty()) { 589 return false; 590 } 591 } 592 return true; 593 } 594 595 public Property getElementProperty() { 596 return elementProperty; 597 } 598 599 public boolean hasElementProperty() { 600 return elementProperty != null; 601 } 602 603 public boolean hasChild(String name) { 604 return getNamedChild(name) != null; 605 } 606 607 public boolean hasChildren(String name) { 608 if (children != null) 609 for (Element child : children) 610 if (child.getName().equals(name)) 611 return true; 612 return false; 613 } 614 615 @Override 616 public String toString() { 617 return name+"="+fhirType() + "["+(children == null || hasValue() ? value : Integer.toString(children.size())+" children")+"]"; 618 } 619 620 @Override 621 public String getIdBase() { 622 return getChildValue("id"); 623 } 624 625 @Override 626 public void setIdBase(String value) { 627 setChildValue("id", value); 628 } 629 630 631 @Override 632 public boolean equalsDeep(Base other) { 633 if (!super.equalsDeep(other)) 634 return false; 635 if (isPrimitive() && other.isPrimitive()) 636 return primitiveValue().equals(other.primitiveValue()); 637 if (isPrimitive() || other.isPrimitive()) 638 return false; 639 Set<String> processed = new HashSet<String>(); 640 for (org.hl7.fhir.r4.model.Property p : children()) { 641 String name = p.getName(); 642 processed.add(name); 643 org.hl7.fhir.r4.model.Property o = other.getChildByName(name); 644 if (!equalsDeep(p, o)) 645 return false; 646 } 647 for (org.hl7.fhir.r4.model.Property p : children()) { 648 String name = p.getName(); 649 if (!processed.contains(name)) { 650 org.hl7.fhir.r4.model.Property o = other.getChildByName(name); 651 if (!equalsDeep(p, o)) 652 return false; 653 } 654 } 655 return true; 656 } 657 658 private boolean equalsDeep(org.hl7.fhir.r4.model.Property p, org.hl7.fhir.r4.model.Property o) { 659 if (o == null || p == null) 660 return false; 661 if (p.getValues().size() != o.getValues().size()) 662 return false; 663 for (int i = 0; i < p.getValues().size(); i++) 664 if (!Base.compareDeep(p.getValues().get(i), o.getValues().get(i), true)) 665 return false; 666 return true; 667 } 668 669 @Override 670 public boolean equalsShallow(Base other) { 671 if (!super.equalsShallow(other)) 672 return false; 673 if (isPrimitive() && other.isPrimitive()) 674 return primitiveValue().equals(other.primitiveValue()); 675 if (isPrimitive() || other.isPrimitive()) 676 return false; 677 return true; //? 678 } 679 680 public Type asType() throws FHIRException { 681 return new ObjectConverter(property.getContext()).convertToType(this); 682 } 683 684 @Override 685 public boolean isMetadataBased() { 686 return true; 687 } 688 689 public boolean isList() { 690 if (elementProperty != null) 691 return elementProperty.isList(); 692 else 693 return property.isList(); 694 } 695 696 @Override 697 public String[] getTypesForProperty(int hash, String name) throws FHIRException { 698 Property p = property.getChildSimpleName(this.name, name); 699 if (p != null) { 700 Set<String> types = new HashSet<String>(); 701 for (TypeRefComponent tr : p.getDefinition().getType()) { 702 types.add(tr.getCode()); 703 } 704 return types.toArray(new String[]{}); 705 } 706 return super.getTypesForProperty(hash, name); 707 708 } 709 710 public void sort() { 711 if (children != null) { 712 List<Element> remove = new ArrayList<Element>(); 713 for (Element child : children) { 714 child.sort(); 715 if (child.isEmpty()) 716 remove.add(child); 717 } 718 children.removeAll(remove); 719 Collections.sort(children, new ElementSortComparator(this, this.property)); 720 } 721 } 722 723 public class ElementSortComparator implements Comparator<Element> { 724 private List<ElementDefinition> children; 725 public ElementSortComparator(Element e, Property property) { 726 String tn = e.getType(); 727 StructureDefinition sd = property.getContext().fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(tn, property.getContext().getOverrideVersionNs())); 728 if (sd != null && !sd.getAbstract()) 729 children = sd.getSnapshot().getElement(); 730 else 731 children = property.getStructure().getSnapshot().getElement(); 732 } 733 734 @Override 735 public int compare(Element e0, Element e1) { 736 int i0 = find(e0); 737 int i1 = find(e1); 738 return Integer.compare(i0, i1); 739 } 740 private int find(Element e0) { 741 int i = e0.elementProperty != null ? children.indexOf(e0.elementProperty.getDefinition()) : children.indexOf(e0.property.getDefinition()); 742 return i; 743 } 744 745 } 746 747 public class ICodingImpl implements ICoding { 748 private String system; 749 private String version; 750 private String code; 751 private String display; 752 private boolean doesSystem; 753 private boolean doesVersion; 754 private boolean doesCode; 755 private boolean doesDisplay; 756 public ICodingImpl(boolean doesCode, boolean doesSystem, boolean doesVersion, boolean doesDisplay) { 757 super(); 758 this.doesCode = doesCode; 759 this.doesSystem = doesSystem; 760 this.doesVersion = doesVersion; 761 this.doesDisplay = doesDisplay; 762 } 763 public String getSystem() { 764 return system; 765 } 766 public String getVersion() { 767 return version; 768 } 769 public String getCode() { 770 return code; 771 } 772 public String getDisplay() { 773 return display; 774 } 775 public boolean hasSystem() { 776 return !Utilities.noString(system); 777 } 778 public boolean hasVersion() { 779 return !Utilities.noString(version); 780 } 781 public boolean hasCode() { 782 return !Utilities.noString(code); 783 } 784 public boolean hasDisplay() { 785 return !Utilities.noString(display); 786 } 787 public boolean supportsSystem() { 788 return doesSystem; 789 } 790 public boolean supportsVersion() { 791 return doesVersion; 792 } 793 public boolean supportsCode() { 794 return doesCode; 795 } 796 public boolean supportsDisplay() { 797 return doesDisplay; 798 } 799 } 800 801 public ICoding getAsICoding() throws FHIRException { 802 if ("code".equals(fhirType())) { 803 if (property.getDefinition().getBinding().getStrength() != BindingStrength.REQUIRED) 804 return null; 805 ICodingImpl c = new ICodingImpl(true, true, false, false); 806 c.code = primitiveValue(); 807 ValueSetExpansionOutcome vse = property.getContext().expandVS(property.getDefinition().getBinding(), true, false); 808 if (vse.getValueset() == null) 809 return null; 810 for (ValueSetExpansionContainsComponent cc : vse.getValueset().getExpansion().getContains()) { 811 if (cc.getCode().equals(c.code)) { 812 c.system = cc.getSystem(); 813 if (cc.hasVersion()) { 814 c.doesVersion = true; 815 c.version = cc.getVersion(); 816 } 817 if (cc.hasDisplay()) { 818 c.doesDisplay = true; 819 c.display = cc.getDisplay(); 820 } 821 } 822 } 823 if (c.system == null) 824 return null; 825 return c; 826 } else if ("Coding".equals(fhirType())) { 827 ICodingImpl c = new ICodingImpl(true, true, true, true); 828 c.system = getNamedChildValue("system"); 829 c.code = getNamedChildValue("code"); 830 c.display = getNamedChildValue("display"); 831 c.version = getNamedChildValue("version"); 832 return c; 833 } else if ("Quantity".equals(fhirType())) { 834 ICodingImpl c = new ICodingImpl(true, true, false, false); 835 c.system = getNamedChildValue("system"); 836 c.code = getNamedChildValue("code"); 837 return c; 838 } else 839 return null; 840 } 841 842 public String getExplicitType() { 843 return explicitType; 844 } 845 846 public void setExplicitType(String explicitType) { 847 this.explicitType = explicitType; 848 } 849 850 851}