Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flexporter - Update randomCode fn to take output type as an optional second param #1527

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/java/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ public static void main(String[] args) throws Exception {
if (flexporterMappingFile.exists()) {
Mapping mapping = Mapping.parseMapping(flexporterMappingFile);
exportOptions.addFlexporterMapping(mapping);
mapping.loadValueSets();

// disable the graalVM warning when FlexporterJavascriptContext is instantiated
System.getProperties().setProperty("polyglot.engine.WarnInterpreterOnly", "false");
} else {
Expand Down
5 changes: 1 addition & 4 deletions src/main/java/RunFlexporter.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
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;
Expand All @@ -14,7 +11,6 @@
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;
Expand Down Expand Up @@ -125,6 +121,7 @@ private static void convertFhir(File mappingFile, File igDirectory, File sourceF
throws IOException {

Mapping mapping = Mapping.parseMapping(mappingFile);
mapping.loadValueSets();

if (igDirectory != null) {
loadIG(igDirectory);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/mitre/synthea/export/FhirR4.java
Original file line number Diff line number Diff line change
Expand Up @@ -3385,7 +3385,7 @@ private static Type convertFhirDateTime(long datetime, boolean time) {
* @param system The system identifier, such as a URI. Optional; may be null.
* @return The converted CodeableConcept
*/
private static CodeableConcept mapCodeToCodeableConcept(Code from, String system) {
public static CodeableConcept mapCodeToCodeableConcept(Code from, String system) {
CodeableConcept to = new CodeableConcept();
system = system == null ? null : ExportHelper.getSystemURI(system);
from.system = ExportHelper.getSystemURI(from.system);
Expand Down
24 changes: 17 additions & 7 deletions src/main/java/org/mitre/synthea/export/flexporter/Actions.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.hl7.fhir.r4.model.Bundle.BundleEntryRequestComponent;
import org.hl7.fhir.r4.model.Bundle.BundleType;
import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Meta;
Expand Down Expand Up @@ -822,7 +823,7 @@ private static Object getValue(Bundle bundle, String valueDef, Resource currentR
} else if (flag.equals("getAttribute")) {
return getAttribute(person, flagValues);
} else if (flag.equals("randomCode")) {
return randomCode(flagValues[0]);
return randomCode(flagValues);
}

return null;
Expand Down Expand Up @@ -900,13 +901,22 @@ private static Base findValue(Bundle bundle, String... args) {
return fieldValues.get(0);
}

private static Map<String, String> randomCode(String valueSetUrl) {
private static Object randomCode(String... args) {
String valueSetUrl = args[0];
String outputType = (args.length > 1) ? args[1] : "Coding";
Code code = RandomCodeGenerator.getCode(valueSetUrl,
(int) (Math.random() * Integer.MAX_VALUE));
Map<String, String> codeAsMap = Map.of(
"system", code.system,
"code", code.code,
"display", code.display == null ? "" : code.display);
return codeAsMap;

if (outputType.equalsIgnoreCase("code")) {
return code.code;
} else if (outputType.equalsIgnoreCase("Coding")) {
return new Coding(code.system, code.code, code.display);
} else if (outputType.equalsIgnoreCase("CodeableConcept")) {
return FhirR4.mapCodeToCodeableConcept(code, null);
} else {
throw new IllegalArgumentException("Unexpected output type for randomCode: " + outputType
+ ". Valid values are: code, Coding, CodeableConcept");
}

}
}
22 changes: 22 additions & 0 deletions src/main/java/org/mitre/synthea/export/flexporter/Mapping.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package org.mitre.synthea.export.flexporter;

import com.fasterxml.jackson.core.JsonProcessingException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;

import org.hl7.fhir.r4.model.ValueSet;
import org.mitre.synthea.helpers.RandomCodeGenerator;
import org.mitre.synthea.helpers.Utilities;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;

Expand All @@ -15,6 +20,7 @@ public class Mapping {
public String applicability;

public Map<String, Object> variables;
public List<Map<String, Object>> customValueSets;

/**
* Each action is a {@code Map>String,?>}. Nested fields within the YAML become ArrayLists and
Expand All @@ -34,4 +40,20 @@ public static Mapping parseMapping(File mappingFile) throws FileNotFoundExceptio

return yaml.loadAs(selectorInputSteam, Mapping.class);
}

/**
* Load the custom ValueSets that this mapping defines, so that the codes can be selected
* in RandomCodeGenerator.
*/
public void loadValueSets() {
try {
if (this.customValueSets != null) {
List<ValueSet> valueSets =
Utilities.parseYamlToResources(this.customValueSets, ValueSet.class);
valueSets.forEach(vs -> RandomCodeGenerator.loadValueSet(null, vs));
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
23 changes: 21 additions & 2 deletions src/main/java/org/mitre/synthea/helpers/RandomCodeGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import okhttp3.ResponseBody;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.UrlValidator;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.ValueSet;
import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent;
import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
Expand Down Expand Up @@ -117,8 +119,25 @@ private static synchronized void expandValueSet(String valueSetUri) {
ResponseBody body = response.body();
if (body != null) {
IParser parser = FhirR4.getContext().newJsonParser();
ValueSet valueSet = (ValueSet) parser.parseResource(body.charStream());
loadValueSet(valueSetUri, valueSet);
Resource resource = (Resource) parser.parseResource(body.charStream());
if (resource instanceof ValueSet) {
loadValueSet(valueSetUri, (ValueSet)resource);
} else if (resource instanceof OperationOutcome) {
OperationOutcome oo = (OperationOutcome)resource;
parser.setPrettyPrint(true);
System.err.println(parser.encodeResourceToString(oo));
String details = oo.getIssueFirstRep().getDetails().getText();

throw new RuntimeException(
"Received OperationOutcome in ValueSet expand response. Detail: "
+ details + ". See log for full resource");
} else {
parser.setPrettyPrint(true);
System.err.println(parser.encodeResourceToString(resource));
throw new RuntimeException(
"Unexpected resourceType received in expand ValueSet response: "
+ resource.getResourceType() + ". See log for full resource");
}
} else {
throw new RuntimeException("Value Set Expansion contained no body");
}
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/org/mitre/synthea/helpers/Utilities.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package org.mitre.synthea.helpers;

import ca.uhn.fhir.parser.IParser;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import com.google.gson.FieldNamingPolicy;
Expand Down Expand Up @@ -36,9 +40,11 @@
import java.util.regex.Pattern;

import org.apache.commons.lang3.Range;
import org.hl7.fhir.r4.model.Resource;
import org.mitre.synthea.engine.Logic;
import org.mitre.synthea.engine.Module;
import org.mitre.synthea.engine.State;
import org.mitre.synthea.export.FhirR4;
import org.mitre.synthea.world.concepts.HealthRecord.Code;

public class Utilities {
Expand Down Expand Up @@ -669,4 +675,36 @@ public static void enableReadingURIFromJar(URI uri) throws IOException {
}
}
}

/**
* Helper method to parse FHIR resources from YAML.
* This is a workaround since the FHIR model classes don't work with our YAML parser.
*
* @param <T> Resource type contained in the YAML
* @param yaml List of pre-parsed YAML as Map&lt;String, Object&gt;
* @param resourceClass Specific resource class, must not be Resource
* @return List of parsed resources
* @throws JsonProcessingException (should never happen)
*/
public static <T extends Resource> List<T> parseYamlToResources(
List<Map<String, Object>> yaml, Class<T> resourceClass)
throws JsonProcessingException {
if (yaml.isEmpty()) {
return Collections.emptyList();
}
ObjectMapper jsonMapper = new ObjectMapper();
IParser jsonParser = FhirR4.getContext().newJsonParser();
List<T> results = new ArrayList<>();
for (Map<String, Object> singleYaml : yaml) {
if (!singleYaml.containsKey("resourceType")) {
// allows the YAML to be cleaner by letting the resourceType be implied
singleYaml.put("resourceType", resourceClass.getSimpleName());
}
String resourceJson = jsonMapper.writeValueAsString(singleYaml);
@SuppressWarnings("unchecked")
T resource = (T) jsonParser.parseResource(resourceJson);
results.add(resource);
}
return results;
}
}
34 changes: 34 additions & 0 deletions src/test/java/org/mitre/synthea/export/flexporter/ActionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,15 @@
import org.hl7.fhir.r4.model.ServiceRequest;
import org.hl7.fhir.r4.model.TimeType;
import org.hl7.fhir.r4.model.Type;
import org.hl7.fhir.r4.model.ValueSet;
import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mitre.synthea.engine.Module;
import org.mitre.synthea.engine.State;
import org.mitre.synthea.export.FhirR4;
import org.mitre.synthea.helpers.RandomCodeGenerator;
import org.mitre.synthea.world.agents.Person;

public class ActionsTest {
Expand All @@ -82,6 +85,7 @@ public static void setupClass() throws FileNotFoundException {
File file = new File(classLoader.getResource("flexporter/test_mapping.yaml").getFile());

testMapping = Mapping.parseMapping(file);
testMapping.loadValueSets();
}

@AfterClass
Expand Down Expand Up @@ -817,4 +821,34 @@ public void testGetAttribute() throws Exception {
assertEquals("Robert Rainbow", name.getText());
}

@Test
public void testRandomCode() {
Bundle b = new Bundle();
b.setType(BundleType.COLLECTION);

Map<String, Object> action = getActionByName("testRandomCode");
Actions.applyAction(b, action, null, null);

Encounter e = (Encounter) b.getEntryFirstRep().getResource();

Encounter.EncounterStatus status = e.getStatus();
assertNotNull(status);
assertTrue(status == Encounter.EncounterStatus.PLANNED
|| status == Encounter.EncounterStatus.FINISHED
|| status == Encounter.EncounterStatus.CANCELLED);

Coding encClass = e.getClass_();
assertNotNull(encClass);
assertEquals("http://terminology.hl7.org/CodeSystem/v3-ActCode", encClass.getSystem());
String code = encClass.getCode();
assertTrue(code.equals("AMB") || code.equals("EMER") || code.equals("ACUTE"));

CodeableConcept type = e.getTypeFirstRep();
assertNotNull(type);
Coding typeCoding = type.getCodingFirstRep();
assertNotNull(typeCoding);
assertEquals("http://terminology.hl7.org/CodeSystem/encounter-type", typeCoding.getSystem());
code = typeCoding.getCode();
assertTrue(code.equals("ADMS") || code.equals("OKI"));
}
}
54 changes: 54 additions & 0 deletions src/test/resources/flexporter/test_mapping.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,49 @@ name: Random Testing
# for now the assumption is 1 file = 1 synthea patient bundle.
applicability: true

# Not a huge fan of this format, but it's better than defining yet another custom syntax
customValueSets:
- url: whats-for-dinner
compose:
include:
- system: http://snomed.info/sct
concept:
- code: 227360002
display: Pinto beans (substance)
- code: 227319009
display: Baked beans canned in tomato sauce with burgers (substance)
- url: http://example.org/encounterStatus
compose:
include:
- system: http://hl7.org/fhir/encounter-status
concept:
- code: planned
display: Planned
- code: finished
display: Finished
- code: cancelled
display: Cancelled
- url: http://example.org/encounterClass
compose:
include:
- system: http://terminology.hl7.org/CodeSystem/v3-ActCode
concept:
- code: AMB
display: ambulatory
- code: EMER
display: emergency
- code: ACUTE
display: inpatient acute
- url: http://example.org/encounterType
compose:
include:
- system: http://terminology.hl7.org/CodeSystem/encounter-type
concept:
- code: ADMS
display: Annual diabetes mellitus screening
- code: OKI
display: Outpatient Kenacort injection

actions:
- name: Apply Profiles
# v1: define specific profiles and an applicability statement on when to apply them
Expand Down Expand Up @@ -280,6 +323,17 @@ actions:
location: ServiceRequest.authoredOn
value: $getField([Procedure.performed]) # datetime choice type

- name: testRandomCode
create_resource:
- resourceType: Encounter
fields:
- location: Encounter.status
value: $randomCode([http://example.org/encounterStatus,code])
- location: Encounter.class
value: $randomCode([http://example.org/encounterClass])
- location: Encounter.type
value: $randomCode([http://example.org/encounterType,CodeableConcept])


- name: testExecuteScript
execute_script:
Expand Down
Loading