diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e69de29bb..9cecd303d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -0,0 +1 @@ +* fix bug for NullPointerException in Bundle convertors when resource is not available. diff --git a/org.hl7.fhir.convertors/CONVERTORS.md b/org.hl7.fhir.convertors/README.md similarity index 86% rename from org.hl7.fhir.convertors/CONVERTORS.md rename to org.hl7.fhir.convertors/README.md index fac030a52..ea5333005 100644 --- a/org.hl7.fhir.convertors/CONVERTORS.md +++ b/org.hl7.fhir.convertors/README.md @@ -1,6 +1,15 @@ # Resource Conversion -##### Let's talk about converting resources between the various versions of FHIR... +## IMPORTANT +----- + +_The conversion code in this module is maintained as part of the development of the standard, but always under +considerable time pressure. Only part of the code is rigorously tested [as detailed here](#reliable-conversion-code). +Implementers should regard this code as a 'scaffold' for actual reliable conversions._ + +**ALWAYS TEST ANY CONVERSION ROUTINES BEFORE USING THEM IN PRODUCTION!** + +_Ideally, this should be via unit tests in your code, or better yet [unit tests contributed to FHIR](#test-cases)._ ### A note regarding syntax @@ -31,7 +40,7 @@ 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 +## Using the conversion library ----- The majority of use cases for conversion will involve using the provided VersionConvertorFactory_V1_V2 classes to convert @@ -193,3 +202,31 @@ Once you've created your new advisor, they can be provided as an argument when c `public static (V1 Resource) convertResource((V2 Resource) src, advisor)` `public static (V2 Resource) convertResource((V1 Resource) src, advisor)` + +## Development notes +----- + +### Reliable conversion code + +The FHIR project maintains and tests conversions on the following resources, from old versions to R5: + +- CodeSystem +- ValueSet +- ConceptMap +- StructureDefinition +- StructureMap +- ImplementationGuide +- CapabilityStatement +- OperationDefinition +- NamingSystem + +These can be relied on and are subject to extensive testing. + +### Test cases + +Some conversions have test cases for particular resources and particular version combinations. Where test cases exist, +they will continue to be maintained and expected to pass. + +Contributing test cases is highly encouraged! To contribute, create a PRs to the +[core library](https://github.com/hapifhir/org.hl7.fhir.core), or even better, to the +[FHIR test cases library](https://github.com/FHIR/fhir-test-cases). diff --git a/org.hl7.fhir.convertors/readme.md b/org.hl7.fhir.convertors/readme.md deleted file mode 100644 index c9faf8154..000000000 --- a/org.hl7.fhir.convertors/readme.md +++ /dev/null @@ -1,32 +0,0 @@ -About the version conversion routines - -The version conversion routines are maintained as part of -the development of the standard, but always under considerable -time pressure. Implementers should regard these as 'scaffolds' for -an actual reliable conversion routine. - -The FHIR project maintains and tests conversions on the following -resources, from old versions to R5: -* CodeSystem -* ValueSet -* ConceptMap -* StructureDefinition -* StructureMap -* ImplementationGuide -* CapabilityStatement -* OperationDefinition -* NamingSystem - -These can be relied on and are subject to extensive testing. - -In addition to this, some of the conversions have test cases -for particular resources and particular version combinations. -Where test cases exist, they will continue to pass and be -maintained. - -So: -* test the conversion routines before using them in production -* contribute test cases to ensure that your use cases continue to be reliable - -Test cases are welcome - make them as PRs to the core library, or even better, -to the FHIR test cases library \ No newline at end of file diff --git a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv10_30/resources10_30/Bundle10_30.java b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv10_30/resources10_30/Bundle10_30.java index 7689d1874..ba17e1dc5 100644 --- a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv10_30/resources10_30/Bundle10_30.java +++ b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv10_30/resources10_30/Bundle10_30.java @@ -60,8 +60,8 @@ public class Bundle10_30 { tgt.addLink(convertBundleLinkComponent(t)); if (src.hasFullUrlElement()) tgt.setFullUrlElement(Uri10_30.convertUri(src.getFullUrlElement())); - org.hl7.fhir.dstu2.model.Resource res = ConversionContext10_30.INSTANCE.getVersionConvertor_10_30().convertResource(src.getResource()); - tgt.setResource(res); + if (src.hasResource()) + tgt.setResource(ConversionContext10_30.INSTANCE.getVersionConvertor_10_30().convertResource(src.getResource())); if (src.hasSearch()) tgt.setSearch(convertBundleEntrySearchComponent(src.getSearch())); if (src.hasRequest()) diff --git a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv10_40/resources10_40/Bundle10_40.java b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv10_40/resources10_40/Bundle10_40.java index becb30cb2..3047c9fbe 100644 --- a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv10_40/resources10_40/Bundle10_40.java +++ b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv10_40/resources10_40/Bundle10_40.java @@ -78,8 +78,8 @@ public class Bundle10_40 { for (org.hl7.fhir.r4.model.Bundle.BundleLinkComponent t : src.getLink()) tgt.addLink(convertBundleLinkComponent(t)); if (src.hasFullUrlElement()) tgt.setFullUrlElement(Uri10_40.convertUri(src.getFullUrlElement())); - org.hl7.fhir.dstu2.model.Resource res = ConversionContext10_40.INSTANCE.getVersionConvertor_10_40().convertResource(src.getResource()); - tgt.setResource(res); + if (src.hasResource()) + tgt.setResource(ConversionContext10_40.INSTANCE.getVersionConvertor_10_40().convertResource(src.getResource())); if (src.hasSearch()) tgt.setSearch(convertBundleEntrySearchComponent(src.getSearch())); if (src.hasRequest()) diff --git a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv10_50/resources10_50/Bundle10_50.java b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv10_50/resources10_50/Bundle10_50.java index 98c3b2807..6b7c5fc60 100644 --- a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv10_50/resources10_50/Bundle10_50.java +++ b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/conv10_50/resources10_50/Bundle10_50.java @@ -78,8 +78,8 @@ public class Bundle10_50 { for (org.hl7.fhir.r5.model.Bundle.BundleLinkComponent t : src.getLink()) tgt.addLink(convertBundleLinkComponent(t)); if (src.hasFullUrlElement()) tgt.setFullUrlElement(Uri10_50.convertUri(src.getFullUrlElement())); - org.hl7.fhir.dstu2.model.Resource res = ConversionContext10_50.INSTANCE.getVersionConvertor_10_50().convertResource(src.getResource()); - tgt.setResource(res); + if (src.hasResource()) + tgt.setResource(ConversionContext10_50.INSTANCE.getVersionConvertor_10_50().convertResource(src.getResource())); if (src.hasSearch()) tgt.setSearch(convertBundleEntrySearchComponent(src.getSearch())); if (src.hasRequest()) diff --git a/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_30/Bundle10_30Test.java b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_30/Bundle10_30Test.java new file mode 100644 index 000000000..020257b87 --- /dev/null +++ b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_30/Bundle10_30Test.java @@ -0,0 +1,35 @@ +package org.hl7.fhir.convertors.conv10_30; + + +import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_30; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +public class Bundle10_30Test { + + @Test + @DisplayName("Test 10_30 bundle conversion when resource is null") + public void testNoResourceBundleConversion() throws IOException { + org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent bec = new org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent() + .setRequest( + new org.hl7.fhir.dstu3.model.Bundle.BundleEntryRequestComponent() + .setMethod(org.hl7.fhir.dstu3.model.Bundle.HTTPVerb.DELETE) + .setUrl("Patient?identifier=123456") + ); + + org.hl7.fhir.dstu3.model.Bundle stu3Bundle = new org.hl7.fhir.dstu3.model.Bundle() + .addEntry(bec); + + org.hl7.fhir.dstu2.model.Resource dstu2Resource = VersionConvertorFactory_10_30.convertResource(stu3Bundle); + Assertions.assertNotNull(dstu2Resource); + Assertions.assertTrue(dstu2Resource instanceof org.hl7.fhir.dstu2.model.Bundle); + + org.hl7.fhir.dstu2.model.Bundle dstu2Bundle = (org.hl7.fhir.dstu2.model.Bundle) dstu2Resource; + Assertions.assertEquals(1, dstu2Bundle.getEntry().size()); + + Assertions.assertNull(dstu2Bundle.getEntry().get(0).getResource()); + } +} diff --git a/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_40/Bundle10_40Test.java b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_40/Bundle10_40Test.java new file mode 100644 index 000000000..c2e290d38 --- /dev/null +++ b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_40/Bundle10_40Test.java @@ -0,0 +1,34 @@ +package org.hl7.fhir.convertors.conv10_40; + +import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_40; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +public class Bundle10_40Test { + + @Test + @DisplayName("Test 10_40 bundle conversion when resource is null") + public void testNoResourceBundleConversion() throws IOException { + org.hl7.fhir.r4.model.Bundle.BundleEntryComponent bec = new org.hl7.fhir.r4.model.Bundle.BundleEntryComponent() + .setRequest( + new org.hl7.fhir.r4.model.Bundle.BundleEntryRequestComponent() + .setMethod(org.hl7.fhir.r4.model.Bundle.HTTPVerb.DELETE) + .setUrl("Patient?identifier=123456") + ); + + org.hl7.fhir.r4.model.Bundle r4Bundle = new org.hl7.fhir.r4.model.Bundle() + .addEntry(bec); + + org.hl7.fhir.dstu2.model.Resource dstu2Resource = VersionConvertorFactory_10_40.convertResource(r4Bundle); + Assertions.assertNotNull(dstu2Resource); + Assertions.assertTrue(dstu2Resource instanceof org.hl7.fhir.dstu2.model.Bundle); + + org.hl7.fhir.dstu2.model.Bundle dstu2Bundle = (org.hl7.fhir.dstu2.model.Bundle) dstu2Resource; + Assertions.assertEquals(1, dstu2Bundle.getEntry().size()); + + Assertions.assertNull(dstu2Bundle.getEntry().get(0).getResource()); + } +} diff --git a/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_50/Bundle10_50Test.java b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_50/Bundle10_50Test.java new file mode 100644 index 000000000..5151c8ca7 --- /dev/null +++ b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv10_50/Bundle10_50Test.java @@ -0,0 +1,34 @@ +package org.hl7.fhir.convertors.conv10_50; + +import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_50; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +public class Bundle10_50Test { + + @Test + @DisplayName("Test 10_50 bundle conversion when resource is null") + public void testNoResourceBundleConversion() throws IOException { + org.hl7.fhir.r5.model.Bundle.BundleEntryComponent bec = new org.hl7.fhir.r5.model.Bundle.BundleEntryComponent() + .setRequest( + new org.hl7.fhir.r5.model.Bundle.BundleEntryRequestComponent() + .setMethod(org.hl7.fhir.r5.model.Bundle.HTTPVerb.DELETE) + .setUrl("Patient?identifier=123456") + ); + + org.hl7.fhir.r5.model.Bundle r5Bundle = new org.hl7.fhir.r5.model.Bundle() + .addEntry(bec); + + org.hl7.fhir.dstu2.model.Resource dstu2Resource = VersionConvertorFactory_10_50.convertResource(r5Bundle); + Assertions.assertNotNull(dstu2Resource); + Assertions.assertTrue(dstu2Resource instanceof org.hl7.fhir.dstu2.model.Bundle); + + org.hl7.fhir.dstu2.model.Bundle dstu2Bundle = (org.hl7.fhir.dstu2.model.Bundle) dstu2Resource; + Assertions.assertEquals(1, dstu2Bundle.getEntry().size()); + + Assertions.assertNull(dstu2Bundle.getEntry().get(0).getResource()); + } +} diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/terminologies/ConceptMapEngine.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/terminologies/ConceptMapEngine.java index 9c8525105..c329533c3 100644 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/terminologies/ConceptMapEngine.java +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/terminologies/ConceptMapEngine.java @@ -1,33 +1,33 @@ package org.hl7.fhir.r4.terminologies; -/* - Copyright (c) 2011+, HL7, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of HL7 nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. - - */ +/* + Copyright (c) 2011+, HL7, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of HL7 nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + */ @@ -64,7 +64,7 @@ public class ConceptMapEngine { for (ConceptMapGroupComponent g : cm.getGroup()) { for (SourceElementComponent e : g.getElement()) { if (code.equals(e.getCode())) { - if (e != null) + if (ct != null) throw new FHIRException("Unable to process translate "+code+" because multiple candidate matches were found in concept map "+cm.getUrl()); ct = e; cg = g; @@ -94,4 +94,4 @@ public class ConceptMapEngine { throw new Error("Not done yet"); } -} \ No newline at end of file +} diff --git a/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/terminologies/ConceptMapEngineTest.java b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/terminologies/ConceptMapEngineTest.java new file mode 100644 index 000000000..6e2992bc2 --- /dev/null +++ b/org.hl7.fhir.r4/src/test/java/org/hl7/fhir/r4/terminologies/ConceptMapEngineTest.java @@ -0,0 +1,92 @@ +package org.hl7.fhir.r4.terminologies; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r4.context.SimpleWorkerContext; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.ConceptMap; +import org.hl7.fhir.r4.model.Enumerations; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ConceptMapEngineTest { + + private static final String CONCEPT_MAP_URL = "https://test-fhir.com/ConceptMap/fake"; + public static final String SOURCE_CODE_STRING = "body-weight"; + public static final String TARGET_CODE_STRING = "vital-signs"; + + @Test + @DisplayName("Coding is translated according to ConceptMap") + void codingTranslate() throws IOException { + + final ConceptMap.SourceElementComponent sourceElementComponent = getSourceElementComponent(); + + final ConceptMapEngine conceptMapEngine = getConceptMapEngine(Arrays.asList(sourceElementComponent)); + Coding coding = new Coding(null, SOURCE_CODE_STRING, "Body Weight"); + + Coding actual = conceptMapEngine.translate(coding, CONCEPT_MAP_URL); + + assertEquals(TARGET_CODE_STRING, actual.getCode()); + } + + @Test + @DisplayName("Coding fails to translate due to multiple candidate matches in ConceptMap") + void codingTranslateFailsForMultipleCandidateMatches() throws IOException { + + final ConceptMap.SourceElementComponent sourceElementComponent = getSourceElementComponent(); + + final ConceptMapEngine conceptMapEngine = getConceptMapEngine(Arrays.asList(sourceElementComponent, + sourceElementComponent + )); + Coding coding = new Coding(null, SOURCE_CODE_STRING, "Body Weight"); + + assertThrows(FHIRException.class, () -> { + conceptMapEngine.translate(coding, CONCEPT_MAP_URL); + }); + } + + @Nonnull + private ConceptMapEngine getConceptMapEngine(Collection elements) throws IOException { + ConceptMap conceptMap = getConceptMap(elements); + + SimpleWorkerContext simpleWorkerContext = mock(SimpleWorkerContext.class); + when(simpleWorkerContext.fetchResource(ConceptMap.class, CONCEPT_MAP_URL)).thenReturn(conceptMap); + + return new ConceptMapEngine(simpleWorkerContext); + } + + @Nonnull + private ConceptMap.SourceElementComponent getSourceElementComponent() { + ConceptMap.TargetElementComponent targetElementComponent = new ConceptMap.TargetElementComponent(); + targetElementComponent.setCode(TARGET_CODE_STRING); + targetElementComponent.setEquivalence(Enumerations.ConceptMapEquivalence.EQUIVALENT); + + ConceptMap.SourceElementComponent sourceElementComponent = new ConceptMap.SourceElementComponent(); + sourceElementComponent.setCode(SOURCE_CODE_STRING); + sourceElementComponent.setTarget(Collections.singletonList(targetElementComponent)); + + return sourceElementComponent; + } + + @Nonnull + private ConceptMap getConceptMap(Collection elements) { + + ConceptMap.ConceptMapGroupComponent conceptMapGroupComponent = new ConceptMap.ConceptMapGroupComponent(); + for (ConceptMap.SourceElementComponent element : elements) { + conceptMapGroupComponent.addElement(element); + } + return new ConceptMap() + .addGroup(conceptMapGroupComponent) + .setUrl(CONCEPT_MAP_URL); + } +}