001package org.hl7.fhir.r5.elementmodel; 002 003/* 004 Copyright (c) 2011+, HL7, Inc. 005 All rights reserved. 006 007 Redistribution and use in source and binary forms, with or without modification, 008 are permitted provided that the following conditions are met: 009 010 * Redistributions of source code must retain the above copyright notice, this 011 list of conditions and the following disclaimer. 012 * Redistributions in binary form must reproduce the above copyright notice, 013 this list of conditions and the following disclaimer in the documentation 014 and/or other materials provided with the distribution. 015 * Neither the name of HL7 nor the names of its contributors may be used to 016 endorse or promote products derived from this software without specific 017 prior written permission. 018 019 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 022 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 023 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 024 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 025 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 026 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 027 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 028 POSSIBILITY OF SUCH DAMAGE. 029 030 */ 031 032 033 034import java.util.ArrayList; 035import java.util.List; 036 037import org.hl7.fhir.exceptions.DefinitionException; 038import org.hl7.fhir.exceptions.FHIRException; 039import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; 040import org.hl7.fhir.r5.conformance.profile.ProfileUtilities.SourcedChildDefinitions; 041import org.hl7.fhir.r5.context.IWorkerContext; 042import org.hl7.fhir.r5.formats.FormatUtilities; 043import org.hl7.fhir.r5.model.ElementDefinition; 044import org.hl7.fhir.r5.model.ElementDefinition.PropertyRepresentation; 045import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; 046import org.hl7.fhir.r5.model.Extension; 047import org.hl7.fhir.r5.model.StructureDefinition; 048import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; 049import org.hl7.fhir.r5.model.TypeDetails; 050import org.hl7.fhir.r5.utils.ToolingExtensions; 051import org.hl7.fhir.r5.utils.TypesUtilities; 052import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 053import org.hl7.fhir.utilities.StringPair; 054import org.hl7.fhir.utilities.Utilities; 055 056public class Property { 057 058 private IWorkerContext context; 059 private ElementDefinition definition; 060 private StructureDefinition structure; 061 private ProfileUtilities profileUtilities; 062 private TypeRefComponent type; 063 064 public Property(IWorkerContext context, ElementDefinition definition, StructureDefinition structure, ProfileUtilities profileUtilities) { 065 this.context = context; 066 this.definition = definition; 067 this.structure = structure; 068 this.profileUtilities = profileUtilities; 069 } 070 071 072 public Property(IWorkerContext context, ElementDefinition definition, StructureDefinition structure, ProfileUtilities profileUtilities, String type) { 073 this.context = context; 074 this.definition = definition; 075 this.structure = structure; 076 this.profileUtilities = profileUtilities; 077 for (TypeRefComponent tr : definition.getType()) { 078 if (tr.getWorkingCode().equals(type)) { 079 this.type = tr; 080 } 081 } 082 } 083 084 public Property(IWorkerContext context, ElementDefinition definition, StructureDefinition structure) { 085 this(context, definition, structure, new ProfileUtilities(context, null, null)); 086 } 087 088 public String getName() { 089 return definition.getPath().substring(definition.getPath().lastIndexOf(".")+1); 090 } 091 092 public String getJsonName() { 093 if (definition.hasExtension(ToolingExtensions.EXT_JSON_NAME)) { 094 return ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_JSON_NAME); 095 } else { 096 return getName(); 097 } 098 } 099 100 public String getXmlName() { 101 if (definition.hasExtension(ToolingExtensions.EXT_XML_NAME)) { 102 return ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_XML_NAME); 103 } else { 104 return getName(); 105 } 106 } 107 108 public String getXmlNamespace() { 109 if (ToolingExtensions.hasExtension(definition, "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace")) { 110 return ToolingExtensions.readStringExtension(definition, "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace"); 111 } else if (ToolingExtensions.hasExtension(structure, "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace")) { 112 return ToolingExtensions.readStringExtension(structure, "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace"); 113 } else { 114 return FormatUtilities.FHIR_NS; 115 } 116 } 117 118 public ElementDefinition getDefinition() { 119 return definition; 120 } 121 122 public String getType() { 123 if (type != null) { 124 return type.getWorkingCode(); 125 } else if (definition.getType().size() == 0) 126 return null; 127 else if (definition.getType().size() > 1) { 128 String tn = definition.getType().get(0).getWorkingCode(); 129 for (int i = 1; i < definition.getType().size(); i++) { 130 if (!tn.equals(definition.getType().get(i).getWorkingCode())) 131 return null; // though really, we shouldn't get here - type != null when definition.getType.size() > 1, or it should be 132 } 133 return tn; 134 } else 135 return definition.getType().get(0).getWorkingCode(); 136 } 137 138 public String getType(String elementName) { 139 if (type != null) { 140 return type.getWorkingCode(); 141 } 142 if (!definition.getPath().contains(".")) 143 return definition.getPath(); 144 ElementDefinition ed = definition; 145 if (definition.hasContentReference()) { 146 String url = null; 147 String path = definition.getContentReference(); 148 if (!path.startsWith("#")) { 149 if (path.contains("#")) { 150 url = path.substring(0, path.indexOf("#")); 151 path = path.substring(path.indexOf("#")+1); 152 } else { 153 throw new Error("Illegal content reference '"+path+"'"); 154 } 155 } else { 156 path = path.substring(1); 157 } 158 StructureDefinition sd = (url == null || url.equals(structure.getUrl())) ? structure : context.fetchResource(StructureDefinition.class, url, structure); 159 if (sd == null) { 160 throw new Error("Unknown Type in content reference '"+path+"'"); 161 } 162 boolean found = false; 163 for (ElementDefinition d : sd.getSnapshot().getElement()) { 164 if (d.hasId() && d.getId().equals(path)) { 165 found = true; 166 ed = d; 167 } 168 } 169 if (!found) 170 throw new Error("Unable to resolve "+definition.getContentReference()+" at "+definition.getPath()+" on "+sd.getUrl()); 171 } 172 if (ed.getType().size() == 0) 173 return null; 174 else if (ed.getType().size() > 1) { 175 String t = ed.getType().get(0).getCode(); 176 boolean all = true; 177 for (TypeRefComponent tr : ed.getType()) { 178 if (!t.equals(tr.getCode())) 179 all = false; 180 } 181 if (all) 182 return t; 183 String tail = ed.getPath().substring(ed.getPath().lastIndexOf(".")+1); 184 if (tail.endsWith("[x]") && elementName != null && elementName.startsWith(tail.substring(0, tail.length()-3))) { 185 String name = elementName.substring(tail.length()-3); 186 return isPrimitive(lowFirst(name)) ? lowFirst(name) : name; 187 } else { 188 if (ToolingExtensions.hasExtension(ed, "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype")) 189 return ToolingExtensions.readStringExtension(ed, "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype"); 190 throw new Error("logic error, gettype when types > 1, name mismatch for "+elementName+" on at "+ed.getPath()); 191 } 192 } else if (ed.getType().get(0).getCode() == null) { 193 if (Utilities.existsInList(ed.getId(), "Element.id", "Extension.url")) 194 return "string"; 195 else 196 return structure.getId(); 197 } else 198 return ed.getType().get(0).getWorkingCode(); 199 } 200 201 public boolean hasType(String elementName) { 202 if (type != null) { 203 return false; // ? 204 } else if (definition.getType().size() == 0) { 205 return false; 206 } else if (isJsonPrimitiveChoice()) { 207 for (TypeRefComponent tr : definition.getType()) { 208 if (elementName.equals(tr.getWorkingCode())) { 209 return true; 210 } 211 } 212 return false; 213 } else if (definition.getType().size() > 1) { 214 String t = definition.getType().get(0).getCode(); 215 boolean all = true; 216 for (TypeRefComponent tr : definition.getType()) { 217 if (!t.equals(tr.getCode())) 218 all = false; 219 } 220 if (all) 221 return true; 222 String tail = definition.getPath().substring(definition.getPath().lastIndexOf(".")+1); 223 if (tail.endsWith("[x]") && elementName.startsWith(tail.substring(0, tail.length()-3))) { 224// String name = elementName.substring(tail.length()-3); 225 return true; 226 } else 227 return false; 228 } else 229 return true; 230 } 231 232 public StructureDefinition getStructure() { 233 return structure; 234 } 235 236 /** 237 * Is the given name a primitive 238 * 239 * @param E.g. "Observation.status" 240 */ 241 public boolean isPrimitiveName(String name) { 242 String code = getType(name); 243 return isPrimitive(code); 244 } 245 246 /** 247 * Is the given type a primitive 248 * 249 * @param E.g. "integer" 250 */ 251 public boolean isPrimitive(String code) { 252 if (Utilities.isAbsoluteUrl(code)) { 253 StructureDefinition sd = context.fetchTypeDefinition(code); 254 return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE; 255 } else { 256 return TypesUtilities.isPrimitive(code); 257 } 258 } 259 260 public boolean isPrimitive() { 261 return isPrimitive(getType()); 262 } 263 private String lowFirst(String t) { 264 return t.substring(0, 1).toLowerCase()+t.substring(1); 265 } 266 267 public boolean isResource() { 268 if (type != null) { 269 String tc = type.getCode(); 270 return (("Resource".equals(tc) || "DomainResource".equals(tc)) || Utilities.existsInList(tc, context.getResourceNames())); 271 } else if (definition.getType().size() > 0) { 272 String tc = definition.getType().get(0).getCode(); 273 return definition.getType().size() == 1 && (("Resource".equals(tc) || "DomainResource".equals(tc)) || Utilities.existsInList(tc, context.getResourceNames())); 274 } 275 else { 276 return !definition.getPath().contains(".") && (structure.getKind() == StructureDefinitionKind.RESOURCE); 277 } 278 } 279 280 public boolean isList() { 281 return !"1".equals(definition.getMax()); 282 } 283 284 public boolean isBaseList() { 285 return !"1".equals(definition.getBase().getMax()); 286 } 287 288 public String getScopedPropertyName() { 289 return definition.getBase().getPath(); 290 } 291 292 private boolean isElementWithOnlyExtension(final ElementDefinition ed, final List<ElementDefinition> children) { 293 boolean result = false; 294 if (!ed.getType().isEmpty()) { 295 result = true; 296 for (final ElementDefinition ele : children) { 297 if (!ele.getPath().contains("extension")) { 298 result = false; 299 break; 300 } 301 } 302 } 303 return result; 304 } 305 306 public boolean IsLogicalAndHasPrimitiveValue(String name) { 307// if (canBePrimitive!= null) 308// return canBePrimitive; 309 310 if (structure.getKind() != StructureDefinitionKind.LOGICAL) 311 return false; 312 if (!hasType(name)) 313 return false; 314 StructureDefinition sd = context.fetchResource(StructureDefinition.class, structure.getUrl().substring(0, structure.getUrl().lastIndexOf("/")+1)+getType(name)); 315 if (sd == null) 316 sd = context.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(getType(name), null)); 317 if (sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE) 318 return true; 319 if (sd == null || sd.getKind() != StructureDefinitionKind.LOGICAL) 320 return false; 321 for (ElementDefinition ed : sd.getSnapshot().getElement()) { 322 if (ed.getPath().equals(sd.getId()+".value") && ed.getType().size() == 1 && isPrimitive(ed.getType().get(0).getCode())) { 323 return true; 324 } 325 } 326 return false; 327 } 328 329 public boolean isChoice() { 330 if (type != null) { 331 return true; 332 } 333 if (definition.getType().size() <= 1) 334 return false; 335 String tn = definition.getType().get(0).getCode(); 336 for (int i = 1; i < definition.getType().size(); i++) 337 if (!definition.getType().get(i).getCode().equals(tn)) 338 return true; 339 return false; 340 } 341 342 343 public List<Property> getChildProperties(String elementName, String statedType) throws FHIRException { 344 ElementDefinition ed = definition; 345 StructureDefinition sd = structure; 346 SourcedChildDefinitions children = profileUtilities.getChildMap(sd, ed); 347 String url = null; 348 if (children.getList().isEmpty() || isElementWithOnlyExtension(ed, children.getList())) { 349 // ok, find the right definitions 350 String t = null; 351 if (ed.getType().size() == 1) 352 t = ed.getType().get(0).getWorkingCode(); 353 else if (ed.getType().size() == 0) 354 throw new Error("types == 0, and no children found on "+getDefinition().getPath()); 355 else { 356 t = ed.getType().get(0).getWorkingCode(); 357 boolean all = true; 358 for (TypeRefComponent tr : ed.getType()) { 359 if (!tr.getWorkingCode().equals(t)) { 360 all = false; 361 break; 362 } 363 } 364 if (!all) { 365 // ok, it's polymorphic 366 if (ed.hasRepresentation(PropertyRepresentation.TYPEATTR)) { 367 t = statedType; 368 if (t == null && ToolingExtensions.hasExtension(ed, "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype")) 369 t = ToolingExtensions.readStringExtension(ed, "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype"); 370 boolean ok = false; 371 for (TypeRefComponent tr : ed.getType()) { 372 if (tr.getWorkingCode().equals(t)) 373 ok = true; 374 if (Utilities.isAbsoluteUrl(tr.getWorkingCode())) { 375 StructureDefinition sdt = context.fetchResource(StructureDefinition.class, tr.getWorkingCode()); 376 if (sdt != null && sdt.getTypeTail().equals(t)) { 377 url = tr.getWorkingCode(); 378 ok = true; 379 } 380 } 381 if (ok) 382 break; 383 } 384 if (!ok) 385 throw new DefinitionException("Type '"+t+"' is not an acceptable type for '"+elementName+"' on property "+definition.getPath()); 386 387 } else { 388 t = elementName.substring(tail(ed.getPath()).length() - 3); 389 if (isPrimitive(lowFirst(t))) 390 t = lowFirst(t); 391 } 392 } 393 } 394 if (!"xhtml".equals(t)) { 395 for (TypeRefComponent aType: ed.getType()) { 396 if (aType.getWorkingCode().equals(t)) { 397 if (aType.hasProfile()) { 398 assert aType.getProfile().size() == 1; 399 url = aType.getProfile().get(0).getValue(); 400 } else { 401 url = ProfileUtilities.sdNs(t, null); 402 } 403 break; 404 } 405 } 406 if (url==null) 407 throw new FHIRException("Unable to find type " + t + " for element " + elementName + " with path " + ed.getPath()); 408 sd = context.fetchResource(StructureDefinition.class, url); 409 if (sd == null) 410 throw new DefinitionException("Unable to find type '"+t+"' for name '"+elementName+"' on property "+definition.getPath()); 411 children = profileUtilities.getChildMap(sd, sd.getSnapshot().getElement().get(0)); 412 } 413 } 414 List<Property> properties = new ArrayList<Property>(); 415 for (ElementDefinition child : children.getList()) { 416 properties.add(new Property(context, child, sd, this.profileUtilities)); 417 } 418 return properties; 419 } 420 421 protected List<Property> getChildProperties(TypeDetails type) throws DefinitionException { 422 ElementDefinition ed = definition; 423 StructureDefinition sd = structure; 424 SourcedChildDefinitions children = profileUtilities.getChildMap(sd, ed); 425 if (children.getList().isEmpty()) { 426 // ok, find the right definitions 427 String t = null; 428 if (ed.getType().size() == 1) 429 t = ed.getType().get(0).getCode(); 430 else if (ed.getType().size() == 0) 431 throw new Error("types == 0, and no children found"); 432 else { 433 t = ed.getType().get(0).getCode(); 434 boolean all = true; 435 for (TypeRefComponent tr : ed.getType()) { 436 if (!tr.getCode().equals(t)) { 437 all = false; 438 break; 439 } 440 } 441 if (!all) { 442 // ok, it's polymorphic 443 t = type.getType(); 444 } 445 } 446 if (!"xhtml".equals(t)) { 447 sd = context.fetchResource(StructureDefinition.class, t); 448 if (sd == null) 449 throw new DefinitionException("Unable to find class '"+t+"' for name '"+ed.getPath()+"' on property "+definition.getPath()); 450 children = profileUtilities.getChildMap(sd, sd.getSnapshot().getElement().get(0)); 451 } 452 } 453 List<Property> properties = new ArrayList<Property>(); 454 for (ElementDefinition child : children.getList()) { 455 properties.add(new Property(context, child, sd, this.profileUtilities)); 456 } 457 return properties; 458 } 459 460 private String tail(String path) { 461 return path.contains(".") ? path.substring(path.lastIndexOf(".")+1) : path; 462 } 463 464 public Property getChild(String elementName, String childName) throws FHIRException { 465 List<Property> children = getChildProperties(elementName, null); 466 for (Property p : children) { 467 if (p.getName().equals(childName)) { 468 return p; 469 } 470 } 471 return null; 472 } 473 474 public Property getChild(String name, TypeDetails type) throws DefinitionException { 475 List<Property> children = getChildProperties(type); 476 for (Property p : children) { 477 if (p.getName().equals(name) || p.getName().equals(name+"[x]")) { 478 return p; 479 } 480 } 481 return null; 482 } 483 484 public Property getChild(String name) throws FHIRException { 485 List<Property> children = getChildProperties(name, null); 486 for (Property p : children) { 487 if (p.getName().equals(name)) { 488 return p; 489 } 490 } 491 return null; 492 } 493 494 public Property getChildSimpleName(String elementName, String name) throws FHIRException { 495 List<Property> children = getChildProperties(elementName, null); 496 for (Property p : children) { 497 if (p.getName().equals(name) || p.getName().equals(name+"[x]")) { 498 return p; 499 } 500 } 501 return null; 502 } 503 504 public IWorkerContext getContext() { 505 return context; 506 } 507 508 @Override 509 public String toString() { 510 return definition.getPath(); 511 } 512 513 514 public boolean isJsonKeyArray() { 515 return definition.hasExtension(ToolingExtensions.EXT_JSON_PROP_KEY); 516 } 517 518 519 public String getJsonKeyProperty() { 520 return ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_JSON_PROP_KEY); 521 } 522 523 524 public boolean hasTypeSpecifier() { 525 return definition.hasExtension(ToolingExtensions.EXT_TYPE_SPEC); 526 } 527 528 529 public List<StringPair> getTypeSpecifiers() { 530 List<StringPair> res = new ArrayList<>(); 531 for (Extension e : definition.getExtensionsByUrl(ToolingExtensions.EXT_TYPE_SPEC)) { 532 res.add(new StringPair(ToolingExtensions.readStringExtension(e, "condition"), ToolingExtensions.readStringExtension(e, "type"))); 533 } 534 return res; 535 } 536 537 538 public Property cloneToType(StructureDefinition sd) { 539 Property res = new Property(context, definition.copy(), sd); 540 res.definition.getType().clear(); 541 res.definition.getType().add(new TypeRefComponent(sd.getUrl())); 542 return res; 543 } 544 545 546 public boolean hasImpliedPrefix() { 547 return definition.hasExtension(ToolingExtensions.EXT_IMPLIED_PREFIX); 548 } 549 550 551 public String getImpliedPrefix() { 552 return ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_IMPLIED_PREFIX); 553 } 554 555 556 public boolean isNullable() { 557 return ToolingExtensions.readBoolExtension(definition, ToolingExtensions.EXT_JSON_NULLABLE); 558 } 559 560 561 public String summary() { 562 return structure.getUrl()+"#"+definition.getId(); 563 } 564 565 566 public boolean canBeEmpty() { 567 if (definition.hasExtension(ToolingExtensions.EXT_JSON_EMPTY)) { 568 return !"absent".equals(ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_JSON_EMPTY)); 569 } else { 570 return false; 571 } 572 } 573 574 575 public boolean isLogical() { 576 return structure.getKind() == StructureDefinitionKind.LOGICAL; 577 } 578 579 580 public ProfileUtilities getUtils() { 581 return profileUtilities; 582 } 583 584 public boolean isJsonPrimitiveChoice() { 585 return ToolingExtensions.readBoolExtension(definition, ToolingExtensions.EXT_JSON_PRIMITIVE_CHOICE); 586 } 587 588 public Object typeSummary() { 589 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(" | "); 590 for (TypeRefComponent t : definition.getType()) { 591 b.append(t.getCode()); 592 } 593 return b.toString(); 594 } 595 596 597 public boolean hasJsonName() { 598 return definition.hasExtension(ToolingExtensions.EXT_JSON_NAME); 599 } 600 601 602 public boolean isTranslatable() { 603 boolean ok = ToolingExtensions.readBoolExtension(definition, ToolingExtensions.EXT_TRANSLATABLE); 604 if (!ok && !Utilities.existsInList(definition.getBase().getPath(), "Reference.reference", "Coding.version", "Identifier.value", "SampledData.offsets", "SampledData.data", "ContactPoint.value")) { 605 String t = getType(); 606 ok = Utilities.existsInList(t, "string", "markdown"); 607 } 608 return ok; 609 } 610 611 612}