Skip to content

Commit

Permalink
Issue #1329 - conditional reference support for transaction bundles (#…
Browse files Browse the repository at this point in the history
…2366)

* Issue #1329 - validate conditional references in ValidationSupport

Signed-off-by: John T.E. Timm <johntimm@us.ibm.com>

* Issue #1329 - update copyright header

Signed-off-by: John T.E. Timm <johntimm@us.ibm.com>

* Issue #1329 - conditional reference support for transaction bundles

Signed-off-by: John T.E. Timm <johntimm@us.ibm.com>

* Issue #1329 - updated getConditionalReferences

Signed-off-by: John T.E. Timm <johntimm@us.ibm.com>

* Issue #1329 - update server integration test

Signed-off-by: John T.E. Timm <johntimm@us.ibm.com>

* Issue #1329 - updated server integration test and test data

Signed-off-by: John T.E. Timm <johntimm@us.ibm.com>

* Issue #1329 - updated error messages per PR feedback

Signed-off-by: John T.E. Timm <johntimm@us.ibm.com>
  • Loading branch information
JohnTimm authored May 14, 2021
1 parent 9075c3b commit 8c843c7
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -693,9 +693,15 @@ public static void checkReferenceType(Reference reference, String elementName, S

if (referenceReference != null && !referenceReference.startsWith("#") && !referenceReference.startsWith(LOCAL_REF_PREFIX)
&& !referenceReference.startsWith(HTTP_PREFIX) && !referenceReference.startsWith(HTTPS_PREFIX)) {
Matcher matcher = REFERENCE_PATTERN.matcher(referenceReference);
if (matcher.matches()) {
resourceType = matcher.group(RESOURCE_TYPE_GROUP);
int index = referenceReference.indexOf("?");
if (index != -1) {
// conditional reference
resourceType = referenceReference.substring(0, index);
} else {
Matcher matcher = REFERENCE_PATTERN.matcher(referenceReference);
if (matcher.matches()) {
resourceType = matcher.group(RESOURCE_TYPE_GROUP);
}
}

// resourceType is required in the reference value
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* (C) Copyright IBM Corp. 2021
*
* SPDX-License-Identifier: Apache-2.0
*/

package com.ibm.fhir.server.test;

import static com.ibm.fhir.model.type.String.string;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;

import java.util.Collections;

import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;

import org.testng.annotations.Test;

import com.ibm.fhir.core.FHIRMediaType;
import com.ibm.fhir.model.resource.Bundle;
import com.ibm.fhir.model.resource.Bundle.Entry;
import com.ibm.fhir.model.resource.Observation;
import com.ibm.fhir.model.resource.OperationOutcome;
import com.ibm.fhir.model.resource.Patient;
import com.ibm.fhir.model.test.TestUtil;
import com.ibm.fhir.model.type.HumanName;
import com.ibm.fhir.model.type.Identifier;
import com.ibm.fhir.model.type.Reference;
import com.ibm.fhir.model.type.Uri;
import com.ibm.fhir.model.type.code.IssueType;

public class ConditionalReferenceTest extends FHIRServerTestBase {
@Test
public void testCreatePatients() {
Patient patient = buildPatient();

WebTarget target = getWebTarget();

Response response = target.path("Patient").path("12345")
.request()
.put(Entity.entity(patient, FHIRMediaType.APPLICATION_FHIR_JSON));
int status = response.getStatus();
assertTrue(status == Response.Status.CREATED.getStatusCode() || status == Response.Status.OK.getStatusCode());

patient = patient.toBuilder()
.id("54321")
.identifier(Collections.singletonList(Identifier.builder()
.system(Uri.of("http://ibm.com/fhir/patient-id"))
.value(string("54321"))
.build()))
.build();

response = target.path("Patient").path("54321")
.request()
.put(Entity.entity(patient, FHIRMediaType.APPLICATION_FHIR_JSON));
status = response.getStatus();
assertTrue(status == Response.Status.CREATED.getStatusCode() || status == Response.Status.OK.getStatusCode());
}

@Test(dependsOnMethods = { "testCreatePatients" })
public void testBundleTransactionConditionalReference() throws Exception {
Bundle bundle = TestUtil.readLocalResource("testdata/conditional-reference-bundle.json");

WebTarget target = getWebTarget();

Response response = target.request()
.post(Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON));
assertResponse(response, Response.Status.OK.getStatusCode());

response = target.path("Observation/67890").request(FHIRMediaType.APPLICATION_FHIR_JSON).get();
assertResponse(response, Response.Status.OK.getStatusCode());

Observation observation = response.readEntity(Observation.class);
assertEquals(observation.getSubject().getReference().getValue(), "Patient/12345");
}

@Test(dependsOnMethods = { "testCreatePatients" })
public void testBundleTransactionInvalidConditionalReferenceNoQueryParameters() throws Exception {
Bundle bundle = TestUtil.readLocalResource("testdata/conditional-reference-bundle.json");

Entry entry = bundle.getEntry().get(0);
entry = entry.toBuilder()
.resource(entry.getResource().as(Observation.class).toBuilder()
.subject(Reference.builder()
.reference(string("Patient?"))
.build())
.build())
.build();

bundle = bundle.toBuilder()
.entry(Collections.singletonList(entry))
.build();

WebTarget target = getWebTarget();

Response response = target.request()
.post(Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON));
assertResponse(response, Response.Status.BAD_REQUEST.getStatusCode());

OperationOutcome outcome = response.readEntity(OperationOutcome.class);
assertEquals(outcome.getIssue().get(0).getCode(), IssueType.INVALID);
assertEquals(outcome.getIssue().get(0).getDetails().getText().getValue(), "Invalid conditional reference: no query parameters found");
}

@Test(dependsOnMethods = { "testCreatePatients" })
public void testBundleTransactionInvalidConditionalReferenceResultParameter() throws Exception {
Bundle bundle = TestUtil.readLocalResource("testdata/conditional-reference-bundle.json");

Entry entry = bundle.getEntry().get(0);
entry = entry.toBuilder()
.resource(entry.getResource().as(Observation.class).toBuilder()
.subject(Reference.builder()
.reference(string("Patient?_count=1"))
.build())
.build())
.build();

bundle = bundle.toBuilder()
.entry(Collections.singletonList(entry))
.build();

WebTarget target = getWebTarget();

Response response = target.request()
.post(Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON));
assertResponse(response, Response.Status.BAD_REQUEST.getStatusCode());

OperationOutcome outcome = response.readEntity(OperationOutcome.class);
assertEquals(outcome.getIssue().get(0).getCode(), IssueType.INVALID);
assertEquals(outcome.getIssue().get(0).getDetails().getText().getValue(), "Invalid conditional reference: only filtering parameters are allowed");
}

@Test(dependsOnMethods = { "testCreatePatients" })
public void testBundleTransactionConditionalReferenceNoResult() throws Exception {
Bundle bundle = TestUtil.readLocalResource("testdata/conditional-reference-bundle.json");

Entry entry = bundle.getEntry().get(0);
entry = entry.toBuilder()
.resource(entry.getResource().as(Observation.class).toBuilder()
.subject(Reference.builder()
.reference(string("Patient?identifier=___invalid___"))
.build())
.build())
.build();

bundle = bundle.toBuilder()
.entry(Collections.singletonList(entry))
.build();

WebTarget target = getWebTarget();

Response response = target.request()
.post(Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON));
assertResponse(response, Response.Status.BAD_REQUEST.getStatusCode());

OperationOutcome outcome = response.readEntity(OperationOutcome.class);
assertEquals(outcome.getIssue().get(0).getCode(), IssueType.NOT_FOUND);
assertEquals(outcome.getIssue().get(0).getDetails().getText().getValue(), "Error resolving conditional reference: search returned no results");
}

