Skip to content

Commit 8c843c7

Browse files
authored
Issue #1329 - conditional reference support for transaction bundles (#2366)
* Issue #1329 - validate conditional references in ValidationSupport Signed-off-by: John T.E. Timm <[email protected]> * Issue #1329 - update copyright header Signed-off-by: John T.E. Timm <[email protected]> * Issue #1329 - conditional reference support for transaction bundles Signed-off-by: John T.E. Timm <[email protected]> * Issue #1329 - updated getConditionalReferences Signed-off-by: John T.E. Timm <[email protected]> * Issue #1329 - update server integration test Signed-off-by: John T.E. Timm <[email protected]> * Issue #1329 - updated server integration test and test data Signed-off-by: John T.E. Timm <[email protected]> * Issue #1329 - updated error messages per PR feedback Signed-off-by: John T.E. Timm <[email protected]>
1 parent 9075c3b commit 8c843c7

File tree

5 files changed

+310
-10
lines changed

5 files changed

+310
-10
lines changed

fhir-model/src/main/java/com/ibm/fhir/model/util/ValidationSupport.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -693,9 +693,15 @@ public static void checkReferenceType(Reference reference, String elementName, S
693693

694694
if (referenceReference != null && !referenceReference.startsWith("#") && !referenceReference.startsWith(LOCAL_REF_PREFIX)
695695
&& !referenceReference.startsWith(HTTP_PREFIX) && !referenceReference.startsWith(HTTPS_PREFIX)) {
696-
Matcher matcher = REFERENCE_PATTERN.matcher(referenceReference);
697-
if (matcher.matches()) {
698-
resourceType = matcher.group(RESOURCE_TYPE_GROUP);
696+
int index = referenceReference.indexOf("?");
697+
if (index != -1) {
698+
// conditional reference
699+
resourceType = referenceReference.substring(0, index);
700+
} else {
701+
Matcher matcher = REFERENCE_PATTERN.matcher(referenceReference);
702+
if (matcher.matches()) {
703+
resourceType = matcher.group(RESOURCE_TYPE_GROUP);
704+
}
699705
}
700706

701707
// resourceType is required in the reference value
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* (C) Copyright IBM Corp. 2021
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package com.ibm.fhir.server.test;
8+
9+
import static com.ibm.fhir.model.type.String.string;
10+
import static org.testng.Assert.assertEquals;
11+
import static org.testng.Assert.assertTrue;
12+
13+
import java.util.Collections;
14+
15+
import javax.ws.rs.client.Entity;
16+
import javax.ws.rs.client.WebTarget;
17+
import javax.ws.rs.core.Response;
18+
19+
import org.testng.annotations.Test;
20+
21+
import com.ibm.fhir.core.FHIRMediaType;
22+
import com.ibm.fhir.model.resource.Bundle;
23+
import com.ibm.fhir.model.resource.Bundle.Entry;
24+
import com.ibm.fhir.model.resource.Observation;
25+
import com.ibm.fhir.model.resource.OperationOutcome;
26+
import com.ibm.fhir.model.resource.Patient;
27+
import com.ibm.fhir.model.test.TestUtil;
28+
import com.ibm.fhir.model.type.HumanName;
29+
import com.ibm.fhir.model.type.Identifier;
30+
import com.ibm.fhir.model.type.Reference;
31+
import com.ibm.fhir.model.type.Uri;
32+
import com.ibm.fhir.model.type.code.IssueType;
33+
34+
public class ConditionalReferenceTest extends FHIRServerTestBase {
35+
@Test
36+
public void testCreatePatients() {
37+
Patient patient = buildPatient();
38+
39+
WebTarget target = getWebTarget();
40+
41+
Response response = target.path("Patient").path("12345")
42+
.request()
43+
.put(Entity.entity(patient, FHIRMediaType.APPLICATION_FHIR_JSON));
44+
int status = response.getStatus();
45+
assertTrue(status == Response.Status.CREATED.getStatusCode() || status == Response.Status.OK.getStatusCode());
46+
47+
patient = patient.toBuilder()
48+
.id("54321")
49+
.identifier(Collections.singletonList(Identifier.builder()
50+
.system(Uri.of("http://ibm.com/fhir/patient-id"))
51+
.value(string("54321"))
52+
.build()))
53+
.build();
54+
55+
response = target.path("Patient").path("54321")
56+
.request()
57+
.put(Entity.entity(patient, FHIRMediaType.APPLICATION_FHIR_JSON));
58+
status = response.getStatus();
59+
assertTrue(status == Response.Status.CREATED.getStatusCode() || status == Response.Status.OK.getStatusCode());
60+
}
61+
62+
@Test(dependsOnMethods = { "testCreatePatients" })
63+
public void testBundleTransactionConditionalReference() throws Exception {
64+
Bundle bundle = TestUtil.readLocalResource("testdata/conditional-reference-bundle.json");
65+
66+
WebTarget target = getWebTarget();
67+
68+
Response response = target.request()
69+
.post(Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON));
70+
assertResponse(response, Response.Status.OK.getStatusCode());
71+
72+
response = target.path("Observation/67890").request(FHIRMediaType.APPLICATION_FHIR_JSON).get();
73+
assertResponse(response, Response.Status.OK.getStatusCode());
74+
75+
Observation observation = response.readEntity(Observation.class);
76+
assertEquals(observation.getSubject().getReference().getValue(), "Patient/12345");
77+
}
78+
79+
@Test(dependsOnMethods = { "testCreatePatients" })
80+
public void testBundleTransactionInvalidConditionalReferenceNoQueryParameters() throws Exception {
81+
Bundle bundle = TestUtil.readLocalResource("testdata/conditional-reference-bundle.json");
82+
83+
Entry entry = bundle.getEntry().get(0);
84+
entry = entry.toBuilder()
85+
.resource(entry.getResource().as(Observation.class).toBuilder()
86+
.subject(Reference.builder()
87+
.reference(string("Patient?"))
88+
.build())
89+
.build())
90+
.build();
91+
92+
bundle = bundle.toBuilder()
93+
.entry(Collections.singletonList(entry))
94+
.build();
95+
96+
WebTarget target = getWebTarget();
97+
98+
Response response = target.request()
99+
.post(Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON));
100+
assertResponse(response, Response.Status.BAD_REQUEST.getStatusCode());
101+
102+
OperationOutcome outcome = response.readEntity(OperationOutcome.class);
103+
assertEquals(outcome.getIssue().get(0).getCode(), IssueType.INVALID);
104+
assertEquals(outcome.getIssue().get(0).getDetails().getText().getValue(), "Invalid conditional reference: no query parameters found");
105+
}
106+
107+
@Test(dependsOnMethods = { "testCreatePatients" })
108+
public void testBundleTransactionInvalidConditionalReferenceResultParameter() throws Exception {
109+
Bundle bundle = TestUtil.readLocalResource("testdata/conditional-reference-bundle.json");
110+
111+
Entry entry = bundle.getEntry().get(0);
112+
entry = entry.toBuilder()
113+
.resource(entry.getResource().as(Observation.class).toBuilder()
114+
.subject(Reference.builder()
115+
.reference(string("Patient?_count=1"))
116+
.build())
117+
.build())
118+
.build();
119+
120+
bundle = bundle.toBuilder()
121+
.entry(Collections.singletonList(entry))
122+
.build();
123+
124+
WebTarget target = getWebTarget();
125+
126+
Response response = target.request()
127+
.post(Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON));
128+
assertResponse(response, Response.Status.BAD_REQUEST.getStatusCode());
129+
130+
OperationOutcome outcome = response.readEntity(OperationOutcome.class);
131+
assertEquals(outcome.getIssue().get(0).getCode(), IssueType.INVALID);
132+
assertEquals(outcome.getIssue().get(0).getDetails().getText().getValue(), "Invalid conditional reference: only filtering parameters are allowed");
133+
}
134+
135+
@Test(dependsOnMethods = { "testCreatePatients" })
136+
public void testBundleTransactionConditionalReferenceNoResult() throws Exception {
137+
Bundle bundle = TestUtil.readLocalResource("testdata/conditional-reference-bundle.json");
138+
139+
Entry entry = bundle.getEntry().get(0);
140+
entry = entry.toBuilder()
141+
.resource(entry.getResource().as(Observation.class).toBuilder()
142+
.subject(Reference.builder()
143+
.reference(string("Patient?identifier=___invalid___"))
144+
.build())
145+
.build())
146+
.build();
147+
148+
bundle = bundle.toBuilder()
149+
.entry(Collections.singletonList(entry))
150+
.build();
151+
152+
WebTarget target = getWebTarget();
153+
154+
Response response = target.request()
155+
.post(Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON));
156+
assertResponse(response, Response.Status.BAD_REQUEST.getStatusCode());
157+
158+
OperationOutcome outcome = response.readEntity(OperationOutcome.class);
159+
assertEquals(outcome.getIssue().get(0).getCode(), IssueType.NOT_FOUND);
160+
assertEquals(outcome.getIssue().get(0).getDetails().getText().getValue(), "Error resolving conditional reference: search returned no results");
161+
}
162+
163+
@Test(dependsOnMethods = { "testCreatePatients" })
164+
public void testBundleTransactionConditionalReferenceMultipleMatches() throws Exception {
165+
Bundle bundle = TestUtil.readLocalResource("testdata/conditional-reference-bundle.json");
166+
167+
Entry entry = bundle.getEntry().get(0);
168+
entry = entry.toBuilder()
169+
.resource(entry.getResource().as(Observation.class).toBuilder()
170+
.subject(Reference.builder()
171+
.reference(string("Patient?family=Doe&given=John"))
172+
.build())
173+
.build())
174+
.build();
175+
176+
bundle = bundle.toBuilder()
177+
.entry(Collections.singletonList(entry))
178+
.build();
179+
180+
WebTarget target = getWebTarget();
181+
182+
Response response = target.request()
183+
.post(Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON));
184+
assertResponse(response, Response.Status.BAD_REQUEST.getStatusCode());
185+
186+
OperationOutcome outcome = response.readEntity(OperationOutcome.class);
187+
assertEquals(outcome.getIssue().get(0).getCode(), IssueType.MULTIPLE_MATCHES);
188+
assertEquals(outcome.getIssue().get(0).getDetails().getText().getValue(), "Error resolving conditional reference: search returned multiple results");
189+
}
190+
191+
private Patient buildPatient() {
192+
return Patient.builder()
193+
.id("12345")
194+
.identifier(Identifier.builder()
195+
.system(Uri.of("http://ibm.com/fhir/patient-id"))
196+
.value(string("12345"))
197+
.build())
198+
.name(HumanName.builder()
199+
.family(string("Doe"))
200+
.given(string("John"))
201+
.build())
202+
.build();
203+
}
204+
}

