Skip to content

Commit

Permalink
Execute phenotype FHIR Bundle (#46)
Browse files Browse the repository at this point in the history
Initial support for JSON file-based Bundle only.  Will expand to URL-based later.
  • Loading branch information
lrasmus committed Mar 5, 2021
1 parent d20afa8 commit c6e83b9
Show file tree
Hide file tree
Showing 12 changed files with 1,639 additions and 24 deletions.
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<bintray.package>phema-elm-to-ohdsi</bintray.package>
<ohdsi.webapi.version>2.6.0</ohdsi.webapi.version>
<ohdsi.circe.version>1.7.0</ohdsi.circe.version>
<hapi.fhir.version>5.3.0</hapi.fhir.version>
</properties>

<repositories>
Expand Down Expand Up @@ -226,6 +227,12 @@
<version>2.3.1</version>
</dependency>

<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r4</artifactId>
<version>${hapi.fhir.version}</version>
</dependency>

<!-- OHDSI dependencies -->
<dependency>
<groupId>org.ohdsi</groupId>
Expand Down
34 changes: 22 additions & 12 deletions src/main/java/edu/phema/elm_to_omop/ElmToOmopConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import edu.phema.elm_to_omop.helper.Config;
import edu.phema.elm_to_omop.helper.MyFormatter;
import edu.phema.elm_to_omop.io.OmopWriter;
import edu.phema.elm_to_omop.phenotype.BundlePhenotype;
import edu.phema.elm_to_omop.phenotype.FilePhenotype;
import edu.phema.elm_to_omop.phenotype.IPhenotype;
import edu.phema.elm_to_omop.phenotype.PhenotypeException;
import edu.phema.elm_to_omop.repository.OmopRepositoryService;
import edu.phema.elm_to_omop.vocabulary.ConceptCodeCsvFileValuesetService;
Expand Down Expand Up @@ -68,21 +70,29 @@ public void run(String[] args) {
OmopRepositoryService omopService = new OmopRepositoryService(domain, source);
OmopWriter omopWriter = new OmopWriter(logger);

// Initialize phenotype, which will do the following:
//
// 1. Determine if the user has specified which expression(s) is/are the phenotype definitions of interest.
// the CQL/ELM can be vague if not explicitly defined otherwise.
// 2. Read the elm file and set up the objects
FilePhenotype phenotype = new FilePhenotype(config.getInputFileName(), config.getPhenotypeExpressions());

// read the value set csv and add to the objects. If the tab is specified, we assume that it is a spreadsheet. Otherwise we will use the
// default CSV reader.
IValuesetService valuesetService = null;
if (config.isTabSpecified()) {
IPhenotype phenotype = null;
if (config.isUsingBundle()) {
BundlePhenotype bundlePhenotype = new BundlePhenotype(
config.getInputBundleName(), config.getPhenotypeExpressions(), omopService);
valuesetService = bundlePhenotype.getValuesetService();
phenotype = bundlePhenotype;
} else {
// Initialize phenotype, which will do the following:
//
// 1. Determine if the user has specified which expression(s) is/are the phenotype definitions of interest.
// the CQL/ELM can be vague if not explicitly defined otherwise.
// 2. Read the elm file and set up the objects
phenotype = new FilePhenotype(config.getInputFileName(), config.getPhenotypeExpressions());

// read the value set csv and add to the objects. If the tab is specified, we assume that it is a spreadsheet. Otherwise we will use the
// default CSV reader.
if (config.isTabSpecified()) {
valuesetService = new SpreadsheetValuesetService(omopService, config.getVsFileName(), config.getTab());
}
else {
}
else {
valuesetService = new ConceptCodeCsvFileValuesetService(omopService, config.getVsFileName(), true);
}
}

List<PhemaConceptSet> conceptSets = valuesetService.getConceptSets();
Expand Down
53 changes: 41 additions & 12 deletions src/main/java/edu/phema/elm_to_omop/helper/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public final class Config {

private String inputFileName;

private String inputBundleName;

private String vsFileName;

private String outFileName;
Expand Down Expand Up @@ -116,6 +118,7 @@ public String getProperty(final String key) {
public void setConfig() {
omopBaseURL = getProperty("OMOP_BASE_URL");
inputFileName = getProperty("INPUT_FILE_NAME");
inputBundleName = getProperty("INPUT_BUNDLE_NAME");
vsFileName = getProperty("VS_FILE_NAME");
outFileName = getProperty("OUT_FILE_NAME");
source = getProperty("SOURCE");
Expand All @@ -125,6 +128,10 @@ public void setConfig() {
runPropertyCheck();
}

public String getInputBundleName() {
return inputBundleName;
}

public String getInputFileName() {
return inputFileName;
}
Expand Down Expand Up @@ -169,6 +176,9 @@ private void setArg(final String inArg) {
if (prop.equalsIgnoreCase("INPUT_FILE_NAME")) {
inputFileName = val;
}
if (prop.equalsIgnoreCase("INPUT_BUNDLE_NAME")) {
inputBundleName = val;
}
if (prop.equalsIgnoreCase("VS_FILE_NAME")) {
vsFileName = val;
}
Expand Down Expand Up @@ -203,25 +213,36 @@ private void runPropertyCheck() {
if (omopBaseURL == null) {
logger.severe("ERROR - missing parameter OMOP_BASE_URL");
}
if (inputFileName == null) {

boolean loadBundle = (inputBundleName != null) && (!inputBundleName.equals(""));
boolean loadFile = (inputFileName != null) && (!inputFileName.equals(""));

if (!loadBundle && !loadFile) {
logger.severe("ERROR - missing either INPUT_FILE_NAME or INPUT_BUNDLE_NAME");
} else if (loadBundle && loadFile) {
logger.severe("ERROR - specify either INPUT_FILE_NAME or INPUT_BUNDLE_NAME, but not both");
} else if (loadFile) {
if (inputFileName == null) {
logger.severe("ERROR - missing parameter INPUT_FILE_NAME");
}
if (vsFileName == null) {
}
if (vsFileName == null) {
logger.severe("ERROR - missing parameter VS_FILE_NAME");
}
if (tab == null) {
logger.severe("INFO - missing optional parameter VS_TAB");
}
}

if (outFileName == null) {
logger.severe("ERROR - missing parameter OUT_FILE_NAME");
}
if (source == null) {
logger.severe("ERROR - missing parameter SOURCE");
}
if (tab == null) {
logger.severe("INFO - missing optional parameter VS_TAB");
}
}

public String configString() {
return String.format("OMOP_BASE_URL=%s, INPUT_FILE_NAME=%s, VS_FILE_NAME=%s, OUT_FILE_NAME=%s, SOURCE=%s, VS_TAB=%s", omopBaseURL, inputFileName, vsFileName, outFileName, source, tab);
return String.format("OMOP_BASE_URL=%s, INPUT_BUNDLE_NAME=%s, INPUT_FILE_NAME=%s, VS_FILE_NAME=%s, OUT_FILE_NAME=%s, SOURCE=%s, VS_TAB=%s", omopBaseURL, inputBundleName, inputFileName, vsFileName, outFileName, source, tab);
}

public String getConfigFileName() {
Expand Down Expand Up @@ -252,6 +273,10 @@ public void setOmopBaseURL(String omopBaseURL) {
this.omopBaseURL = omopBaseURL;
}

public void setInputBundleName(String inputBundleName) {
this.inputBundleName = inputBundleName;
}

public void setInputFileName(String inputFileName) {
this.inputFileName = inputFileName;
}
Expand All @@ -276,11 +301,15 @@ public void setPhenotypeExpressions(List<String> phenotypeExpressions) {
this.phenotypeExpressions = phenotypeExpressions;
}

/**
* Determine if a value has been specified for the value set spreadsheet tab.
* @return
*/
public boolean isTabSpecified() {
public boolean isUsingBundle() {
return !this.inputBundleName.equals("");
}

/**
* Determine if a value has been specified for the value set spreadsheet tab.
* @return
*/
public boolean isTabSpecified() {
return (this.tab != null && !this.tab.equals(""));
}
}
154 changes: 154 additions & 0 deletions src/main/java/edu/phema/elm_to_omop/io/FhirReader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package edu.phema.elm_to_omop.io;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import edu.phema.elm_to_omop.phenotype.BundlePhenotype;
import edu.phema.elm_to_omop.phenotype.PhenotypeException;
import edu.phema.elm_to_omop.repository.IOmopRepositoryService;
import edu.phema.elm_to_omop.repository.OmopRepositoryException;
import edu.phema.elm_to_omop.vocabulary.FhirBundleConceptSetService;
import edu.phema.elm_to_omop.vocabulary.IValuesetService;
import edu.phema.elm_to_omop.vocabulary.ValuesetServiceException;
import edu.phema.elm_to_omop.vocabulary.phema.PhemaConceptSet;
import edu.phema.elm_to_omop.vocabulary.phema.PhemaValueSet;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Library;
import org.hl7.fhir.r4.model.ResourceType;
import org.hl7.fhir.r4.model.ValueSet;
import org.ohdsi.circe.vocabulary.Concept;
import org.ohdsi.circe.vocabulary.ConceptSetExpression;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class FhirReader {
// Initial file-based support will focus just on JSON files
public static final String BUNDLE_EXTENSION = ".json";
private IOmopRepositoryService repository;
private static Map<String, String> codeSystemUriToOmopName = new HashMap<String, String>() {{
// TODO Add more mappings as needed
put("http://www.ama-assn.org/go/cpt", "CPT4");
put("http://hl7.org/fhir/sid/icd-9-cm", "ICD9CM");
put("http://hl7.org/fhir/sid/icd-9-proc", "ICD9Proc");
put("http://loinc.org", "LOINC");
put("http://www.nlm.nih.gov/research/umls/rxnorm", "RxNorm");
put("http://snomed.info/sct", "SNOMED");
put("http://hl7.org/fhir/sid/icd-10", "ICD10");
put("http://hl7.org/fhir/sid/icd-10-cm", "ICD10CM");
put("http://www.icd10data.com/icd10pcs", "ICD10PCS");
put("http://hl7.org/fhir/sid/icd-10-pcs", "ICD10PCS");
put("http://hl7.org/fhir/sid/ndc", "NDC");
put("http://hl7.org/fhir/ndfrt", "NDFRT");
put("http://unitsofmeasure.org", "UCUM");
put("http://www.whocc.no/atc", "ATC");
put("http://terminology.hl7.org/CodeSystem/HCPCS", "HCPCS");
}};

public FhirReader(IOmopRepositoryService repository) {
this.repository = repository;
}

/**
* Given a FHIR Bundle stored as a JSON file, read all components (libraries, valuesets, etc.)
* and prepare it as a consolidated BundlePhenotype
* @param bundleFilePath The path to the Bundle JSON file
* @param omopService OMOP service to resolve concepts
* @return The consoldiated BundlePhenotype
* @throws PhenotypeException
*/
public static BundlePhenotype readBundleFromFile(String bundleFilePath, IOmopRepositoryService omopService) throws PhenotypeException {
File file = new File(bundleFilePath);
FhirContext ctx = FhirContext.forR4();
IParser parser = ctx.newJsonParser();
try {
Bundle bundle = parser.parseResource(Bundle.class, new FileReader(file));
List<Bundle.BundleEntryComponent> entries = bundle.getEntry();
// Load all of the libraries - this includes phenotype and supporting libraries
List<Library> libraryEntries = entries.stream()
.filter(x -> x.getResource().getResourceType() == ResourceType.Library)
.map(x -> (Library)x.getResource())
.collect(Collectors.toList());

List<ValueSet> valueSetEntries = entries.stream()
.filter(x -> x.getResource().getResourceType() == ResourceType.ValueSet)
.map(x -> (ValueSet)x.getResource())
.collect(Collectors.toList());

IValuesetService service = new FhirBundleConceptSetService(valueSetEntries, omopService);
BundlePhenotype phenotype = new BundlePhenotype();
phenotype.addLibraries(libraryEntries);
phenotype.setValuesetService(service);
return phenotype;
} catch (Exception e) {
throw new PhenotypeException("Error reading phenotype bundle", e);
}
}

/**
* Convert FHIR ValueSet resources to the PhemaConceptSet representation
* @param valueSets List of FHIR ValueSet resources
* @return List of PhemaConceptSets
* @throws IOException
* @throws OmopRepositoryException
* @throws InvalidFormatException
* @throws ValuesetServiceException
*/
public List<PhemaConceptSet> getConceptSets(List<ValueSet> valueSets) throws IOException, OmopRepositoryException, InvalidFormatException, ValuesetServiceException {
List<PhemaConceptSet> conceptSets = new ArrayList<PhemaConceptSet>(valueSets.size());
for (int index = 0; index < valueSets.size(); index++) {
ValueSet valueSet = valueSets.get(index);
conceptSets.add(getConceptSet(valueSet, index));
}
return conceptSets;
}

/**
* Convert a ValueSet to a PhemaConceptSet
* @param valueSet FHIR ValueSet resource to convert
* @param index The index the valueset appears - must be unique
* @return PhemaConceptSet containing all codes from the value set
* @throws OmopRepositoryException
* @throws ValuesetServiceException
*/
private PhemaConceptSet getConceptSet(ValueSet valueSet, int index) throws OmopRepositoryException, ValuesetServiceException {
// TODO - account for more than just "include", but this will work for now
List<Concept> concepts = new ArrayList<>();
List<ValueSet.ConceptSetComponent> components = valueSet.getCompose().getInclude();
for (ValueSet.ConceptSetComponent component : components) {
String codeSystem = component.getSystem();
if (!codeSystemUriToOmopName.containsKey(codeSystem)) {
throw new ValuesetServiceException(String.format("Unable to transform the code system %s", codeSystem));
}
codeSystem = codeSystemUriToOmopName.get(codeSystem);
for (ValueSet.ConceptReferenceComponent concept : component.getConcept()) {
concepts.addAll(repository.vocabularySearch(concept.getCode(), codeSystem));
}
}

PhemaConceptSet conceptSet = new PhemaConceptSet();
conceptSet.setOid(valueSet.getIdElement().getIdPart());
conceptSet.id = index;
conceptSet.name = valueSet.getName();
ArrayList<ConceptSetExpression.ConceptSetItem> itemsList = new ArrayList<>();
for (Concept concept : concepts) {
ConceptSetExpression.ConceptSetItem item = new ConceptSetExpression.ConceptSetItem();
item.concept = concept;
itemsList.add(item);
}

ConceptSetExpression.ConceptSetItem[] items = new ConceptSetExpression.ConceptSetItem[itemsList.size()];
ConceptSetExpression expression = new ConceptSetExpression();
expression.items = itemsList.toArray(items);

conceptSet.expression = expression;

return conceptSet;
}
}
Loading

0 comments on commit c6e83b9

Please sign in to comment.