
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 seamless 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 pe.setSlicer(true); 334 } 335 i++; 336 while (i < list.size() && list.get(i).getPath().equals(defn.getPath())) { 337 StructureDefinition ext = getExtensionDefinition(list.get(i)); 338 if (ext != null) { 339 res.add(new PEDefinitionExtension(this, list.get(i).getSliceName(), profile, list.get(i), defn, ext, parent.path())); 340 } else if (isTypeSlicing(defn)) { 341 res.add(new PEDefinitionTypeSlice(this, list.get(i).getSliceName(), profile, list.get(i), defn, parent.path())); 342 } else { 343 res.add(new PEDefinitionSlice(this, list.get(i).getSliceName(), profile, list.get(i), defn, parent.path())); 344 } 345 i++; 346 } 347 } else { 348 res.add(pe); 349 i++; 350 } 351 } else { 352 i++; 353 } 354 } else { 355 i++; 356 } 357 } 358 } 359 return res; 360 } else if (list.isEmpty()) { 361 throw new DefinitionException("not done yet!"); 362 } else { 363 throw new DefinitionException("not done yet"); 364 } 365 } 366 367 private boolean passElementPropsCheck(ElementDefinition bdefn) { 368 switch (elementProps) { 369 case EXTENSION: 370 return !Utilities.existsInList(bdefn.getBase().getPath(), "Element.id"); 371 case NONE: 372 return !Utilities.existsInList(bdefn.getBase().getPath(), "Element.id", "Element.extension"); 373 case EXTENSION_ID: 374 default: 375 return true; 376 } 377 } 378 379 private boolean isTypeSlicing(ElementDefinition defn) { 380 ElementDefinitionSlicingComponent sl = defn.getSlicing(); 381 return sl.getRules() == SlicingRules.CLOSED && sl.getDiscriminator().size() == 1 && 382 sl.getDiscriminatorFirstRep().getType() == DiscriminatorType.TYPE && "$this".equals(sl.getDiscriminatorFirstRep().getPath()); 383 } 384 385 private boolean include(ElementDefinition defn) { 386 if (fixedPropsDefault) { 387 return true; 388 } else { 389 return !(defn.hasFixed() || defn.hasPattern()); 390 } 391 } 392 393 protected List<PEDefinition> listSlices(StructureDefinition profileStructure, ElementDefinition definition, PEDefinition parent) { 394 List<ElementDefinition> list = pu.getSliceList(profileStructure, definition); 395 List<PEDefinition> res = new ArrayList<>(); 396 for (ElementDefinition ed : list) { 397 if (profileStructure.getDerivation() == TypeDerivationRule.CONSTRAINT && profileStructure.getType().equals("Extension")) { 398 res.add(new PEDefinitionSubExtension(this, profileStructure, ed, parent.path())); 399 } else { 400 PEDefinitionElement pe = new PEDefinitionElement(this, profileStructure, ed, parent.path()); 401 pe.setRecursing(definition == ed || (profileStructure.getDerivation() == TypeDerivationRule.SPECIALIZATION && profileStructure.getType().equals("Extension"))); 402 res.add(pe); 403 } 404 } 405 return res; 406 } 407 408 409 private boolean checkType(ElementDefinition defn, String url) { 410 for (TypeRefComponent t : defn.getType()) { 411 if (("http://hl7.org/fhir/StructureDefinition/"+t.getWorkingCode()).equals(url)) { 412 return true; 413 } 414 for (CanonicalType u : t.getProfile()) { 415 if (url.equals(u.getValue())) { 416 return true; 417 } 418 } 419 } 420 return false; 421 } 422 423 424 private StructureDefinition getExtensionDefinition(ElementDefinition ed) { 425 if ("Extension".equals(ed.getTypeFirstRep().getWorkingCode()) && ed.getTypeFirstRep().getProfile().size() == 1) { 426 return context.fetchResource(StructureDefinition.class, ed.getTypeFirstRep().getProfile().get(0).asStringValue()); 427 } else { 428 return null; 429 } 430 } 431 432 433 private ElementDefinition getByName(List<ElementDefinition> blist, String name) { 434 for (ElementDefinition ed : blist) { 435 if (name.equals(ed.getName())) { 436 return ed; 437 } 438 } 439 return null; 440 } 441 442 443 protected PEType makeType(TypeRefComponent t) { 444 if (t.hasProfile()) { 445 StructureDefinition sd = context.fetchResource(StructureDefinition.class, t.getProfile().get(0).getValue()); 446 if (sd == null) { 447 return new PEType(tail(t.getProfile().get(0).getValue()), t.getWorkingCode(), t.getProfile().get(0).getValue()); 448 } else { 449 return new PEType(sd.getName(), t.getWorkingCode(), t.getProfile().get(0).getValue()); 450 } 451 } else { 452 return makeType(t.getWorkingCode()); 453 } 454 } 455 456 protected PEType makeType(TypeRefComponent t, CanonicalType u) { 457 StructureDefinition sd = context.fetchResource(StructureDefinition.class, u.getValue()); 458 if (sd == null) { 459 return new PEType(tail(u.getValue()), t.getWorkingCode(), u.getValue()); 460 } else { 461 return new PEType(sd.getName(), t.getWorkingCode(), u.getValue()); 462 } 463 } 464 465 466 protected PEType makeType(String tn) { 467 return new PEType(tn, tn, "http://hl7.org/fhir/StructureDefinition/"+ tn); 468 } 469 470 private String tail(String value) { 471 return value.contains("/") ? value.substring(value.lastIndexOf("/")+1) : value; 472 } 473 474 protected List<ElementDefinition> getChildren(StructureDefinition profileStructure, ElementDefinition definition) { 475 return pu.getChildList(profileStructure, definition); 476 } 477 478 private PEInstance loadInstance(PEDefinition defn, Resource resource) { 479 return new PEInstance(this, defn, resource, resource, defn.name()); 480 } 481 482 public IWorkerContext getContext() { 483 return context; 484 } 485 486 protected void populateByProfile(Base base, PEDefinition definition) { 487 for (PEDefinition pe : definition.children(true)) { 488 System.out.println("PopulateByProfile for "+pe.path); 489 if (pe.fixedValue()) { 490 if (pe.definition().hasPattern()) { 491 base.setProperty(pe.schemaName(), pe.definition().getPattern()); 492 } else { 493 base.setProperty(pe.schemaName(), pe.definition().getFixed()); 494 } 495 } else if (!pe.isSlicer()) { 496 for (int i = 0; i < pe.min(); i++) { 497 Base b = null; 498 if (pe.schemaName().endsWith("[x]")) { 499 if (pe.types().size() == 1) { 500 b = base.addChild(pe.schemaName().replace("[x]", Utilities.capitalize(pe.types().get(0).getType()))); 501 } 502 } else if (!pe.isBaseList()) { 503 b = base.makeProperty(pe.schemaName().hashCode(), pe.schemaName()); 504 } else { 505 b = base.addChild(pe.schemaName()); 506 } 507 if (b != null) { 508 populateByProfile(b, pe); 509 } 510 } 511 } 512 } 513 } 514 515 public String makeSliceExpression(StructureDefinition profile, ElementDefinitionSlicingComponent slicing, ElementDefinition definition) { 516 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(" and "); 517 for (ElementDefinitionSlicingDiscriminatorComponent d : slicing.getDiscriminator()) { 518 switch (d.getType()) { 519 case EXISTS: 520 throw new DefinitionException("The discriminator type 'exists' is not supported by the PEBuilder"); 521 case PATTERN: 522 throw new DefinitionException("The discriminator type 'pattern' is not supported by the PEBuilder"); 523 case POSITION: 524 throw new DefinitionException("The discriminator type 'position' is not supported by the PEBuilder"); 525 case PROFILE: 526 throw new DefinitionException("The discriminator type 'profile' is not supported by the PEBuilder"); 527 case TYPE: 528 throw new DefinitionException("The discriminator type 'type' is not supported by the PEBuilder"); 529 case VALUE: 530 String path = d.getPath(); 531 if (path.contains(".")) { 532 throw new DefinitionException("The discriminator path '"+path+"' is not supported by the PEBuilder"); 533 } 534 ElementDefinition ed = getChildElement(profile, definition, path); 535 if (ed == null) { 536 throw new DefinitionException("The discriminator path '"+path+"' could not be resolved by the PEBuilder"); 537 } 538 if (!ed.hasFixed()) { 539 throw new DefinitionException("The discriminator path '"+path+"' has no fixed value - this is not supported by the PEBuilder"); 540 } 541 if (!ed.getFixed().isPrimitive()) { 542 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"); 543 } 544 b.append(path+" = '"+ed.getFixed().primitiveValue()+"'"); 545 break; 546 case NULL: 547 throw new DefinitionException("The discriminator type 'null' is not supported by the PEBuilder"); 548 default: 549 throw new DefinitionException("The discriminator type '??' is not supported by the PEBuilder"); 550 } 551 } 552 return b.toString(); 553 } 554 555 private ElementDefinition getChildElement(StructureDefinition profile, ElementDefinition definition, String path) { 556 List<ElementDefinition> elements = pu.getChildList(profile, definition); 557 if (elements.size() == 0) { 558 profile = definition.getTypeFirstRep().hasProfile() ? context.fetchResource(StructureDefinition.class, definition.getTypeFirstRep().getProfile().get(0).asStringValue()) : 559 context.fetchTypeDefinition(definition.getTypeFirstRep().getWorkingCode()); 560 elements = pu.getChildList(profile, profile.getSnapshot().getElementFirstRep()); 561 } 562 return getByName(elements, path); 563 } 564 565 public List<Base> exec(Resource resource, Base data, String fhirpath) { 566 return fpe.evaluate(this, resource, resource, data, fhirpath); 567 } 568 569 public boolean isResource(String name) { 570 return cu.isResource(name); 571 } 572}