fhir-server-test/src/test/java/com/ibm/fhir/server/test/RemoteTermServiceProviderTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* (C) Copyright IBM Corp. 2020, 2021
2+
* (C) Copyright IBM Corp. 2021
33
*
44
* SPDX-License-Identifier: Apache-2.0
55
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"resourceType": "Bundle",
3+
"id": "20160113160203",
4+
"type": "transaction",
5+
"entry": [
6+
{
7+
"fullUrl": "urn:uuid:c72aa430-2ddc-456e-7a09-dea8264671d8",
8+
"resource": {
9+
"resourceType": "Observation",
10+
"id": "67890",
11+
"status": "final",
12+
"code": {
13+
"text": "test"
14+
},
15+
"subject": {
16+
"reference": "Patient?identifier=http://ibm.com/fhir/patient-id|12345"
17+
}
18+
},
19+
"request": {
20+
"method": "PUT",
21+
"url": "Observation/67890"
22+
}
23+
}
24+
]
25+
}

fhir-server/src/main/java/com/ibm/fhir/server/util/FHIRRestHelper.java

+71-6
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
import com.ibm.fhir.model.type.code.IssueSeverity;
7878
import com.ibm.fhir.model.type.code.IssueType;
7979
import com.ibm.fhir.model.type.code.SearchEntryMode;
80+
import com.ibm.fhir.model.util.CollectingVisitor;
8081
import com.ibm.fhir.model.util.FHIRUtil;
8182
import com.ibm.fhir.model.util.ModelSupport;
8283
import com.ibm.fhir.model.util.ReferenceMappingVisitor;
@@ -1734,11 +1735,11 @@ private List<Entry> processEntriesByMethod(Bundle requestBundle, Map<Integer, En
17341735
} else if (request.getMethod().equals(HTTPVerb.POST)) {
17351736
Entry validationResponseEntry = validationResponseEntries.get(entryIndex);
17361737
responseEntries[entryIndex] = processEntryForPost(requestEntry, validationResponseEntry, responseIndexAndEntries,
1737-
entryIndex, localRefMap, requestURL, absoluteUri, requestDescription.toString(), initialTime);
1738+
entryIndex, localRefMap, requestURL, absoluteUri, requestDescription.toString(), initialTime, (bundleType == BundleType.Value.TRANSACTION));
17381739
} else if (request.getMethod().equals(HTTPVerb.PUT)) {
17391740
Entry validationResponseEntry = validationResponseEntries.get(entryIndex);
17401741
responseEntries[entryIndex] = processEntryForPut(requestEntry, validationResponseEntry, responseIndexAndEntries,
1741-
entryIndex, localRefMap, requestURL, absoluteUri, requestDescription.toString(), initialTime, skippableUpdates);
1742+
entryIndex, localRefMap, requestURL, absoluteUri, requestDescription.toString(), initialTime, skippableUpdates, (bundleType == BundleType.Value.TRANSACTION));
17421743
} else if (request.getMethod().equals(HTTPVerb.PATCH)) {
17431744
responseEntries[entryIndex] = processEntryforPatch(requestEntry, requestURL,entryIndex,
17441745
requestDescription.toString(), initialTime, skippableUpdates);
@@ -2006,7 +2007,7 @@ private void updateOperationContext(FHIROperationContext operationContext, Strin
20062007
* @throws Exception
20072008
*/
20082009
private Entry processEntryForPost(Entry requestEntry, Entry validationResponseEntry, Map<Integer, Entry> responseIndexAndEntries,
2009-
Integer entryIndex, Map<String, String> localRefMap, FHIRUrlParser requestURL, String absoluteUri, String requestDescription, long initialTime)
2010+
Integer entryIndex, Map<String, String> localRefMap, FHIRUrlParser requestURL, String absoluteUri, String requestDescription, long initialTime, boolean transaction)
20102011
throws Exception {
20112012

20122013
String[] pathTokens = requestURL.getPathTokens();
@@ -2081,6 +2082,10 @@ private Entry processEntryForPost(Entry requestEntry, Entry validationResponseEn
20812082
throw buildRestException(msg, IssueType.NOT_FOUND);
20822083
}
20832084

2085+
if (transaction) {
2086+
resolveConditionalReferences(resource, localRefMap);
2087+
}
2088+
20842089
// Convert any local references found within the resource to their corresponding external reference.
20852090
ReferenceMappingVisitor<Resource> visitor = new ReferenceMappingVisitor<Resource>(localRefMap);
20862091
resource.accept(visitor);
@@ -2121,6 +2126,62 @@ private Entry processEntryForPost(Entry requestEntry, Entry validationResponseEn
21212126
}
21222127
}
21232128

2129+
private void resolveConditionalReferences(Resource resource, Map<String, String> localRefMap) throws Exception {
2130+
for (String conditionalReference : getConditionalReferences(resource)) {
2131+
if (localRefMap.containsKey(conditionalReference)) {
2132+
continue;
2133+
}
2134+
2135+
FHIRUrlParser parser = new FHIRUrlParser(conditionalReference);
2136+
String type = parser.getPathTokens()[0];
2137+
2138+
MultivaluedMap<String, String> queryParameters = parser.getQueryParameters();
2139+
if (queryParameters.isEmpty()) {
2140+
throw buildRestException("Invalid conditional reference: no query parameters found", IssueType.INVALID);
2141+
}
2142+
2143+
if (queryParameters.keySet().stream().anyMatch(key -> SearchConstants.SEARCH_RESULT_PARAMETER_NAMES.contains(key))) {
2144+
throw buildRestException("Invalid conditional reference: only filtering parameters are allowed", IssueType.INVALID);
2145+
}
2146+
2147+
queryParameters.add("_summary", "true");
2148+
queryParameters.add("_count", "1");
2149+
2150+
Bundle bundle = doSearch(type, null, null, queryParameters, null, resource, false);
2151+
2152+
int total = bundle.getTotal().getValue();
2153+
2154+
if (total == 0) {
2155+
throw buildRestException("Error resolving conditional reference: search returned no results", IssueType.NOT_FOUND);
2156+
}
2157+
2158+
if (total > 1) {
2159+
throw buildRestException("Error resolving conditional reference: search returned multiple results", IssueType.MULTIPLE_MATCHES);
2160+
}
2161+
2162+
localRefMap.put(conditionalReference, type + "/" + bundle.getEntry().get(0).getResource().getId());
2163+
}
2164+
}
2165+
2166+
private Set<String> getConditionalReferences(Resource resource) {
2167+
Set<String> conditionalReferences = new HashSet<>();
2168+
CollectingVisitor<Reference> visitor = new CollectingVisitor<>(Reference.class);
2169+
resource.accept(visitor);
2170+
for (Reference reference : visitor.getResult()) {
2171+
if (reference.getReference() != null && reference.getReference().getValue() != null) {
2172+
String value = reference.getReference().getValue();
2173+
if (!value.startsWith("#") &&
2174+
!value.startsWith("urn:") &&
2175+
!value.startsWith("http:") &&
2176+
!value.startsWith("https:") &&
2177+
value.contains("?")) {
2178+
conditionalReferences.add(value);
2179+
}
2180+
}
2181+
}
2182+
return conditionalReferences;
2183+
}
2184+
21242185
/**
21252186
* Processes a request entry with a request method of PUT.
21262187
*
@@ -2150,7 +2211,7 @@ private Entry processEntryForPost(Entry requestEntry, Entry validationResponseEn
21502211
*/
21512212
private Entry processEntryForPut(Entry requestEntry, Entry validationResponseEntry, Map<Integer, Entry> responseIndexAndEntries,
21522213
Integer entryIndex, Map<String, String> localRefMap, FHIRUrlParser requestURL, String absoluteUri, String requestDescription,
2153-
long initialTime, boolean skippableUpdate) throws Exception {
2214+
long initialTime, boolean skippableUpdate, boolean transaction) throws Exception {
21542215

21552216
String[] pathTokens = requestURL.getPathTokens();
21562217
String type = null;
@@ -2177,6 +2238,10 @@ private Entry processEntryForPut(Entry requestEntry, Entry validationResponseEnt
21772238
// Retrieve the resource from the request entry.
21782239
Resource resource = requestEntry.getResource();
21792240

2241+
if (transaction) {
2242+
resolveConditionalReferences(resource, localRefMap);
2243+
}
2244+
21802245
// Convert any local references found within the resource to their corresponding external reference.
21812246
ReferenceMappingVisitor<Resource> visitor = new ReferenceMappingVisitor<Resource>(localRefMap);
21822247
resource.accept(visitor);
@@ -2341,9 +2406,9 @@ private MultivaluedMap<String, String> getQueryParameterMap(String queryString)
23412406
* @return local reference map
23422407
*/
23432408
private Map<String, String> buildLocalRefMap(Bundle requestBundle, Map<Integer, Entry> validationResponseEntries) throws Exception {
2344-
Map<String, String> localRefMap = new HashMap<>();
2409+
Map<String, String> localRefMap = new HashMap<>();
23452410

2346-
for (int entryIndex=0; entryIndex<requestBundle.getEntry().size(); ++entryIndex) {
2411+
for (int entryIndex = 0; entryIndex < requestBundle.getEntry().size(); entryIndex++) {
23472412
Entry requestEntry = requestBundle.getEntry().get(entryIndex);
23482413
Entry.Request request = requestEntry.getRequest();
23492414
Entry validationResponseEntry = validationResponseEntries.get(entryIndex);

0 commit comments

Comments
 (0)