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 throw new Error("logic error, gettype when types > 1"); 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 return TypesUtilities.isPrimitive(code); 253 // was this... but this can be very inefficient compared to hard coding the list 254// StructureDefinition sd = context.fetchTypeDefinition(code); 255// return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE; 256 } 257 258 public boolean isPrimitive() { 259 return isPrimitive(getType()); 260 } 261 private String lowFirst(String t) { 262 return t.substring(0, 1).toLowerCase()+t.substring(1); 263 } 264 265 public boolean isResource() { 266 if (type != null) { 267 String tc = type.getCode(); 268 return (("Resource".equals(tc) || "DomainResource".equals(tc)) || Utilities.existsInList(tc, context.getResourceNames())); 269 } else if (definition.getType().size() > 0) { 270 String tc = definition.getType().get(0).getCode(); 271 return definition.getType().size() == 1 && (("Resource".equals(tc) || "DomainResource".equals(tc)) || Utilities.existsInList(tc, context.getResourceNames())); 272 } 273 else 274 return !definition.getPath().contains(".") && (structure.getKind() == StructureDefinitionKind.RESOURCE); 275 } 276 277 public boolean isList() { 278 return !"1".equals(definition.getMax()); 279 } 280 281 public boolean isBaseList() { 282 return !"1".equals(definition.getBase().getMax()); 283 } 284 285 public String getScopedPropertyName() { 286 return definition.getBase().getPath(); 287 } 288 289 private boolean isElementWithOnlyExtension(final ElementDefinition ed, final List<ElementDefinition> children) { 290 boolean result = false; 291 if (!ed.getType().isEmpty()) { 292 result = true; 293 for (final ElementDefinition ele : children) { 294 if (!ele.getPath().contains("extension")) { 295 result = false; 296 break; 297 } 298 } 299 } 300 return result; 301 } 302 303 public boolean IsLogicalAndHasPrimitiveValue(String name) { 304// if (canBePrimitive!= null) 305// return canBePrimitive; 306 307 if (structure.getKind() != StructureDefinitionKind.LOGICAL) 308 return false; 309 if (!hasType(name)) 310 return false; 311 StructureDefinition sd = context.fetchResource(StructureDefinition.class, structure.getUrl().substring(0, structure.getUrl().lastIndexOf("/")+1)+getType(name)); 312 if (sd == null) 313 sd = context.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(getType(name), null)); 314 if (sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE) 315 return true; 316 if (sd == null || sd.getKind() != StructureDefinitionKind.LOGICAL) 317 return false; 318 for (ElementDefinition ed : sd.getSnapshot().getElement()) { 319 if (ed.getPath().equals(sd.getId()+".value") && ed.getType().size() == 1 && isPrimitive(ed.getType().get(0).getCode())) { 320 return true; 321 } 322 } 323 return false; 324 } 325 326 public boolean isChoice() { 327 if (type != null) { 328 return true; 329 } 330 if (definition.getType().size() <= 1) 331 return false; 332 String tn = definition.getType().get(0).getCode(); 333 for (int i = 1; i < definition.getType().size(); i++) 334 if (!definition.getType().get(i).getCode().equals(tn)) 335 return true; 336 return false; 337 } 338 339 340 public List<Property> getChildProperties(String elementName, String statedType) throws FHIRException { 341 ElementDefinition ed = definition; 342 StructureDefinition sd = structure; 343 SourcedChildDefinitions children = profileUtilities.getChildMap(sd, ed); 344 String url = null; 345 if (children.getList().isEmpty() || isElementWithOnlyExtension(ed, children.getList())) { 346 // ok, find the right definitions 347 String t = null; 348 if (ed.getType().size() == 1) 349 t = ed.getType().get(0).getWorkingCode(); 350 else if (ed.getType().size() == 0) 351 throw new Error("types == 0, and no children found on "+getDefinition().getPath()); 352 else { 353 t = ed.getType().get(0).getWorkingCode(); 354 boolean all = true; 355 for (TypeRefComponent tr : ed.getType()) { 356 if (!tr.getWorkingCode().equals(t)) { 357 all = false; 358 break; 359 } 360 } 361 if (!all) { 362 // ok, it's polymorphic 363 if (ed.hasRepresentation(PropertyRepresentation.TYPEATTR)) { 364 t = statedType; 365 if (t == null && ToolingExtensions.hasExtension(ed, "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype")) 366 t = ToolingExtensions.readStringExtension(ed, "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype"); 367 boolean ok = false; 368 for (TypeRefComponent tr : ed.getType()) { 369 if (tr.getWorkingCode().equals(t)) 370 ok = true; 371 if (Utilities.isAbsoluteUrl(tr.getWorkingCode())) { 372 StructureDefinition sdt = context.fetchResource(StructureDefinition.class, tr.getWorkingCode()); 373 if (sdt != null && sdt.getType().equals(t)) { 374 url = tr.getWorkingCode(); 375 ok = true; 376 } 377 } 378 if (ok) 379 break; 380 } 381 if (!ok) 382 throw new DefinitionException("Type '"+t+"' is not an acceptable type for '"+elementName+"' on property "+definition.getPath()); 383 384 } else { 385 t = elementName.substring(tail(ed.getPath()).length() - 3); 386 if (isPrimitive(lowFirst(t))) 387 t = lowFirst(t); 388 } 389 } 390 } 391 if (!"xhtml".equals(t)) { 392 for (TypeRefComponent aType: ed.getType()) { 393 if (aType.getWorkingCode().equals(t)) { 394 if (aType.hasProfile()) { 395 assert aType.getProfile().size() == 1; 396 url = aType.getProfile().get(0).getValue(); 397 } else { 398 url = ProfileUtilities.sdNs(t, null); 399 } 400 break; 401 } 402 } 403 if (url==null) 404 throw new FHIRException("Unable to find type " + t + " for element " + elementName + " with path " + ed.getPath()); 405 sd = context.fetchResource(StructureDefinition.class, url); 406 if (sd == null) 407 throw new DefinitionException("Unable to find type '"+t+"' for name '"+elementName+"' on property "+definition.getPath()); 408 children = profileUtilities.getChildMap(sd, sd.getSnapshot().getElement().get(0)); 409 } 410 } 411 List<Property> properties = new ArrayList<Property>(); 412 for (ElementDefinition child : children.getList()) { 413 properties.add(new Property(context, child, sd, this.profileUtilities)); 414 } 415 return properties; 416 } 417 418 protected List<Property> getChildProperties(TypeDetails type) throws DefinitionException { 419 ElementDefinition ed = definition; 420 StructureDefinition sd = structure; 421 SourcedChildDefinitions children = profileUtilities.getChildMap(sd, ed); 422 if (children.getList().isEmpty()) { 423 // ok, find the right definitions 424 String t = null; 425 if (ed.getType().size() == 1) 426 t = ed.getType().get(0).getCode(); 427 else if (ed.getType().size() == 0) 428 throw new Error("types == 0, and no children found"); 429 else { 430 t = ed.getType().get(0).getCode(); 431 boolean all = true; 432 for (TypeRefComponent tr : ed.getType()) { 433 if (!tr.getCode().equals(t)) { 434 all = false; 435 break; 436 } 437 } 438 if (!all) { 439 // ok, it's polymorphic 440 t = type.getType(); 441 } 442 } 443 if (!"xhtml".equals(t)) { 444 sd = context.fetchResource(StructureDefinition.class, t); 445 if (sd == null) 446 throw new DefinitionException("Unable to find class '"+t+"' for name '"+ed.getPath()+"' on property "+definition.getPath()); 447 children = profileUtilities.getChildMap(sd, sd.getSnapshot().getElement().get(0)); 448 } 449 } 450 List<Property> properties = new ArrayList<Property>(); 451 for (ElementDefinition child : children.getList()) { 452 properties.add(new Property(context, child, sd, this.profileUtilities)); 453 } 454 return properties; 455 } 456 457 private String tail(String path) { 458 return path.contains(".") ? path.substring(path.lastIndexOf(".")+1) : path; 459 } 460 461 public Property getChild(String elementName, String childName) throws FHIRException { 462 List<Property> children = getChildProperties(elementName, null); 463 for (Property p : children) { 464 if (p.getName().equals(childName)) { 465 return p; 466 } 467 } 468 return null; 469 } 470 471 public Property getChild(String name, TypeDetails type) throws DefinitionException { 472 List<Property> children = getChildProperties(type); 473 for (Property p : children) { 474 if (p.getName().equals(name) || p.getName().equals(name+"[x]")) { 475 return p; 476 } 477 } 478 return null; 479 } 480 481 public Property getChild(String name) throws FHIRException { 482 List<Property> children = getChildProperties(name, null); 483 for (Property p : children) { 484 if (p.getName().equals(name)) { 485 return p; 486 } 487 } 488 return null; 489 } 490 491 public Property getChildSimpleName(String elementName, String name) throws FHIRException { 492 List<Property> children = getChildProperties(elementName, null); 493 for (Property p : children) { 494 if (p.getName().equals(name) || p.getName().equals(name+"[x]")) { 495 return p; 496 } 497 } 498 return null; 499 } 500 501 public IWorkerContext getContext() { 502 return context; 503 } 504 505 @Override 506 public String toString() { 507 return definition.getPath(); 508 } 509 510 511 public boolean isJsonKeyArray() { 512 return definition.hasExtension(ToolingExtensions.EXT_JSON_PROP_KEY); 513 } 514 515 516 public String getJsonKeyProperty() { 517 return ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_JSON_PROP_KEY); 518 } 519 520 521 public boolean hasTypeSpecifier() { 522 return definition.hasExtension(ToolingExtensions.EXT_TYPE_SPEC); 523 } 524 525 526 public List<StringPair> getTypeSpecifiers() { 527 List<StringPair> res = new ArrayList<>(); 528 for (Extension e : definition.getExtensionsByUrl(ToolingExtensions.EXT_TYPE_SPEC)) { 529 res.add(new StringPair(ToolingExtensions.readStringExtension(e, "condition"), ToolingExtensions.readStringExtension(e, "type"))); 530 } 531 return res; 532 } 533 534 535 public Property cloneToType(StructureDefinition sd) { 536 Property res = new Property(context, definition.copy(), sd); 537 res.definition.getType().clear(); 538 res.definition.getType().add(new TypeRefComponent(sd.getUrl())); 539 return res; 540 } 541 542 543 public boolean hasImpliedPrefix() { 544 return definition.hasExtension(ToolingExtensions.EXT_IMPLIED_PREFIX); 545 } 546 547 548 public String getImpliedPrefix() { 549 return ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_IMPLIED_PREFIX); 550 } 551 552 553 public boolean isNullable() { 554 return ToolingExtensions.readBoolExtension(definition, ToolingExtensions.EXT_JSON_NULLABLE); 555 } 556 557 558 public String summary() { 559 return structure.getUrl()+"#"+definition.getId(); 560 } 561 562 563 public boolean canBeEmpty() { 564 if (definition.hasExtension(ToolingExtensions.EXT_JSON_EMPTY)) { 565 return !"absent".equals(ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_JSON_EMPTY)); 566 } else { 567 return false; 568 } 569 } 570 571 572 public boolean isLogical() { 573 return structure.getKind() == StructureDefinitionKind.LOGICAL; 574 } 575 576 577 public ProfileUtilities getUtils() { 578 return profileUtilities; 579 } 580 581 public boolean isJsonPrimitiveChoice() { 582 return ToolingExtensions.readBoolExtension(definition, ToolingExtensions.EXT_JSON_PRIMITIVE_CHOICE); 583 } 584 585 public Object typeSummary() { 586 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(" | "); 587 for (TypeRefComponent t : definition.getType()) { 588 b.append(t.getCode()); 589 } 590 return b.toString(); 591 } 592 593 594 public boolean hasJsonName() { 595 return definition.hasExtension(ToolingExtensions.EXT_JSON_NAME); 596 } 597 598 599}