Skip to content

Commit

Permalink
Issue #1329 - conditional reference support for transaction bundles
Browse files Browse the repository at this point in the history
Signed-off-by: John T.E. Timm <johntimm@us.ibm.com>
  • Loading branch information
JohnTimm committed May 14, 2021
1 parent 62521a5 commit 42566fd
Show file tree
Hide file tree
Showing 2 changed files with 209 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
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;
Expand All @@ -17,15 +20,20 @@

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 testCreatePatient() {
public void testCreatePatients() {
Patient patient = buildPatient();

WebTarget target = getWebTarget();
Expand All @@ -35,20 +43,153 @@ public void testCreatePatient() {
.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 = { "testCreatePatient" })
public void testBundleTransaction() throws Exception {
@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));

int status = response.getStatus();
assertTrue(status == Response.Status.OK.getStatusCode());

response = target.path("Observation/67890").request(FHIRMediaType.APPLICATION_FHIR_JSON).get();
status = response.getStatus();
assertTrue(status == 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));
int status = response.getStatus();
assertTrue(status == Response.Status.BAD_REQUEST.getStatusCode());

OperationOutcome outcome = response.readEntity(OperationOutcome.class);
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));
int status = response.getStatus();
assertTrue(status == Response.Status.BAD_REQUEST.getStatusCode());

OperationOutcome outcome = response.readEntity(OperationOutcome.class);
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));
int status = response.getStatus();
assertTrue(status == 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 must return exactly one result");
}

@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));
int status = response.getStatus();
assertTrue(status == 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 must return exactly one result");
}

private Patient buildPatient() {
Expand Down
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 @@ -1810,7 +1811,7 @@ private List<Entry> processEntriesByMethod(Bundle requestBundle, Map<Integer, En
txn.commit();
txn = null;
}

return Arrays.asList(responseEntries);

} finally {
Expand Down Expand Up @@ -1987,7 +1988,7 @@ private Entry processEntryForGet(Entry.Request entryRequest, FHIRUrlParser reque
* @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 @@ -2057,6 +2058,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 @@ -2097,6 +2102,54 @@ 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 != 1) {
IssueType issueType = (total == 0) ? IssueType.NOT_FOUND : IssueType.MULTIPLE_MATCHES;
throw buildRestException("Error resolving conditional reference: search must return exactly one result", issueType);
}

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("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 @@ -2126,7 +2179,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 @@ -2153,6 +2206,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 @@ -2317,9 +2374,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 42566fd

Please sign in to comment.