Skip to content

Commit 01dcc06

Browse files
authored
#300 - Support loading drug ingredients (#301)
1 parent 951f685 commit 01dcc06

6 files changed

Lines changed: 264 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ See the [documentation on Initializer's logging properties](readme/rtprops.md#lo
217217

218218
#### Version 2.10.0
219219
* Support enhanced methods for loading htmlforms when running htmlformentry 5.5.0+
220+
* Support loading drug ingredients within the drug domain, for compatible OpenMRS versions
220221

221222
#### Version 2.9.0
222223
* Fix for InitializerSerializer to ensure compatibility with OpenMRS version 2.7.0+

api/src/main/java/org/openmrs/module/initializer/api/drugs/DrugsCsvParser.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ public class DrugsCsvParser extends CsvParser<Drug, BaseLineProcessor<Drug>> {
1818

1919
private final MappingsDrugLineProcessor mappingsProcessor;
2020

21+
private final IngredientsDrugLineProcessor ingredientsProcessor;
22+
2123
@Autowired
2224
public DrugsCsvParser(@Qualifier("conceptService") ConceptService conceptService,
2325
@Qualifier("initializer.drugLineProcessor") DrugLineProcessor processor,
24-
@Qualifier("initializer.mappingsDrugLineProcessor") MappingsDrugLineProcessor mappingsProcessor) {
26+
@Qualifier("initializer.mappingsDrugLineProcessor") MappingsDrugLineProcessor mappingsProcessor,
27+
@Qualifier("initializer.ingredientsDrugLineProcessor") IngredientsDrugLineProcessor ingredientsProcessor) {
2528
super(processor);
2629
this.mappingsProcessor = mappingsProcessor;
30+
this.ingredientsProcessor = ingredientsProcessor;
2731
this.conceptService = conceptService;
2832
}
2933

@@ -61,5 +65,6 @@ protected void setLineProcessors(String version) {
6165
lineProcessors.clear();
6266
lineProcessors.add(getSingleLineProcessor());
6367
lineProcessors.add(mappingsProcessor);
68+
lineProcessors.add(ingredientsProcessor);
6469
}
6570
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package org.openmrs.module.initializer.api.drugs;
2+
3+
import org.apache.commons.lang.StringUtils;
4+
import org.openmrs.Concept;
5+
import org.openmrs.Drug;
6+
import org.openmrs.DrugIngredient;
7+
import org.openmrs.api.ConceptService;
8+
import org.openmrs.module.initializer.api.CsvLine;
9+
import org.openmrs.module.initializer.api.utils.Utils;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
import org.springframework.beans.factory.annotation.Autowired;
13+
import org.springframework.beans.factory.annotation.Qualifier;
14+
import org.springframework.stereotype.Component;
15+
16+
import java.util.Iterator;
17+
import java.util.LinkedHashMap;
18+
import java.util.Map;
19+
import java.util.Set;
20+
import java.util.TreeSet;
21+
22+
@Component("initializer.ingredientsDrugLineProcessor")
23+
public class IngredientsDrugLineProcessor extends DrugLineProcessor {
24+
25+
protected final Logger log = LoggerFactory.getLogger(getClass());
26+
27+
protected static String HEADER_INGREDIENT = "ingredient";
28+
29+
protected static String HEADER_INGREDIENT_STRENGTH = "strength";
30+
31+
protected static String HEADER_INGREDIENT_UNITS = "units";
32+
33+
@Autowired
34+
public IngredientsDrugLineProcessor(@Qualifier("conceptService") ConceptService conceptService) {
35+
super(conceptService);
36+
}
37+
38+
public Drug fill(Drug drug, CsvLine line) throws IllegalArgumentException {
39+
40+
// Enable loading of 0-N drug ingredients
41+
42+
Set<String> ingredientHeaders = new TreeSet<>();
43+
for (String header : line.getHeaderLine()) {
44+
header = header.toLowerCase();
45+
if (header.startsWith(HEADER_INGREDIENT)) {
46+
String[] headerComponents = header.split(" ");
47+
if (headerComponents.length < 2) {
48+
throw new IllegalArgumentException(
49+
"Invalid ingredient header '" + header + "'. Ingredient number expected.");
50+
}
51+
ingredientHeaders.add(headerComponents[0] + " " + headerComponents[1]);
52+
}
53+
}
54+
55+
// Only modify ingredients in a given drug if ingredient headers are found in the csv
56+
if (!ingredientHeaders.isEmpty()) {
57+
58+
// Construct a new ingredient from each one specified in the csv
59+
Map<Concept, DrugIngredient> newIngredients = new LinkedHashMap<>();
60+
for (String ingredientHeader : ingredientHeaders) {
61+
62+
DrugIngredient newIngredient = new DrugIngredient();
63+
64+
// Ingredient is required. Only add an ingredient for the drug if it is specified
65+
String ingredientLookup = line.getString(ingredientHeader);
66+
if (StringUtils.isNotBlank(ingredientLookup)) {
67+
Concept ingredient = Utils.fetchConcept(ingredientLookup, conceptService);
68+
if (ingredient == null) {
69+
throw new IllegalArgumentException("No concept found for '" + ingredientLookup + "'");
70+
}
71+
newIngredient.setIngredient(ingredient);
72+
73+
// Strength is not required, but if non-null, needs to be a double
74+
Double strength = null;
75+
String strengthLookup = line.getString(ingredientHeader + " " + HEADER_INGREDIENT_STRENGTH);
76+
if (StringUtils.isNotBlank(strengthLookup)) {
77+
try {
78+
strength = Double.parseDouble(strengthLookup);
79+
}
80+
catch (NumberFormatException e) {
81+
throw new IllegalArgumentException("Invalid strength, must be a number: " + strengthLookup);
82+
}
83+
}
84+
newIngredient.setStrength(strength);
85+
86+
// Units are not required, but if present, must refer to a valid concept
87+
Concept units = null;
88+
String unitsLookup = line.getString(ingredientHeader + " " + HEADER_INGREDIENT_UNITS);
89+
if (StringUtils.isNotBlank(unitsLookup)) {
90+
units = Utils.fetchConcept(unitsLookup, conceptService);
91+
if (units == null) {
92+
throw new IllegalArgumentException("No concept found for '" + unitsLookup + "'");
93+
}
94+
}
95+
newIngredient.setUnits(units);
96+
97+
newIngredients.put(ingredient, newIngredient);
98+
}
99+
}
100+
101+
// Remove or update any of the existing ingredients on the current drug
102+
for (Iterator<DrugIngredient> i = drug.getIngredients().iterator(); i.hasNext();) {
103+
DrugIngredient existingIngredient = i.next();
104+
DrugIngredient newIngredient = newIngredients.remove(existingIngredient.getIngredient());
105+
if (newIngredient == null) {
106+
i.remove(); // If an existing ingredient is not found in the new ingredients, remove it
107+
log.debug("Removing ingredient '" + existingIngredient.getIngredient() + "' from " + drug.getName());
108+
} else {
109+
// If an existing ingredient is matched in the new ingredients, update it
110+
existingIngredient.setStrength(newIngredient.getStrength());
111+
existingIngredient.setUnits(newIngredient.getUnits());
112+
log.debug("Updated ingredient '" + existingIngredient + "' for " + drug.getName());
113+
}
114+
}
115+
116+
// Add new ingredients to the drug
117+
for (DrugIngredient newIngredient : newIngredients.values()) {
118+
newIngredient.setDrug(drug);
119+
drug.getIngredients().add(newIngredient);
120+
log.debug("Added ingredient '" + newIngredient.getIngredient() + "' to " + drug.getName());
121+
}
122+
123+
}
124+
125+
return drug;
126+
}
127+
}

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

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package org.openmrs.module.initializer.api;
1111

12+
import java.util.Collection;
1213
import java.util.Locale;
1314

1415
import org.junit.Assert;
@@ -17,6 +18,7 @@
1718
import org.openmrs.Concept;
1819
import org.openmrs.ConceptName;
1920
import org.openmrs.Drug;
21+
import org.openmrs.DrugIngredient;
2022
import org.openmrs.api.ConceptService;
2123
import org.openmrs.module.initializer.DomainBaseModuleContextSensitiveTest;
2224
import org.openmrs.module.initializer.api.drugs.DrugsLoader;
@@ -93,6 +95,20 @@ public void setup() {
9395
d.setStrength("100mg");
9496
d = cs.saveDrug(d);
9597
}
98+
{
99+
Drug d = new Drug();
100+
d.setUuid("8abf401a-7f65-11f0-9e36-be568b1ab237");
101+
d.setName("Drug with Erythromycine");
102+
d.setConcept(cs.getConceptByName("d4T"));
103+
d.setStrength("20mg");
104+
DrugIngredient ingredient = new DrugIngredient();
105+
ingredient.setDrug(d);
106+
ingredient.setIngredient(cs.getConceptByName("Erythromycine"));
107+
ingredient.setStrength(20.0);
108+
ingredient.setUnits(cs.getConceptByName("mg"));
109+
d.getIngredients().add(ingredient);
110+
cs.saveDrug(d);
111+
}
96112
}
97113

98114
@Test
@@ -141,4 +157,89 @@ public void load_shouldLoadDrugsAccordingToCsvFiles() {
141157
Assert.assertTrue(d.getRetired());
142158
}
143159
}
160+
161+
@Test
162+
public void load_shouldAddIngredientsToDrugs() {
163+
loader.load();
164+
{
165+
Drug d = cs.getDrug("Combo Drug");
166+
Assert.assertNotNull(d);
167+
Assert.assertEquals(cs.getConceptByName("d4T"), d.getConcept());
168+
Assert.assertEquals(cs.getConceptByName("Tablet"), d.getDosageForm());
169+
Assert.assertEquals("10mg", d.getStrength());
170+
Collection<DrugIngredient> ingredients = d.getIngredients();
171+
Concept erythromycine = cs.getConceptByName("Erythromycine");
172+
Concept cetirizine = cs.getConceptByName("Cetirizine");
173+
Concept mg = cs.getConceptByName("mg");
174+
Assert.assertEquals(2, ingredients.size());
175+
for (DrugIngredient ingredient : ingredients) {
176+
if (ingredient.getIngredient().equals(erythromycine)) {
177+
Assert.assertEquals((Double) 4.0, ingredient.getStrength());
178+
Assert.assertEquals(mg, ingredient.getUnits());
179+
} else if (ingredient.getIngredient().equals(cetirizine)) {
180+
Assert.assertEquals((Double) 6.0, ingredient.getStrength());
181+
Assert.assertEquals(mg, ingredient.getUnits());
182+
} else {
183+
Assert.fail("Unexpected ingredient " + ingredient);
184+
}
185+
}
186+
}
187+
}
188+
189+
@Test
190+
public void load_shouldRemoveIngredientsFromDrugs() {
191+
192+
Drug drug = cs.getDrugByUuid("8abf401a-7f65-11f0-9e36-be568b1ab237");
193+
Assert.assertNotNull(drug);
194+
Assert.assertEquals("Drug with Erythromycine", drug.getName());
195+
Assert.assertEquals(1, drug.getIngredients().size());
196+
DrugIngredient erythromycine = drug.getIngredients().iterator().next();
197+
Assert.assertEquals(cs.getConceptByName("Erythromycine"), erythromycine.getIngredient());
198+
Assert.assertEquals((Double) 20.0, erythromycine.getStrength());
199+
Assert.assertEquals(cs.getConceptByName("mg"), erythromycine.getUnits());
200+
201+
loader.load();
202+
203+
drug = cs.getDrugByUuid("8abf401a-7f65-11f0-9e36-be568b1ab237");
204+
Assert.assertNotNull(drug);
205+
Assert.assertEquals("Drug without Erythromycine", drug.getName());
206+
Assert.assertEquals(1, drug.getIngredients().size());
207+
DrugIngredient cetirizine = drug.getIngredients().iterator().next();
208+
Assert.assertNotEquals(erythromycine.getUuid(), cetirizine.getUuid());
209+
Assert.assertEquals(cs.getConceptByName("Cetirizine"), cetirizine.getIngredient());
210+
Assert.assertEquals((Double) 15.0, cetirizine.getStrength());
211+
Assert.assertEquals(cs.getConceptByName("mg"), cetirizine.getUnits());
212+
}
213+
214+
@Test
215+
public void load_shouldModifyIngredientsInDrugs() {
216+
217+
loader.load();
218+
219+
Drug drug = cs.getDrugByUuid("8abf401a-7f65-11f0-9e36-be568b1ab237");
220+
Assert.assertNotNull(drug);
221+
Assert.assertEquals("Drug without Erythromycine", drug.getName());
222+
Assert.assertEquals(1, drug.getIngredients().size());
223+
DrugIngredient cetirizine = drug.getIngredients().iterator().next();
224+
Assert.assertEquals(cs.getConceptByName("Cetirizine"), cetirizine.getIngredient());
225+
Assert.assertEquals((Double) 15.0, cetirizine.getStrength());
226+
227+
cetirizine.setStrength(20.0);
228+
cs.saveDrug(drug);
229+
230+
drug = cs.getDrugByUuid("8abf401a-7f65-11f0-9e36-be568b1ab237");
231+
Assert.assertEquals(1, drug.getIngredients().size());
232+
DrugIngredient originalIngredient = drug.getIngredients().iterator().next();
233+
Assert.assertEquals((Double) 20.0, originalIngredient.getStrength());
234+
235+
loader.getDirUtil().deleteChecksums();
236+
loader.load();
237+
238+
drug = cs.getDrugByUuid("8abf401a-7f65-11f0-9e36-be568b1ab237");
239+
Assert.assertEquals(1, drug.getIngredients().size());
240+
DrugIngredient modifiedIngredient = drug.getIngredients().iterator().next();
241+
// NOTE: Ideally we'd test this, but due to a bug in core with saving drug ingredients, it doesn't work unless you are on very specific versions of core
242+
//Assert.assertEquals(originalIngredient.getUuid(), modifiedIngredient.getUuid());
243+
Assert.assertEquals((Double) 15.0, modifiedIngredient.getStrength());
244+
}
144245
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Uuid,Void/Retire,Name,Concept Drug,Concept Dosage Form,Strength,Ingredient 1,Ingredient 1 Strength,Ingredient 1 Units,Ingredient 2,Ingredient 2 Strength,Ingredient 2 Units,_version:1
2+
e1c5584e-7f62-11f0-9e36-be568b1ab237,,Combo Drug,d4T,Tablet,10mg,Erythromycine,4,mg,Cetirizine,6.0,mg,
3+
8abf401a-7f65-11f0-9e36-be568b1ab237,,Drug without Erythromycine,d4T,Tablet,20mg,Cetirizine,15.0,mg,"","","",

