Skip to content

Commit ec6e84e

Browse files
authored
Helper class and function to update a location ancestry with lineage tags (#20)
* Add helper class and function to update a location ancestry with lineage tags * Proper variable names Use correct client Remove redundant tag update Update return type * Refactor into smaller functions for easier maintainability Format code * Add tests * Upgrade version * Rename method * Add test packages * Test LocationHelper.java
1 parent 50354f0 commit ec6e84e

File tree

4 files changed

+290
-2
lines changed

4 files changed

+290
-2
lines changed

pom.xml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>org.smartregister</groupId>
88
<artifactId>fhir-common-utils</artifactId>
9-
<version>1.0.1-SNAPSHOT</version>
9+
<version>1.0.2-SNAPSHOT</version>
1010

1111
<distributionManagement>
1212
<repository>
@@ -26,6 +26,8 @@
2626
<maven.compiler.target>11</maven.compiler.target>
2727
<hapi.fhir.base.version>5.5.0</hapi.fhir.base.version>
2828
<junit.version>4.13.1</junit.version>
29+
<hapifhir.version>7.4.3</hapifhir.version>
30+
<mockito.version>5.15.2</mockito.version>
2931
</properties>
3032

3133
<dependencies>
@@ -39,6 +41,11 @@
3941
<artifactId>org.hl7.fhir.r4</artifactId>
4042
<version>5.4.10</version>
4143
</dependency>
44+
<dependency>
45+
<groupId>ca.uhn.hapi.fhir</groupId>
46+
<artifactId>hapi-fhir-client</artifactId>
47+
<version>7.4.3</version>
48+
</dependency>
4249

4350
<!-- Test Dependencies-->
4451
<dependency>
@@ -47,6 +54,11 @@
4754
<scope>test</scope>
4855
<version>${junit.version}</version>
4956
</dependency>
50-
57+
<dependency>
58+
<groupId>org.mockito</groupId>
59+
<artifactId>mockito-core</artifactId>
60+
<version>${mockito.version}</version>
61+
<scope>test</scope>
62+
</dependency>
5163
</dependencies>
5264
</project>
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package org.smartregister.helpers;
2+
3+
import java.util.ArrayList;
4+
import java.util.LinkedList;
5+
import java.util.List;
6+
import java.util.Queue;
7+
import java.util.stream.Collectors;
8+
9+
import org.hl7.fhir.instance.model.api.IBaseBundle;
10+
import org.hl7.fhir.r4.model.Bundle;
11+
import org.hl7.fhir.r4.model.Coding;
12+
import org.hl7.fhir.r4.model.Location;
13+
import org.hl7.fhir.r4.model.StringType;
14+
import org.hl7.fhir.r4.model.UriType;
15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
17+
import org.smartregister.utils.Constants;
18+
19+
import com.google.common.annotations.VisibleForTesting;
20+
21+
import ca.uhn.fhir.rest.api.SearchStyleEnum;
22+
import ca.uhn.fhir.rest.client.api.IGenericClient;
23+
import ca.uhn.fhir.rest.client.impl.GenericClient;
24+
import ca.uhn.fhir.rest.gclient.IQuery;
25+
import ca.uhn.fhir.rest.gclient.ReferenceClientParam;
26+
27+
public class LocationHelper {
28+
29+
private static final Logger logger = LoggerFactory.getLogger(LocationHelper.class);
30+
31+
// Custom class to pair a Location with its lineage ancestors
32+
static class LocationWithTags {
33+
Location location;
34+
List<String> ancestorIds;
35+
36+
public LocationWithTags(Location location, List<String> ancestorIds) {
37+
this.location = location;
38+
this.ancestorIds = ancestorIds;
39+
}
40+
}
41+
42+
public static Location updateLocationLineage(IGenericClient client, String locationId) {
43+
Queue<LocationWithTags> locationsQueue = new LinkedList<>();
44+
Location location = client.read().resource(Location.class).withId(locationId).execute();
45+
46+
List<String> ancestorIds = getParentLocationLineage(client, location);
47+
locationsQueue.add(new LocationWithTags(location, ancestorIds));
48+
49+
while (!locationsQueue.isEmpty()) {
50+
LocationWithTags currentLocationWithTags = locationsQueue.poll();
51+
Location currentLocation = currentLocationWithTags.location;
52+
String currentLocationId = currentLocation.getIdElement().getIdPart();
53+
54+
updateLocationTags(client, currentLocation, currentLocationWithTags.ancestorIds);
55+
List<Location> childLocations = fetchChildLocations(client, currentLocationId);
56+
57+
for (Location childLocation : childLocations) {
58+
locationsQueue.add(
59+
new LocationWithTags(
60+
childLocation,
61+
new ArrayList<>(currentLocationWithTags.ancestorIds)));
62+
}
63+
}
64+
return location;
65+
}
66+
67+
@VisibleForTesting
68+
protected static List<String> getParentLocationLineage(
69+
IGenericClient client, Location location) {
70+
if (!location.hasPartOf() || !location.getPartOf().hasReference()) {
71+
return new ArrayList<>();
72+
}
73+
74+
String parentLocationId = location.getPartOf().getReferenceElement().getIdPart();
75+
Location parentLocation =
76+
client.read().resource(Location.class).withId(parentLocationId).execute();
77+
78+
List<String> ancestorIds =
79+
parentLocation.getMeta().getTag().stream()
80+
.filter(
81+
tag ->
82+
Constants.DEFAULT_LOCATION_LINEAGE_TAG_URL.equals(
83+
tag.getSystem()))
84+
.map(Coding::getCode)
85+
.collect(Collectors.toList());
86+
87+
ancestorIds.add(parentLocationId);
88+
client.update().resource(location).execute();
89+
return ancestorIds;
90+
}
91+
92+
@VisibleForTesting
93+
protected static void updateLocationTags(
94+
IGenericClient client, Location location, List<String> ancestorIds) {
95+
logger.info("Adding lineage tags to Location Id : {}", location.getIdElement().getIdPart());
96+
97+
List<Coding> newTags =
98+
location.getMeta().getTag().stream()
99+
.filter(
100+
tag ->
101+
!Constants.DEFAULT_LOCATION_LINEAGE_TAG_URL.equals(
102+
tag.getSystem()))
103+
.collect(Collectors.toList());
104+
105+
for (String tag : ancestorIds) {
106+
newTags.add(
107+
new Coding()
108+
.setSystem(Constants.DEFAULT_LOCATION_LINEAGE_TAG_URL)
109+
.setCode(tag));
110+
}
111+
112+
location.getMeta().setTag(newTags);
113+
client.update().resource(location).execute();
114+
}
115+
116+
private static List<Location> fetchChildLocations(IGenericClient client, String locationId) {
117+
IQuery<IBaseBundle> query =
118+
client.search()
119+
.forResource(Location.class)
120+
.where(
121+
new ReferenceClientParam(Location.SP_PARTOF)
122+
.hasAnyOfIds(locationId));
123+
124+
Bundle childLocationBundle =
125+
query.usingStyle(SearchStyleEnum.POST)
126+
.count(100)
127+
.returnBundle(Bundle.class)
128+
.execute();
129+
130+
if (childLocationBundle != null) {
131+
fetchAllBundlePagesAndInject(client, childLocationBundle);
132+
return childLocationBundle.getEntry().stream()
133+
.map(Bundle.BundleEntryComponent::getResource)
134+
.filter(resource -> resource instanceof Location)
135+
.map(resource -> (Location) resource)
136+
.collect(Collectors.toList());
137+
}
138+
return new ArrayList<>();
139+
}
140+
141+
/**
142+
* This is a recursive function which updates the result bundle with results of all pages
143+
* whenever there's an entry for Bundle.LINK_NEXT
144+
*
145+
* @param fhirClient the Generic FHIR Client instance
146+
* @param resultBundle the result bundle from the first request
147+
*/
148+
public static void fetchAllBundlePagesAndInject(
149+
IGenericClient fhirClient, Bundle resultBundle) {
150+
151+
if (resultBundle.getLink(Bundle.LINK_NEXT) != null) {
152+
153+
cleanUpBundlePaginationNextLinkServerBaseUrl((GenericClient) fhirClient, resultBundle);
154+
155+
Bundle pageResultBundle = fhirClient.loadPage().next(resultBundle).execute();
156+
157+
resultBundle.getEntry().addAll(pageResultBundle.getEntry());
158+
resultBundle.setLink(pageResultBundle.getLink());
159+
160+
fetchAllBundlePagesAndInject(fhirClient, resultBundle);
161+
}
162+
163+
resultBundle.setLink(
164+
resultBundle.getLink().stream()
165+
.filter(
166+
bundleLinkComponent ->
167+
!Bundle.LINK_NEXT.equals(bundleLinkComponent.getRelation()))
168+
.collect(Collectors.toList()));
169+
resultBundle.getMeta().setLastUpdated(resultBundle.getMeta().getLastUpdated());
170+
}
171+
172+
public static void cleanUpBundlePaginationNextLinkServerBaseUrl(
173+
GenericClient fhirClient, Bundle resultBundle) {
174+
String cleanUrl =
175+
cleanHapiPaginationLinkBaseUrl(
176+
resultBundle.getLink(Bundle.LINK_NEXT).getUrl(), fhirClient.getUrlBase());
177+
resultBundle
178+
.getLink()
179+
.replaceAll(
180+
bundleLinkComponent ->
181+
Bundle.LINK_NEXT.equals(bundleLinkComponent.getRelation())
182+
? new Bundle.BundleLinkComponent(
183+
new StringType(Bundle.LINK_NEXT),
184+
new UriType(cleanUrl))
185+
: bundleLinkComponent);
186+
}
187+
188+
public static String cleanHapiPaginationLinkBaseUrl(
189+
String originalUrl, String fhirServerBaseUrl) {
190+
return originalUrl.indexOf('?') > -1
191+
? fhirServerBaseUrl + originalUrl.substring(originalUrl.indexOf('?'))
192+
: fhirServerBaseUrl;
193+
}
194+
}

src/main/java/org/smartregister/utils/Constants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ public interface Constants {
4949
String CODE = "code";
5050
String MEMBER = "member";
5151
String GROUP = "Group";
52+
String DEFAULT_LOCATION_LINEAGE_TAG_URL = "http://smartregister.org/CodeSystem/location-lineage";
5253
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package org.smartregister.helpers;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertNotNull;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.times;
7+
import static org.mockito.Mockito.verify;
8+
9+
import java.util.Collections;
10+
11+
import org.hl7.fhir.instance.model.api.IBaseResource;
12+
import org.hl7.fhir.r4.model.Bundle;
13+
import org.hl7.fhir.r4.model.Location;
14+
import org.junit.Before;
15+
import org.junit.Test;
16+
import org.mockito.Mockito;
17+
import org.mockito.MockitoAnnotations;
18+
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
19+
20+
import ca.uhn.fhir.rest.api.SearchStyleEnum;
21+
import ca.uhn.fhir.rest.client.api.IGenericClient;
22+
import ca.uhn.fhir.rest.gclient.ICriterion;
23+
import ca.uhn.fhir.rest.gclient.IQuery;
24+
import ca.uhn.fhir.rest.gclient.IRead;
25+
import ca.uhn.fhir.rest.gclient.IReadExecutable;
26+
import ca.uhn.fhir.rest.gclient.IReadTyped;
27+
import ca.uhn.fhir.rest.gclient.IUntypedQuery;
28+
29+
public class LocationHelperTest {
30+
31+
IGenericClient client;
32+
33+
@Before
34+
public void setUp() {
35+
MockitoAnnotations.initMocks(this);
36+
client = mock(IGenericClient.class, new ReturnsDeepStubs());
37+
}
38+
39+
@Test
40+
public void testUpdateLocationLineage() {
41+
Location location = new Location();
42+
location.setId("location1");
43+
location.setPartOf(null);
44+
45+
Location childLocation = new Location();
46+
childLocation.setId("location2");
47+
childLocation.setPartOf(new org.hl7.fhir.r4.model.Reference("Location/location1"));
48+
49+
Bundle childLocationBundle = new Bundle();
50+
Bundle.BundleEntryComponent entry = new Bundle.BundleEntryComponent();
51+
entry.setResource(childLocation);
52+
childLocationBundle.setEntry(Collections.singletonList(entry));
53+
54+
IRead iReadMock = mock(IRead.class);
55+
IReadTyped<IBaseResource> iReadTypedMock = mock(IReadTyped.class);
56+
IReadExecutable<Location> iReadExecutableMock = mock(IReadExecutable.class);
57+
58+
IUntypedQuery<Location> iUntypedQueryMock = mock(IUntypedQuery.class);
59+
IQuery<Location> iQueryMock = mock(IQuery.class);
60+
61+
Mockito.doReturn(iReadMock).when(client).read();
62+
Mockito.doReturn(iReadTypedMock).when(iReadMock).resource(Location.class);
63+
Mockito.doReturn(iReadExecutableMock).when(iReadTypedMock).withId("location1");
64+
Mockito.doReturn(location).when(iReadExecutableMock).execute();
65+
66+
Mockito.doReturn(iUntypedQueryMock).when(client).search();
67+
Mockito.doReturn(iQueryMock).when(iUntypedQueryMock).forResource(Location.class);
68+
Mockito.doReturn(iQueryMock).when(iQueryMock).where(Mockito.any(ICriterion.class));
69+
Mockito.doReturn(iQueryMock).when(iQueryMock).usingStyle(SearchStyleEnum.POST);
70+
Mockito.doReturn(iQueryMock).when(iQueryMock).count(100);
71+
Mockito.doReturn(iQueryMock).when(iQueryMock).returnBundle(Bundle.class);
72+
Mockito.doReturn(childLocationBundle, (Bundle) null).when(iQueryMock).execute();
73+
74+
Location result = LocationHelper.updateLocationLineage(client, "location1");
75+
76+
assertNotNull(result);
77+
assertEquals("location1", result.getIdElement().getIdPart());
78+
verify(client, times(1)).read();
79+
verify(client, times(2)).search();
80+
}
81+
}

0 commit comments

Comments
 (0)