diff --git a/README.md b/README.md index 98cbf8c6..6a92e1e9 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ * [Setting up and controlling logging](#setting-up-and-controlling-logging) - [Get in touch](#get-in-touch) - [Releases notes](#releases-notes) + + [Version 2.8.0](#version-280) + [Version 2.7.0](#version-270) + [Version 2.6.0](#version-260) + [Version 2.5.2](#version-252) @@ -59,6 +60,7 @@ configuration/ ├── encountertypes/ ├── encounterroles/ ├── fhirconceptsources/ + ├── fhirobservationcategorymaps/ ├── fhirpatientidentifiersystems/ ├── globalproperties/ ├── htmlforms/ @@ -149,6 +151,7 @@ This is the list of currently supported domains in their loading order: 1. [Cohort Attribute Types (CSV files)](readme/cohort.md#domain-cohortattributetypes) 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) @@ -172,6 +175,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*) ### Test your OpenMRS configs See the [Initializer Validator README page](readme/validator.md). @@ -194,6 +198,9 @@ See the [documentation on Initializer's logging properties](readme/rtprops.md#lo ## Releases notes +#### Version 2.8.0 +* Support for FHIR2 module and domains related to the metadata needed for FHIR + #### Version 2.7.0 * Added support for 'queues' domain. * Added support for 'addresshierarchy' domain. diff --git a/api/src/main/java/org/openmrs/module/initializer/Domain.java b/api/src/main/java/org/openmrs/module/initializer/Domain.java index ac1e66e2..19c91696 100644 --- a/api/src/main/java/org/openmrs/module/initializer/Domain.java +++ b/api/src/main/java/org/openmrs/module/initializer/Domain.java @@ -46,6 +46,7 @@ public enum Domain { COHORT_TYPES, COHORT_ATTRIBUTE_TYPES, FHIR_CONCEPT_SOURCES, + FHIR_OBSERVATION_CATEGORY_MAPS, FHIR_PATIENT_IDENTIFIER_SYSTEMS, AMPATH_FORMS, AMPATH_FORMS_TRANSLATIONS, diff --git a/api/src/main/java/org/openmrs/module/initializer/api/CsvParser.java b/api/src/main/java/org/openmrs/module/initializer/api/CsvParser.java index bab06e56..b4cad0e4 100644 --- a/api/src/main/java/org/openmrs/module/initializer/api/CsvParser.java +++ b/api/src/main/java/org/openmrs/module/initializer/api/CsvParser.java @@ -225,7 +225,7 @@ private T initialize(String[] line) throws APIException { final CsvLine csvLine = new CsvLine(headerLine, line); // - // 1. Boostrapping + // 1. Bootstrapping // T instance = bootstrap(csvLine); diff --git a/api/src/main/java/org/openmrs/module/initializer/api/fhir/ocm/FhirObservationCategoryMapCsvParser.java b/api/src/main/java/org/openmrs/module/initializer/api/fhir/ocm/FhirObservationCategoryMapCsvParser.java new file mode 100644 index 00000000..dab4a67f --- /dev/null +++ b/api/src/main/java/org/openmrs/module/initializer/api/fhir/ocm/FhirObservationCategoryMapCsvParser.java @@ -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> { + + 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 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; + } +} diff --git a/api/src/main/java/org/openmrs/module/initializer/api/fhir/ocm/FhirObservationCategoryMapLineProcessor.java b/api/src/main/java/org/openmrs/module/initializer/api/fhir/ocm/FhirObservationCategoryMapLineProcessor.java new file mode 100644 index 00000000..7f1c867c --- /dev/null +++ b/api/src/main/java/org/openmrs/module/initializer/api/fhir/ocm/FhirObservationCategoryMapLineProcessor.java @@ -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 { + + 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; + } +} diff --git a/api/src/main/java/org/openmrs/module/initializer/api/fhir/ocm/FhirObservationCategoryMapLoader.java b/api/src/main/java/org/openmrs/module/initializer/api/fhir/ocm/FhirObservationCategoryMapLoader.java new file mode 100644 index 00000000..f01f5a4e --- /dev/null +++ b/api/src/main/java/org/openmrs/module/initializer/api/fhir/ocm/FhirObservationCategoryMapLoader.java @@ -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 { + + @Autowired + public void setParser(FhirObservationCategoryMapCsvParser parser) { + this.parser = parser; + } +} diff --git a/api/src/test/java/org/openmrs/module/initializer/api/FhirObservationCategoryMapIntegrationTest.java b/api/src/test/java/org/openmrs/module/initializer/api/FhirObservationCategoryMapIntegrationTest.java new file mode 100644 index 00000000..69e13116 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/initializer/api/FhirObservationCategoryMapIntegrationTest.java @@ -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 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 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"))); + } + } +} diff --git a/api/src/test/resources/testAppDataDir/configuration/fhirobservationcategorymaps/observationcategorymaps.csv b/api/src/test/resources/testAppDataDir/configuration/fhirobservationcategorymaps/observationcategorymaps.csv new file mode 100644 index 00000000..64cd682c --- /dev/null +++ b/api/src/test/resources/testAppDataDir/configuration/fhirobservationcategorymaps/observationcategorymaps.csv @@ -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, diff --git a/readme/fhir.md b/readme/fhir.md index 97b9adfb..ced11e2f 100644 --- a/readme/fhir.md +++ b/readme/fhir.md @@ -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: + +| Uuid |Void/Retire | Fhir observation category | Concept class | _order:1000 | +| - | - | - | - | - | +| e518de2a-be31-4202-9772-cc65c3ef7227 | | laboratory | Test | | + +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". \ No newline at end of file