Skip to content

Commit

Permalink
Merge pull request #1219 from synthetichealth/flexporter
Browse files Browse the repository at this point in the history
Flexporter
  • Loading branch information
jawalonoski authored Nov 22, 2023
2 parents 0087993 + de38f5f commit 6795013
Show file tree
Hide file tree
Showing 23 changed files with 28,660 additions and 54 deletions.
30 changes: 24 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ dependencies {
implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-dstu2:6.1.0'
implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:6.1.0'
implementation 'ca.uhn.hapi.fhir:hapi-fhir-client:6.1.0'

implementation 'ca.uhn.hapi.fhir:hapi-fhir-validation:6.1.0'
implementation 'ca.uhn.hapi.fhir:hapi-fhir-validation-resources-r4:6.1.0'
implementation 'ca.uhn.hapi.fhir:hapi-fhir-validation-resources-dstu3:6.1.0'
implementation 'ca.uhn.hapi.fhir:hapi-fhir-validation-resources-dstu2:6.1.0'
// C-CDA export uses Apache FreeMarker templates
implementation 'org.freemarker:freemarker:2.3.31'

Expand All @@ -66,11 +71,14 @@ dependencies {
implementation 'guru.nidi:graphviz-java:0.18.1'
// JavaScript engine included for graphviz. It gets used
// if someone does not have graphviz installed in their environment
implementation 'org.graalvm.js:js:22.2.0'
implementation 'org.graalvm.js:js:22.3.3'
implementation 'org.graalvm.js:js-scriptengine:22.3.3'
implementation 'org.graalvm.sdk:graal-sdk:22.3.3'

// CSV Stuff
implementation 'org.apache.commons:commons-csv:1.9.0'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.13.4'
implementation 'org.yaml:snakeyaml:1.32'
implementation 'org.yaml:snakeyaml:1.33'
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'org.apache.commons:commons-text:1.9'
implementation 'commons-validator:commons-validator:1.7'
Expand Down Expand Up @@ -125,10 +133,6 @@ dependencies {
testImplementation 'org.powermock:powermock-module-junit4:2.0.9'
testImplementation 'org.powermock:powermock-api-mockito2:2.0.9'
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.33.2'
testImplementation 'ca.uhn.hapi.fhir:hapi-fhir-validation:6.1.0'
testImplementation 'ca.uhn.hapi.fhir:hapi-fhir-validation-resources-r4:6.1.0'
testImplementation 'ca.uhn.hapi.fhir:hapi-fhir-validation-resources-dstu3:6.1.0'
testImplementation 'ca.uhn.hapi.fhir:hapi-fhir-validation-resources-dstu2:6.1.0'
testImplementation 'com.helger:ph-schematron:5.6.5'
testImplementation 'com.helger:ph-commons:9.5.5'
testImplementation 'com.squareup.okhttp3:mockwebserver:4.10.0'
Expand Down Expand Up @@ -180,6 +184,7 @@ task graphviz(type: JavaExec) {
mainClass = "Graphviz"
}


task rifMinimize(type: JavaExec) {
group 'Application'
description 'Filter exported RIF files to produce minimal set that covers all claim types'
Expand Down Expand Up @@ -231,6 +236,19 @@ shadowJar {
task uberJar() {
}

task flexporter(type: JavaExec) {
group 'Application'
description 'Apply transformations to FHIR'
classpath sourceSets.main.runtimeClasspath
mainClass = "RunFlexporter"
// args are called "arams" because they are called with -P,
// ex. gradle run -Params="['arg1', 'args2']"
// see https://stackoverflow.com/questions/27604283/gradle-task-pass-arguments-to-java-application
if (project.hasProperty("arams")) {
args Eval.me(arams)
}
}

task concepts(type: JavaExec) {
group 'Application'
description 'Create a list of simulated concepts'
Expand Down
18 changes: 18 additions & 0 deletions run_flexporter
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env sh

##############################################################################
##
## Flexporter launcher for UN*X
##
##############################################################################

ARGS=

for arg in "$@"
do
ARGS=$ARGS\'$arg\',
# Trailing comma ok, don't need to remove it
done

./gradlew flexporter -Params="[$ARGS]"

24 changes: 23 additions & 1 deletion src/main/java/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import org.mitre.synthea.engine.Generator;
import org.mitre.synthea.engine.Module;
import org.mitre.synthea.export.Exporter;
import org.mitre.synthea.export.flexporter.Mapping;
import org.mitre.synthea.helpers.Config;
import org.mitre.synthea.helpers.Utilities;

Expand Down Expand Up @@ -56,6 +58,7 @@ public static void usage() {
*/
public static void main(String[] args) throws Exception {
Generator.GeneratorOptions options = new Generator.GeneratorOptions();
Exporter.ExporterRuntimeOptions exportOptions = new Exporter.ExporterRuntimeOptions();

boolean validArgs = true;
boolean overrideFutureDateError = false;
Expand Down Expand Up @@ -199,6 +202,25 @@ public static void main(String[] args) throws Exception {
throw new FileNotFoundException(String.format(
"Specified keep-patients file (%s) does not exist", value));
}
} else if (currArg.equals("-fm")) {
String value = argsQ.poll();
File flexporterMappingFile = new File(value);
if (flexporterMappingFile.exists()) {
Mapping mapping = Mapping.parseMapping(flexporterMappingFile);
exportOptions.addFlexporterMapping(mapping);
} else {
throw new FileNotFoundException(String.format(
"Specified flexporter mapping file (%s) does not exist", value));
}
} else if (currArg.equals("-ig")) {
String value = argsQ.poll();
File igFile = new File(value);
if (igFile.exists()) {
RunFlexporter.loadIG(igFile);
} else {
throw new FileNotFoundException(String.format(
"Specified IG directory (%s) does not exist", value));
}
} else if (currArg.startsWith("--")) {
String configSetting;
String value;
Expand Down Expand Up @@ -230,7 +252,7 @@ public static void main(String[] args) throws Exception {
}

if (validArgs && validateConfig(options, overrideFutureDateError)) {
Generator generator = new Generator(options);
Generator generator = new Generator(options, exportOptions);
generator.run();
}
}
Expand Down
222 changes: 222 additions & 0 deletions src/main/java/RunFlexporter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Map;
import java.util.Queue;

import org.apache.commons.io.FilenameUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.ValueSet;
import org.mitre.synthea.export.FhirR4;
import org.mitre.synthea.export.flexporter.Actions;
import org.mitre.synthea.export.flexporter.FhirPathUtils;
import org.mitre.synthea.export.flexporter.FlexporterJavascriptContext;
import org.mitre.synthea.export.flexporter.Mapping;
import org.mitre.synthea.helpers.RandomCodeGenerator;

/**
* Entrypoint for the "Flexporter" when run as a standalone gradle task.
* The Flexporter is primarily expected to be used to apply transformations
* to newly generated Synthea patients, but can also be run against existing FHIR bundles.
* These bundles are assumed to have been generated by Synthea, but depending on the mapping,
* they don't necessarily have to be.
*/
public class RunFlexporter {

/**
* Main method. Invoke the flexporter with given arguments:
* -fm {Flexporter Mapping file path}
* -s {Source FHIR file path}
* -ig {Implementation Guide file path}
*
* @param args Command line args as described above
*/
public static void main(String[] args) throws Exception {
Queue<String> argsQ = new ArrayDeque<String>(Arrays.asList(args));

File igDirectory = null;
File sourceFile = null;
File mappingFile = null;

while (!argsQ.isEmpty()) {
String currArg = argsQ.poll();

if (currArg.equals("-ig")) {
String value = argsQ.poll();

if (value == null) {
throw new FileNotFoundException("No implementation guide directory provided");
}

igDirectory = new File(value);

if (!igDirectory.isDirectory()) {
throw new FileNotFoundException(String.format(
"Specified implementation guide directory (%s) does not exist or is not a directory",
value));
} else if (isDirEmpty(igDirectory.toPath())) {
throw new FileNotFoundException(
String.format("Specified implementation guide directory (%s) is empty", value));
}

} else if (currArg.equals("-fm")) {
String value = argsQ.poll();

if (value == null) {
throw new FileNotFoundException("No mapping file provided");
}

mappingFile = new File(value);

if (!mappingFile.exists()) {
throw new FileNotFoundException(
String.format("Specified mapping file (%s) does not exist", value));
}

} else if (currArg.equals("-s")) {
String value = argsQ.poll();
sourceFile = new File(value);

if (value == null) {
throw new FileNotFoundException("No Synthea source FHIR provided");
}

if (!sourceFile.exists()) {
throw new FileNotFoundException(
String.format("Specified Synthea source FHIR (%s) does not exist", value));
}
}
}

if (mappingFile == null || sourceFile == null) {
usage();
System.exit(1);
}

convertFhir(mappingFile, igDirectory, sourceFile);
}


private static Bundle convertFhir(Bundle bundle, Mapping mapping) {
if (FhirPathUtils.appliesToBundle(bundle, mapping.applicability, mapping.variables)) {
bundle = Actions.applyMapping(bundle, mapping, null, new FlexporterJavascriptContext());
}

return bundle;
}

private static void convertFhir(File mappingFile, File igDirectory, File sourceFhir)
throws IOException {

Mapping mapping = Mapping.parseMapping(mappingFile);

if (igDirectory != null) {
loadIG(igDirectory);
}

IParser parser = FhirR4.getContext().newJsonParser().setPrettyPrint(true);

handleFile(sourceFhir, mapping, parser);
}

private static void handleFile(File sourceFhir, Mapping mapping, IParser parser)
throws IOException {
if (sourceFhir.isDirectory()) {
for (File subfile : sourceFhir.listFiles()) {
if (subfile.isDirectory() || subfile.getName().endsWith(".json")) {
handleFile(subfile, mapping, parser);
}
}

} else {
String fhirJson = new String(Files.readAllBytes(sourceFhir.toPath()));
Bundle bundle = parser.parseResource(Bundle.class, fhirJson);

for (BundleEntryComponent bec : bundle.getEntry()) {
Resource r = bec.getResource();
if (r.getId().startsWith("urn:uuid:")) {
// HAPI does some weird stuff with IDs
// by default in Synthea they are just plain UUIDs
// and the entry.fullUrl is urn:uuid:(id)
// but somehow when they get parsed back in, the id is urn:uuid:etc
// which then doesn't get written back out at the end
// so this removes the "urn:uuid:" bit if it got added
r.setId(r.getId().substring(9));
}
}

bundle = convertFhir(bundle, mapping);

String bundleJson = parser.encodeResourceToString(bundle);

new File("./output/flexporter/").mkdirs();

String outFileName = "" + System.currentTimeMillis() + "_" + sourceFhir.getName();

File outFile =
new File("./output/flexporter/" + outFileName);

Files.write(outFile.toPath(), bundleJson.getBytes(Charset.defaultCharset()),
StandardOpenOption.CREATE_NEW);

System.out.println("Wrote " + outFile);
}
}

static void loadIG(File igDirectory) throws IOException {
File[] artifacts = igDirectory.listFiles();

for (File artifact : artifacts) {
if (artifact.isFile() && FilenameUtils.getExtension(artifact.toString()).equals("json")) {

IParser parser = FhirR4.getContext().newJsonParser();

String fhirJson = new String(Files.readAllBytes(artifact.toPath()));
IBaseResource resource = null;

try {
resource = parser.parseResource(fhirJson);
} catch (DataFormatException dfe) {
// why does an IG contain bad data?
System.err.println("Warning: Unable to parse IG artifact " + artifact.getAbsolutePath());
dfe.printStackTrace();
}

if (resource instanceof ValueSet) {
try {
RandomCodeGenerator.loadValueSet(null, (ValueSet)resource);
} catch (Exception e) {
System.err.println("WARNING: Unable to load ValueSet " + artifact.getAbsolutePath());
e.printStackTrace();
}
}
}
}
}

private static boolean isDirEmpty(final Path directory) throws IOException {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(directory)) {
return !dirStream.iterator().hasNext();
}
}

private static void usage() {
System.out.println("Usage: run_flexporter -fm MAPPING_FILE -s SOURCE_FHIR [-ig IG_FOLDER]");
}
}
Loading

0 comments on commit 6795013

Please sign in to comment.