001package org.hl7.fhir.r5.profilemodel; 002 003import java.util.ArrayList; 004import java.util.List; 005 006import org.apache.commons.lang3.NotImplementedException; 007import org.hl7.fhir.exceptions.DefinitionException; 008import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; 009import org.hl7.fhir.r5.context.ContextUtilities; 010import org.hl7.fhir.r5.context.IWorkerContext; 011import org.hl7.fhir.r5.model.Base; 012import org.hl7.fhir.r5.model.CanonicalType; 013import org.hl7.fhir.r5.model.ElementDefinition; 014import org.hl7.fhir.r5.model.ElementDefinition.DiscriminatorType; 015import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionSlicingComponent; 016import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionSlicingDiscriminatorComponent; 017import org.hl7.fhir.r5.model.ElementDefinition.SlicingRules; 018import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; 019import org.hl7.fhir.r5.model.Resource; 020import org.hl7.fhir.r5.model.ResourceFactory; 021import org.hl7.fhir.r5.model.StructureDefinition; 022import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule; 023import org.hl7.fhir.r5.utils.FHIRPathEngine; 024import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 025import org.hl7.fhir.utilities.Utilities; 026 027/** 028 * Factory class for the ProfiledElement sub-system 029 * 030 * *** NOTE: This sub-system is still under development *** 031 * 032 * This subsystem takes a profile and creates a view of the profile that stitches 033 * all the parts together, and presents it as a seemless tree. There's two views: 034 * 035 * - definition: A logical view of the contents of the profile 036 * - instance: a logical view of a resource that conforms to the profile 037 * 038 * The tree of elements in the profile model is different to the the base resource: 039 * - some elements are removed (max = 0) 040 * - extensions are turned into named elements 041 * - slices are turned into named elements 042 * - element properties - doco, cardinality, binding etc is updated for what the profile says 043 * 044 * Definition 045 * ---------- 046 * This presents a single view of the contents of a resource as specified by 047 * the profile. It's suitable for use in any kind of tree view. 048 * 049 * Each node has a unique name amongst it's siblings, but this name may not be 050 * the name in the instance, since slicing splits up a single named element into 051 * different definitions. 052 * 053 * Each node has: 054 * - name (unique amongst siblings) 055 * - schema name (the actual name in the instance) 056 * - min cardinality 057 * - max cardinality 058 * - short documentation (for the tree view) 059 * - full documentation (markdown source) 060 * - profile definition - the full definition in the profile 061 * - base definition - the full definition at the resource level 062 * - types() - a list of possible types 063 * - children(type) - a list of child nodes for the provided type 064 * - expansion - if there's a binding, the codes in the expansion based on the binding 065 * 066 * Note that the tree may not have leaves; the trees recurse indefinitely because 067 * extensions have extensions etc. So you can't do a depth-first search of the tree 068 * without some kind of decision to stop at a given point. 069 * 070 * Instance 071 * -------- 072 * 073 * todo 074 * 075 * @author grahamegrieve 076 * 077 */ 078public class PEBuilder { 079 080 public enum PEElementPropertiesPolicy { 081 NONE, EXTENSION, EXTENSION_ID 082 } 083 084 private IWorkerContext context; 085 private ProfileUtilities pu; 086 private ContextUtilities cu; 087 private PEElementPropertiesPolicy elementProps; 088 private boolean fixedPropsDefault; 089 private FHIRPathEngine fpe; 090 091 /** 092 * @param context - must be loaded with R5 definitions 093 * @param elementProps - whether to include Element.id and Element.extension in the tree. Recommended choice: Extension 094 */ 095 public PEBuilder(IWorkerContext context, PEElementPropertiesPolicy elementProps, boolean fixedPropsDefault) { 096 super(); 097 this.context = context; 098 this.elementProps = elementProps; 099 this.fixedPropsDefault = fixedPropsDefault; 100 pu = new ProfileUtilities(context, null, null); 101 cu = new ContextUtilities(context); 102 fpe = new FHIRPathEngine(context, pu); 103 } 104 105 /** 106 * Given a profile, return a tree of the elements defined in the profile model. This builds the profile model 107 * for the provided version of the nominated profile 108 * 109 * The tree of elements in the profile model is different to those defined in the base resource: 110 * - some elements are removed (max = 0) 111 * - extensions are turned into named elements 112 * - slices are turned into named elements 113 * - element properties - doco, cardinality, binding etc is updated for what the profile says 114 * 115 * Warning: profiles and resources are recursive; you can't iterate this tree until it you get 116 * to the leaves because there are nodes that don't terminate (extensions have extensions) 117 * 118 */ 119 public PEDefinition buildPEDefinition(StructureDefinition profile) { 120 if (!profile.hasSnapshot()) { 121 throw new DefinitionException("Profile '"+profile.getVersionedUrl()+"' does not have a snapshot"); 122 } 123 return new PEDefinitionResource(this, profile, profile.getName()); 124 } 125 126 /** 127 * Given a profile, return a tree of the elements defined in the profile model. This builds the profile model 128 * for the latest version of the nominated profile 129 * 130 * The tree of elements in the profile model is different to those defined in the base resource: 131 * - some elements are removed (max = 0) 132 * - extensions are turned into named elements 133 * - slices are turned into named elements 134 * - element properties - doco, cardinality, binding etc is updated for what the profile says 135 * 136 * Warning: profiles and resources are recursive; you can't iterate this tree until it you get 137 * to the leaves because there are nodes that don't terminate (extensions have extensions) 138 * 139 */ 140 public PEDefinition buildPEDefinition(String url) { 141 StructureDefinition profile = getProfile(url); 142 if (profile == null) { 143 throw new DefinitionException("Unable to find profile for URL '"+url+"'"); 144 } 145 if (!profile.hasSnapshot()) { 146 throw new DefinitionException("Profile '"+url+"' does not have a snapshot"); 147 } 148 return new PEDefinitionResource(this, profile, profile.getName()); 149 } 150 151 /** 152 * Given a profile, return a tree of the elements defined in the profile model. This builds the profile model 153 * for the nominated version of the nominated profile 154 * 155 * The tree of elements in the profile model is different to the the base resource: 156 * - some elements are removed (max = 0) 157 * - extensions are turned into named elements 158 * - slices are turned into named elements 159 * - element properties - doco, cardinality, binding etc is updated for what the profile says 160 * 161 * Warning: profiles and resources can be recursive; you can't iterate this tree until it you get 162 * to the leaves because you will never get to a child that doesn't have children 163 * 164 */ 165 public PEDefinition buildPEDefinition(String url, String version) { 166 StructureDefinition profile = getProfile(url, version); 167 if (profile == null) { 168 throw new DefinitionException("Unable to find profile for URL '"+url+"'"); 169 } 170 if (!profile.hasSnapshot()) { 171 throw new DefinitionException("Profile '"+url+"' does not have a snapshot"); 172 } 173 return new PEDefinitionResource(this, profile, profile.getName()); 174 } 175 176 /** 177 * Given a resource and a profile, return a tree of instance data as defined by the profile model 178 * using the latest version of the profile 179 * 180 * The tree is a facade to the underlying resource - all actual data is stored against the resource, 181 * and retrieved on the fly from the resource, so that applications can work at either level, as 182 * convenient. 183 * 184 * Note that there's a risk that deleting something through the resource while holding 185 * a handle to a PEInstance that is a facade on what is deleted leaves an orphan facade 186 * that will continue to function, but is making changes to resource content that is no 187 * longer part of the resource 188 * 189 */ 190 public PEInstance buildPEInstance(String url, Resource resource) { 191 PEDefinition defn = buildPEDefinition(url); 192 return loadInstance(defn, resource); 193 } 194 195 /** 196 * Given a resource and a profile, return a tree of instance data as defined by the profile model 197 * using the provided version of the profile 198 * 199 * The tree is a facade to the underlying resource - all actual data is stored against the resource, 200 * and retrieved on the fly from the resource, so that applications can work at either level, as 201 * convenient. 202 * 203 * Note that there's a risk that deleting something through the resource while holding 204 * a handle to a PEInstance that is a facade on what is deleted leaves an orphan facade 205 * that will continue to function, but is making changes to resource content that is no 206 * longer part of the resource 207 * 208 */ 209 public PEInstance buildPEInstance(StructureDefinition profile, Resource resource) { 210 PEDefinition defn = buildPEDefinition(profile); 211 return loadInstance(defn, resource); 212 } 213 214 /** 215 * Given a resource and a profile, return a tree of instance data as defined by the profile model 216 * using the nominated version of the profile 217 * 218 * The tree is a facade to the underlying resource - all actual data is stored against the resource, 219 * and retrieved on the fly from the resource, so that applications can work at either level, as 220 * convenient. 221 * 222 * Note that there's a risk that deleting something through the resource while holding 223 * a handle to a PEInstance that is a facade on what is deleted leaves an orphan facade 224 * that will continue to function, but is making changes to resource content that is no 225 * longer part of the resource 226 */ 227 public PEInstance buildPEInstance(String url, String version, Resource resource) { 228 PEDefinition defn = buildPEDefinition(url, version); 229 return loadInstance(defn, resource); 230 } 231 232 /** 233 * For the current version of a profile, construct a resource and fill out any fixed or required elements 234 * 235 * Note that fixed values are filled out irrespective of the value of fixedProps when the builder is created 236 * 237 * @param url identifies the profile 238 * @param version identifies the version of the profile 239 * @param meta whether to mark the profile in Resource.meta.profile 240 * @return constructed resource 241 */ 242 public Resource createResource(String url, String version, boolean meta) { 243 PEDefinition definition = buildPEDefinition(url, version); 244 Resource res = ResourceFactory.createResource(definition.types().get(0).getType()); 245 populateByProfile(res, definition); 246 if (meta) { 247 res.getMeta().addProfile(definition.profile.getUrl()); 248 } 249 return res; 250 } 251 252 /** 253 * For the provided version of a profile, construct a resource and fill out any fixed or required elements 254 * 255 * Note that fixed values are filled out irrespective of the value of fixedProps when the builder is created 256 * 257 * @param profile the profile 258 * @param meta whether to mark the profile in Resource.meta.profile 259 * @return constructed resource 260 */ 261 public Resource createResource(StructureDefinition profile, boolean meta) { 262 PEDefinition definition = buildPEDefinition(profile); 263 Resource res = ResourceFactory.createResource(definition.types().get(0).getType()); 264 populateByProfile(res, definition); 265 if (meta) { 266 res.getMeta().addProfile(definition.profile.getUrl()); 267 } 268 return res; 269 } 270 271 /** 272 * For the current version of a profile, construct a resource and fill out any fixed or required elements 273 * 274 * Note that fixed values are filled out irrespective of the value of fixedProps when the builder is created 275 * 276 * @param url identifies the profile 277 * @param meta whether to mark the profile in Resource.meta.profile 278 * @return constructed resource 279 */ 280 public Resource createResource(String url, boolean meta) { 281 PEDefinition definition = buildPEDefinition(url); 282 Resource res = ResourceFactory.createResource(definition.types().get(0).getType()); 283 populateByProfile(res, definition); 284 if (meta) { 285 res.getMeta().addProfile(definition.profile.getUrl()); 286 } 287 return res; 288 } 289 290 291 292 // -- methods below here are only used internally to the package 293 294 private StructureDefinition getProfile(String url) { 295 return context.fetchResource(StructureDefinition.class, url); 296 } 297 298 299 private StructureDefinition getProfile(String url, String version) { 300 return context.fetchResource(StructureDefinition.class, url, version); 301 } 302// 303// protected List<PEDefinition> listChildren(boolean allFixed, StructureDefinition profileStructure, ElementDefinition definition, TypeRefComponent t, CanonicalType u) { 304// // TODO Auto-generated method stub 305// return null; 306// } 307 308 protected List<PEDefinition> listChildren(boolean allFixed, PEDefinition parent, StructureDefinition profileStructure, ElementDefinition definition, String url, String... omitList) { 309 StructureDefinition profile = profileStructure; 310 List<ElementDefinition> list = pu.getChildList(profile, definition); 311 if (definition.getType().size() == 1 || (!definition.getPath().contains(".")) || list.isEmpty()) { 312 assert url == null || checkType(definition, url); 313 List<PEDefinition> res = new ArrayList<>(); 314 if (list.size() == 0) { 315 profile = context.fetchResource(StructureDefinition.class, url); 316 list = pu.getChildList(profile, profile.getSnapshot().getElementFirstRep()); 317 } 318 if (list.size() > 0) { 319 int i = 0; 320 while (i < list.size()) { 321 ElementDefinition defn = list.get(i); 322 if (!defn.getMax().equals("0") && (allFixed || include(defn))) { 323 if (passElementPropsCheck(defn) && !Utilities.existsInList(defn.getName(), omitList)) { 324 PEDefinitionElement pe = new PEDefinitionElement(this, profile, defn, parent.path()); 325 pe.setRecursing(definition == defn || (profile.getDerivation() == TypeDerivationRule.SPECIALIZATION && profile.getType().equals("Extension"))); 326 if (cu.isPrimitiveDatatype(definition.getTypeFirstRep().getWorkingCode()) && "value".equals(pe.name())) { 327 pe.setMustHaveValue(definition.getMustHaveValue()); 328 } 329 pe.setInFixedValue(definition.hasFixed() || definition.hasPattern() || parent.isInFixedValue()); 330 if (defn.hasSlicing()) { 331 if (defn.getSlicing().getRules() != SlicingRules.CLOSED) { 332 res.add(pe); 333 } 334 i++; 335 while (i < list.size() && list.get(i).getPath().equals(defn.getPath())) { 336 StructureDefinition ext = getExtensionDefinition(list.get(i)); 337 if (ext != null) { 338 res.add(new PEDefinitionExtension(this, list.get(i).getSliceName(), profile, list.get(i), defn, ext, parent.path())); 339 } else if (isTypeSlicing(defn)) { 340 res.add(new PEDefinitionTypeSlice(this, list.get(i).getSliceName(), profile, list.get(i), defn, parent.path())); 341 } else { 342 res.add(new PEDefinitionSlice(this, list.get(i).getSliceName(), profile, list.get(i), defn, parent.path())); 343 } 344 i++; 345 } 346 } else { 347 res.add(pe); 348 i++; 349 } 350 } else { 351 i++; 352 } 353 } else { 354 i++; 355 } 356 } 357 } 358 return res; 359 } else if (list.isEmpty()) { 360 throw new DefinitionException("not done yet!"); 361 } else { 362 throw new DefinitionException("not done yet"); 363 } 364 } 365 366 private boolean passElementPropsCheck(ElementDefinition bdefn) { 367 switch (elementProps) { 368 case EXTENSION: 369 return !Utilities.existsInList(bdefn.getBase().getPath(), "Element.id"); 370 case NONE: 371 return !Utilities.existsInList(bdefn.getBase().getPath(), "Element.id", "Element.extension"); 372 case EXTENSION_ID: 373 default: 374 return true; 375 } 376 } 377 378 private boolean isTypeSlicing(ElementDefinition defn) { 379 ElementDefinitionSlicingComponent sl = defn.getSlicing(); 380 return sl.getRules() == SlicingRules.CLOSED && sl.getDiscriminator().size() == 1 && 381 sl.getDiscriminatorFirstRep().getType() == DiscriminatorType.TYPE && "$this".equals(sl.getDiscriminatorFirstRep().getPath()); 382 } 383 384 private boolean include(ElementDefinition defn) { 385 if (fixedPropsDefault) { 386 return true; 387 } else { 388 return !(defn.hasFixed() || defn.hasPattern()); 389 } 390 } 391 392 protected List<PEDefinition> listSlices(StructureDefinition profileStructure, ElementDefinition definition, PEDefinition parent) { 393 List<ElementDefinition> list = pu.getSliceList(profileStructure, definition); 394 List<PEDefinition> res = new ArrayList<>(); 395 for (ElementDefinition ed : list) { 396 if (profileStructure.getDerivation() == TypeDerivationRule.CONSTRAINT && profileStructure.getType().equals("Extension")) { 397 res.add(new PEDefinitionSubExtension(this, profileStructure, ed, parent.path())); 398 } else { 399 PEDefinitionElement pe = new PEDefinitionElement(this, profileStructure, ed, parent.path()); 400 pe.setRecursing(definition == ed || (profileStructure.getDerivation() == TypeDerivationRule.SPECIALIZATION && profileStructure.getType().equals("Extension"))); 401 res.add(pe); 402 } 403 } 404 return res; 405 } 406 407 408 private boolean checkType(ElementDefinition defn, String url) { 409 for (TypeRefComponent t : defn.getType()) { 410 if (("http://hl7.org/fhir/StructureDefinition/"+t.getWorkingCode()).equals(url)) { 411 return true; 412 } 413 for (CanonicalType u : t.getProfile()) { 414 if (url.equals(u.getValue())) { 415 return true; 416 } 417 } 418 } 419 return false; 420 } 421 422 423 private StructureDefinition getExtensionDefinition(ElementDefinition ed) { 424 if ("Extension".equals(ed.getTypeFirstRep().getWorkingCode()) && ed.getTypeFirstRep().getProfile().size() == 1) { 425 return context.fetchResource(StructureDefinition.class, ed.getTypeFirstRep().getProfile().get(0).asStringValue()); 426 } else { 427 return null; 428 } 429 } 430 431 432 private ElementDefinition getByName(List<ElementDefinition> blist, String name) { 433 for (ElementDefinition ed : blist) { 434 if (name.equals(ed.getName())) { 435 return ed; 436 } 437 } 438 return null; 439 } 440 441 442 protected PEType makeType(TypeRefComponent t) { 443 if (t.hasProfile()) { 444 StructureDefinition sd = context.fetchResource(StructureDefinition.class, t.getProfile().get(0).getValue()); 445 if (sd == null) { 446 return new PEType(tail(t.getProfile().get(0).getValue()), t.getWorkingCode(), t.getProfile().get(0).getValue()); 447 } else { 448 return new PEType(sd.getName(), t.getWorkingCode(), t.getProfile().get(0).getValue()); 449 } 450 } else { 451 return makeType(t.getWorkingCode()); 452 } 453 } 454 455 protected PEType makeType(TypeRefComponent t, CanonicalType u) { 456 StructureDefinition sd = context.fetchResource(StructureDefinition.class, u.getValue()); 457 if (sd == null) { 458 return new PEType(tail(u.getValue()), t.getWorkingCode(), u.getValue()); 459 } else { 460 return new PEType(sd.getName(), t.getWorkingCode(), u.getValue()); 461 } 462 } 463 464 465 protected PEType makeType(String tn) { 466 return new PEType(tn, tn, "http://hl7.org/fhir/StructureDefinition/"+ tn); 467 } 468 469 private String tail(String value) { 470 return value.contains("/") ? value.substring(value.lastIndexOf("/")+1) : value; 471 } 472 473 protected List<ElementDefinition> getChildren(StructureDefinition profileStructure, ElementDefinition definition) { 474 return pu.getChildList(profileStructure, definition); 475 } 476 477 private PEInstance loadInstance(PEDefinition defn, Resource resource) { 478 return new PEInstance(this, defn, resource, resource, defn.name()); 479 } 480 481 public IWorkerContext getContext() { 482 return context; 483 } 484 485 protected void populateByProfile(Base base, PEDefinition definition) { 486 for (PEDefinition pe : definition.children(true)) { 487 if (pe.fixedValue()) { 488 if (pe.definition().hasPattern()) { 489 base.setProperty(pe.schemaName(), pe.definition().getPattern()); 490 } else { 491 base.setProperty(pe.schemaName(), pe.definition().getFixed()); 492 } 493 } else { 494 for (int i = 0; i < pe.min(); i++) { 495 Base b = null; 496 if (pe.schemaName().endsWith("[x]")) { 497 if (pe.types().size() == 1) { 498 b = base.addChild(pe.schemaName().replace("[x]", Utilities.capitalize(pe.types().get(0).getType()))); 499 } 500 } else { 501 b = base.addChild(pe.schemaName()); 502 } 503 if (b != null) { 504 populateByProfile(b, pe); 505 } 506 } 507 } 508 } 509 } 510 511 public String makeSliceExpression(StructureDefinition profile, ElementDefinitionSlicingComponent slicing, ElementDefinition definition) { 512 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(" and "); 513 for (ElementDefinitionSlicingDiscriminatorComponent d : slicing.getDiscriminator()) { 514 switch (d.getType()) { 515 case EXISTS: 516 throw new DefinitionException("The discriminator type 'exists' is not supported by the PEBuilder"); 517 case PATTERN: 518 throw new DefinitionException("The discriminator type 'pattern' is not supported by the PEBuilder"); 519 case POSITION: 520 throw new DefinitionException("The discriminator type 'position' is not supported by the PEBuilder"); 521 case PROFILE: 522 throw new DefinitionException("The discriminator type 'profile' is not supported by the PEBuilder"); 523 case TYPE: 524 throw new DefinitionException("The discriminator type 'type' is not supported by the PEBuilder"); 525 case VALUE: 526 String path = d.getPath(); 527 if (path.contains(".")) { 528 throw new DefinitionException("The discriminator path '"+path+"' is not supported by the PEBuilder"); 529 } 530 ElementDefinition ed = getChildElement(profile, definition, path); 531 if (ed == null) { 532 throw new DefinitionException("The discriminator path '"+path+"' could not be resolved by the PEBuilder"); 533 } 534 if (!ed.hasFixed()) { 535 throw new DefinitionException("The discriminator path '"+path+"' has no fixed value - this is not supported by the PEBuilder"); 536 } 537 if (!ed.getFixed().isPrimitive()) { 538 throw new DefinitionException("The discriminator path '"+path+"' has a fixed value that is not a primitive ("+ed.getFixed().fhirType()+") - this is not supported by the PEBuilder"); 539 } 540 b.append(path+" = '"+ed.getFixed().primitiveValue()+"'"); 541 break; 542 case NULL: 543 throw new DefinitionException("The discriminator type 'null' is not supported by the PEBuilder"); 544 default: 545 throw new DefinitionException("The discriminator type '??' is not supported by the PEBuilder"); 546 } 547 } 548 return b.toString(); 549 } 550 551 private ElementDefinition getChildElement(StructureDefinition profile, ElementDefinition definition, String path) { 552 List<ElementDefinition> elements = pu.getChildList(profile, definition); 553 if (elements.size() == 0) { 554 profile = definition.getTypeFirstRep().hasProfile() ? context.fetchResource(StructureDefinition.class, definition.getTypeFirstRep().getProfile().get(0).asStringValue()) : 555 context.fetchTypeDefinition(definition.getTypeFirstRep().getWorkingCode()); 556 elements = pu.getChildList(profile, profile.getSnapshot().getElementFirstRep()); 557 } 558 return getByName(elements, path); 559 } 560 561 public List<Base> exec(Resource resource, Base data, String fhirpath) { 562 return fpe.evaluate(this, resource, resource, data, fhirpath); 563 } 564 565 public boolean isResource(String name) { 566 return cu.isResource(name); 567 } 568}