From 8c843c76a1ccbbfccd96ab75c3a950a5c827ba51 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Fri, 14 May 2021 16:49:10 -0400 Subject: [PATCH] Issue #1329 - conditional reference support for transaction bundles (#2366) * Issue #1329 - validate conditional references in ValidationSupport Signed-off-by: John T.E. Timm * Issue #1329 - update copyright header Signed-off-by: John T.E. Timm * Issue #1329 - conditional reference support for transaction bundles Signed-off-by: John T.E. Timm * Issue #1329 - updated getConditionalReferences Signed-off-by: John T.E. Timm * Issue #1329 - update server integration test Signed-off-by: John T.E. Timm * Issue #1329 - updated server integration test and test data Signed-off-by: John T.E. Timm * Issue #1329 - updated error messages per PR feedback Signed-off-by: John T.E. Timm --- .../fhir/model/util/ValidationSupport.java | 12 +- .../server/test/ConditionalReferenceTest.java | 204 ++++++++++++++++++ .../test/RemoteTermServiceProviderTest.java | 2 +- .../conditional-reference-bundle.json | 25 +++ .../ibm/fhir/server/util/FHIRRestHelper.java | 77 ++++++- 5 files changed, 310 insertions(+), 10 deletions(-) create mode 100644 fhir-server-test/src/test/java/com/ibm/fhir/server/test/ConditionalReferenceTest.java create mode 100644 fhir-server-test/src/test/resources/testdata/conditional-reference-bundle.json diff --git a/fhir-model/src/main/java/com/ibm/fhir/model/util/ValidationSupport.java b/fhir-model/src/main/java/com/ibm/fhir/model/util/ValidationSupport.java index 3a151a81974..b8497ebb379 100644 --- a/fhir-model/src/main/java/com/ibm/fhir/model/util/ValidationSupport.java +++ b/fhir-model/src/main/java/com/ibm/fhir/model/util/ValidationSupport.java @@ -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 diff --git a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/ConditionalReferenceTest.java b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/ConditionalReferenceTest.java new file mode 100644 index 00000000000..dba870d44a2 --- /dev/null +++ b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/ConditionalReferenceTest.java @@ -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(); + } +} diff --git a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/RemoteTermServiceProviderTest.java b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/RemoteTermServiceProviderTest.java index 60d22af3b1e..cff1998611d 100644 --- a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/RemoteTermServiceProviderTest.java +++ b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/RemoteTermServiceProviderTest.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020, 2021 + * (C) Copyright IBM Corp. 2021 * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/fhir-server-test/src/test/resources/testdata/conditional-reference-bundle.json b/fhir-server-test/src/test/resources/testdata/conditional-reference-bundle.json new file mode 100644 index 00000000000..30055beb8be --- /dev/null +++ b/fhir-server-test/src/test/resources/testdata/conditional-reference-bundle.json @@ -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" + } + } + ] +} \ No newline at end of file diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/util/FHIRRestHelper.java b/fhir-server/src/main/java/com/ibm/fhir/server/util/FHIRRestHelper.java index 0f903fae518..7fa6189aa01 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/util/FHIRRestHelper.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/util/FHIRRestHelper.java @@ -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; @@ -1734,11 +1735,11 @@ private List processEntriesByMethod(Bundle requestBundle, Map responseIndexAndEntries, - Integer entryIndex, Map localRefMap, FHIRUrlParser requestURL, String absoluteUri, String requestDescription, long initialTime) + Integer entryIndex, Map localRefMap, FHIRUrlParser requestURL, String absoluteUri, String requestDescription, long initialTime, boolean transaction) throws Exception { String[] pathTokens = requestURL.getPathTokens(); @@ -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 visitor = new ReferenceMappingVisitor(localRefMap); resource.accept(visitor); @@ -2121,6 +2126,62 @@ private Entry processEntryForPost(Entry requestEntry, Entry validationResponseEn } } + private void resolveConditionalReferences(Resource resource, Map localRefMap) throws Exception { + for (String conditionalReference : getConditionalReferences(resource)) { + if (localRefMap.containsKey(conditionalReference)) { + continue; + } + + FHIRUrlParser parser = new FHIRUrlParser(conditionalReference); + String type = parser.getPathTokens()[0]; + + MultivaluedMap 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 getConditionalReferences(Resource resource) { + Set conditionalReferences = new HashSet<>(); + CollectingVisitor 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. * @@ -2150,7 +2211,7 @@ private Entry processEntryForPost(Entry requestEntry, Entry validationResponseEn */ private Entry processEntryForPut(Entry requestEntry, Entry validationResponseEntry, Map responseIndexAndEntries, Integer entryIndex, Map 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; @@ -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 visitor = new ReferenceMappingVisitor(localRefMap); resource.accept(visitor); @@ -2341,9 +2406,9 @@ private MultivaluedMap getQueryParameterMap(String queryString) * @return local reference map */ private Map buildLocalRefMap(Bundle requestBundle, Map validationResponseEntries) throws Exception { - Map localRefMap = new HashMap<>(); + Map localRefMap = new HashMap<>(); - for (int entryIndex=0; entryIndex