@Test(dependsOnMethods = { "testCreatePatients" })
public void testBundleTransactionConditionalReferenceMultipleMatches() throws Exception {
Bundle bundle = TestUtil.readLocalResource("testdata/conditional-reference-bundle.json");

Entry entry = bundle.getEntry().get(0);
entry = entry.toBuilder()
.resource(entry.getResource().as(Observation.class).toBuilder()
.subject(Reference.builder()
.reference(string("Patient?family=Doe&given=John"))
.build())
.build())
.build();

bundle = bundle.toBuilder()
.entry(Collections.singletonList(entry))
.build();

WebTarget target = getWebTarget();

Response response = target.request()
.post(Entity.entity(bundle, FHIRMediaType.APPLICATION_FHIR_JSON));
assertResponse(response, Response.Status.BAD_REQUEST.getStatusCode());

OperationOutcome outcome = response.readEntity(OperationOutcome.class);
assertEquals(outcome.getIssue().get(0).getCode(), IssueType.MULTIPLE_MATCHES);
assertEquals(outcome.getIssue().get(0).getDetails().getText().getValue(), "Error resolving conditional reference: search returned multiple results");
}

private Patient buildPatient() {
return Patient.builder()
.id("12345")
.identifier(Identifier.builder()
.system(Uri.of("http://ibm.com/fhir/patient-id"))
.value(string("12345"))
.build())
.name(HumanName.builder()
.family(string("Doe"))
.given(string("John"))
.build())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* (C) Copyright IBM Corp. 2020, 2021
* (C) Copyright IBM Corp. 2021
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"resourceType": "Bundle",
"id": "20160113160203",
"type": "transaction",
"entry": [
{
"fullUrl": "urn:uuid:c72aa430-2ddc-456e-7a09-dea8264671d8",
"resource": {
"resourceType": "Observation",
"id": "67890",
"status": "final",
"code": {
"text": "test"
},
"subject": {
"reference": "Patient?identifier=http://ibm.com/fhir/patient-id|12345"
}
},
"request": {
"method": "PUT",
"url": "Observation/67890"
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
import com.ibm.fhir.model.type.code.IssueSeverity;
import com.ibm.fhir.model.type.code.IssueType;
import com.ibm.fhir.model.type.code.SearchEntryMode;
import com.ibm.fhir.model.util.CollectingVisitor;
import com.ibm.fhir.model.util.FHIRUtil;
import com.ibm.fhir.model.util.ModelSupport;
import com.ibm.fhir.model.util.ReferenceMappingVisitor;
Expand Down Expand Up @@ -1734,11 +1735,11 @@ private List<Entry> processEntriesByMethod(Bundle requestBundle, Map<Integer, En
} else if (request.getMethod().equals(HTTPVerb.POST)) {
Entry validationResponseEntry = validationResponseEntries.get(entryIndex);
responseEntries[entryIndex] = processEntryForPost(requestEntry, validationResponseEntry, responseIndexAndEntries,
entryIndex, localRefMap, requestURL, absoluteUri, requestDescription.toString(), initialTime);
entryIndex, localRefMap, requestURL, absoluteUri, requestDescription.toString(), initialTime, (bundleType == BundleType.Value.TRANSACTION));
} else if (request.getMethod().equals(HTTPVerb.PUT)) {
Entry validationResponseEntry = validationResponseEntries.get(entryIndex);
responseEntries[entryIndex] = processEntryForPut(requestEntry, validationResponseEntry, responseIndexAndEntries,
entryIndex, localRefMap, requestURL, absoluteUri, requestDescription.toString(), initialTime, skippableUpdates);
entryIndex, localRefMap, requestURL, absoluteUri, requestDescription.toString(), initialTime, skippableUpdates, (bundleType == BundleType.Value.TRANSACTION));
} else if (request.getMethod().equals(HTTPVerb.PATCH)) {
responseEntries[entryIndex] = processEntryforPatch(requestEntry, requestURL,entryIndex,
requestDescription.toString(), initialTime, skippableUpdates);
Expand Down Expand Up @@ -2006,7 +2007,7 @@ private void updateOperationContext(FHIROperationContext operationContext, Strin
* @throws Exception
*/
private Entry processEntryForPost(Entry requestEntry, Entry validationResponseEntry, Map<Integer, Entry> responseIndexAndEntries,
Integer entryIndex, Map<String, String> localRefMap, FHIRUrlParser requestURL, String absoluteUri, String requestDescription, long initialTime)
Integer entryIndex, Map<String, String> localRefMap, FHIRUrlParser requestURL, String absoluteUri, String requestDescription, long initialTime, boolean transaction)
throws Exception {

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

if (transaction) {
resolveConditionalReferences(resource, localRefMap);
}

// Convert any local references found within the resource to their corresponding external reference.
ReferenceMappingVisitor<Resource> visitor = new ReferenceMappingVisitor<Resource>(localRefMap);
resource.accept(visitor);
Expand Down Expand Up @@ -2121,6 +2126,62 @@ private Entry processEntryForPost(Entry requestEntry, Entry validationResponseEn
}
}

private void resolveConditionalReferences(Resource resource, Map<String, String> localRefMap) throws Exception {
for (String conditionalReference : getConditionalReferences(resource)) {
if (localRefMap.containsKey(conditionalReference)) {
continue;
}

FHIRUrlParser parser = new FHIRUrlParser(conditionalReference);
String type = parser.getPathTokens()[0];

MultivaluedMap<String, String> queryParameters = parser.getQueryParameters();
if (queryParameters.isEmpty()) {
throw buildRestException("Invalid conditional reference: no query parameters found", IssueType.INVALID);
}

if (queryParameters.keySet().stream().anyMatch(key -> SearchConstants.SEARCH_RESULT_PARAMETER_NAMES.contains(key))) {
throw buildRestException("Invalid conditional reference: only filtering parameters are allowed", IssueType.INVALID);
}

queryParameters.add("_summary", "true");
queryParameters.add("_count", "1");

Bundle bundle = doSearch(type, null, null, queryParameters, null, resource, false);

int total = bundle.getTotal().getValue();

if (total == 0) {
throw buildRestException("Error resolving conditional reference: search returned no results", IssueType.NOT_FOUND);
}

if (total > 1) {
throw buildRestException("Error resolving conditional reference: search returned multiple results", IssueType.MULTIPLE_MATCHES);
}

localRefMap.put(conditionalReference, type + "/" + bundle.getEntry().get(0).getResource().getId());
}
}

private Set<String> getConditionalReferences(Resource resource) {
Set<String> conditionalReferences = new HashSet<>();
CollectingVisitor<Reference> visitor = new CollectingVisitor<>(Reference.class);
resource.accept(visitor);
for (Reference reference : visitor.getResult()) {
if (reference.getReference() != null && reference.getReference().getValue() != null) {
String value = reference.getReference().getValue();
if (!value.startsWith("#") &&
!value.startsWith("urn:") &&
!value.startsWith("http:") &&
!value.startsWith("https:") &&
value.contains("?")) {
conditionalReferences.add(value);
}
}
}
return conditionalReferences;
}

/**
* Processes a request entry with a request method of PUT.
*
Expand Down Expand Up @@ -2150,7 +2211,7 @@ private Entry processEntryForPost(Entry requestEntry, Entry validationResponseEn
*/
private Entry processEntryForPut(Entry requestEntry, Entry validationResponseEntry, Map<Integer, Entry> responseIndexAndEntries,
Integer entryIndex, Map<String, String> localRefMap, FHIRUrlParser requestURL, String absoluteUri, String requestDescription,
long initialTime, boolean skippableUpdate) throws Exception {
long initialTime, boolean skippableUpdate, boolean transaction) throws Exception {

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

if (transaction) {
resolveConditionalReferences(resource, localRefMap);
}

// Convert any local references found within the resource to their corresponding external reference.
ReferenceMappingVisitor<Resource> visitor = new ReferenceMappingVisitor<Resource>(localRefMap);
resource.accept(visitor);
Expand Down Expand Up @@ -2341,9 +2406,9 @@ private MultivaluedMap<String, String> getQueryParameterMap(String queryString)
* @return local reference map
*/
private Map<String, String> buildLocalRefMap(Bundle requestBundle, Map<Integer, Entry> validationResponseEntries) throws Exception {
Map<String, String> localRefMap = new HashMap<>();
Map<String, String> localRefMap = new HashMap<>();

for (int entryIndex=0; entryIndex<requestBundle.getEntry().size(); ++entryIndex) {
for (int entryIndex = 0; entryIndex < requestBundle.getEntry().size(); entryIndex++) {
Entry requestEntry = requestBundle.getEntry().get(entryIndex);
Entry.Request request = requestEntry.getRequest();
Entry validationResponseEntry = validationResponseEntries.get(entryIndex);
Expand Down

0 comments on commit 8c843c7

Please sign in to comment.