Skip to content

Add domain for FHIR ObservationCategoryMaps #201

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ configuration/
├── drugs/
├── encountertypes/
├── fhirconceptsources/
├── fhirobservationcategorymaps/
├── fhirpatientidentifiersystems/
├── globalproperties/
├── htmlforms/
Expand Down Expand Up @@ -115,6 +116,7 @@ This is the list of currently supported domains in their loading order:
1. [Metadata Term Mappings (CSV files)](readme/mdm.md#domain-metadatatermmappings)
1. [FHIR Concept Sources (CSV files)](readme/fhir.md#domain-fhirconceptsources)
1. [FHIR Patient Identifier Systems (CSV Files)](readme/fhir.md#domain-fhirpatientidentifiersystems)
1. [FHIR Observation Category Maps (CSV Files)](readme/fhir.md#domain-fhirobservationcategorymaps)
1. [AMPATH Forms (JSON files)](readme/ampathforms.md)
1. [AMPATH Forms Translations (JSON files)](readme/ampathformstranslations.md)
1. [HTML Forms (XML files)](readme/htmlforms.md)
Expand All @@ -138,6 +140,7 @@ mvn clean package
* Metadata Sharing 1.2.2 (*compatible*)
* Metadata Mapping 1.3.4 (*compatible*)
* Open Concept Lab 1.2.9 (*compatible*)
* FHIR2 1.2.0 (*compatible*)

### How to test out your OpenMRS configs?
See the [Initializer Validator README page](readme/validator.md).
Expand Down Expand Up @@ -172,6 +175,7 @@ https://github.com/mekomsolutions/openmrs-module-initializer/issues
* Enhancement to ensure that reloading Concept CSVs does not clear Members/Answers if those columns aren't part of CSV file.
* 'concepts' domain to support a new expandable `MAPPINGS` header, thereby discouraging the older `Same as mappings`.
* Concept references expanded to allow use of concept names in locales other than the default system locale
* Support for FHIR2 module and domains related to the metadata needed for FHIR

#### Version 2.3.0
* Added configuration options for logging.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public enum Domain {
METADATA_SET_MEMBERS,
METADATA_TERM_MAPPINGS,
FHIR_CONCEPT_SOURCES,
FHIR_OBSERVATION_CATEGORY_MAPS,
FHIR_PATIENT_IDENTIFIER_SYSTEMS,
AMPATH_FORMS,
AMPATH_FORMS_TRANSLATIONS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ private T initialize(String[] line) throws APIException {
final CsvLine csvLine = new CsvLine(headerLine, line);

//
// 1. Boostrapping
// 1. Bootstrapping
//
T instance = bootstrap(csvLine);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.openmrs.module.initializer.api.fhir.ocm;

import org.hibernate.SessionFactory;
import org.openmrs.ConceptClass;
import org.openmrs.annotation.OpenmrsProfile;
import org.openmrs.module.fhir2.model.FhirObservationCategoryMap;
import org.openmrs.module.initializer.Domain;
import org.openmrs.module.initializer.api.BaseLineProcessor;
import org.openmrs.module.initializer.api.CsvLine;
import org.openmrs.module.initializer.api.CsvParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

import static org.openmrs.module.initializer.Domain.FHIR_OBSERVATION_CATEGORY_MAPS;

@OpenmrsProfile(modules = { "fhir2:1.*" })
public class FhirObservationCategoryMapCsvParser extends CsvParser<FhirObservationCategoryMap, BaseLineProcessor<FhirObservationCategoryMap>> {

public static final String FHIR_OBS_CATEGORY_HEADER = "Fhir observation category";

public static final String CONCEPT_CLASS_HEADER = "Concept class";

private final SessionFactory sessionFactory;

@Autowired
protected FhirObservationCategoryMapCsvParser(@Qualifier("sessionFactory") SessionFactory sessionFactory,
BaseLineProcessor<FhirObservationCategoryMap> lineProcessor) {
super(lineProcessor);

this.sessionFactory = sessionFactory;
}

@Override
public Domain getDomain() {
return FHIR_OBSERVATION_CATEGORY_MAPS;
}

@Override
public FhirObservationCategoryMap bootstrap(CsvLine line) throws IllegalArgumentException {
FhirObservationCategoryMap result = null;

String fhirObsCategory = line.get(FHIR_OBS_CATEGORY_HEADER);
String conceptClass = line.get(CONCEPT_CLASS_HEADER);

if (fhirObsCategory != null && !fhirObsCategory.isEmpty() && conceptClass != null && !conceptClass.isEmpty()) {
result = (FhirObservationCategoryMap) sessionFactory.getCurrentSession()
.createQuery("from " + FhirObservationCategoryMap.class.getSimpleName()
+ " where observationCategory = :fhirObsCategory and conceptClass = (" + "select cc from "
+ ConceptClass.class.getSimpleName() + " cc where cc.name = :conceptClass or cc.uuid = :conceptClass" + ")")
.setParameter("fhirObsCategory", fhirObsCategory).setParameter("conceptClass", conceptClass)
.uniqueResult();
}

if (result == null && line.getUuid() != null && !line.getUuid().isEmpty()) {
result = (FhirObservationCategoryMap) sessionFactory.getCurrentSession()
.createQuery("from " + FhirObservationCategoryMap.class.getSimpleName() + " where uuid = :uuid")
.setParameter("uuid", line.getUuid()).uniqueResult();
}

if (result == null) {
result = new FhirObservationCategoryMap();
result.setUuid(line.getUuid());
}

return result;
}

@Override
public FhirObservationCategoryMap save(FhirObservationCategoryMap instance) {
sessionFactory.getCurrentSession().saveOrUpdate(instance);
return instance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.openmrs.module.initializer.api.fhir.ocm;

import org.apache.commons.lang3.StringUtils;
import org.openmrs.ConceptClass;
import org.openmrs.annotation.OpenmrsProfile;
import org.openmrs.api.ConceptService;
import org.openmrs.module.fhir2.model.FhirObservationCategoryMap;
import org.openmrs.module.initializer.api.BaseLineProcessor;
import org.openmrs.module.initializer.api.CsvLine;
import org.springframework.beans.factory.annotation.Autowired;

import static org.openmrs.module.initializer.api.fhir.ocm.FhirObservationCategoryMapCsvParser.CONCEPT_CLASS_HEADER;
import static org.openmrs.module.initializer.api.fhir.ocm.FhirObservationCategoryMapCsvParser.FHIR_OBS_CATEGORY_HEADER;

@OpenmrsProfile(modules = { "fhir2:1.*" })
public class FhirObservationCategoryMapLineProcessor extends BaseLineProcessor<FhirObservationCategoryMap> {

private final ConceptService conceptService;

@Autowired
public FhirObservationCategoryMapLineProcessor(ConceptService conceptService) {
this.conceptService = conceptService;
}

@Override
public FhirObservationCategoryMap fill(FhirObservationCategoryMap instance, CsvLine line)
throws IllegalArgumentException {
if (StringUtils.isBlank(instance.getUuid()) && StringUtils.isBlank(line.getUuid())) {
throw new IllegalArgumentException("No UUID was found for FHIR observation category map");
}

String fhirObsCategory = line.get(FHIR_OBS_CATEGORY_HEADER, true);
if (StringUtils.isBlank(fhirObsCategory) && !instance.getRetired()) {
throw new IllegalArgumentException("'" + FHIR_OBS_CATEGORY_HEADER
+ "' was not found for FHIR observation category map " + instance.getUuid());
}

String conceptClass = line.get(CONCEPT_CLASS_HEADER, true);
if (StringUtils.isBlank(conceptClass) && !instance.getRetired()) {
throw new IllegalArgumentException(
"'" + CONCEPT_CLASS_HEADER + "' was not found for FHIR observation category map " + instance.getUuid());
}

ConceptClass cc = conceptService.getConceptClassByName(conceptClass);
if (cc == null && !instance.getRetired()) {
throw new IllegalArgumentException("Concept class " + conceptClass
+ " was not found while creating FHIR observation category map " + instance.getUuid());
}

if (StringUtils.isNotBlank(line.getUuid())) {
instance.setUuid(line.getUuid());
}

instance.setObservationCategory(fhirObsCategory);
instance.setConceptClass(cc);

return instance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.openmrs.module.initializer.api.fhir.ocm;

import org.openmrs.annotation.OpenmrsProfile;
import org.openmrs.module.fhir2.model.FhirObservationCategoryMap;
import org.openmrs.module.initializer.api.loaders.BaseCsvLoader;
import org.springframework.beans.factory.annotation.Autowired;

@OpenmrsProfile(modules = { "fhir2:1.*" })
public class FhirObservationCategoryMapLoader extends BaseCsvLoader<FhirObservationCategoryMap, FhirObservationCategoryMapCsvParser> {

@Autowired
public void setParser(FhirObservationCategoryMapCsvParser parser) {
this.parser = parser;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.openmrs.module.initializer.api;

import java.util.List;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.junit.Before;
import org.junit.Test;
import org.openmrs.ConceptClass;
import org.openmrs.api.ConceptService;
import org.openmrs.api.context.Context;
import org.openmrs.module.fhir2.model.FhirObservationCategoryMap;
import org.openmrs.module.initializer.DomainBaseModuleContextSensitiveTest;
import org.openmrs.module.initializer.api.fhir.ocm.FhirObservationCategoryMapLoader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.hasSize;

public class FhirObservationCategoryMapIntegrationTest extends DomainBaseModuleContextSensitiveTest {

@Autowired
@Qualifier("sessionFactory")
private SessionFactory sessionFactory;

@Autowired
private ConceptService conceptService;

@Autowired
private FhirObservationCategoryMapLoader loader;

@Before
public void setup() {
{
ConceptClass conceptClass = conceptService.getConceptClassByName("Test");
if (conceptClass == null) {
conceptClass = new ConceptClass();
conceptClass.setName("Test");
conceptClass.setUuid(ConceptClass.TEST_UUID);
sessionFactory.getCurrentSession().saveOrUpdate(conceptClass);
}
}

{
ConceptClass conceptClass = conceptService.getConceptClassByName("Finding");
if (conceptClass == null) {
conceptClass = new ConceptClass();
conceptClass.setName("Finding");
conceptClass.setUuid(ConceptClass.FINDING_UUID);
sessionFactory.getCurrentSession().saveOrUpdate(conceptClass);
}
}

{
FhirObservationCategoryMap observationCategoryMap = new FhirObservationCategoryMap();
observationCategoryMap.setObservationCategory("laboratory");
observationCategoryMap.setConceptClass(conceptService.getConceptClassByName("Test"));
observationCategoryMap.setUuid("2309836d-bf13-4fea-b2d4-87bb997425c7");
sessionFactory.getCurrentSession().saveOrUpdate(observationCategoryMap);
}

Context.flushSession();
}

@Test
public void loader_shouldLoadFhirObservationCategoryMapsAccordingToCsvFiles() {
// Replay
loader.load();

// Verify
Session session = sessionFactory.getCurrentSession();
Query getObsCategoryByCategoryQuery = session.createQuery("from " + FhirObservationCategoryMap.class.getSimpleName()
+ " where observationCategory = :observationCategory");

{
List<FhirObservationCategoryMap> observationCategories = getObsCategoryByCategoryQuery
.setParameter("observationCategory", "laboratory").list();

assertThat(observationCategories, hasSize(2));
assertThat(observationCategories,
hasItem(allOf(hasProperty("uuid", equalTo("e518de2a-be31-4202-9772-cc65c3ef7227")),
hasProperty("conceptClass", hasProperty("name", equalTo("Test"))))));
assertThat(observationCategories,
hasItem(allOf(hasProperty("uuid", equalTo("2374215a-8808-4eee-b5a5-9190423862a0")),
hasProperty("conceptClass", hasProperty("name", equalTo("LabSet"))))));
}

{
List<FhirObservationCategoryMap> observationCategories = getObsCategoryByCategoryQuery
.setParameter("observationCategory", "exam").list();

assertThat(observationCategories, hasSize(1));
assertThat(observationCategories.get(0), hasProperty("uuid", equalTo("5f8e2dd2-ce1f-42d3-bb34-55acb3f58c5d")));
assertThat(observationCategories.get(0).getConceptClass(), hasProperty("name", equalTo("Finding")));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Uuid,Void/Retire,Fhir observation category,Concept class,_order:1000
e518de2a-be31-4202-9772-cc65c3ef7227,,laboratory,Test,
5f8e2dd2-ce1f-42d3-bb34-55acb3f58c5d,,exam,Finding,
2374215a-8808-4eee-b5a5-9190423862a0,,laboratory,LabSet,
34 changes: 26 additions & 8 deletions readme/fhir.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,32 @@ The format of this CSV should be as follows:

Headers that start with an underscore such as `_order:1000` are metadata headers. The values in the columns under those headers are never read by the CSV parser.

###### Header `Patient identifier type`
## Domain 'fhirobservationcategorymaps'

This is *required* for every entry and is what is used to identify the underlying patient identifier type. This can refer to the name (if unique) or uuid of the patient identifier type that this entry refers to. This must refer to an existing Patient Identifier Type, added via the patientidentifiertypes domain. This will not create or modify the underlying patient identifier types. The name of this underlying patient identifier type will be used as the name of the FHIR patient identifier system.
The **fhirobservationcategorymaps** subfolder contains CSV import files for defining mappings between [FHIR Observation category
values](https://www.hl7.org/fhir/valueset-observation-category.html) and OpenMRS ConceptClasses. When configured, this ensures
that the `Observation.category` field on a FHIR Observation will have the value specified by the mapping and the ConceptClass.

###### Header `Url`
`ConceptClass`es referenced in this domain must exist before a category map can be created. However, arbitrary strings can
be used for the category name. However, it is recommended to use strings in the
[FHIR ValueSet for Observation category](https://www.hl7.org/fhir/valueset-observation-category.html) wherever possible to
ensure maximum compatibility.

Please note that the ability to map OpenMRS ConceptClasses to FHIR Observation categories should be regarded as an experimental
feature and may be subject to change in the future.

This is a possible example of its contents:
```bash
fhirobservationcategorymaps/
├──categorymaps.csv
└── ...
```

The format of this CSV should be as follows:

| <sub>Uuid</sub> |<sub>Void/Retire</sub> | <sub>Fhir observation category</sub> | <sub>Concept class</sub> | <sub>_order:1000</sub> |
| - | - | - | - | - |
| <sub>e518de2a-be31-4202-9772-cc65c3ef7227</sub> | | <sub>laboratory</sub> | <sub>Test</sub> | |

Headers that start with an underscore such as `_order:1000` are metadata headers. The values in the columns under those headers are never read by the CSV parser.

This is the URL of the code system in FHIR. For terminologies identified
[in the FHIR CodeSystem registry](https://www.hl7.org/fhir/terminologies-systems.html), this should be the preferred URL for
that code system, e.g. SNOMED CT is "http://snomed.info/sct". If the code system is not defined by HL7 or that table, then
the code systems own preferred URL should be used, e.g., for CIEL we tend to use
"https://api.openconceptlab.org/orgs/CIEL/sources/CIEL".