If AutoInflateBinaries is enabled, binaries are created on the disk only for the first resource entry of the bundle (#5420)
* If AutoInflateBinaries is enabled, binaries created on disk by bundled requests are created only for the first resource entry - fix
This commit is contained in:
parent
4e295a59fb
commit
64cc704a40
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
type: fix
|
||||||
|
issue: 5419
|
||||||
|
title: "Previously, when `AllowAutoInflateBinaries` was enabled in `JpaStorageSettings` and bundles with multiple
|
||||||
|
resources were submitted, binaries were created on the disk only for the first resource entry of the bundle.
|
||||||
|
This has been fixed."
|
|
@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.binary.interceptor.BinaryStorageInterceptor;
|
||||||
import ca.uhn.fhir.jpa.binstore.MemoryBinaryStorageSvcImpl;
|
import ca.uhn.fhir.jpa.binstore.MemoryBinaryStorageSvcImpl;
|
||||||
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
|
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
|
||||||
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
|
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
|
||||||
|
import ca.uhn.fhir.model.primitive.IdDt;
|
||||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
|
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
|
||||||
|
@ -18,13 +19,16 @@ import ca.uhn.fhir.rest.client.api.IHttpResponse;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
import ca.uhn.fhir.util.HapiExtensions;
|
import ca.uhn.fhir.util.HapiExtensions;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
|
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseMetaType;
|
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r4.model.Binary;
|
import org.hl7.fhir.r4.model.Binary;
|
||||||
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
import org.hl7.fhir.r4.model.DocumentReference;
|
import org.hl7.fhir.r4.model.DocumentReference;
|
||||||
import org.hl7.fhir.r4.model.Enumerations;
|
import org.hl7.fhir.r4.model.Enumerations;
|
||||||
import org.hl7.fhir.r4.model.Extension;
|
import org.hl7.fhir.r4.model.Extension;
|
||||||
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
import org.hl7.fhir.r4.model.Resource;
|
||||||
import org.hl7.fhir.r4.model.StringType;
|
import org.hl7.fhir.r4.model.StringType;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
@ -32,7 +36,6 @@ import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.EnumSource;
|
import org.junit.jupiter.params.provider.EnumSource;
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -62,6 +65,7 @@ public class BinaryStorageInterceptorR4Test extends BaseResourceProviderR4Test {
|
||||||
public static final byte[] FEW_BYTES = {4, 3, 2, 1};
|
public static final byte[] FEW_BYTES = {4, 3, 2, 1};
|
||||||
public static final byte[] SOME_BYTES = {1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1, 8, 9, 0, 10, 9};
|
public static final byte[] SOME_BYTES = {1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1, 8, 9, 0, 10, 9};
|
||||||
public static final byte[] SOME_BYTES_2 = {6, 7, 8, 7, 6, 5, 4, 3, 2, 1, 5, 5, 5, 6};
|
public static final byte[] SOME_BYTES_2 = {6, 7, 8, 7, 6, 5, 4, 3, 2, 1, 5, 5, 5, 6};
|
||||||
|
public static final byte[] SOME_BYTES_3 = {5, 5, 5, 6, 6, 7, 7, 7, 8, 8, 8};
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(BinaryStorageInterceptorR4Test.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(BinaryStorageInterceptorR4Test.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -381,12 +385,8 @@ public class BinaryStorageInterceptorR4Test extends BaseResourceProviderR4Test {
|
||||||
|
|
||||||
// Create a resource with a big enough docRef
|
// Create a resource with a big enough docRef
|
||||||
DocumentReference docRef = new DocumentReference();
|
DocumentReference docRef = new DocumentReference();
|
||||||
DocumentReference.DocumentReferenceContentComponent content = docRef.addContent();
|
addDocumentAttachmentData(docRef, SOME_BYTES);
|
||||||
content.getAttachment().setContentType("application/octet-stream");
|
addDocumentAttachmentData(docRef, SOME_BYTES_2);
|
||||||
content.getAttachment().setData(SOME_BYTES);
|
|
||||||
DocumentReference.DocumentReferenceContentComponent content2 = docRef.addContent();
|
|
||||||
content2.getAttachment().setContentType("application/octet-stream");
|
|
||||||
content2.getAttachment().setData(SOME_BYTES_2);
|
|
||||||
DaoMethodOutcome outcome = myDocumentReferenceDao.create(docRef, mySrd);
|
DaoMethodOutcome outcome = myDocumentReferenceDao.create(docRef, mySrd);
|
||||||
|
|
||||||
// Make sure it was externalized
|
// Make sure it was externalized
|
||||||
|
@ -422,18 +422,73 @@ public class BinaryStorageInterceptorR4Test extends BaseResourceProviderR4Test {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateBinaryAttachments_bundleWithMultipleDocumentReferences_createdAndReadBackSuccessfully() {
|
||||||
|
// Create Patient
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.addIdentifier().setSystem("urn:system").setValue("001");
|
||||||
|
patient.addName().addGiven("Johnny").setFamily("Walker");
|
||||||
|
|
||||||
|
// Create first DocumentReference with a big enough attachments
|
||||||
|
DocumentReference docRef = new DocumentReference();
|
||||||
|
addDocumentAttachmentData(docRef, SOME_BYTES);
|
||||||
|
addDocumentAttachmentData(docRef, SOME_BYTES_2);
|
||||||
|
|
||||||
|
// Create second DocumentReference with a big enough attachment
|
||||||
|
DocumentReference docRef2 = new DocumentReference();
|
||||||
|
addDocumentAttachmentData(docRef2, SOME_BYTES_3);
|
||||||
|
|
||||||
|
// Create Bundle
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(Bundle.BundleType.TRANSACTION);
|
||||||
|
// Patient entry component
|
||||||
|
addBundleEntry(bundle, patient, "Patient");
|
||||||
|
// First DocumentReference entry component
|
||||||
|
addBundleEntry(bundle, docRef, "DocumentReference");
|
||||||
|
// Second DocumentReference entry component
|
||||||
|
addBundleEntry(bundle, docRef2, "DocumentReference");
|
||||||
|
|
||||||
|
// Execute transaction
|
||||||
|
Bundle output = myClient.transaction().withBundle(bundle).execute();
|
||||||
|
ourLog.debug(myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(output));
|
||||||
|
|
||||||
|
// Verify bundle response
|
||||||
|
assertEquals(3, output.getEntry().size());
|
||||||
|
output.getEntry().forEach(entry -> assertEquals("201 Created", entry.getResponse().getStatus()));
|
||||||
|
|
||||||
|
// Read back and verify first DocumentReference and attachments
|
||||||
|
IIdType firstDocRef = new IdType(output.getEntry().get(1).getResponse().getLocation());
|
||||||
|
DocumentReference firstDoc = myDocumentReferenceDao.read(firstDocRef, mySrd);
|
||||||
|
assertEquals("application/octet-stream", firstDoc.getContentFirstRep().getAttachment().getContentType());
|
||||||
|
assertArrayEquals(SOME_BYTES, firstDoc.getContentFirstRep().getAttachment().getData());
|
||||||
|
assertEquals("application/octet-stream", firstDoc.getContent().get(1).getAttachment().getContentType());
|
||||||
|
assertArrayEquals(SOME_BYTES_2, firstDoc.getContent().get(1).getAttachment().getData());
|
||||||
|
|
||||||
|
// Read back and verify second DocumentReference and attachment
|
||||||
|
IIdType secondDocRef = new IdType(output.getEntry().get(2).getResponse().getLocation());
|
||||||
|
DocumentReference secondDoc = myDocumentReferenceDao.read(secondDocRef, mySrd);
|
||||||
|
assertEquals("application/octet-stream", secondDoc.getContentFirstRep().getAttachment().getContentType());
|
||||||
|
assertArrayEquals(SOME_BYTES_3, secondDoc.getContentFirstRep().getAttachment().getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addBundleEntry(Bundle theBundle, Resource theResource, String theUrl) {
|
||||||
|
Bundle.BundleEntryComponent getComponent = new Bundle.BundleEntryComponent();
|
||||||
|
Bundle.BundleEntryRequestComponent requestComponent = new Bundle.BundleEntryRequestComponent();
|
||||||
|
requestComponent.setMethod(Bundle.HTTPVerb.POST);
|
||||||
|
requestComponent.setUrl(theUrl);
|
||||||
|
getComponent.setRequest(requestComponent);
|
||||||
|
getComponent.setResource(theResource);
|
||||||
|
getComponent.setFullUrl(IdDt.newRandomUuid().getValue());
|
||||||
|
theBundle.addEntry(getComponent);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdateRejectsIncorrectBinary() {
|
public void testUpdateRejectsIncorrectBinary() {
|
||||||
|
|
||||||
// Create a resource with a big enough docRef
|
// Create a resource with a big enough docRef
|
||||||
DocumentReference docRef = new DocumentReference();
|
DocumentReference docRef = new DocumentReference();
|
||||||
DocumentReference.DocumentReferenceContentComponent content = docRef.addContent();
|
addDocumentAttachmentData(docRef, SOME_BYTES);
|
||||||
content.getAttachment().setContentType("application/octet-stream");
|
addDocumentAttachmentData(docRef, SOME_BYTES_2);
|
||||||
content.getAttachment().setData(SOME_BYTES);
|
|
||||||
DocumentReference.DocumentReferenceContentComponent content2 = docRef.addContent();
|
|
||||||
content2.getAttachment().setContentType("application/octet-stream");
|
|
||||||
content2.getAttachment().setData(SOME_BYTES_2);
|
|
||||||
DaoMethodOutcome outcome = myDocumentReferenceDao.create(docRef, mySrd);
|
DaoMethodOutcome outcome = myDocumentReferenceDao.create(docRef, mySrd);
|
||||||
|
|
||||||
// Make sure it was externalized
|
// Make sure it was externalized
|
||||||
|
@ -449,13 +504,13 @@ public class BinaryStorageInterceptorR4Test extends BaseResourceProviderR4Test {
|
||||||
docRef = new DocumentReference();
|
docRef = new DocumentReference();
|
||||||
docRef.setId(id.toUnqualifiedVersionless());
|
docRef.setId(id.toUnqualifiedVersionless());
|
||||||
docRef.setStatus(Enumerations.DocumentReferenceStatus.CURRENT);
|
docRef.setStatus(Enumerations.DocumentReferenceStatus.CURRENT);
|
||||||
content = docRef.addContent();
|
DocumentReference.DocumentReferenceContentComponent content = docRef.addContent();
|
||||||
content.getAttachment().setContentType("application/octet-stream");
|
content.getAttachment().setContentType("application/octet-stream");
|
||||||
content.getAttachment().getDataElement().addExtension(
|
content.getAttachment().getDataElement().addExtension(
|
||||||
HapiExtensions.EXT_EXTERNALIZED_BINARY_ID,
|
HapiExtensions.EXT_EXTERNALIZED_BINARY_ID,
|
||||||
new StringType(binaryId)
|
new StringType(binaryId)
|
||||||
);
|
);
|
||||||
content2 = docRef.addContent();
|
DocumentReference.DocumentReferenceContentComponent content2 = docRef.addContent();
|
||||||
content2.getAttachment().setContentType("application/octet-stream");
|
content2.getAttachment().setContentType("application/octet-stream");
|
||||||
content2.getAttachment().getDataElement().addExtension(
|
content2.getAttachment().getDataElement().addExtension(
|
||||||
HapiExtensions.EXT_EXTERNALIZED_BINARY_ID,
|
HapiExtensions.EXT_EXTERNALIZED_BINARY_ID,
|
||||||
|
@ -497,5 +552,10 @@ public class BinaryStorageInterceptorR4Test extends BaseResourceProviderR4Test {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addDocumentAttachmentData(DocumentReference theDocumentReference, byte[] theData) {
|
||||||
|
DocumentReference.DocumentReferenceContentComponent content = theDocumentReference.addContent();
|
||||||
|
content.getAttachment().setContentType("application/octet-stream");
|
||||||
|
content.getAttachment().setData(theData);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -251,7 +251,7 @@ public class BinaryStorageInterceptor<T extends IPrimitiveType<byte[]>> {
|
||||||
}
|
}
|
||||||
if (myBinaryStorageSvc.isValidBlobId(newBlobId)) {
|
if (myBinaryStorageSvc.isValidBlobId(newBlobId)) {
|
||||||
List<DeferredBinaryTarget> deferredBinaryTargets =
|
List<DeferredBinaryTarget> deferredBinaryTargets =
|
||||||
getOrCreateDeferredBinaryStorageMap(theTransactionDetails);
|
getOrCreateDeferredBinaryStorageList(theResource);
|
||||||
DeferredBinaryTarget newDeferredBinaryTarget =
|
DeferredBinaryTarget newDeferredBinaryTarget =
|
||||||
new DeferredBinaryTarget(newBlobId, nextTarget, data);
|
new DeferredBinaryTarget(newBlobId, nextTarget, data);
|
||||||
deferredBinaryTargets.add(newDeferredBinaryTarget);
|
deferredBinaryTargets.add(newDeferredBinaryTarget);
|
||||||
|
@ -289,21 +289,29 @@ public class BinaryStorageInterceptor<T extends IPrimitiveType<byte[]>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private List<DeferredBinaryTarget> getOrCreateDeferredBinaryStorageMap(TransactionDetails theTransactionDetails) {
|
@SuppressWarnings("unchecked")
|
||||||
return theTransactionDetails.getOrCreateUserData(getDeferredListKey(), ArrayList::new);
|
private List<DeferredBinaryTarget> getOrCreateDeferredBinaryStorageList(IBaseResource theResource) {
|
||||||
|
Object deferredBinaryTargetList = theResource.getUserData(getDeferredListKey());
|
||||||
|
if (deferredBinaryTargetList == null) {
|
||||||
|
deferredBinaryTargetList = new ArrayList<>();
|
||||||
|
theResource.setUserData(getDeferredListKey(), deferredBinaryTargetList);
|
||||||
|
}
|
||||||
|
return (List<DeferredBinaryTarget>) deferredBinaryTargetList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
|
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
|
||||||
public void storeLargeBinariesBeforeCreatePersistence(
|
public void storeLargeBinariesBeforeCreatePersistence(
|
||||||
TransactionDetails theTransactionDetails, IBaseResource theResource, Pointcut thePoincut)
|
TransactionDetails theTransactionDetails, IBaseResource theResource, Pointcut thePointcut)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
if (theTransactionDetails == null) {
|
if (theResource == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<DeferredBinaryTarget> deferredBinaryTargets = theTransactionDetails.getUserData(getDeferredListKey());
|
Object deferredBinaryTargetList = theResource.getUserData(getDeferredListKey());
|
||||||
if (deferredBinaryTargets != null) {
|
|
||||||
|
if (deferredBinaryTargetList != null) {
|
||||||
IIdType resourceId = theResource.getIdElement();
|
IIdType resourceId = theResource.getIdElement();
|
||||||
for (DeferredBinaryTarget next : deferredBinaryTargets) {
|
for (DeferredBinaryTarget next : (List<DeferredBinaryTarget>) deferredBinaryTargetList) {
|
||||||
String blobId = next.getBlobId();
|
String blobId = next.getBlobId();
|
||||||
IBinaryTarget target = next.getBinaryTarget();
|
IBinaryTarget target = next.getBinaryTarget();
|
||||||
InputStream dataStream = next.getDataStream();
|
InputStream dataStream = next.getDataStream();
|
||||||
|
|
Loading…
Reference in New Issue