Skip to content

Commit 5aa760c

Browse files
Change export transactionTime to end-time. (#6842)
* Change export transactionTime to end-time.
1 parent 556d016 commit 5aa760c

File tree

4 files changed

+105
-40
lines changed

4 files changed

+105
-40
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
type: fix
3+
issue: 6841
4+
title: "The bulk export status reported by $poll-export-status now correctly shows the start-time, not the end-time.
5+
This avoids missing changes or updates in subsequent $export calls which use this as the `_since` parameter."

hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkDataExportProvider.java

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import ca.uhn.fhir.util.JsonUtil;
4747
import ca.uhn.fhir.util.OperationOutcomeUtil;
4848
import com.google.common.annotations.VisibleForTesting;
49+
import jakarta.annotation.Nonnull;
4950
import jakarta.servlet.http.HttpServletResponse;
5051
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
5152
import org.hl7.fhir.instance.model.api.IIdType;
@@ -398,41 +399,13 @@ public void exportPollStatus(
398399
response.setStatus(Constants.STATUS_HTTP_200_OK);
399400
response.setContentType(Constants.CT_JSON);
400401

401-
// Create a JSON response
402-
BulkExportResponseJson bulkResponseDocument = new BulkExportResponseJson();
403-
bulkResponseDocument.setTransactionTime(info.getEndTime()); // completed
404-
405-
bulkResponseDocument.setRequiresAccessToken(true);
406-
407-
String report = info.getReport();
408-
if (isEmpty(report)) {
402+
if (isEmpty(info.getReport())) {
409403
// this should never happen, but just in case...
410404
ourLog.error("No report for completed bulk export job.");
411405
response.getWriter().close();
412406
} else {
413-
BulkExportJobResults results = JsonUtil.deserialize(report, BulkExportJobResults.class);
414-
bulkResponseDocument.setMsg(results.getReportMsg());
415-
bulkResponseDocument.setRequest(results.getOriginalRequestUrl());
416-
417407
String serverBase = BulkDataExportUtil.getServerBase(theRequestDetails);
418-
419-
// an output is required, even if empty, according to HL7 FHIR IG
420-
bulkResponseDocument.getOutput();
421-
422-
for (Map.Entry<String, List<String>> entrySet :
423-
results.getResourceTypeToBinaryIds().entrySet()) {
424-
String resourceType = entrySet.getKey();
425-
List<String> binaryIds = entrySet.getValue();
426-
for (String binaryId : binaryIds) {
427-
IIdType iId = new IdType(binaryId);
428-
String nextUrl = serverBase + "/"
429-
+ iId.toUnqualifiedVersionless().getValue();
430-
bulkResponseDocument
431-
.addOutput()
432-
.setType(resourceType)
433-
.setUrl(nextUrl);
434-
}
435-
}
408+
BulkExportResponseJson bulkResponseDocument = buildCompleteResponseDocument(serverBase, info);
436409
JsonUtil.serialize(bulkResponseDocument, response.getWriter());
437410
response.getWriter().close();
438411
}
@@ -449,6 +422,7 @@ public void exportPollStatus(
449422
myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToWriter(oo, response.getWriter());
450423
response.getWriter().close();
451424
break;
425+
//noinspection DefaultNotLastCaseInSwitch
452426
default:
453427
// Deliberate fall through
454428
ourLog.warn(
@@ -459,6 +433,7 @@ public void exportPollStatus(
459433
case QUEUED:
460434
case IN_PROGRESS:
461435
case CANCELLED:
436+
//noinspection deprecation - we need to support old jobs after upgrade.
462437
case ERRORED:
463438
if (theRequestDetails.getRequestType() == RequestTypeEnum.DELETE) {
464439
handleDeleteRequest(theJobId, response, info.getStatus());
@@ -474,6 +449,34 @@ public void exportPollStatus(
474449
}
475450
}
476451

452+
@Nonnull
453+
static BulkExportResponseJson buildCompleteResponseDocument(String theServerBase, JobInstance theJob) {
454+
// Create a JSON response
455+
BulkExportResponseJson response = new BulkExportResponseJson();
456+
response.setTransactionTime(theJob.getStartTime());
457+
response.setRequiresAccessToken(true);
458+
459+
BulkExportJobResults results = JsonUtil.deserialize(theJob.getReport(), BulkExportJobResults.class);
460+
response.setMsg(results.getReportMsg());
461+
response.setRequest(results.getOriginalRequestUrl());
462+
463+
// an output is required, even if empty, according to HL7 FHIR IG
464+
response.getOutput();
465+
466+
for (Map.Entry<String, List<String>> entrySet :
467+
results.getResourceTypeToBinaryIds().entrySet()) {
468+
String resourceType = entrySet.getKey();
469+
List<String> binaryIds = entrySet.getValue();
470+
for (String binaryId : binaryIds) {
471+
IIdType iId = new IdType(binaryId);
472+
String nextUrl =
473+
theServerBase + "/" + iId.toUnqualifiedVersionless().getValue();
474+
response.addOutput().setType(resourceType).setUrl(nextUrl);
475+
}
476+
}
477+
return response;
478+
}
479+
477480
private void handleDeleteRequest(
478481
IPrimitiveType<String> theJobId, HttpServletResponse response, StatusEnum theOrigStatus)
479482
throws IOException {

hapi-fhir-storage-batch2-jobs/src/test/java/ca/uhn/fhir/batch2/jobs/export/BulkDataExportProviderTest.java

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
package ca.uhn.fhir.batch2.jobs.export;
22

3+
import ca.uhn.fhir.batch2.model.JobInstance;
34
import ca.uhn.fhir.context.FhirContext;
45
import ca.uhn.fhir.context.FhirVersionEnum;
6+
import ca.uhn.fhir.jpa.api.model.BulkExportJobResults;
7+
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportResponseJson;
8+
import ca.uhn.fhir.util.JsonUtil;
9+
import org.junit.jupiter.api.Test;
510
import org.junit.jupiter.api.extension.ExtendWith;
611
import org.junit.jupiter.params.ParameterizedTest;
712
import org.junit.jupiter.params.provider.Arguments;
813
import org.junit.jupiter.params.provider.MethodSource;
914
import org.mockito.junit.jupiter.MockitoExtension;
1015

16+
import java.util.Date;
17+
import java.util.List;
18+
import java.util.Map;
1119
import java.util.Set;
1220
import java.util.stream.Stream;
1321

1422
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
23+
import static org.junit.jupiter.api.Assertions.assertEquals;
1524

1625
@ExtendWith(MockitoExtension.class)
1726
class BulkDataExportProviderTest {
18-
private static final Set<FhirVersionEnum> PATIENT_COMPARTMENT_FHIR_VERSIONS_SUPPORT_DEVICE = Set.of(FhirVersionEnum.DSTU2, FhirVersionEnum.DSTU2_1, FhirVersionEnum.DSTU2_HL7ORG, FhirVersionEnum.DSTU3, FhirVersionEnum.R4, FhirVersionEnum.R4B);
19-
2027
private static Stream<Arguments> fhirContexts() {
2128
return Stream.of(
2229
Arguments.arguments(FhirContext.forDstu2()),
@@ -44,4 +51,38 @@ void checkDeviceIsSupportedInPatientCompartment(FhirContext theFhirContext) {
4451
assertThat(resourceNames).doesNotContain("Device");
4552
}
4653
}
54+
55+
@Test
56+
void testCompleteStatusDocument() {
57+
// given
58+
JobInstance job = new JobInstance();
59+
60+
Date now = new Date();
61+
Date start = new Date(now.getTime() - 10000);
62+
Date end = new Date(now.getTime() - 2000);
63+
job.setStartTime(start);
64+
job.setEndTime(end);
65+
66+
String reportMessage = "Report Message";
67+
BulkExportJobResults jobResults = new BulkExportJobResults();
68+
jobResults.setReportMsg(reportMessage);
69+
jobResults.setOriginalRequestUrl("http://example.com/fhir-endpoint/Group/123/$export");
70+
jobResults.setResourceTypeToBinaryIds(Map.of("Patient", List.of("Binary/1123", "Binary/1124")));
71+
72+
job.setReport(JsonUtil.serialize(jobResults));
73+
74+
// when
75+
BulkExportResponseJson response = BulkDataExportProvider.buildCompleteResponseDocument("http://example.com/fhir-endpoint", job);
76+
77+
// then
78+
// see https://hl7.org/fhir/uv/bulkdata/export.html#response---complete-status
79+
assertEquals(start, response.getTransactionTime(), "transactionTime should be useful as the _since param in the next $export request");
80+
assertEquals(reportMessage, response.getMsg());
81+
assertEquals(true, response.getRequiresAccessToken());
82+
assertEquals(jobResults.getOriginalRequestUrl(), response.getRequest());
83+
assertEquals(2, response.getOutput().size());
84+
assertEquals(new BulkExportResponseJson.Output().setType("Patient").setUrl("http://example.com/fhir-endpoint/Binary/1123"), response.getOutput().get(0));
85+
86+
}
87+
4788
}

hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/bulk/export/model/BulkExportResponseJson.java

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import com.fasterxml.jackson.annotation.JsonProperty;
2828
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2929
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
30+
import org.apache.commons.lang3.builder.EqualsBuilder;
31+
import org.apache.commons.lang3.builder.HashCodeBuilder;
3032

3133
import java.util.ArrayList;
3234
import java.util.Date;
@@ -52,15 +54,16 @@ public class BulkExportResponseJson {
5254
@JsonProperty("requiresAccessToken")
5355
private Boolean myRequiresAccessToken;
5456

57+
@JsonInclude
5558
@JsonProperty("output")
56-
private List<Output> myOutput;
59+
private final List<Output> myOutput = new ArrayList<>();
5760

5861
/*
5962
* Note that we override the include here as ONC regulations require that we actually serialize the empty error array.
6063
*/
6164
@JsonInclude
6265
@JsonProperty("error")
63-
private List<Output> myError = new ArrayList<>();
66+
private final List<Output> myError = new ArrayList<>();
6467

6568
@JsonProperty("message")
6669
private String myMsg;
@@ -93,16 +96,10 @@ public BulkExportResponseJson setRequiresAccessToken(Boolean theRequiresAccessTo
9396
}
9497

9598
public List<Output> getOutput() {
96-
if (myOutput == null) {
97-
myOutput = new ArrayList<>();
98-
}
9999
return myOutput;
100100
}
101101

102102
public List<Output> getError() {
103-
if (myError == null) {
104-
myError = new ArrayList<>();
105-
}
106103
return myError;
107104
}
108105

@@ -145,5 +142,24 @@ public Output setUrl(String theUrl) {
145142
myUrl = theUrl;
146143
return this;
147144
}
145+
146+
@Override
147+
public boolean equals(Object theO) {
148+
if (this == theO) return true;
149+
150+
if (!(theO instanceof Output)) return false;
151+
152+
Output output = (Output) theO;
153+
154+
return new EqualsBuilder()
155+
.append(myType, output.myType)
156+
.append(myUrl, output.myUrl)
157+
.isEquals();
158+
}
159+
160+
@Override
161+
public int hashCode() {
162+
return new HashCodeBuilder(17, 37).append(myType).append(myUrl).toHashCode();
163+
}
148164
}
149165
}

0 commit comments

Comments
 (0)