Skip to content

Commit

Permalink
initial work Supporting FML transforms between different FHIR versions
Browse files Browse the repository at this point in the history
  • Loading branch information
oliveregger committed Aug 21, 2024
1 parent 036975c commit 5cdcf65
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r5.model.StructureMap;
import org.hl7.fhir.validation.ValidationEngine;

import ch.ahdis.matchbox.engine.cli.VersionUtil;
Expand Down Expand Up @@ -114,7 +115,6 @@ public CdaMappingEngine getEngineR4() throws FHIRException, IOException, URISynt
engine.getContext().setCanRunWithoutTerminology(true);
engine.getContext().setNoTerminologyServer(true);
engine.getContext().setPackageTracker(engine);

return engine;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import org.hl7.fhir.r5.model.Narrative.NarrativeStatus;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.model.StructureMap;
import org.hl7.fhir.r5.model.UriType;
import org.hl7.fhir.r5.renderers.RendererFactory;
import org.hl7.fhir.r5.renderers.utils.RenderingContext;
import org.hl7.fhir.r5.utils.EOperationOutcome;
Expand All @@ -63,6 +64,7 @@
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine;
import org.hl7.fhir.utilities.ByteProvider;
import org.hl7.fhir.utilities.FhirPublication;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.VersionUtilities;
import org.hl7.fhir.utilities.json.model.JsonObject;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
Expand All @@ -74,6 +76,7 @@
import org.hl7.fhir.validation.ValidationEngine;
import org.hl7.fhir.validation.instance.InstanceValidator;

import ch.ahdis.matchbox.engine.MatchboxEngine.FilesystemPackageCacheMode;
import ch.ahdis.matchbox.engine.cli.VersionUtil;
import ch.ahdis.matchbox.mappinglanguage.MatchboxStructureMapUtilities;
import ch.ahdis.matchbox.mappinglanguage.TransformSupportServices;
Expand Down Expand Up @@ -187,6 +190,8 @@ public MatchboxEngine getEngineR4() throws MatchboxEngineCreationException {
engine.loadPackage(getClass().getResourceAsStream("/hl7.fhir.r4.core.tgz"));
engine.loadPackage(getClass().getResourceAsStream("/hl7.terminology#5.4.0.tgz"));
engine.loadPackage(getClass().getResourceAsStream("/hl7.fhir.uv.extensions.r4#1.0.0.tgz"));
removeStructureMaps(engine);
engine.loadPackage(getClass().getResourceAsStream("/hl7.fhir.uv.xver#[email protected]"));
} catch (final IOException e) {
throw new IgLoadException(e);
}
Expand All @@ -209,6 +214,16 @@ public MatchboxEngine getEngineR4() throws MatchboxEngineCreationException {
return engine;
}

/**
* remove old StructureMaps from the context, especially from hl7.fhir.uv.extensions.r4#1.0.0 which are replaced by newer versions
* @param engine
*/
public void removeStructureMaps(MatchboxEngine engine) {
for (StructureMap map : engine.getContext().fetchResourcesByType(StructureMap.class)) {
engine.getContext().dropResource(map);
}
}