readme/drugs.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,31 @@ In order to specify mappings on a given concept, the values for those mappings c
6060
| - | - | - | - | - |
6161
| ... | <sub>5089</sub> | <sub>27113001</sub> | <sub>5089</sub> | <sub>WEIGHT (KG)</sub> |
6262

63+
###### Ingredient headers
64+
OpenMRS supports the ability to associated 0-N ingredients with a Drug, where an ingredient is made up of an ingredient (Concept), strength (Double), and units (Concept)
65+
66+
Important note: Due to a bug in OpenMRS core, use of Ingredient headers will silently fail if your core version does not meet one of the following minimum versions:
67+
* 2.8.1+
68+
* 2.7.6+
69+
See [TRUNK-6411](https://openmrs.atlassian.net/browse/TRUNK-6411) for more information on this bug.
70+
71+
Ingredients may be specified via headers that start with the `Ingredient` prefix, followed by a numeric index. You may add any number of ingredients by adding additional numeric indices.
72+
These headers should be named as follows, where `N` should be replaced with a numerical index that is unique to each ingredient.
73+
74+
###### Header `Ingredient N`
75+
The value under this header links to the concept representing the ingredient. This can be a concept name (if unique), concept mapping, or UUID.
76+
77+
###### Header `Ingredient N Strength`
78+
The value under this heading is expected to be a numeric quantity. This can include decimals but is not required.
79+
80+
###### Header `Ingredient N Units`
81+
The value under this header links to the concept representing the units related to the strength. This can be a concept name (if unique), concept mapping, or UUID.
82+
83+
For example, a drug containing 2 ingredients might contain the following ingredient columns:
84+
85+
| ... | <sub>Ingredient 1</sub> | <sub>Ingredient 1 Strength</sub> | <sub>Ingredient 1 Units</sub> | <sub>Ingredient 2</sub> | <sub>Ingredient 2 Strength</sub> | <sub>Ingredient 2 Units</sub> |
86+
| --- |-------------------------|----------------------------------|--------------------------------|-------------------------|----------------------------------|-------------------------------|
87+
| ... | <sub>Lopinavir</sub> | <sub>200</sub> | <sub>Milligram</sub> | <sub>Ritonavir<sub> | <sub>50</sub> | <sub>Milligram</sub> |
88+
6389
#### Further examples:
6490
Please look at the test configuration folder for sample import files for all domains, see [here](../api/src/test/resources/testAppDataDir/configuration).

0 commit comments

Comments
 (0)