Skip to content

Commit 8e457f3

Browse files
committed
Add domain for FHIR ObservationCategoryMaps
1 parent 3d7e174 commit 8e457f3

File tree

10 files changed

+302
-4
lines changed

10 files changed

+302
-4
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ configuration/
2929
├── drugs/
3030
├── encountertypes/
3131
├── fhirconceptsources/
32+
├── fhirobservationcategorymaps/
3233
├── fhirpatientidentifiersystems/
3334
├── globalproperties/
3435
├── htmlforms/
@@ -114,6 +115,7 @@ This is the list of currently supported domains in their loading order:
114115
1. [Metadata Term Mappings (CSV files)](readme/mdm.md#domain-metadatatermmappings)
115116
1. [FHIR Concept Sources (CSV files)](readme/fhir.md#domain-fhirconceptsources)
116117
1. [FHIR Patient Identifier Systems (CSV Files)](readme/fhir.md#domain-fhirpatientidentifiersystems)
118+
1. [FHIR Observation Category Maps (CSV Files)](readme/fhir.md#domain-fhirobservationcategorymaps)
117119
1. [AMPATH Forms (JSON files)](readme/ampathforms.md)
118120
1. [HTML Forms (XML files)](readme/htmlforms.md)
119121

@@ -136,6 +138,7 @@ mvn clean package
136138
* Metadata Sharing 1.2.2 (*compatible*)
137139
* Metadata Mapping 1.3.4 (*compatible*)
138140
* Open Concept Lab 1.2.9 (*compatible*)
141+
* FHIR2 1.2.0 (*compatible*)
139142

140143
### How to test out your OpenMRS configs?
141144
See the [Initializer Validator README page](readme/validator.md).
@@ -162,6 +165,7 @@ https://github.com/mekomsolutions/openmrs-module-initializer/issues
162165
#### Version 2.4.0
163166
* 'concepts' domain to support a new expandable `MAPPINGS` header, thereby discouraging the older `Same as mappings`.
164167
* Concept references expanded to allow use of concept names in locales other than the default system locale
168+
* Support for FHIR2 module and domains related to the metadata needed for FHIR
165169

166170
#### Version 2.3.0
167171
* Added configuration options for logging.

api/src/main/java/org/openmrs/module/initializer/Domain.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public enum Domain {
4242
METADATA_SET_MEMBERS,
4343
METADATA_TERM_MAPPINGS,
4444
FHIR_CONCEPT_SOURCES,
45+
FHIR_OBSERVATION_CATEGORY_MAPS,
4546
FHIR_PATIENT_IDENTIFIER_SYSTEMS,
4647
AMPATH_FORMS,
4748
HTML_FORMS;

api/src/main/java/org/openmrs/module/initializer/api/CsvParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ private T initialize(String[] line) throws APIException {
220220
final CsvLine csvLine = new CsvLine(headerLine, line);
221221

222222
//
223-
// 1. Boostrapping
223+
// 1. Bootstrapping
224224
//
225225
T instance = bootstrap(csvLine);
226226

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package org.openmrs.module.initializer.api.fhir.ocm;
2+
3+
import org.hibernate.SessionFactory;
4+
import org.openmrs.ConceptClass;
5+
import org.openmrs.annotation.OpenmrsProfile;
6+
import org.openmrs.module.fhir2.model.FhirObservationCategoryMap;
7+
import org.openmrs.module.initializer.Domain;
8+
import org.openmrs.module.initializer.api.BaseLineProcessor;
9+
import org.openmrs.module.initializer.api.CsvLine;
10+
import org.openmrs.module.initializer.api.CsvParser;
11+
import org.springframework.beans.factory.annotation.Autowired;
12+
import org.springframework.beans.factory.annotation.Qualifier;
13+
import org.springframework.stereotype.Component;
14+
15+
import static org.openmrs.module.initializer.Domain.FHIR_OBSERVATION_CATEGORY_MAPS;
16+
17+
@Component
18+
@OpenmrsProfile(modules = { "fhir2:1.*" })
19+
public class FhirObservationCategoryMapCsvParser extends CsvParser<FhirObservationCategoryMap, BaseLineProcessor<FhirObservationCategoryMap>> {
20+
21+
public static final String FHIR_OBS_CATEGORY_HEADER = "Fhir observation category";
22+
23+
public static final String CONCEPT_CLASS_HEADER = "Concept class";
24+
25+
private final SessionFactory sessionFactory;
26+
27+
@Autowired
28+
protected FhirObservationCategoryMapCsvParser(@Qualifier("sessionFactory") SessionFactory sessionFactory,
29+
BaseLineProcessor<FhirObservationCategoryMap> lineProcessor) {
30+
super(lineProcessor);
31+
32+
this.sessionFactory = sessionFactory;
33+
}
34+
35+
@Override
36+
public Domain getDomain() {
37+
return FHIR_OBSERVATION_CATEGORY_MAPS;
38+
}
39+
40+
@Override
41+
public FhirObservationCategoryMap bootstrap(CsvLine line) throws IllegalArgumentException {
42+
FhirObservationCategoryMap result = null;
43+
44+
String fhirObsCategory = line.get(FHIR_OBS_CATEGORY_HEADER);
45+
String conceptClass = line.get(CONCEPT_CLASS_HEADER);
46+
47+
if (fhirObsCategory != null && !fhirObsCategory.isEmpty() && conceptClass != null && !conceptClass.isEmpty()) {
48+
result = (FhirObservationCategoryMap) sessionFactory.getCurrentSession()
49+
.createQuery("from " + FhirObservationCategoryMap.class.getSimpleName()
50+
+ " where observationCategory = :fhirObsCategory and conceptClass = (" + "select cc from "
51+
+ ConceptClass.class.getSimpleName() + " cc where cc.name = :conceptClassName" + ")")
52+
.setParameter("fhirObsCategory", fhirObsCategory).setParameter("conceptClassName", conceptClass)
53+
.uniqueResult();
54+
}
55+
56+
if (result == null && line.getUuid() != null && !line.getUuid().isEmpty()) {
57+
result = (FhirObservationCategoryMap) sessionFactory.getCurrentSession()
58+
.createQuery("from " + FhirObservationCategoryMap.class.getSimpleName() + " where uuid = :uuid")
59+
.setParameter("uuid", line.getUuid()).uniqueResult();
60+
}
61+
62+
if (result == null) {
63+
result = new FhirObservationCategoryMap();
64+
result.setUuid(line.getUuid());
65+
}
66+
67+
return result;
68+
}
69+
70+
@Override
71+
public FhirObservationCategoryMap save(FhirObservationCategoryMap instance) {
72+
sessionFactory.getCurrentSession().saveOrUpdate(instance);
73+
return instance;
74+
}
75+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package org.openmrs.module.initializer.api.fhir.ocm;
2+
3+
import org.apache.commons.lang3.StringUtils;
4+
import org.openmrs.ConceptClass;
5+
import org.openmrs.ConceptSource;
6+
import org.openmrs.annotation.OpenmrsProfile;
7+
import org.openmrs.api.ConceptService;
8+
import org.openmrs.module.fhir2.model.FhirConceptSource;
9+
import org.openmrs.module.fhir2.model.FhirObservationCategoryMap;
10+
import org.openmrs.module.initializer.api.BaseLineProcessor;
11+
import org.openmrs.module.initializer.api.CsvLine;
12+
import org.springframework.beans.factory.annotation.Autowired;
13+
import org.springframework.stereotype.Component;
14+
15+
import static org.openmrs.module.initializer.api.fhir.cs.FhirConceptSourceCsvParser.CONCEPT_SOURCE_NAME_HEADER;
16+
import static org.openmrs.module.initializer.api.fhir.ocm.FhirObservationCategoryMapCsvParser.CONCEPT_CLASS_HEADER;
17+
import static org.openmrs.module.initializer.api.fhir.ocm.FhirObservationCategoryMapCsvParser.FHIR_OBS_CATEGORY_HEADER;
18+
19+
@Component
20+
@OpenmrsProfile(modules = { "fhir2:1.*" })
21+
public class FhirObservationCategoryMapLineProcessor extends BaseLineProcessor<FhirObservationCategoryMap> {
22+
23+
private final ConceptService conceptService;
24+
25+
@Autowired
26+
public FhirObservationCategoryMapLineProcessor(ConceptService conceptService) {
27+
this.conceptService = conceptService;
28+
}
29+
30+
@Override
31+
public FhirObservationCategoryMap fill(FhirObservationCategoryMap instance, CsvLine line)
32+
throws IllegalArgumentException {
33+
if (StringUtils.isBlank(instance.getUuid()) && StringUtils.isBlank(line.getUuid())) {
34+
throw new IllegalArgumentException("No UUID was found for FHIR observation category map");
35+
}
36+
37+
String fhirObsCategory = line.get(FHIR_OBS_CATEGORY_HEADER, true);
38+
if (StringUtils.isBlank(fhirObsCategory) && !instance.getRetired()) {
39+
throw new IllegalArgumentException("'" + FHIR_OBS_CATEGORY_HEADER
40+
+ "' was not found for FHIR observation category map " + instance.getUuid());
41+
}
42+
43+
String conceptClass = line.get(CONCEPT_CLASS_HEADER, true);
44+
if (StringUtils.isBlank(conceptClass) && !instance.getRetired()) {
45+
throw new IllegalArgumentException(
46+
"'" + CONCEPT_CLASS_HEADER + "' was not found for FHIR observation category map " + instance.getUuid());
47+
}
48+
49+
ConceptClass cc = conceptService.getConceptClassByName(conceptClass);
50+
if (cc == null && !instance.getRetired()) {
51+
throw new IllegalArgumentException("Concept class " + conceptClass
52+
+ " was not found while creating FHIR observation category map " + instance.getUuid());
53+
}
54+
55+
if (StringUtils.isNotBlank(line.getUuid())) {
56+
instance.setUuid(line.getUuid());
57+
}
58+
59+
instance.setObservationCategory(fhirObsCategory);
60+
instance.setConceptClass(cc);
61+
62+
return instance;
63+
}
64+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.openmrs.module.initializer.api.fhir.ocm;
2+
3+
import org.openmrs.annotation.OpenmrsProfile;
4+
import org.openmrs.module.fhir2.model.FhirConceptSource;
5+
import org.openmrs.module.fhir2.model.FhirObservationCategoryMap;
6+
import org.openmrs.module.initializer.api.loaders.BaseCsvLoader;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.stereotype.Component;
9+
10+
@Component
11+
@OpenmrsProfile(modules = { "fhir2:1.*" })
12+
public class FhirObservationCategoryMapLoader extends BaseCsvLoader<FhirObservationCategoryMap, FhirObservationCategoryMapCsvParser> {
13+
14+
@Autowired
15+
public void setParser(FhirObservationCategoryMapCsvParser parser) {
16+
this.parser = parser;
17+
}
18+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package org.openmrs.module.initializer.api;
2+
3+
import java.util.List;
4+
5+
import org.hibernate.Query;
6+
import org.hibernate.Session;
7+
import org.hibernate.SessionFactory;
8+
import org.junit.Before;
9+
import org.junit.Test;
10+
import org.openmrs.ConceptClass;
11+
import org.openmrs.api.ConceptService;
12+
import org.openmrs.api.context.Context;
13+
import org.openmrs.module.fhir2.model.FhirObservationCategoryMap;
14+
import org.openmrs.module.initializer.DomainBaseModuleContextSensitiveTest;
15+
import org.openmrs.module.initializer.api.fhir.ocm.FhirObservationCategoryMapLoader;
16+
import org.springframework.beans.factory.annotation.Autowired;
17+
import org.springframework.beans.factory.annotation.Qualifier;
18+
19+
import static org.hamcrest.MatcherAssert.assertThat;
20+
import static org.hamcrest.Matchers.allOf;
21+
import static org.hamcrest.Matchers.equalTo;
22+
import static org.hamcrest.Matchers.hasItem;
23+
import static org.hamcrest.Matchers.hasProperty;
24+
import static org.hamcrest.Matchers.hasSize;
25+
26+
public class FhirObservationCategoryMapIntegrationTest extends DomainBaseModuleContextSensitiveTest {
27+
28+
@Autowired
29+
@Qualifier("sessionFactory")
30+
private SessionFactory sessionFactory;
31+
32+
@Autowired
33+
private ConceptService conceptService;
34+
35+
@Autowired
36+
private FhirObservationCategoryMapLoader loader;
37+
38+
@Before
39+
public void setup() {
40+
{
41+
ConceptClass conceptClass = conceptService.getConceptClassByName("Test");
42+
if (conceptClass == null) {
43+
conceptClass = new ConceptClass();
44+
conceptClass.setName("Test");
45+
conceptClass.setUuid(ConceptClass.TEST_UUID);
46+
sessionFactory.getCurrentSession().saveOrUpdate(conceptClass);
47+
}
48+
}
49+
50+
{
51+
ConceptClass conceptClass = conceptService.getConceptClassByName("Finding");
52+
if (conceptClass == null) {
53+
conceptClass = new ConceptClass();
54+
conceptClass.setName("Finding");
55+
conceptClass.setUuid(ConceptClass.FINDING_UUID);
56+
sessionFactory.getCurrentSession().saveOrUpdate(conceptClass);
57+
}
58+
}
59+
60+
{
61+
FhirObservationCategoryMap observationCategoryMap = new FhirObservationCategoryMap();
62+
observationCategoryMap.setObservationCategory("laboratory");
63+
observationCategoryMap.setConceptClass(conceptService.getConceptClassByName("Test"));
64+
observationCategoryMap.setUuid("2309836d-bf13-4fea-b2d4-87bb997425c7");
65+
sessionFactory.getCurrentSession().saveOrUpdate(observationCategoryMap);
66+
}
67+
68+
Context.flushSession();
69+
}
70+
71+
@Test
72+
public void loader_shouldLoadFhirObservationCategoryMapsAccordingToCsvFiles() {
73+
// Replay
74+
loader.load();
75+
76+
// Verify
77+
Session session = sessionFactory.getCurrentSession();
78+
Query getObsCategoryByCategoryQuery = session.createQuery("from " + FhirObservationCategoryMap.class.getSimpleName()
79+
+ " where observationCategory = :observationCategory");
80+
81+
{
82+
List<FhirObservationCategoryMap> observationCategories = getObsCategoryByCategoryQuery
83+
.setParameter("observationCategory", "laboratory").list();
84+
85+
assertThat(observationCategories, hasSize(2));
86+
assertThat(observationCategories,
87+
hasItem(allOf(hasProperty("uuid", equalTo("e518de2a-be31-4202-9772-cc65c3ef7227")),
88+
hasProperty("conceptClass", hasProperty("name", equalTo("Test"))))));
89+
assertThat(observationCategories,
90+
hasItem(allOf(hasProperty("uuid", equalTo("2374215a-8808-4eee-b5a5-9190423862a0")),
91+
hasProperty("conceptClass", hasProperty("name", equalTo("LabSet"))))));
92+
}
93+
94+
{
95+
List<FhirObservationCategoryMap> observationCategories = getObsCategoryByCategoryQuery
96+
.setParameter("observationCategory", "exam").list();
97+
98+
assertThat(observationCategories, hasSize(1));
99+
assertThat(observationCategories.get(0), hasProperty("uuid", equalTo("5f8e2dd2-ce1f-42d3-bb34-55acb3f58c5d")));
100+
assertThat(observationCategories.get(0).getConceptClass(), hasProperty("name", equalTo("Finding")));
101+
}
102+
}
103+
}

api/src/test/java/org/openmrs/module/initializer/api/UtilsTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ public void unProxy_shouldReturnOriginalClassName() {
288288
Assert.assertEquals("EncounterType", Utils.unProxy("EncounterType_$$_javassist_26"));
289289
Assert.assertEquals("EncounterType", Utils.unProxy("EncounterType"));
290290
}
291-
291+
292292
@Test
293293
public void fetchConcept_shouldFetchConceptByUuid() {
294294
ConceptService cs = mock(ConceptService.class);
@@ -300,7 +300,7 @@ public void fetchConcept_shouldFetchConceptByUuid() {
300300
when(cs.getConceptByName("concept:lookup")).thenReturn(nameConcept);
301301
Assert.assertEquals(uuidConcept, Utils.fetchConcept("concept:lookup", cs));
302302
}
303-
303+
304304
@Test
305305
public void fetchConcept_shouldFetchConceptByMapping() {
306306
ConceptService cs = mock(ConceptService.class);
@@ -311,7 +311,7 @@ public void fetchConcept_shouldFetchConceptByMapping() {
311311
when(cs.getConceptByName("concept:lookup")).thenReturn(nameConcept);
312312
Assert.assertEquals(mappingConcept, Utils.fetchConcept("concept:lookup", cs));
313313
}
314-
314+
315315
@Test
316316
public void fetchConcept_shouldFetchConceptByName() {
317317
ConceptService cs = mock(ConceptService.class);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Uuid,Void/Retire,Fhir observation category,Concept class,_order:1000
2+
e518de2a-be31-4202-9772-cc65c3ef7227,,laboratory,Test,
3+
5f8e2dd2-ce1f-42d3-bb34-55acb3f58c5d,,exam,Finding,
4+
2374215a-8808-4eee-b5a5-9190423862a0,,laboratory,LabSet,

readme/fhir.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,32 @@ The format of this CSV should be as follows:
6565

6666
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.
6767

68+
## Domain 'fhirobservationcategorymaps'
69+
70+
The **fhirobservationcategorymaps** subfolder contains CSV import files for defining mappings between [FHIR Observation category
71+
values](https://www.hl7.org/fhir/valueset-observation-category.html) and OpenMRS ConceptClasses. When configured, this ensures
72+
that the `Observation.category` field on a FHIR Observation will have the value specified by the mapping and the ConceptClass.
73+
74+
`ConceptClass`es referenced in this domain must exist before a category map can be created. However, arbitrary strings can
75+
be used for the category name. However, it is recommended to use strings in the
76+
[FHIR ValueSet for Observation category](https://www.hl7.org/fhir/valueset-observation-category.html) wherever possible to
77+
ensure maximum compatibility.
78+
79+
Please note that the ability to map OpenMRS ConceptClasses to FHIR Observation categories should be regarded as an experimental
80+
feature and may be subject to change in the future.
81+
82+
This is a possible example of its contents:
83+
```bash
84+
fhirobservationcategorymaps/
85+
├──categorymaps.csv
86+
└── ...
87+
```
88+
89+
The format of this CSV should be as follows:
90+
91+
| <sub>Uuid</sub> |<sub>Void/Retire</sub> | <sub>Fhir observation category</sub> | <sub>Concept class</sub> | <sub>_order:1000</sub> |
92+
| - | - | - | - | - |
93+
| <sub>e518de2a-be31-4202-9772-cc65c3ef7227</sub> | | <sub>laboratory</sub> | <sub>Test</sub> | |
94+
95+
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.
96+

0 commit comments

Comments
 (0)