Skip to content

Commit

Permalink
$bulkdata-status needs to indicate when no data was exported #2676
Browse files Browse the repository at this point in the history
Example output:

{
    "transactionTime": "2021-08-17T14:52:04.044Z",
    "request":
"https://localhost:9443/fhir-server/api/v4/$export?_outputFormat=application/fhir%2Bndjson&_type=Coverage",
    "requiresAccessToken": false,
    "output": [
    ],
    "extension": {
        "outcome": {
            "resourceType": "OperationOutcome",
            "issue": [
                {
                    "severity": "information",
                    "code": "informational",
                    "details": {
                        "text": "No Data Exported"
                    }
                }
            ]
        }
    }
}

Signed-off-by: Paul Bastide <pbastide@us.ibm.com>
  • Loading branch information
prb112 committed Aug 17, 2021
1 parent 39c322f commit 88b5f50
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -991,4 +991,49 @@ public void testBaseExportWithTypeFilter() throws Exception {
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
}
}

@Test(groups = { TEST_GROUP_NAME })
public void testBaseExportWithNoResults() throws Exception {
if (ON) {
List<String> types = Arrays.asList("Coverage");
Response response = doPost(
BASE_VALID_URL,
FHIRMediaType.APPLICATION_FHIR_JSON, FORMAT_NDJSON,
Instant.of("2123-01-01T08:21:26.94-04:00"), // Date far in the future...
types,
null,
"default",
"default");
assertEquals(response.getStatus(), Response.Status.ACCEPTED.getStatusCode());

// check the content-location that's returned.
String contentLocation = response.getHeaderString("Content-Location");
if (DEBUG) {
System.out.println("Content Location: " + contentLocation);
}

assertTrue(contentLocation.contains(BASE_VALID_STATUS_URL));
exportStatusUrl = contentLocation;

do {
response = doGet(exportStatusUrl, FHIRMediaType.APPLICATION_FHIR_JSON, "default", "default");
// 202 accept means the request is still under processing
// 200 mean export is finished
assertEquals(Status.Family.familyOf(response.getStatus()), Status.Family.SUCCESSFUL);
Thread.sleep(1000);
} while (response.getStatus() == Response.Status.ACCEPTED.getStatusCode());

assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
String body = response.readEntity(String.class);

JsonObject jsonObject = JSON_READER_FACTORY.createReader(new StringReader(body)).readObject();
assertTrue(jsonObject.containsKey("output"));

JsonArray arr = jsonObject.getJsonArray("output");
assertTrue(arr.isEmpty());

JsonObject obj = jsonObject.getJsonObject("extension");
assertTrue(obj.containsKey("outcome"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package com.ibm.fhir.operation.bulkdata.client;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
Expand All @@ -16,6 +17,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64.Encoder;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
Expand All @@ -42,6 +44,7 @@
import com.ibm.fhir.config.FHIRRequestContext;
import com.ibm.fhir.core.FHIRMediaType;
import com.ibm.fhir.exception.FHIROperationException;
import com.ibm.fhir.model.generator.exception.FHIRGeneratorException;
import com.ibm.fhir.model.type.Instant;
import com.ibm.fhir.model.type.code.IssueType;
import com.ibm.fhir.model.util.ModelSupport;
Expand Down Expand Up @@ -693,7 +696,7 @@ private PollingLocationResponse process(JobExecutionResponse response) {
String exitStatus = response.getExitStatus();
log.fine(() -> "The Exit Status is '" + exitStatus + "'");

// Export Jobs
// Export Jobs with data in the exitStatus field
if (!"COMPLETED".equals(exitStatus) && !"bulkimportchunkjob".equals(response.getJobName())) {
List<String> resourceTypeInfs = Arrays.asList(exitStatus.split("\\s*:\\s*"));
List<PollingLocationResponse.Output> outputList = new ArrayList<>();
Expand Down Expand Up @@ -732,9 +735,19 @@ private PollingLocationResponse process(JobExecutionResponse response) {
}
result.setOutput(outputList);
}

// Export that has no data exported
else if ("COMPLETED".equals(exitStatus) && !"bulkimportchunkjob".equals(response.getJobName())) {
log.fine(() -> "Outputlist is empty");
try {
result.addOperationOutcomeToExtension(PollingLocationResponse.EMPTY_RESULTS_DURING_EXPORT);
} catch (FHIRGeneratorException | IOException e) {
log.severe("Unexpected issue while serializing a fixed value");
}
List<PollingLocationResponse.Output> outputs = Collections.emptyList();
result.setOutput(outputs);
}
// Import Jobs
if ("bulkimportchunkjob".equals(response.getJobName())) {
else if ("bulkimportchunkjob".equals(response.getJobName())) {
// Currently there is no output
log.fine("Hit the case where we don't form output with counts");
List<Input> inputs = response.getJobParameters().getInputs();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,57 @@

package com.ibm.fhir.operation.bulkdata.model;

import static com.ibm.fhir.model.type.String.string;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import com.ibm.fhir.config.FHIRConfigHelper;
import com.ibm.fhir.config.FHIRConfiguration;
import com.ibm.fhir.model.format.Format;
import com.ibm.fhir.model.generator.FHIRGenerator;
import com.ibm.fhir.model.generator.exception.FHIRGeneratorException;
import com.ibm.fhir.model.resource.OperationOutcome;
import com.ibm.fhir.model.resource.OperationOutcome.Issue;
import com.ibm.fhir.model.type.CodeableConcept;
import com.ibm.fhir.model.type.code.IssueSeverity;
import com.ibm.fhir.model.type.code.IssueType;

import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.json.JsonReader;
import jakarta.json.JsonReaderFactory;
import jakarta.json.stream.JsonGenerator;
import jakarta.json.stream.JsonGeneratorFactory;

import com.ibm.fhir.config.FHIRConfigHelper;
import com.ibm.fhir.config.FHIRConfiguration;

/**
* ResponseMetadata to manipulate the response back to the client.
* This response object is intent for the polling location.
*/
public class PollingLocationResponse {
private static final JsonReaderFactory JSON_READER_FACTORY = Json.createReaderFactory(null);

public static final OperationOutcome EMPTY_RESULTS_DURING_EXPORT = OperationOutcome.builder()
.issue(Issue.builder()
.severity(IssueSeverity.INFORMATION)
.code(IssueType.INFORMATIONAL)
.details(CodeableConcept.builder()
.text(string("No Data Exported"))
.build())
.build())
.build();

private String transactionTime;
private String request;
private Boolean requiresAccessToken;
private List<Output> output;
private List<Output> error;
private JsonObject extension;

public String getTransactionTime() {
return transactionTime;
Expand Down Expand Up @@ -70,6 +98,30 @@ public void setError(List<Output> error) {
this.error = error;
}

public JsonObject getExtension() {
return extension;
}

public void setExtension(JsonObject extension) {
this.extension = extension;
}

public void addOperationOutcomeToExtension(OperationOutcome outcome) throws FHIRGeneratorException, IOException {
// Convert to the JsonObject and add as "operationOutcome" to the existing extension.
try (StringWriter writer = new StringWriter();) {
FHIRGenerator.generator(Format.JSON).generate(outcome, writer);

String outcomeString = writer.toString();
try (ByteArrayInputStream bais = new ByteArrayInputStream(outcomeString.getBytes());
JsonReader jsonReader = JSON_READER_FACTORY.createReader(bais, StandardCharsets.UTF_8)) {
JsonObject jsonObject = Json.createObjectBuilder()
.add("outcome", jsonReader.readObject())
.build();
this.setExtension(jsonObject);
}
}
}

public static class Output {
private String type;
private String url;
Expand Down Expand Up @@ -151,12 +203,10 @@ public static void generate(JsonGenerator generatorOutput, Output output) throws

if (output.getCount() != null) {
generatorOutput.write("count", Long.parseLong(output.getCount()));

}

if (output.getInputUrl() != null) {
generatorOutput.write("inputUrl", output.getInputUrl());

}

generatorOutput.writeEnd();
Expand Down Expand Up @@ -214,6 +264,11 @@ public static String generate(PollingLocationResponse response) throws IOExcepti
}
generator.writeEnd();
}

if (response.getExtension() != null) {
generator.write("extension", response.getExtension());
}

generator.writeEnd();
}
o = writer.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
package com.ibm.fhir.operation.bulkdata.model;

import static com.ibm.fhir.model.type.String.string;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
Expand All @@ -19,17 +20,34 @@

import org.testng.annotations.Test;

import com.ibm.fhir.model.generator.exception.FHIRGeneratorException;
import com.ibm.fhir.model.resource.OperationOutcome;
import com.ibm.fhir.model.resource.OperationOutcome.Issue;
import com.ibm.fhir.model.type.CodeableConcept;
import com.ibm.fhir.model.type.Instant;
import com.ibm.fhir.model.type.code.IssueSeverity;
import com.ibm.fhir.model.type.code.IssueType;
import com.ibm.fhir.operation.bulkdata.model.PollingLocationResponse.Output;

import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.json.stream.JsonGenerator;
import jakarta.json.stream.JsonGeneratorFactory;

/**
* Simple Test for the Rough Response defined in the BulkData Export
*/
public class PollingLocationResponseTest {
public static final OperationOutcome TEST_OK = OperationOutcome.builder()
.issue(Issue.builder()
.severity(IssueSeverity.INFORMATION)
.code(IssueType.INFORMATIONAL)
.details(CodeableConcept.builder()
.text(string("Test OK"))
.build())
.build())
.build();

private static final Map<java.lang.String, Object> properties =
Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true);
private static final JsonGeneratorFactory PRETTY_PRINTING_GENERATOR_FACTORY =
Expand Down Expand Up @@ -134,6 +152,66 @@ public void testResponseMetadataJsonFull() throws IOException {
+ " \"request\": \"request\",\n" + " \"requiresAccessToken\": false\n" + "}");
}

@Test
public void testResponseMetadataJsonFullWithExtension() throws IOException {
PollingLocationResponse metadata = new PollingLocationResponse();
metadata.setRequest("request");
metadata.setRequiresAccessToken(Boolean.FALSE);
Instant now = Instant.now(ZoneOffset.UTC);
String s = now.getValue().format(Instant.PARSER_FORMATTER).toString();
metadata.setTransactionTime(s);

JsonObject extension = Json.createObjectBuilder().add("test", false).build();
metadata.setExtension(extension);

assertEquals(
PollingLocationResponse.Writer.generate(metadata)
.replaceFirst(s, ""),
"{\n"
+ " \"transactionTime\": \"\",\n"
+ " \"request\": \"request\",\n"
+ " \"requiresAccessToken\": false,\n"
+ " \"extension\": {\n"
+ " \"test\": false\n"
+ " }\n"
+ "}");
}

@Test
public void testResponseMetadataJsonFullWithExtensionAndOperationOutcome() throws IOException, FHIRGeneratorException {
PollingLocationResponse metadata = new PollingLocationResponse();
metadata.setRequest("request");
metadata.setRequiresAccessToken(Boolean.FALSE);
Instant now = Instant.now(ZoneOffset.UTC);
String s = now.getValue().format(Instant.PARSER_FORMATTER).toString();
metadata.setTransactionTime(s);

metadata.addOperationOutcomeToExtension(TEST_OK);

assertEquals(
PollingLocationResponse.Writer.generate(metadata)
.replaceFirst(s, ""),
"{\n"
+ " \"transactionTime\": \"\",\n"
+ " \"request\": \"request\",\n"
+ " \"requiresAccessToken\": false,\n"
+ " \"extension\": {\n"
+ " \"outcome\": {\n"
+ " \"resourceType\": \"OperationOutcome\",\n"
+ " \"issue\": [\n"
+ " {\n"
+ " \"severity\": \"information\",\n"
+ " \"code\": \"informational\",\n"
+ " \"details\": {\n"
+ " \"text\": \"Test OK\"\n"
+ " }\n"
+ " }\n"
+ " ]\n"
+ " }\n"
+ " }\n"
+ "}");
}

@Test
public void testResponseMetadataJsonFullWithOutput() throws IOException {
Output output = new Output("type1", "url1", "1000");
Expand Down Expand Up @@ -170,6 +248,32 @@ public void testResponseMetadataJsonFullWithOutput() throws IOException {
+ " \"count\": 2000\n" + " }\n" + " ]\n" + "}");
}

@Test
public void testResponseMetadataJsonFullWithEmptyOutput() throws IOException {
List<Output> outputs = new ArrayList<>();

PollingLocationResponse metadata = new PollingLocationResponse();

metadata.setRequest("request");
metadata.setRequiresAccessToken(Boolean.FALSE);
Instant now = Instant.now(ZoneOffset.UTC);
String s = now.getValue().format(Instant.PARSER_FORMATTER).toString();
metadata.setTransactionTime(s);
assertNotNull(metadata.getTransactionTime());

metadata.setOutput(outputs);
assertEquals(
PollingLocationResponse.Writer.generate(metadata)
.replaceFirst(s, ""),
"{\n"
+ " \"transactionTime\": \"\",\n"
+ " \"request\": \"request\",\n"
+ " \"requiresAccessToken\": false,\n"
+ " \"output\": [\n"
+ " ]\n"
+ "}");
}

@Test
public void testResponseMetadataJsonFullWithOutputAndError() throws IOException {
Output output = new Output("type1", "url1", "1000");
Expand Down

0 comments on commit 88b5f50

Please sign in to comment.