This commit is contained in:
Grahame Grieve 2022-01-11 05:19:04 +11:00
commit fc5a57c10b
11 changed files with 271 additions and 70 deletions

View File

@ -0,0 +1 @@
* fix bug for NullPointerException in Bundle convertors when resource is not available.

View File

@ -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, <T extends BaseAdvisor> advisor)`
`public static (V2 Resource) convertResource((V1 Resource) src, <T extends BaseAdvisor> 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).

View File

@ -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

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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");
}
}
}

View File

@ -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<ConceptMap.SourceElementComponent> 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<ConceptMap.SourceElementComponent> elements) {
ConceptMap.ConceptMapGroupComponent conceptMapGroupComponent = new ConceptMap.ConceptMapGroupComponent();
for (ConceptMap.SourceElementComponent element : elements) {
conceptMapGroupComponent.addElement(element);
}
return new ConceptMap()
.addGroup(conceptMapGroupComponent)
.setUrl(CONCEPT_MAP_URL);
}
}