From 9b91f1389f8a14af2da9ee5d693f1992f08cc7dc Mon Sep 17 00:00:00 2001 From: dotasek Date: Tue, 9 May 2023 17:33:55 -0400 Subject: [PATCH] Update Conversion package docs (#1257) * Reorganize README.md, expand details * Update conversion README.md document --- org.hl7.fhir.convertors/README.md | 251 ++++++++++++------ .../convertors/advisors/Expression50Test.java | 2 +- .../AllergyIntolerance10_30Test.java | 2 +- .../AllergyIntolerance40_50Test.java | 36 +++ 4 files changed, 211 insertions(+), 80 deletions(-) create mode 100644 org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv40_50/AllergyIntolerance40_50Test.java diff --git a/org.hl7.fhir.convertors/README.md b/org.hl7.fhir.convertors/README.md index ea5333005..7b4e6b3e5 100644 --- a/org.hl7.fhir.convertors/README.md +++ b/org.hl7.fhir.convertors/README.md @@ -11,12 +11,33 @@ Implementers should regard this code as a 'scaffold' for actual reliable convers _Ideally, this should be via unit tests in your code, or better yet [unit tests contributed to FHIR](#test-cases)._ -### A note regarding syntax +## Using the Conversion package +### Basic Usage + +The majority of conversion tasks should be performed by the Version Convertor factories: + +* VersionConvertorFactory_10_30 +* VersionConvertorFactory_10_40 +* VersionConvertorFactory_10_50 +* VersionConvertorFactory_14_30 +* VersionConvertorFactory_14_40 +* VersionConvertorFactory_14_50 +* VersionConvertorFactory_30_40 +* VersionConvertorFactory_30_50 +* VersionConvertorFactory_40_50 +* VersionConvertorFactory_43_50 + +These factories all use the following convention: + +`VersionConvertorFactory_` + `(VERSION A)` + `_` + `(VERSION B)` + +Each factory allows conversion between two versions of FHIR, specified by `VERSION A` and `VERSION B`. The syntax of each version is described briefly in the following section. + +### Conversion Version Syntax ----- -Within the code, we use a set naming convention to organize the classes used for conversion between the various versions -of FHIR. +Within the code, we use a set naming convention to organize the classes used for conversion between the various versions of FHIR. | Version | Code | | :--- | :---: | @@ -26,27 +47,12 @@ of FHIR. | r4 | 40 | | r5 | 50 | -The files themselves follow the naming convention: +So, for example, VersionConvertorFactory_10_40 allows the conversion of resources and types to and from dtu2 (10) and r4 (50). -`(NAME)` + `(VERSION CODE)` + `_` + `(VERSION CODE)` - -Where `NAME` is the proper name of the resource or datatype being converted, and the two `VERSION CODE` indicate the two -versions of FHIR that the code will convert the given resource or datatype between. - -So, in the repository, you may come across a file name `Account30_40`. This would indicate that the code in this -file is related to the conversion of the Account resource between versions [dstu3](http://hl7.org/fhir/STU3/account.html) -and [r4](http://hl7.org/fhir/R4/account.html) - -**N.B.** This information is only for code navigation purposes. It is important that when converting between versions -you use the provided conversion factory classes as your entry point. - -## Using the conversion library +### Conversion Factory Usage ----- -The majority of use cases for conversion will involve using the provided VersionConvertorFactory_V1_V2 classes to convert -to and from the various versions of FHIR. - -They provide two statically accessed base methods for converting resources: +Each VersionConvertorFactory provides two statically accessed base methods for converting resources: `public static (V1 Resource) convertResource((V2 Resource) src)` @@ -65,41 +71,131 @@ case of r5), so the result will need to be cast to the correct class. Example: ```java - // Converting a r5 StructureDefinition to dstu3. - org.hl7.fhir.r5.model.StructureDefinition r5_structure_def = new StructureDefinition(); - org.hl7.fhir.dstu3.model.StructureDefinition dstu3_converted_structure_def - = (StructureDefinition) VersionConvertorFactory_30_50.convertResource(r5_structure_def); + // Converting a r4 AllergyIntolerance to r5. + org.hl7.fhir.r5.model.Resource r5Resource = VersionConvertorFactory_40_50.convertResource(r4AllergyIntolerance); + org.hl7.fhir.r5.model.AllergyIntolerance r5AllergyIntolerance = (org.hl7.fhir.r5.model.AllergyIntolerance) r5Resource; ``` -### It gets complicated... +## Developers Notes ------ +If you are developing or debugging conversion routines, you will likely need to access the individual Resource, DataType, and Primitive conversion classes. -As the specification has evolved over time, the versions of FHIR have built on top of one another, adding new fields -within existing resources, changing the name of existing resources, or adding entirely new resources altogether. As a -result of this conversions are inherently lossy operations. +These are located in the following packages: -A quick example of this would be [ValueSet Expression](https://www.hl7.org/fhir/extension-valueset-expression.html) -extension type. This exists in the r4 version of the specification, but no such type exists in dstu2. +* org.hl7.fhir.convertors.conv10_30 +* org.hl7.fhir.convertors.conv10_40 +* org.hl7.fhir.convertors.conv10_50 +* org.hl7.fhir.convertors.conv14_30 +* org.hl7.fhir.convertors.conv14_40 +* org.hl7.fhir.convertors.conv14_50 +* org.hl7.fhir.convertors.conv30_40 +* org.hl7.fhir.convertors.conv30_50 +* org.hl7.fhir.convertors.conv40_50 +* org.hl7.fhir.convertors.conv43_50 -If we were to convert a R4 resource, such as a questionnaire, that contained an extension of this type from r4 -> dstu2, -without any special intervention, the extension would be ignored, and the data would be lost in the conversion process. +These classes follow the convention: + +`(NAME)` + `(VERSION A)` + `_` + `(VERSION B)` + +Where `NAME` is the proper name of the resource or datatype being converted, and `VERSION A` and `VERSION B` indicate the two versions of FHIR that the code will convert the given resource or datatype between (See [Conversion Version Syntax](#-conversion-version-syntax) for version details). + +So, in the repository, you may come across a file name `AllergyIntolerance40_50`. This would indicate that the code in this file is related to the conversion of the AllergyIntolerance resource between versions [r4](http://hl7.org/fhir/r4/allergyintolerance.html) and [r5](https://hl7.org/fhir/r5/allergyintolerance.html) + +Note that these classes are not intended to be used directly. When actually converting resources, the provided conversion factory classes are intended to be used as the entry point. For example, to convert a dstu3 AllergyIntolerance resource, the above conversion would not use `AllergyIntolerance40_50` directly, but would instead call: `VersionConvertorFactory_40_50.convertResource(dstu3AllergyIntolerance)`. `VersionConvertorFactory_40_50` would call `AllergyIntolerance40_50` internally to convert `r4AllergyIntolerance`. + +### Common Conversion Scenarios + +Conversion classes are implemented using some simple, repeatable patterns. `AllegeryIntolerance40_50` will be used as an example of this. Each conversion class for a resource will have two entry points, allowing for conversions to be done to and from the two versions in the convertor. + +```java +public static org.hl7.fhir.r5.model.AllergyIntolerance convertAllergyIntolerance(org.hl7.fhir.r4.model.AllergyIntolerance src); +public static org.hl7.fhir.r4.model.AllergyIntolerance convertAllergyIntolerance(org.hl7.fhir.r5.model.AllergyIntolerance src) +``` + +Initially, a target resource is created in the appropriate method. Upon the completion of the conversion, this target resource is returned. In our case, the target resource is +version 50, or r5. + +```java +public static org.hl7.fhir.r5.model.AllergyIntolerance convertAllergyIntolerance(org.hl7.fhir.r4.model.AllergyIntolerance src){ + + //... + + org.hl7.fhir.r5.model.AllergyIntolerance tgt = new org.hl7.fhir.r5.model.AllergyIntolerance(); + + //... +} +``` + +After the target resource is created, the elements of the source resource need to be converted to the target version, and added to the target resource. Many elements can be copied automatically using the static methods provided in the `ConversionContext` and `VersionConvertor` classes. These classes follow the convention: + +`ConversionContext` + `(VERSION A)` + `_` + `(VERSION B)` + +and + +`VersionConvertor` + `(VERSION A)` + `_` + `(VERSION B)` + +An example usage is in the copying of [DomainResource](https://build.fhir.org/domainresource.html) elements (`text`, `contained`, `extension`, and `modifierExtension`). In FHIR, all listed Resources except Bundle, Parameters and Binary extend DomainResource. Copying DomainResource elements is done using the following code: + +```java +ConversionContext40_50.INSTANCE.getVersionConvertor_40_50().copyDomainResource(src, tgt); +``` + +For elements more specific to the resource being converted, we find the appropriate type convertor class, and set the target element directly: + +```java +if (src.hasClinicalStatus()) + tgt.setClinicalStatus(CodeableConcept40_50.convertCodeableConcept(src.getClinicalStatus())); +``` + +### Converting Extensions + +A special case exists for the conversion of extensions. As mentioned above, the `copyDomainResource(src, tgt)` method is used to copy the extensions from one resource to another. This applies a default conversion process to all extensions (see [Using conversion advisors](#using-conversion-advisors) for details). + +However, in some conversion cases, an extension may exist that can be converted into a resource element. An example of this is the `acceptUnknown` element in the dstu3 [CapabilityStatement](http://hl7.org/fhir/STU3/capabilitystatement-definitions.html#CapabilityStatement.acceptUnknown) resource. This element does not exist in versions r4 and up, so is converted into an extension with the url `http://hl7.org/fhir/3.0/StructureDefinition/extension-CapabilityStatement.acceptUnknown`. Should this extension exist in a resource being converted to a CapabilityStatement in dstu3, the convertor needs to convert this extension to an element, and to indicate to the copyDomainResource method that the extension should not be copied. + +First, since the copyDomainResource occurs early in the conversion process, we need to indicate all the ignored URLs using the vararg parameter `extensionUrlsToIgnore`: + +```java +// Call copyDomainResource(DomainResource src, DomainResource tgt, String... extensionUrlsToIgnore) +ConversionContext30_50.INSTANCE.getVersionConvertor_30_50().copyDomainResource(src, tgt, ACCEPT_UNKNOWN_EXTENSION_URL); +``` + +Then, we need to handle any instances matching that extension URL. In this case, the `acceptUnknown` element can be set. + +```java + if (src.hasExtension(ACCEPT_UNKNOWN_EXTENSION_URL)) + tgt.setAcceptUnknown(org.hl7.fhir.dstu3.model.CapabilityStatement.UnknownContentCode.fromCode(src.getExtensionByUrl(ACCEPT_UNKNOWN_EXTENSION_URL).getValue().primitiveValue())); + +``` + +A similar pattern is used to manage extensions in resource elements: + +``` +copyElement(DomainResource src, DomainResource tgt,, String... extensionUrlsToIgnore) +``` + +After all necessary elements are converted, the conversion is complete, and the target resource is returned. + +## Extending Conversion Functionality + +As the FHIR specification has evolved over time, the versions of FHIR have built on top of one another, adding new fields within existing resources, changing the name of existing resources, or adding entirely new resources altogether. As a result of this conversions are inherently lossy operations. + +A quick example of this would be [ValueSet Expression](https://www.hl7.org/fhir/extension-valueset-expression.html) extension type. This exists in the r4 version of the specification, but no such type exists in dstu2. + +If we were to convert a R4 resource, such as a questionnaire, that contained an extension of this type from r4 -> dstu2, without any special intervention, the extension would be ignored, and the data would be lost in the conversion process. This is where advisors come in. ### Using conversion advisors ----- -When you call the base conversion factory methods `convertType(...)` or `convertResource(...)`, the library does a -predefined conversion, using the standard conversion (which could be a lossy one, or one that makes assumptions). +When you call the base conversion factory methods `convertType(...)` or `convertResource(...)`, the library does a predefined conversion, using the standard conversion (which could be a lossy one, or one that makes assumptions). -These defaults/assumptions are defined in the convertor advisor classes. Each pair of versions has a BaseAdvisor, which -is used by default when you call the factory methods. For example, here is the advisor class which handles conversions -between dstu2 and r5: +These defaults/assumptions are defined in the convertor advisor classes. Each pair of versions has a BaseAdvisor, which is used by default when you call the factory methods. For example, here is the advisor class which handles conversions between dstu2 and r5: ```java public class BaseAdvisor_10_50 extends BaseAdvisor50 { - final List conformanceIgnoredUrls = Collections.singletonList("http://hl7.org/fhir/3.0/StructureDefinition/extension-CapabilityStatement.acceptUnknown"); + private final List> ignoredExtensionTypes = new ArrayList<>(Collections.singletonList(Expression.class)); public BaseAdvisor_10_50() { @@ -111,8 +207,8 @@ public class BaseAdvisor_10_50 extends BaseAdvisor50 paths = Arrays.asList(path.split(",")); - return (paths.get(paths.size() - 1).equals("Conformance")) && (conformanceIgnoredUrls.contains(url)); + // no globally ignored extensions here. + return false; } public boolean ignoreType(@Nonnull String path, @@ -122,12 +218,9 @@ public class BaseAdvisor_10_50 extends BaseAdvisor50 advisor)` +For example, the example `ExpressionAdvisor50` class can be used in converting a Questionnaire resource like so: + +```java +VersionConvertorFactory_10_50.convertResource(r5_input, new ExpressionAdvisor50()); +``` + + ## Development notes ----- diff --git a/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/advisors/Expression50Test.java b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/advisors/Expression50Test.java index ac7550954..2a52630fa 100644 --- a/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/advisors/Expression50Test.java +++ b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/advisors/Expression50Test.java @@ -35,7 +35,7 @@ public class Expression50Test { @Test @DisplayName("Ensure base advisor ignores Expression types and doesn't explode.") - public void testBaseAdvisorExpressionIgore() throws IOException { + public void testBaseAdvisorExpressionIgnore() throws IOException { Expression exp = new Expression(); exp.setExpression("x + y = z"); Extension ext = new Extension(); diff --git a/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_30/AllergyIntolerance10_30Test.java b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_30/AllergyIntolerance10_30Test.java index 53f6fac92..75b97d30e 100644 --- a/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_30/AllergyIntolerance10_30Test.java +++ b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_30/AllergyIntolerance10_30Test.java @@ -28,7 +28,7 @@ public class AllergyIntolerance10_30Test { InputStream stu_exepected_input = this.getClass().getResourceAsStream(stu_path); org.hl7.fhir.dstu2.model.AllergyIntolerance dstu2 = (org.hl7.fhir.dstu2.model.AllergyIntolerance) new org.hl7.fhir.dstu2.formats.JsonParser().parse(dstu2_input); - org.hl7.fhir.dstu3.model.Resource stu_actual = VersionConvertorFactory_10_30.convertResource(dstu2, new BaseAdvisor_10_30()); + org.hl7.fhir.dstu3.model.Resource stu_actual = VersionConvertorFactory_10_30.convertResource(dstu2); org.hl7.fhir.dstu3.formats.JsonParser stu_parser = new org.hl7.fhir.dstu3.formats.JsonParser(); org.hl7.fhir.dstu3.model.Resource stu_expected = stu_parser.parse(stu_exepected_input); diff --git a/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv40_50/AllergyIntolerance40_50Test.java b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv40_50/AllergyIntolerance40_50Test.java new file mode 100644 index 000000000..8d7e82ac8 --- /dev/null +++ b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv40_50/AllergyIntolerance40_50Test.java @@ -0,0 +1,36 @@ +package org.hl7.fhir.convertors.conv40_50; + +import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_40; +import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50; +import org.hl7.fhir.r4.model.AllergyIntolerance; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +public class AllergyIntolerance40_50Test { + @Test + @DisplayName("Test r4 -> r5 conversion for AllergyIntolerance with resolved clinical status") + public void test1() { + // Given resource with dstu3 resource with resolved clinicalStatus + org.hl7.fhir.r4.model.AllergyIntolerance r4AllergyIntolerance = new org.hl7.fhir.r4.model.AllergyIntolerance(); + + r4AllergyIntolerance.setClinicalStatus(new CodeableConcept(new Coding().setCode("resolved"))); + // When convertor is called + org.hl7.fhir.r5.model.Resource r5Resource = VersionConvertorFactory_40_50.convertResource(r4AllergyIntolerance); + + // Then r5 resource should have resolved clinicalStatus + Assertions.assertTrue(r5Resource instanceof org.hl7.fhir.r5.model.AllergyIntolerance); + org.hl7.fhir.r5.model.AllergyIntolerance r5AllergyIntolerance = (org.hl7.fhir.r5.model.AllergyIntolerance) r5Resource; + + List r5AllergyCodeableConcept = r5AllergyIntolerance.getClinicalStatus().getCoding(); + Assertions.assertEquals(1, r5AllergyCodeableConcept.size()); + + String r5AllergyCode = r5AllergyCodeableConcept.get(0).getCode(); + Assertions.assertEquals("resolved", r5AllergyCode); + + } +}