/**
* Returns a FHIR R5 engine configured with hl7 terminology
*
Expand All @@ -226,6 +241,8 @@ public MatchboxEngine getEngineR5() throws MatchboxEngineCreationException {
engine.loadPackage(getClass().getResourceAsStream("/hl7.fhir.r5.core.tgz"));
engine.loadPackage(getClass().getResourceAsStream("/hl7.terminology#5.4.0.tgz"));
engine.loadPackage(getClass().getResourceAsStream("/hl7.fhir.uv.extensions#1.0.0.tgz"));
removeStructureMaps(engine);
engine.loadPackage(getClass().getResourceAsStream("/hl7.fhir.uv.xver#[email protected]"));
} catch (final IOException e) {
throw new IgLoadException(e);
}
Expand Down Expand Up @@ -357,15 +374,25 @@ public Resource transformToFhir(String input, boolean inputJson, String mapUri)
public String transform(String input, boolean inputJson, String mapUri, boolean outputJson)
throws FHIRException, IOException {
log.info("Start transform: " + mapUri);

SimpleWorkerContext context = this.getContext();
StructureMap map = context.fetchResource(StructureMap.class, mapUri);

String fhirVersionTarget = getFhirVersion(getCanonicalFromStructureMap(map, StructureMap.StructureMapModelMode.TARGET));
if (fhirVersionTarget !=null && !fhirVersionTarget.equals(this.getVersion())) {
log.info("Loading additional FHIR version for Target into context" + fhirVersionTarget);
context = getContextForFhirVersion(fhirVersionTarget);
}

Element transformed = transform(ByteProvider.forBytes(input.getBytes("UTF-8")), (inputJson ? FhirFormat.JSON : FhirFormat.XML),
mapUri);
mapUri, context);
ByteArrayOutputStream boas = new ByteArrayOutputStream();
if (outputJson)
new org.hl7.fhir.r5.elementmodel.JsonParser(getContext()).compose(transformed, boas,
new org.hl7.fhir.r5.elementmodel.JsonParser(context).compose(transformed, boas,
IParser.OutputStyle.PRETTY,
null);
else
new org.hl7.fhir.r5.elementmodel.XmlParser(getContext()).compose(transformed, boas,
new org.hl7.fhir.r5.elementmodel.XmlParser(context).compose(transformed, boas,
IParser.OutputStyle.PRETTY,
null);
String result = new String(boas.toByteArray());
Expand All @@ -378,22 +405,54 @@ public String transform(String input, boolean inputJson, String mapUri, boolean
* Adapted transform operation from Validation Engine to use patched
* MatchboxStructureMapUtilities
*/
public org.hl7.fhir.r5.elementmodel.Element transform(ByteProvider source, FhirFormat cntType, String mapUri)
public org.hl7.fhir.r5.elementmodel.Element transform(ByteProvider source, FhirFormat cntType, String mapUri, SimpleWorkerContext targetContext)
throws FHIRException, IOException {
SimpleWorkerContext context = this.getContext();

// usual case is that source and target are in the same FHIR version as in the context, however it could be that either source or target are in a different FHIR version
// if this is the case we do lazy loading of the additional FHIR version into the context

StructureMap map = context.fetchResource(StructureMap.class, mapUri);
String fhirVersion = getFhirVersion(getCanonicalFromStructureMap(map, StructureMap.StructureMapModelMode.SOURCE));
if (!fhirVersion.equals(this.getVersion())) {
log.info("Loading additional FHIR version for Source into context" + fhirVersion);
context.loadFromPackage("hl7.fhir.r5.core", fhirVersion);
String fhirVersionSource = getFhirVersion(getCanonicalFromStructureMap(map, StructureMap.StructureMapModelMode.SOURCE));
if (fhirVersionSource !=null && !fhirVersionSource.equals(this.getVersion())) {
log.info("Loading additional FHIR version for Source into context" + fhirVersionSource);
context = getContextForFhirVersion(fhirVersionSource);
}
org.hl7.fhir.r5.elementmodel.Element src = Manager.parseSingle(context, new ByteArrayInputStream(source.getBytes()),
cntType);
return transform(src, mapUri);
return transform(src, mapUri, targetContext);
}

/**
* Adapted transform operation from Validation Engine to use patched
* MatchboxStructureMapUtilities
*/
public SimpleWorkerContext getContextForFhirVersion(String fhirVersion)
throws FHIRException, IOException {
SimpleWorkerContext contextForFhirVersion = null;
if (fhirVersion.startsWith("4.0")) {
MatchboxEngine engine = new MatchboxEngineBuilder().getEngineR4();
contextForFhirVersion = engine.getContext();
}
if (fhirVersion.startsWith("5.0")) {
MatchboxEngine engine = new MatchboxEngineBuilder().getEngineR5();
contextForFhirVersion = engine.getContext();
}
if (contextForFhirVersion != null ) {
// we need to copy now all StructureDefinitions from this Version to the new context
for (StructureDefinition sd : contextForFhirVersion.listStructures()) {
StructureDefinition sdn = sd.copy();
if (sdn.getKind()!=null && sdn.getKind() != StructureDefinition.StructureDefinitionKind.LOGICAL && !"Extensions".equals(sdn.getType())) {
sdn.setUrl(sdn.getUrl().replace("http://hl7.org/fhir/", "http://hl7.org/fhir/"+fhirVersion+"/"));
sdn.addExtension().setUrl("http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace")
.setValue(new UriType("http://hl7.org/fhir"));
this.getContext().cacheResource(sdn);
}
contextForFhirVersion.cacheResource(sd);
}
}

return contextForFhirVersion;
}

/**
Expand All @@ -405,12 +464,12 @@ public org.hl7.fhir.r5.elementmodel.Element transform(ByteProvider source, FhirF
* @throws FHIRException
* @throws IOException
*/
public org.hl7.fhir.r5.elementmodel.Element transform(org.hl7.fhir.r5.elementmodel.Element src, String mapUri)
public org.hl7.fhir.r5.elementmodel.Element transform(org.hl7.fhir.r5.elementmodel.Element src, String mapUri, SimpleWorkerContext targetContext)
throws FHIRException, IOException {
SimpleWorkerContext context = this.getContext();
List<Base> outputs = new ArrayList<>();
StructureMapUtilities scu = new MatchboxStructureMapUtilities(context,
new TransformSupportServices(context, outputs), this);
new TransformSupportServices(targetContext!=null ? targetContext : context, outputs), this);
StructureMap map = context.fetchResource(StructureMap.class, mapUri);
if (map == null) {
log.error("Unable to find map " + mapUri + " (Known Maps = " + context.listMapUrls() + ")");
Expand All @@ -420,6 +479,7 @@ public org.hl7.fhir.r5.elementmodel.Element transform(org.hl7.fhir.r5.elementmod
+ (map.getDateElement() != null && !map.getDateElement().isEmpty() ? "(" + map.getDateElement().asStringValue() + ")" : ""));

org.hl7.fhir.r5.elementmodel.Element resource = getTargetResourceFromStructureMap(map);

scu.transform(null, src, map, resource);
resource.populatePaths(null);
return resource;
Expand Down Expand Up @@ -468,6 +528,13 @@ private org.hl7.fhir.r5.elementmodel.Element getTargetResourceFromStructureMap(S
throw new FHIRException("Unable to determine resource URL for target type");
}

if (Utilities.isAbsoluteUrl(targetTypeUrl)) {
int index = targetTypeUrl.indexOf("/"+this.getVersion().substring(0,3)+"/");
if (index >= 0) {
targetTypeUrl = targetTypeUrl.substring(0, index)+targetTypeUrl.substring(index+4);
}
}

StructureDefinition structureDefinition = null;
for (StructureDefinition sd : context.fetchResourcesByType(StructureDefinition.class)) {
if (sd.getUrl().equalsIgnoreCase(targetTypeUrl)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.utils.structuremap.ITransformerServices;
import org.hl7.fhir.utilities.Utilities;

public class TransformSupportServices implements ITransformerServices {

Expand All @@ -44,6 +45,14 @@ public TransformSupportServices(IWorkerContext worker, List<Base> outputs) {
@Override
public Base createType(Object appInfo, String name) throws FHIRException {
StructureDefinition sd = context.fetchResource(StructureDefinition.class, name);
if (sd == null) {
if (Utilities.existsInList(name, "http://hl7.org/fhirpath/System.String")) {
sd = context.fetchTypeDefinition("string");
}
}
if (sd == null) {
throw new FHIRException("Unable to create type "+name);
}
return Manager.build(context, sd);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2109,7 +2109,14 @@ public <T extends Resource> T fetchResourceWithExceptionByVersion(Class<T> class
if (class_ == StructureDefinition.class) {
uri = ProfileUtilities.sdNs(uri, null);
}
synchronized (lock) {
// matchbox patch for #265, fhirVersioned URL's eg (return urls with fhirVersion)
if (Utilities.isAbsoluteUrl(uri)) {
int index = uri.indexOf("/"+this.version.substring(0,3)+"/");
if (index >= 0) {
uri = uri.substring(0, index)+uri.substring(index+4);
}
}
synchronized (lock) {

if (version == null) {
if (uri.contains("|")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.FhirPublication;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.I18nConstants;
import org.hl7.fhir.utilities.validation.ValidationOptions;
import org.hl7.fhir.utilities.xhtml.NodeType;
import org.hl7.fhir.utilities.xhtml.XhtmlNode;
import org.hl7.fhir.validation.instance.type.StructureMapValidator.VariableDefn;

import java.io.IOException;
import java.util.*;
Expand Down Expand Up @@ -1329,11 +1331,11 @@ private void executeRule(String indent, TransformContext context, StructureMap m
for (StructureMapGroupRuleComponent childrule : rule.getRule()) {
executeRule(indent + " ", context, map, v, group, childrule, false);
}
} else if (rule.hasDependent()) {
} else if (rule.hasDependent() && !checkisSimple(rule)) {
for (StructureMapGroupRuleDependentComponent dependent : rule.getDependent()) {
executeDependency(indent + " ", context, map, v, group, dependent);
}
} else if (rule.getSource().size() == 1 && rule.getSourceFirstRep().hasVariable() && rule.getTarget().size() == 1 && rule.getTargetFirstRep().hasVariable() && rule.getTargetFirstRep().getTransform() == StructureMapTransform.CREATE && !rule.getTargetFirstRep().hasParameter()) {
} else if (checkisSimple(rule) || (rule.getSource().size() == 1 && rule.getSourceFirstRep().hasVariable() && rule.getTarget().size() == 1 && rule.getTargetFirstRep().hasVariable() && rule.getTargetFirstRep().getTransform() == StructureMapTransform.CREATE && !rule.getTargetFirstRep().hasParameter())) {
// simple inferred, map by type
if (debug) {
log(v.summary());
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package ch.ahdis.matchbox.engine.tests;

/*
* #%L
* Matchbox Engine
* %%
* Copyright (C) 2022 ahdis
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

import org.apache.commons.io.IOUtils;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.formats.XmlParser;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.ExplanationOfBenefit;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Questionnaire;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StructureDefinition;
import org.hl7.fhir.r4.model.StructureMap;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import ch.ahdis.matchbox.engine.MatchboxEngine;
import ch.ahdis.matchbox.engine.MatchboxEngine.MatchboxEngineBuilder;

/**
* https://build.fhir.org/ig/HL7/fhir-cross-version/package.tgz
*/
class FhirXVersTests {

static private MatchboxEngine engine;

private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FhirMappingLanguageTests.class);

@BeforeAll
static void setUpBeforeClass() throws Exception {
engine = new MatchboxEngineBuilder().getEngineR4();
}

@BeforeEach
void setUp() throws Exception {
}

public String getFileAsStringFromResources(String file) throws IOException {
InputStream in = FhirXVersTests.class.getResourceAsStream("/xvers" + file);
return IOUtils.toString(in, StandardCharsets.UTF_8);
}

public StructureDefinition getStructureDefinitionFromFile(String file) throws IOException {
return (StructureDefinition) new org.hl7.fhir.r4.formats.XmlParser()
.parse(FhirMappingLanguageTests.class.getResourceAsStream(file));
}

@Test
void testMedication5to4inR4() throws FHIRException, IOException {
MatchboxEngine engine = new MatchboxEngine(FhirXVersTests.engine);
String result = engine.transform(getFileAsStringFromResources("/medication-r5-med0301.json"), true,
"http://hl7.org/fhir/uv/xver/StructureMap/Medication5to4", true);
log.info(result);
CompareUtil.compare(getFileAsStringFromResources("/medication-r4-med0301.json"), result, false);
}

@Test
void testMedication4to5inR4() throws FHIRException, IOException {
MatchboxEngine engine = new MatchboxEngine(FhirXVersTests.engine);
String result = engine.transform(getFileAsStringFromResources("/medication-r4-med0301.json"), true,
"http://hl7.org/fhir/uv/xver/StructureMap/Medication4to5", true);
log.info(result);
CompareUtil.compare(getFileAsStringFromResources("/medication-r5-med0301.json"), result, false);
}


}
Loading

0 comments on commit 5cdcf65

Please sign in to comment.