Auto externalize binary contents (#1550)
* Auto externaliZe binary attachments * Work on externalization * Auto externalize binary content * Work on tests * Fix alert
This commit is contained in:
parent
985cd49892
commit
bfbc73caaf
|
@ -23,10 +23,10 @@ package ca.uhn.fhir.rest.api.server;
|
|||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
/**
|
||||
* This interface is a parameter type for the {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_PRESHOW_RESOURCE}
|
||||
* This interface is a parameter type for the {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_PRESHOW_RESOURCES}
|
||||
* hook.
|
||||
*/
|
||||
public interface IPreResourceShowDetails {
|
||||
public interface IPreResourceShowDetails extends Iterable<IBaseResource> {
|
||||
|
||||
/**
|
||||
* @return Returns the number of resources being shown
|
||||
|
|
|
@ -24,9 +24,10 @@ import com.google.common.collect.Lists;
|
|||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class SimplePreResourceShowDetails implements IPreResourceShowDetails {
|
||||
public class SimplePreResourceShowDetails implements IPreResourceShowDetails, Iterable<IBaseResource> {
|
||||
|
||||
private final List<IBaseResource> myResources;
|
||||
private final boolean[] mySubSets;
|
||||
|
@ -64,4 +65,9 @@ public class SimplePreResourceShowDetails implements IPreResourceShowDetails {
|
|||
Validate.isTrue(theIndex < myResources.size(), "Invalid index {} - theIndex must be < %d", theIndex, myResources.size());
|
||||
mySubSets[theIndex] = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<IBaseResource> iterator() {
|
||||
return myResources.iterator();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ public interface IModelVisitor2 {
|
|||
/**
|
||||
*
|
||||
*/
|
||||
boolean acceptUndeclaredExtension(IBaseExtension<?, ?> theNextExt, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath);
|
||||
default boolean acceptUndeclaredExtension(IBaseExtension<?, ?> theNextExt, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { return true; }
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ public interface IBaseBinary extends IBaseResource {
|
|||
|
||||
byte[] getContent();
|
||||
|
||||
IPrimitiveType<byte[]> getContentElement();
|
||||
|
||||
String getContentAsBase64();
|
||||
|
||||
String getContentType();
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package ca.uhn.fhir.rest.api.server;
|
||||
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class SimplePreResourceShowDetailsTest {
|
||||
|
||||
@Mock
|
||||
private IBaseResource myResource1;
|
||||
@Mock
|
||||
private IBaseResource myResource2;
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testSetResource_TooLow() {
|
||||
SimplePreResourceShowDetails details = new SimplePreResourceShowDetails(myResource1);
|
||||
details.setResource(-1, myResource2);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testSetResource_TooHigh() {
|
||||
SimplePreResourceShowDetails details = new SimplePreResourceShowDetails(myResource1);
|
||||
details.setResource(2, myResource2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetResource() {
|
||||
SimplePreResourceShowDetails details = new SimplePreResourceShowDetails(myResource1);
|
||||
details.setResource(0, myResource2);
|
||||
assertSame(myResource2, details.iterator().next());
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ package ca.uhn.fhir.jpa.binstore;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException;
|
||||
import com.google.common.hash.HashFunction;
|
||||
import com.google.common.hash.Hashing;
|
||||
|
@ -34,6 +33,8 @@ import javax.annotation.Nonnull;
|
|||
import java.io.InputStream;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc {
|
||||
private final SecureRandom myRandom;
|
||||
private final String CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
|
@ -66,7 +67,8 @@ abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc {
|
|||
myMinimumBinarySize = theMinimumBinarySize;
|
||||
}
|
||||
|
||||
String newRandomId() {
|
||||
@Override
|
||||
public String newBlobId() {
|
||||
StringBuilder b = new StringBuilder();
|
||||
for (int i = 0; i < ID_LENGTH; i++) {
|
||||
int nextInt = Math.abs(myRandom.nextInt());
|
||||
|
@ -89,13 +91,13 @@ abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc {
|
|||
|
||||
@Nonnull
|
||||
CountingInputStream createCountingInputStream(InputStream theInputStream) {
|
||||
InputStream is = ByteStreams.limit(theInputStream, myMaximumBinarySize + 1L);
|
||||
InputStream is = ByteStreams.limit(theInputStream, getMaximumBinarySize() + 1L);
|
||||
return new CountingInputStream(is) {
|
||||
@Override
|
||||
public int getCount() {
|
||||
int retVal = super.getCount();
|
||||
if (retVal > myMaximumBinarySize) {
|
||||
throw new PayloadTooLargeException("Binary size exceeds maximum: " + myMaximumBinarySize);
|
||||
if (retVal > getMaximumBinarySize()) {
|
||||
throw new PayloadTooLargeException("Binary size exceeds maximum: " + getMaximumBinarySize());
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
@ -103,4 +105,11 @@ abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc {
|
|||
}
|
||||
|
||||
|
||||
String provideIdForNewBlob(String theBlobIdOrNull) {
|
||||
String id = theBlobIdOrNull;
|
||||
if (isBlank(theBlobIdOrNull)) {
|
||||
id = newBlobId();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,20 +86,13 @@ public class BinaryAccessProvider {
|
|||
|
||||
IBinaryTarget target = findAttachmentForRequest(resource, path, theRequestDetails);
|
||||
|
||||
Optional<? extends IBaseExtension<?, ?>> attachmentId = target
|
||||
.getTarget()
|
||||
.getExtension()
|
||||
.stream()
|
||||
.filter(t -> JpaConstants.EXT_EXTERNALIZED_BINARY_ID.equals(t.getUrl()))
|
||||
.findFirst();
|
||||
|
||||
Optional<String> attachmentId = target.getAttachmentId();
|
||||
if (attachmentId.isPresent()) {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
IPrimitiveType<String> value = (IPrimitiveType<String>) attachmentId.get().getValue();
|
||||
String blobId = value.getValueAsString();
|
||||
String blobId = attachmentId.get();
|
||||
|
||||
IBinaryStorageSvc.StoredDetails blobDetails = myBinaryStorageSvc.fetchBlobDetails(theResourceId, blobId);
|
||||
StoredDetails blobDetails = myBinaryStorageSvc.fetchBlobDetails(theResourceId, blobId);
|
||||
if (blobDetails == null) {
|
||||
String msg = myCtx.getLocalizer().getMessage(BinaryAccessProvider.class, "unknownBlobId");
|
||||
throw new InvalidRequestException(msg);
|
||||
|
@ -179,7 +172,7 @@ public class BinaryAccessProvider {
|
|||
if (size > 0) {
|
||||
if (myBinaryStorageSvc != null) {
|
||||
if (myBinaryStorageSvc.shouldStoreBlob(size, theResourceId, requestContentType)) {
|
||||
IBinaryStorageSvc.StoredDetails storedDetails = myBinaryStorageSvc.storeBlob(theResourceId, requestContentType, theRequestDetails.getInputStream());
|
||||
StoredDetails storedDetails = myBinaryStorageSvc.storeBlob(theResourceId, null, requestContentType, theRequestDetails.getInputStream());
|
||||
size = storedDetails.getBytes();
|
||||
blobId = storedDetails.getBlobId();
|
||||
Validate.notBlank(blobId, "BinaryStorageSvc returned a null blob ID"); // should not happen
|
||||
|
@ -192,19 +185,7 @@ public class BinaryAccessProvider {
|
|||
size = bytes.length;
|
||||
target.setData(bytes);
|
||||
} else {
|
||||
|
||||
target
|
||||
.getTarget()
|
||||
.getExtension()
|
||||
.removeIf(t -> JpaConstants.EXT_EXTERNALIZED_BINARY_ID.equals(t.getUrl()));
|
||||
target.setData(null);
|
||||
|
||||
IBaseExtension<?, ?> ext = target.getTarget().addExtension();
|
||||
ext.setUrl(JpaConstants.EXT_EXTERNALIZED_BINARY_ID);
|
||||
ext.setUserData(JpaConstants.EXTENSION_EXT_SYSTEMDEFINED, Boolean.TRUE);
|
||||
IPrimitiveType<String> blobIdString = (IPrimitiveType<String>) myCtx.getElementDefinition("string").newInstance();
|
||||
blobIdString.setValueAsString(blobId);
|
||||
ext.setValue(blobIdString);
|
||||
replaceDataWithExtension(target, blobId);
|
||||
}
|
||||
|
||||
target.setContentType(requestContentType);
|
||||
|
@ -217,52 +198,81 @@ public class BinaryAccessProvider {
|
|||
return outcome.getResource();
|
||||
}
|
||||
|
||||
public void replaceDataWithExtension(IBinaryTarget theTarget, String theBlobId) {
|
||||
theTarget
|
||||
.getTarget()
|
||||
.getExtension()
|
||||
.removeIf(t -> JpaConstants.EXT_EXTERNALIZED_BINARY_ID.equals(t.getUrl()));
|
||||
theTarget.setData(null);
|
||||
|
||||
IBaseExtension<?, ?> ext = theTarget.getTarget().addExtension();
|
||||
ext.setUrl(JpaConstants.EXT_EXTERNALIZED_BINARY_ID);
|
||||
ext.setUserData(JpaConstants.EXTENSION_EXT_SYSTEMDEFINED, Boolean.TRUE);
|
||||
IPrimitiveType<String> blobIdString = (IPrimitiveType<String>) myCtx.getElementDefinition("string").newInstance();
|
||||
blobIdString.setValueAsString(theBlobId);
|
||||
ext.setValue(blobIdString);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private IBinaryTarget findAttachmentForRequest(IBaseResource theResource, String thePath, ServletRequestDetails theRequestDetails) {
|
||||
FhirContext ctx = theRequestDetails.getFhirContext();
|
||||
|
||||
Optional<IBase> type = ctx.newFluentPath().evaluateFirst(theResource, thePath, IBase.class);
|
||||
String resType = myCtx.getResourceDefinition(theResource).getName();
|
||||
Optional<IBase> type = myCtx.newFluentPath().evaluateFirst(theResource, thePath, IBase.class);
|
||||
String resType = this.myCtx.getResourceDefinition(theResource).getName();
|
||||
if (!type.isPresent()) {
|
||||
String msg = myCtx.getLocalizer().getMessageSanitized(BinaryAccessProvider.class, "unknownPath", resType, thePath);
|
||||
String msg = this.myCtx.getLocalizer().getMessageSanitized(BinaryAccessProvider.class, "unknownPath", resType, thePath);
|
||||
throw new InvalidRequestException(msg);
|
||||
}
|
||||
IBase element = type.get();
|
||||
|
||||
Optional<IBinaryTarget> binaryTarget = toBinaryTarget(element);
|
||||
|
||||
if (binaryTarget.isPresent() == false) {
|
||||
BaseRuntimeElementDefinition<?> def2 = myCtx.getElementDefinition(element.getClass());
|
||||
String msg = this.myCtx.getLocalizer().getMessageSanitized(BinaryAccessProvider.class, "unknownType", resType, thePath, def2.getName());
|
||||
throw new InvalidRequestException(msg);
|
||||
} else {
|
||||
return binaryTarget.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Optional<IBinaryTarget> toBinaryTarget(IBase theElement) {
|
||||
IBinaryTarget binaryTarget = null;
|
||||
|
||||
// Path is attachment
|
||||
BaseRuntimeElementDefinition<?> def = ctx.getElementDefinition(type.get().getClass());
|
||||
BaseRuntimeElementDefinition<?> def = myCtx.getElementDefinition(theElement.getClass());
|
||||
if (def.getName().equals("Attachment")) {
|
||||
ICompositeType attachment = (ICompositeType) type.get();
|
||||
return new IBinaryTarget() {
|
||||
ICompositeType attachment = (ICompositeType) theElement;
|
||||
binaryTarget = new IBinaryTarget() {
|
||||
@Override
|
||||
public void setSize(Integer theSize) {
|
||||
AttachmentUtil.setSize(myCtx, attachment, theSize);
|
||||
AttachmentUtil.setSize(BinaryAccessProvider.this.myCtx, attachment, theSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return AttachmentUtil.getOrCreateContentType(myCtx, attachment).getValueAsString();
|
||||
return AttachmentUtil.getOrCreateContentType(BinaryAccessProvider.this.myCtx, attachment).getValueAsString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getData() {
|
||||
IPrimitiveType<byte[]> dataDt = AttachmentUtil.getOrCreateData(theRequestDetails.getFhirContext(), attachment);
|
||||
IPrimitiveType<byte[]> dataDt = AttachmentUtil.getOrCreateData(myCtx, attachment);
|
||||
return dataDt.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBaseHasExtensions getTarget() {
|
||||
return (IBaseHasExtensions) AttachmentUtil.getOrCreateData(theRequestDetails.getFhirContext(), attachment);
|
||||
return (IBaseHasExtensions) AttachmentUtil.getOrCreateData(myCtx, attachment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentType(String theContentType) {
|
||||
AttachmentUtil.setContentType(myCtx, attachment, theContentType);
|
||||
AttachmentUtil.setContentType(BinaryAccessProvider.this.myCtx, attachment, theContentType);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setData(byte[] theBytes) {
|
||||
AttachmentUtil.setData(theRequestDetails.getFhirContext(), attachment, theBytes);
|
||||
AttachmentUtil.setData(myCtx, attachment, theBytes);
|
||||
}
|
||||
|
||||
|
||||
|
@ -271,8 +281,8 @@ public class BinaryAccessProvider {
|
|||
|
||||
// Path is Binary
|
||||
if (def.getName().equals("Binary")) {
|
||||
IBaseBinary binary = (IBaseBinary) type.get();
|
||||
return new IBinaryTarget() {
|
||||
IBaseBinary binary = (IBaseBinary) theElement;
|
||||
binaryTarget = new IBinaryTarget() {
|
||||
@Override
|
||||
public void setSize(Integer theSize) {
|
||||
// ignore
|
||||
|
@ -290,7 +300,7 @@ public class BinaryAccessProvider {
|
|||
|
||||
@Override
|
||||
public IBaseHasExtensions getTarget() {
|
||||
return (IBaseHasExtensions) BinaryUtil.getOrCreateData(myCtx, binary);
|
||||
return (IBaseHasExtensions) BinaryUtil.getOrCreateData(BinaryAccessProvider.this.myCtx, binary);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -308,9 +318,7 @@ public class BinaryAccessProvider {
|
|||
};
|
||||
}
|
||||
|
||||
String msg = myCtx.getLocalizer().getMessageSanitized(BinaryAccessProvider.class, "unknownType", resType, thePath, def.getName());
|
||||
throw new InvalidRequestException(msg);
|
||||
|
||||
return Optional.ofNullable(binaryTarget);
|
||||
}
|
||||
|
||||
private String validateResourceTypeAndPath(@IdParam IIdType theResourceId, @OperationParam(name = "path", min = 1, max = 1) IPrimitiveType<String> thePath) {
|
||||
|
@ -340,25 +348,5 @@ public class BinaryAccessProvider {
|
|||
return dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an Attachment datatype or Binary resource, since they both
|
||||
* hold binary content but don't look entirely similar
|
||||
*/
|
||||
private interface IBinaryTarget {
|
||||
|
||||
void setSize(Integer theSize);
|
||||
|
||||
String getContentType();
|
||||
|
||||
void setContentType(String theContentType);
|
||||
|
||||
byte[] getData();
|
||||
|
||||
void setData(byte[] theBytes);
|
||||
|
||||
IBaseHasExtensions getTarget();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -20,20 +20,32 @@ package ca.uhn.fhir.jpa.binstore;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
||||
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.interceptor.api.Hook;
|
||||
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import ca.uhn.fhir.util.IModelVisitor2;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.hl7.fhir.instance.model.api.*;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -45,12 +57,40 @@ public class BinaryStorageInterceptor {
|
|||
private IBinaryStorageSvc myBinaryStorageSvc;
|
||||
@Autowired
|
||||
private FhirContext myCtx;
|
||||
@Autowired
|
||||
private BinaryAccessProvider myBinaryAccessProvider;
|
||||
|
||||
private Class<? extends IPrimitiveType<byte[]>> myBinaryType;
|
||||
private String myDeferredListKey;
|
||||
private long myAutoDeExternalizeMaximumBytes = 10 * FileUtils.ONE_MB;
|
||||
|
||||
/**
|
||||
* Any externalized binaries will be rehydrated if their size is below this thhreshold when
|
||||
* reading the resource back. Default is 10MB.
|
||||
*/
|
||||
public long getAutoDeExternalizeMaximumBytes() {
|
||||
return myAutoDeExternalizeMaximumBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Any externalized binaries will be rehydrated if their size is below this thhreshold when
|
||||
* reading the resource back. Default is 10MB.
|
||||
*/
|
||||
public void setAutoDeExternalizeMaximumBytes(long theAutoDeExternalizeMaximumBytes) {
|
||||
myAutoDeExternalizeMaximumBytes = theAutoDeExternalizeMaximumBytes;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
myBinaryType = (Class<? extends IPrimitiveType<byte[]>>) myCtx.getElementDefinition("base64Binary").getImplementingClass();
|
||||
myDeferredListKey = getClass().getName() + "_" + hashCode() + "_DEFERRED_LIST";
|
||||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE)
|
||||
public void expungeResource(AtomicInteger theCounter, IBaseResource theResource) {
|
||||
|
||||
Class<? extends IBase> binaryType = myCtx.getElementDefinition("base64Binary").getImplementingClass();
|
||||
List<? extends IBase> binaryElements = myCtx.newTerser().getAllPopulatedChildElementsOfType(theResource, binaryType);
|
||||
List<? extends IBase> binaryElements = myCtx.newTerser().getAllPopulatedChildElementsOfType(theResource, myBinaryType);
|
||||
|
||||
List<String> attachmentIds = binaryElements
|
||||
.stream()
|
||||
|
@ -67,4 +107,159 @@ public class BinaryStorageInterceptor {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
|
||||
public void extractLargeBinariesBeforeCreate(ServletRequestDetails theRequestDetails, IBaseResource theResource, Pointcut thePoincut) throws IOException {
|
||||
extractLargeBinaries(theRequestDetails, theResource, thePoincut);
|
||||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
|
||||
public void extractLargeBinariesBeforeUpdate(ServletRequestDetails theRequestDetails, IBaseResource theResource, Pointcut thePoincut) throws IOException {
|
||||
extractLargeBinaries(theRequestDetails, theResource, thePoincut);
|
||||
}
|
||||
|
||||
private void extractLargeBinaries(ServletRequestDetails theRequestDetails, IBaseResource theResource, Pointcut thePoincut) throws IOException {
|
||||
IIdType resourceId = theResource.getIdElement();
|
||||
if (!resourceId.hasResourceType() && resourceId.hasIdPart()) {
|
||||
String resourceType = myCtx.getResourceDefinition(theResource).getName();
|
||||
resourceId = new IdType(resourceType + "/" + resourceId.getIdPart());
|
||||
}
|
||||
|
||||
List<IBinaryTarget> attachments = recursivelyScanResourceForBinaryData(theResource);
|
||||
for (IBinaryTarget nextTarget : attachments) {
|
||||
byte[] data = nextTarget.getData();
|
||||
if (data != null && data.length > 0) {
|
||||
|
||||
long nextPayloadLength = data.length;
|
||||
String nextContentType = nextTarget.getContentType();
|
||||
boolean shouldStoreBlob = myBinaryStorageSvc.shouldStoreBlob(nextPayloadLength, resourceId, nextContentType);
|
||||
if (shouldStoreBlob) {
|
||||
|
||||
String newBlobId;
|
||||
if (resourceId.hasIdPart()) {
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
|
||||
StoredDetails storedDetails = myBinaryStorageSvc.storeBlob(resourceId, null, nextContentType, inputStream);
|
||||
newBlobId = storedDetails.getBlobId();
|
||||
} else {
|
||||
assert thePoincut == Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED : thePoincut.name();
|
||||
newBlobId = myBinaryStorageSvc.newBlobId();
|
||||
List<DeferredBinaryTarget> deferredBinaryTargets = getOrCreateDeferredBinaryStorageMap(theRequestDetails);
|
||||
DeferredBinaryTarget newDeferredBinaryTarget = new DeferredBinaryTarget(newBlobId, nextTarget, data);
|
||||
deferredBinaryTargets.add(newDeferredBinaryTarget);
|
||||
}
|
||||
|
||||
myBinaryAccessProvider.replaceDataWithExtension(nextTarget, newBlobId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<DeferredBinaryTarget> getOrCreateDeferredBinaryStorageMap(ServletRequestDetails theRequestDetails) {
|
||||
List<DeferredBinaryTarget> deferredBinaryTargets = (List<DeferredBinaryTarget>) theRequestDetails.getUserData().get(getDeferredListKey());
|
||||
if (deferredBinaryTargets == null) {
|
||||
deferredBinaryTargets = new ArrayList<>();
|
||||
theRequestDetails.getUserData().put(getDeferredListKey(), deferredBinaryTargets);
|
||||
}
|
||||
return deferredBinaryTargets;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
|
||||
public void storeLargeBinariesBeforeCreatePersistence(ServletRequestDetails theRequestDetails, IBaseResource theResource, Pointcut thePoincut) throws IOException {
|
||||
List<DeferredBinaryTarget> deferredBinaryTargets = (List<DeferredBinaryTarget>) theRequestDetails.getUserData().get(getDeferredListKey());
|
||||
if (deferredBinaryTargets != null) {
|
||||
IIdType resourceId = theResource.getIdElement();
|
||||
for (DeferredBinaryTarget next : deferredBinaryTargets) {
|
||||
String blobId = next.getBlobId();
|
||||
IBinaryTarget target = next.getBinaryTarget();
|
||||
InputStream dataStream = next.getDataStream();
|
||||
String contentType = target.getContentType();
|
||||
myBinaryStorageSvc.storeBlob(resourceId, blobId, contentType, dataStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getDeferredListKey() {
|
||||
return myDeferredListKey;
|
||||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRESHOW_RESOURCES)
|
||||
public void preShow(IPreResourceShowDetails theDetails) throws IOException {
|
||||
long unmarshalledByteCount = 0;
|
||||
|
||||
for (IBaseResource nextResource : theDetails) {
|
||||
|
||||
IIdType resourceId = nextResource.getIdElement();
|
||||
List<IBinaryTarget> attachments = recursivelyScanResourceForBinaryData(nextResource);
|
||||
|
||||
for (IBinaryTarget nextTarget : attachments) {
|
||||
Optional<String> attachmentId = nextTarget.getAttachmentId();
|
||||
if (attachmentId.isPresent()) {
|
||||
|
||||
StoredDetails blobDetails = myBinaryStorageSvc.fetchBlobDetails(resourceId, attachmentId.get());
|
||||
if (blobDetails == null) {
|
||||
String msg = myCtx.getLocalizer().getMessage(BinaryAccessProvider.class, "unknownBlobId");
|
||||
throw new InvalidRequestException(msg);
|
||||
}
|
||||
|
||||
if ((unmarshalledByteCount + blobDetails.getBytes()) < myAutoDeExternalizeMaximumBytes) {
|
||||
|
||||
byte[] bytes = myBinaryStorageSvc.fetchBlob(resourceId, attachmentId.get());
|
||||
nextTarget.setData(bytes);
|
||||
unmarshalledByteCount += blobDetails.getBytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private List<IBinaryTarget> recursivelyScanResourceForBinaryData(IBaseResource theResource) {
|
||||
List<IBinaryTarget> binaryTargets = new ArrayList<>();
|
||||
myCtx.newTerser().visit(theResource, new IModelVisitor2() {
|
||||
@Override
|
||||
public boolean acceptElement(IBase theElement, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
|
||||
|
||||
if (theElement.getClass().equals(myBinaryType)) {
|
||||
IBase parent = theContainingElementPath.get(theContainingElementPath.size() - 2);
|
||||
Optional<IBinaryTarget> binaryTarget = myBinaryAccessProvider.toBinaryTarget(parent);
|
||||
if (binaryTarget.isPresent()) {
|
||||
binaryTargets.add(binaryTarget.get());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return binaryTargets;
|
||||
}
|
||||
|
||||
private static class DeferredBinaryTarget {
|
||||
private final String myBlobId;
|
||||
private final IBinaryTarget myBinaryTarget;
|
||||
private final InputStream myDataStream;
|
||||
|
||||
private DeferredBinaryTarget(String theBlobId, IBinaryTarget theBinaryTarget, byte[] theData) {
|
||||
myBlobId = theBlobId;
|
||||
myBinaryTarget = theBinaryTarget;
|
||||
myDataStream = new ByteArrayInputStream(theData);
|
||||
}
|
||||
|
||||
String getBlobId() {
|
||||
return myBlobId;
|
||||
}
|
||||
|
||||
IBinaryTarget getBinaryTarget() {
|
||||
return myBinaryTarget;
|
||||
}
|
||||
|
||||
InputStream getDataStream() {
|
||||
return myDataStream;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.binstore;
|
|||
|
||||
import ca.uhn.fhir.jpa.dao.data.IBinaryStorageEntityDao;
|
||||
import ca.uhn.fhir.jpa.model.entity.BinaryStorageEntity;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import com.google.common.hash.HashingInputStream;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.io.input.CountingInputStream;
|
||||
|
@ -57,13 +58,13 @@ public class DatabaseBlobBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
|
|||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.SUPPORTS)
|
||||
public StoredDetails storeBlob(IIdType theResourceId, String theContentType, InputStream theInputStream) {
|
||||
public StoredDetails storeBlob(IIdType theResourceId, String theBlobIdOrNull, String theContentType, InputStream theInputStream) {
|
||||
Date publishedDate = new Date();
|
||||
|
||||
HashingInputStream hashingInputStream = createHashingInputStream(theInputStream);
|
||||
CountingInputStream countingInputStream = createCountingInputStream(hashingInputStream);
|
||||
|
||||
String id = newRandomId();
|
||||
String id = super.provideIdForNewBlob(theBlobIdOrNull);
|
||||
|
||||
BinaryStorageEntity entity = new BinaryStorageEntity();
|
||||
entity.setResourceId(theResourceId.toUnqualifiedVersionless().getValue());
|
||||
|
@ -80,7 +81,7 @@ public class DatabaseBlobBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
|
|||
|
||||
TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
|
||||
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
txTemplate.execute(t->{
|
||||
txTemplate.execute(t -> {
|
||||
myEntityManager.persist(entity);
|
||||
return null;
|
||||
});
|
||||
|
@ -88,7 +89,7 @@ public class DatabaseBlobBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
|
|||
// Update the entity with the final byte count and hash
|
||||
long bytes = countingInputStream.getCount();
|
||||
String hash = hashingInputStream.hash().toString();
|
||||
txTemplate.execute(t-> {
|
||||
txTemplate.execute(t -> {
|
||||
myBinaryStorageEntityDao.setSize(id, (int) bytes);
|
||||
myBinaryStorageEntityDao.setHash(id, hash);
|
||||
return null;
|
||||
|
@ -126,12 +127,7 @@ public class DatabaseBlobBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
|
|||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
InputStream inputStream = entityOpt.get().getBlob().getBinaryStream();
|
||||
IOUtils.copy(inputStream, theOutputStream);
|
||||
} catch (SQLException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
copyBlobToOutputStream(theOutputStream, entityOpt.get());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -141,4 +137,30 @@ public class DatabaseBlobBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
|
|||
Optional<BinaryStorageEntity> entityOpt = myBinaryStorageEntityDao.findByIdAndResourceId(theBlobId, theResourceId.toUnqualifiedVersionless().getValue());
|
||||
entityOpt.ifPresent(theBinaryStorageEntity -> myBinaryStorageEntityDao.delete(theBinaryStorageEntity));
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] fetchBlob(IIdType theResourceId, String theBlobId) throws IOException {
|
||||
BinaryStorageEntity entityOpt = myBinaryStorageEntityDao
|
||||
.findByIdAndResourceId(theBlobId, theResourceId.toUnqualifiedVersionless().getValue())
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Unknown blob ID: " + theBlobId + " for resource ID " + theResourceId));
|
||||
|
||||
return copyBlobToByteArray(entityOpt);
|
||||
}
|
||||
|
||||
void copyBlobToOutputStream(OutputStream theOutputStream, BinaryStorageEntity theEntity) throws IOException {
|
||||
try (InputStream inputStream = theEntity.getBlob().getBinaryStream()) {
|
||||
IOUtils.copy(inputStream, theOutputStream);
|
||||
} catch (SQLException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] copyBlobToByteArray(BinaryStorageEntity theEntity) throws IOException {
|
||||
int size = theEntity.getSize();
|
||||
try {
|
||||
return IOUtils.toByteArray(theEntity.getBlob().getBinaryStream(), size);
|
||||
} catch (SQLException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.binstore;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
@ -31,6 +32,7 @@ import org.apache.commons.io.IOUtils;
|
|||
import org.apache.commons.io.input.CountingInputStream;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -63,8 +65,8 @@ public class FilesystemBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
|
|||
}
|
||||
|
||||
@Override
|
||||
public StoredDetails storeBlob(IIdType theResourceId, String theContentType, InputStream theInputStream) throws IOException {
|
||||
String id = newRandomId();
|
||||
public StoredDetails storeBlob(IIdType theResourceId, String theBlobIdOrNull, String theContentType, InputStream theInputStream) throws IOException {
|
||||
String id = super.provideIdForNewBlob(theBlobIdOrNull);
|
||||
File storagePath = getStoragePath(id, true);
|
||||
|
||||
// Write binary file
|
||||
|
@ -111,17 +113,31 @@ public class FilesystemBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
|
|||
|
||||
@Override
|
||||
public boolean writeBlob(IIdType theResourceId, String theBlobId, OutputStream theOutputStream) throws IOException {
|
||||
InputStream inputStream = getInputStream(theResourceId, theBlobId);
|
||||
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
IOUtils.copy(inputStream, theOutputStream);
|
||||
theOutputStream.close();
|
||||
} finally {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private InputStream getInputStream(IIdType theResourceId, String theBlobId) throws FileNotFoundException {
|
||||
File storagePath = getStoragePath(theBlobId, false);
|
||||
InputStream inputStream = null;
|
||||
if (storagePath != null) {
|
||||
File file = getStorageFilename(storagePath, theResourceId, theBlobId);
|
||||
if (file.exists()) {
|
||||
try (InputStream inputStream = new FileInputStream(file)) {
|
||||
IOUtils.copy(inputStream, theOutputStream);
|
||||
theOutputStream.close();
|
||||
}
|
||||
inputStream = new FileInputStream(file);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -139,6 +155,20 @@ public class FilesystemBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] fetchBlob(IIdType theResourceId, String theBlobId) throws IOException {
|
||||
StoredDetails details = fetchBlobDetails(theResourceId, theBlobId);
|
||||
try (InputStream inputStream = getInputStream(theResourceId, theBlobId)) {
|
||||
|
||||
if (inputStream != null) {
|
||||
return IOUtils.toByteArray(inputStream, details.getBytes());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
throw new ResourceNotFoundException("Unknown blob ID: " + theBlobId + " for resource ID " + theResourceId);
|
||||
}
|
||||
|
||||
private void delete(File theStorageFile, String theBlobId) {
|
||||
Validate.isTrue(theStorageFile.delete(), "Failed to delete file for blob %s", theBlobId);
|
||||
}
|
||||
|
@ -164,7 +194,7 @@ public class FilesystemBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
|
|||
private File getStoragePath(String theId, boolean theCreate) {
|
||||
File path = myBasePath;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
path = new File(path, theId.substring(i, i+1));
|
||||
path = new File(path, theId.substring(i, i + 1));
|
||||
if (!path.exists()) {
|
||||
if (theCreate) {
|
||||
mkdir(path);
|
||||
|
|
|
@ -20,22 +20,12 @@ package ca.uhn.fhir.jpa.binstore;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.util.JsonDateDeserializer;
|
||||
import ca.uhn.fhir.jpa.util.JsonDateSerializer;
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.google.common.hash.HashingInputStream;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
|
||||
public interface IBinaryStorageSvc {
|
||||
|
||||
|
@ -77,15 +67,22 @@ public interface IBinaryStorageSvc {
|
|||
*/
|
||||
boolean shouldStoreBlob(long theSize, IIdType theResourceId, String theContentType);
|
||||
|
||||
/**
|
||||
* Generate a new blob ID that will be passed to {@link #storeBlob(IIdType, String, String, InputStream)} later
|
||||
*/
|
||||
String newBlobId();
|
||||
|
||||
/**
|
||||
* Store a new binary blob
|
||||
*
|
||||
* @param theResourceId The resource ID that owns this blob. Note that it should not be possible to retrieve a blob without both the resource ID and the blob ID being correct.
|
||||
* @param theContentType The content type to associate with this blob
|
||||
* @param theInputStream An InputStream to read from. This method should close the stream when it has been fully consumed.
|
||||
* @param theResourceId The resource ID that owns this blob. Note that it should not be possible to retrieve a blob without both the resource ID and the blob ID being correct.
|
||||
* @param theBlobIdOrNull If set, forces
|
||||
* @param theContentType The content type to associate with this blob
|
||||
* @param theInputStream An InputStream to read from. This method should close the stream when it has been fully consumed.
|
||||
* @return Returns details about the stored data
|
||||
*/
|
||||
StoredDetails storeBlob(IIdType theResourceId, String theContentType, InputStream theInputStream) throws IOException;
|
||||
@Nonnull
|
||||
StoredDetails storeBlob(IIdType theResourceId, String theBlobIdOrNull, String theContentType, InputStream theInputStream) throws IOException;
|
||||
|
||||
StoredDetails fetchBlobDetails(IIdType theResourceId, String theBlobId) throws IOException;
|
||||
|
||||
|
@ -96,100 +93,12 @@ public interface IBinaryStorageSvc {
|
|||
|
||||
void expungeBlob(IIdType theResourceId, String theBlobId);
|
||||
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
class StoredDetails {
|
||||
|
||||
@JsonProperty("blobId")
|
||||
private String myBlobId;
|
||||
@JsonProperty("bytes")
|
||||
private long myBytes;
|
||||
@JsonProperty("contentType")
|
||||
private String myContentType;
|
||||
@JsonProperty("hash")
|
||||
private String myHash;
|
||||
@JsonProperty("published")
|
||||
@JsonSerialize(using = JsonDateSerializer.class)
|
||||
@JsonDeserialize(using = JsonDateDeserializer.class)
|
||||
private Date myPublished;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public StoredDetails() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public StoredDetails(@Nonnull String theBlobId, long theBytes, @Nonnull String theContentType, HashingInputStream theIs, Date thePublished) {
|
||||
myBlobId = theBlobId;
|
||||
myBytes = theBytes;
|
||||
myContentType = theContentType;
|
||||
myHash = theIs.hash().toString();
|
||||
myPublished = thePublished;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("blobId", myBlobId)
|
||||
.append("bytes", myBytes)
|
||||
.append("contentType", myContentType)
|
||||
.append("hash", myHash)
|
||||
.append("published", myPublished)
|
||||
.toString();
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return myHash;
|
||||
}
|
||||
|
||||
public StoredDetails setHash(String theHash) {
|
||||
myHash = theHash;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Date getPublished() {
|
||||
return myPublished;
|
||||
}
|
||||
|
||||
public StoredDetails setPublished(Date thePublished) {
|
||||
myPublished = thePublished;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getContentType() {
|
||||
return myContentType;
|
||||
}
|
||||
|
||||
public StoredDetails setContentType(String theContentType) {
|
||||
myContentType = theContentType;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getBlobId() {
|
||||
return myBlobId;
|
||||
}
|
||||
|
||||
public StoredDetails setBlobId(String theBlobId) {
|
||||
myBlobId = theBlobId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public long getBytes() {
|
||||
return myBytes;
|
||||
}
|
||||
|
||||
public StoredDetails setBytes(long theBytes) {
|
||||
myBytes = theBytes;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Fetch the contents of the given blob
|
||||
*
|
||||
* @param theResourceId The resource ID
|
||||
* @param theBlobId The blob ID
|
||||
* @return The payload as a byte array
|
||||
*/
|
||||
byte[] fetchBlob(IIdType theResourceId, String theBlobId) throws IOException;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package ca.uhn.fhir.jpa.binstore;
|
||||
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
/**
|
||||
* Wraps an Attachment datatype or Binary resource, since they both
|
||||
* hold binary content but don't look entirely similar
|
||||
*/
|
||||
interface IBinaryTarget {
|
||||
|
||||
void setSize(Integer theSize);
|
||||
|
||||
String getContentType();
|
||||
|
||||
void setContentType(String theContentType);
|
||||
|
||||
byte[] getData();
|
||||
|
||||
void setData(byte[] theBytes);
|
||||
|
||||
IBaseHasExtensions getTarget();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
default Optional<String> getAttachmentId() {
|
||||
return getTarget()
|
||||
.getExtension()
|
||||
.stream()
|
||||
.filter(t -> JpaConstants.EXT_EXTERNALIZED_BINARY_ID.equals(t.getUrl()))
|
||||
.filter(t -> t.getValue() instanceof IPrimitiveType)
|
||||
.map(t -> (IPrimitiveType<String>) t.getValue())
|
||||
.map(t -> t.getValue())
|
||||
.filter(t -> isNotBlank(t))
|
||||
.findFirst();
|
||||
}
|
||||
}
|
|
@ -49,8 +49,8 @@ public class MemoryBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl impleme
|
|||
}
|
||||
|
||||
@Override
|
||||
public StoredDetails storeBlob(IIdType theResourceId, String theContentType, InputStream theInputStream) throws IOException {
|
||||
String id = newRandomId();
|
||||
public StoredDetails storeBlob(IIdType theResourceId, String theBlobIdOrNull, String theContentType, InputStream theInputStream) throws IOException {
|
||||
String id = super.provideIdForNewBlob(theBlobIdOrNull);
|
||||
String key = toKey(theResourceId, id);
|
||||
|
||||
HashingInputStream hashingIs = createHashingInputStream(theInputStream);
|
||||
|
@ -88,8 +88,18 @@ public class MemoryBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl impleme
|
|||
myDetailsMap.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] fetchBlob(IIdType theResourceId, String theBlobId) {
|
||||
String key = toKey(theResourceId, theBlobId);
|
||||
return myDataMap.get(key);
|
||||
}
|
||||
|
||||
private String toKey(IIdType theResourceId, String theBlobId) {
|
||||
return theBlobId + '-' + theResourceId.toUnqualifiedVersionless().getValue();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
myDetailsMap.clear();
|
||||
myDataMap.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,12 @@ public class NullBinaryStorageSvcImpl implements IBinaryStorageSvc {
|
|||
}
|
||||
|
||||
@Override
|
||||
public StoredDetails storeBlob(IIdType theResourceId, String theContentType, InputStream theInputStream) {
|
||||
public String newBlobId() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredDetails storeBlob(IIdType theResourceId, String theBlobIdOrNull, String theContentType, InputStream theInputStream) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
@ -71,4 +76,9 @@ public class NullBinaryStorageSvcImpl implements IBinaryStorageSvc {
|
|||
public void expungeBlob(IIdType theIdElement, String theBlobId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] fetchBlob(IIdType theResourceId, String theBlobId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
package ca.uhn.fhir.jpa.binstore;
|
||||
|
||||
import ca.uhn.fhir.jpa.util.JsonDateDeserializer;
|
||||
import ca.uhn.fhir.jpa.util.JsonDateSerializer;
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.google.common.hash.HashingInputStream;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Date;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
public class StoredDetails {
|
||||
|
||||
@JsonProperty("blobId")
|
||||
private String myBlobId;
|
||||
@JsonProperty("bytes")
|
||||
private long myBytes;
|
||||
@JsonProperty("contentType")
|
||||
private String myContentType;
|
||||
@JsonProperty("hash")
|
||||
private String myHash;
|
||||
@JsonProperty("published")
|
||||
@JsonSerialize(using = JsonDateSerializer.class)
|
||||
@JsonDeserialize(using = JsonDateDeserializer.class)
|
||||
private Date myPublished;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public StoredDetails() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public StoredDetails(@Nonnull String theBlobId, long theBytes, @Nonnull String theContentType, HashingInputStream theIs, Date thePublished) {
|
||||
myBlobId = theBlobId;
|
||||
myBytes = theBytes;
|
||||
myContentType = theContentType;
|
||||
myHash = theIs.hash().toString();
|
||||
myPublished = thePublished;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("blobId", myBlobId)
|
||||
.append("bytes", myBytes)
|
||||
.append("contentType", myContentType)
|
||||
.append("hash", myHash)
|
||||
.append("published", myPublished)
|
||||
.toString();
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return myHash;
|
||||
}
|
||||
|
||||
public StoredDetails setHash(String theHash) {
|
||||
myHash = theHash;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Date getPublished() {
|
||||
return myPublished;
|
||||
}
|
||||
|
||||
public StoredDetails setPublished(Date thePublished) {
|
||||
myPublished = thePublished;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getContentType() {
|
||||
return myContentType;
|
||||
}
|
||||
|
||||
public StoredDetails setContentType(String theContentType) {
|
||||
myContentType = theContentType;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getBlobId() {
|
||||
return myBlobId;
|
||||
}
|
||||
|
||||
public StoredDetails setBlobId(String theBlobId) {
|
||||
myBlobId = theBlobId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public long getBytes() {
|
||||
return myBytes;
|
||||
}
|
||||
|
||||
public StoredDetails setBytes(long theBytes) {
|
||||
myBytes = theBytes;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
|
@ -1352,6 +1352,16 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
return update(theResource, theMatchUrl, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) {
|
||||
return update(theResource, theMatchUrl, true, theRequestDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) {
|
||||
return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequest) {
|
||||
if (theResource == null) {
|
||||
|
@ -1448,16 +1458,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
return outcome;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) {
|
||||
return update(theResource, theMatchUrl, true, theRequestDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) {
|
||||
return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodOutcome validate(T theResource, IIdType theId, String theRawResource, EncodingEnum theEncoding, ValidationModeEnum theMode, String theProfile, RequestDetails theRequest) {
|
||||
if (theRequest != null) {
|
||||
|
|
|
@ -251,6 +251,11 @@ public class TermConcept implements Serializable {
|
|||
return myId;
|
||||
}
|
||||
|
||||
public TermConcept setId(Long theId) {
|
||||
myId = theId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Long getIndexStatus() {
|
||||
return myIndexStatus;
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
|
|||
@Autowired
|
||||
private ITermVersionAdapterSvc myTerminologyVersionAdapterSvc;
|
||||
@Autowired
|
||||
private ITermCodeSystemStorageSvc myConceptStorageSvc;
|
||||
private ITermCodeSystemStorageSvc myCodeSystemStorageSvc;
|
||||
|
||||
@Override
|
||||
public void addConceptToStorageQueue(TermConcept theConcept) {
|
||||
|
@ -122,7 +122,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
|
|||
ourLog.info("Saving {} deferred concepts...", count);
|
||||
while (codeCount < count && myDeferredConcepts.size() > 0) {
|
||||
TermConcept next = myDeferredConcepts.remove(0);
|
||||
codeCount += myConceptStorageSvc.saveConcept(next);
|
||||
codeCount += myCodeSystemStorageSvc.saveConcept(next);
|
||||
}
|
||||
|
||||
if (codeCount > 0) {
|
||||
|
@ -192,29 +192,29 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
|
|||
return;
|
||||
}
|
||||
|
||||
TransactionTemplate tt = new TransactionTemplate(myTransactionMgr);
|
||||
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
if (isDeferredConceptsOrConceptLinksToSaveLater()) {
|
||||
tt.execute(t -> {
|
||||
processDeferredConcepts();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
TransactionTemplate tt = new TransactionTemplate(myTransactionMgr);
|
||||
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
if (isDeferredConceptsOrConceptLinksToSaveLater()) {
|
||||
tt.execute(t -> {
|
||||
processDeferredConcepts();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
if (isDeferredValueSets()) {
|
||||
tt.execute(t -> {
|
||||
processDeferredValueSets();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
if (isDeferredConceptMaps()) {
|
||||
tt.execute(t -> {
|
||||
processDeferredConceptMaps();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
if (isDeferredValueSets()) {
|
||||
tt.execute(t -> {
|
||||
processDeferredValueSets();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
if (isDeferredConceptMaps()) {
|
||||
tt.execute(t -> {
|
||||
processDeferredConceptMaps();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -269,6 +269,26 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
|
|||
mySchedulerService.scheduleFixedDelay(SCHEDULE_INTERVAL_MILLIS, false, jobDefinition);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setTransactionManagerForUnitTest(PlatformTransactionManager theTxManager) {
|
||||
myTransactionMgr = theTxManager;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setDaoConfigForUnitTest(DaoConfig theDaoConfig) {
|
||||
myDaoConfig = theDaoConfig;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setCodeSystemStorageSvcForUnitTest(ITermCodeSystemStorageSvc theCodeSystemStorageSvc) {
|
||||
myCodeSystemStorageSvc = theCodeSystemStorageSvc;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setConceptDaoForUnitTest(ITermConceptDao theConceptDao) {
|
||||
myConceptDao = theConceptDao;
|
||||
}
|
||||
|
||||
public static class SaveDeferredJob extends FireAtIntervalJob {
|
||||
|
||||
@Autowired
|
||||
|
|
|
@ -14,7 +14,7 @@ public class BaseBinaryStorageSvcImplTest {
|
|||
@Test
|
||||
public void testNewRandomId() {
|
||||
MemoryBinaryStorageSvcImpl svc = new MemoryBinaryStorageSvcImpl();
|
||||
String id = svc.newRandomId();
|
||||
String id = svc.newBlobId();
|
||||
ourLog.info(id);
|
||||
assertThat(id, matchesPattern("^[a-zA-Z0-9]{100}$"));
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package ca.uhn.fhir.jpa.binstore;
|
||||
|
||||
import ca.uhn.fhir.jpa.config.TestR4Config;
|
||||
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
|
||||
import ca.uhn.fhir.jpa.model.entity.BinaryStorageEntity;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -10,15 +11,18 @@ import org.springframework.context.annotation.Bean;
|
|||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.sql.Blob;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.Matchers.matchesPattern;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ContextConfiguration(classes = DatabaseBlobBinaryStorageSvcImplTest.MyConfig.class)
|
||||
public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
|
||||
|
@ -39,7 +43,7 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
|
|||
ByteArrayInputStream inputStream = new ByteArrayInputStream(SOME_BYTES);
|
||||
String contentType = "image/png";
|
||||
IdType resourceId = new IdType("Binary/123");
|
||||
IBinaryStorageSvc.StoredDetails outcome = mySvc.storeBlob(resourceId, contentType, inputStream);
|
||||
StoredDetails outcome = mySvc.storeBlob(resourceId, null, contentType, inputStream);
|
||||
|
||||
myCaptureQueriesListener.logAllQueriesForCurrentThread();
|
||||
|
||||
|
@ -56,7 +60,7 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
|
|||
* Read back the details
|
||||
*/
|
||||
|
||||
IBinaryStorageSvc.StoredDetails details = mySvc.fetchBlobDetails(resourceId, outcome.getBlobId());
|
||||
StoredDetails details = mySvc.fetchBlobDetails(resourceId, outcome.getBlobId());
|
||||
assertEquals(16L, details.getBytes());
|
||||
assertEquals(outcome.getBlobId(), details.getBlobId());
|
||||
assertEquals("image/png", details.getContentType());
|
||||
|
@ -71,10 +75,70 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
|
|||
mySvc.writeBlob(resourceId, outcome.getBlobId(), capture);
|
||||
|
||||
assertArrayEquals(SOME_BYTES, capture.toByteArray());
|
||||
|
||||
|
||||
assertArrayEquals(SOME_BYTES, mySvc.fetchBlob(resourceId, outcome.getBlobId()));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testStoreAndRetrieveWithManualId() throws IOException {
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
/*
|
||||
* Store the binary
|
||||
*/
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(SOME_BYTES);
|
||||
String contentType = "image/png";
|
||||
IdType resourceId = new IdType("Binary/123");
|
||||
StoredDetails outcome = mySvc.storeBlob(resourceId, "ABCDEFG", contentType, inputStream);
|
||||
assertEquals("ABCDEFG", outcome.getBlobId());
|
||||
|
||||
myCaptureQueriesListener.logAllQueriesForCurrentThread();
|
||||
|
||||
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size());
|
||||
assertEquals(1, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size());
|
||||
assertEquals(2, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size());
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
assertEquals(16, outcome.getBytes());
|
||||
|
||||
/*
|
||||
* Read back the details
|
||||
*/
|
||||
|
||||
StoredDetails details = mySvc.fetchBlobDetails(resourceId, outcome.getBlobId());
|
||||
assertEquals(16L, details.getBytes());
|
||||
assertEquals(outcome.getBlobId(), details.getBlobId());
|
||||
assertEquals("image/png", details.getContentType());
|
||||
assertEquals("dc7197cfab936698bef7818975c185a9b88b71a0a0a2493deea487706ddf20cb", details.getHash());
|
||||
assertNotNull(details.getPublished());
|
||||
|
||||
/*
|
||||
* Read back the contents
|
||||
*/
|
||||
|
||||
ByteArrayOutputStream capture = new ByteArrayOutputStream();
|
||||
mySvc.writeBlob(resourceId, outcome.getBlobId(), capture);
|
||||
|
||||
assertArrayEquals(SOME_BYTES, capture.toByteArray());
|
||||
assertArrayEquals(SOME_BYTES, mySvc.fetchBlob(resourceId, outcome.getBlobId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFetchBlobUnknown() throws IOException {
|
||||
try {
|
||||
mySvc.fetchBlob(new IdType("Patient/123"), "1111111");
|
||||
fail();
|
||||
} catch (ResourceNotFoundException e) {
|
||||
assertEquals("Unknown blob ID: 1111111 for resource ID Patient/123", e.getMessage());
|
||||
}
|
||||
|
||||
StoredDetails details = mySvc.fetchBlobDetails(new IdType("Patient/123"), "1111111");
|
||||
assertNull(details);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testExpunge() throws IOException {
|
||||
|
||||
|
@ -84,12 +148,12 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
|
|||
ByteArrayInputStream inputStream = new ByteArrayInputStream(SOME_BYTES);
|
||||
String contentType = "image/png";
|
||||
IdType resourceId = new IdType("Binary/123");
|
||||
IBinaryStorageSvc.StoredDetails outcome = mySvc.storeBlob(resourceId, contentType, inputStream);
|
||||
StoredDetails outcome = mySvc.storeBlob(resourceId, null, contentType, inputStream);
|
||||
String blobId = outcome.getBlobId();
|
||||
|
||||
// Expunge
|
||||
mySvc.expungeBlob(resourceId, blobId);
|
||||
|
||||
|
||||
ByteArrayOutputStream capture = new ByteArrayOutputStream();
|
||||
assertFalse(mySvc.writeBlob(resourceId, outcome.getBlobId(), capture));
|
||||
assertEquals(0, capture.size());
|
||||
|
@ -106,7 +170,7 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
|
|||
ByteArrayInputStream inputStream = new ByteArrayInputStream(SOME_BYTES);
|
||||
String contentType = "image/png";
|
||||
IdType resourceId = new IdType("Binary/123");
|
||||
IBinaryStorageSvc.StoredDetails outcome = mySvc.storeBlob(resourceId, contentType, inputStream);
|
||||
StoredDetails outcome = mySvc.storeBlob(resourceId, null, contentType, inputStream);
|
||||
|
||||
// Right ID
|
||||
ByteArrayOutputStream capture = new ByteArrayOutputStream();
|
||||
|
@ -120,6 +184,40 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyBlobToOutputStream_Exception() throws SQLException {
|
||||
DatabaseBlobBinaryStorageSvcImpl svc = new DatabaseBlobBinaryStorageSvcImpl();
|
||||
|
||||
BinaryStorageEntity mockInput = new BinaryStorageEntity();
|
||||
Blob blob = mock(Blob.class);
|
||||
when(blob.getBinaryStream()).thenThrow(new SQLException("FOO"));
|
||||
mockInput.setBlob(blob);
|
||||
|
||||
try {
|
||||
svc.copyBlobToOutputStream(new ByteArrayOutputStream(), (mockInput));
|
||||
fail();
|
||||
} catch (IOException e) {
|
||||
assertThat(e.getMessage(), containsString("FOO"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyBlobToByteArray_Exception() throws SQLException {
|
||||
DatabaseBlobBinaryStorageSvcImpl svc = new DatabaseBlobBinaryStorageSvcImpl();
|
||||
|
||||
BinaryStorageEntity mockInput = new BinaryStorageEntity();
|
||||
Blob blob = mock(Blob.class);
|
||||
when(blob.getBinaryStream()).thenThrow(new SQLException("FOO"));
|
||||
mockInput.setBlob(blob);
|
||||
|
||||
try {
|
||||
svc.copyBlobToByteArray(mockInput);
|
||||
fail();
|
||||
} catch (IOException e) {
|
||||
assertThat(e.getMessage(), containsString("FOO"));
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
public static class MyConfig {
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package ca.uhn.fhir.jpa.binstore;
|
||||
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
|
@ -21,7 +20,7 @@ import static org.junit.Assert.*;
|
|||
|
||||
public class FilesystemBinaryStorageSvcImplTest {
|
||||
|
||||
public static final byte[] SOME_BYTES = {2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1};
|
||||
private static final byte[] SOME_BYTES = {2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1};
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(FilesystemBinaryStorageSvcImplTest.class);
|
||||
private File myPath;
|
||||
private FilesystemBinaryStorageSvcImpl mySvc;
|
||||
|
@ -41,11 +40,11 @@ public class FilesystemBinaryStorageSvcImplTest {
|
|||
public void testStoreAndRetrieve() throws IOException {
|
||||
IIdType id = new IdType("Patient/123");
|
||||
String contentType = "image/png";
|
||||
IBinaryStorageSvc.StoredDetails outcome = mySvc.storeBlob(id, contentType, new ByteArrayInputStream(SOME_BYTES));
|
||||
StoredDetails outcome = mySvc.storeBlob(id, null, contentType, new ByteArrayInputStream(SOME_BYTES));
|
||||
|
||||
ourLog.info("Got id: {}", outcome);
|
||||
|
||||
IBinaryStorageSvc.StoredDetails details = mySvc.fetchBlobDetails(id, outcome.getBlobId());
|
||||
StoredDetails details = mySvc.fetchBlobDetails(id, outcome.getBlobId());
|
||||
assertEquals(16L, details.getBytes());
|
||||
assertEquals(outcome.getBlobId(), details.getBlobId());
|
||||
assertEquals("image/png", details.getContentType());
|
||||
|
@ -56,6 +55,42 @@ public class FilesystemBinaryStorageSvcImplTest {
|
|||
mySvc.writeBlob(id, outcome.getBlobId(), capture);
|
||||
|
||||
assertArrayEquals(SOME_BYTES, capture.toByteArray());
|
||||
assertArrayEquals(SOME_BYTES, mySvc.fetchBlob(id, outcome.getBlobId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStoreAndRetrieveManualId() throws IOException {
|
||||
IIdType id = new IdType("Patient/123");
|
||||
String contentType = "image/png";
|
||||
String blobId = "ABCDEFGHIJKLMNOPQRSTUV";
|
||||
StoredDetails outcome = mySvc.storeBlob(id, blobId, contentType, new ByteArrayInputStream(SOME_BYTES));
|
||||
assertEquals(blobId, outcome.getBlobId());
|
||||
|
||||
ourLog.info("Got id: {}", outcome);
|
||||
|
||||
StoredDetails details = mySvc.fetchBlobDetails(id, outcome.getBlobId());
|
||||
assertEquals(16L, details.getBytes());
|
||||
assertEquals(outcome.getBlobId(), details.getBlobId());
|
||||
assertEquals("image/png", details.getContentType());
|
||||
assertEquals("dc7197cfab936698bef7818975c185a9b88b71a0a0a2493deea487706ddf20cb", details.getHash());
|
||||
assertNotNull(details.getPublished());
|
||||
|
||||
ByteArrayOutputStream capture = new ByteArrayOutputStream();
|
||||
mySvc.writeBlob(id, outcome.getBlobId(), capture);
|
||||
|
||||
assertArrayEquals(SOME_BYTES, capture.toByteArray());
|
||||
assertArrayEquals(SOME_BYTES, mySvc.fetchBlob(id, outcome.getBlobId()));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testFetchBlobUnknown() throws IOException {
|
||||
try {
|
||||
mySvc.fetchBlob(new IdType("Patient/123"), "1111111");
|
||||
fail();
|
||||
} catch (ResourceNotFoundException e) {
|
||||
assertEquals("Unknown blob ID: 1111111 for resource ID Patient/123", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -63,11 +98,11 @@ public class FilesystemBinaryStorageSvcImplTest {
|
|||
public void testExpunge() throws IOException {
|
||||
IIdType id = new IdType("Patient/123");
|
||||
String contentType = "image/png";
|
||||
IBinaryStorageSvc.StoredDetails outcome = mySvc.storeBlob(id, contentType, new ByteArrayInputStream(SOME_BYTES));
|
||||
StoredDetails outcome = mySvc.storeBlob(id, null, contentType, new ByteArrayInputStream(SOME_BYTES));
|
||||
|
||||
ourLog.info("Got id: {}", outcome);
|
||||
|
||||
IBinaryStorageSvc.StoredDetails details = mySvc.fetchBlobDetails(id, outcome.getBlobId());
|
||||
StoredDetails details = mySvc.fetchBlobDetails(id, outcome.getBlobId());
|
||||
assertEquals(16L, details.getBytes());
|
||||
assertEquals(outcome.getBlobId(), details.getBlobId());
|
||||
assertEquals("image/png", details.getContentType());
|
||||
|
@ -89,7 +124,7 @@ public class FilesystemBinaryStorageSvcImplTest {
|
|||
IIdType id = new IdType("Patient/123");
|
||||
String contentType = "image/png";
|
||||
try {
|
||||
mySvc.storeBlob(id, contentType, new ByteArrayInputStream(SOME_BYTES));
|
||||
mySvc.storeBlob(id, null, contentType, new ByteArrayInputStream(SOME_BYTES));
|
||||
fail();
|
||||
} catch (PayloadTooLargeException e) {
|
||||
assertEquals("Binary size exceeds maximum: 5", e.getMessage());
|
||||
|
|
|
@ -16,7 +16,7 @@ public class NullBinaryStorageSvcImplTest {
|
|||
|
||||
@Test(expected = UnsupportedOperationException.class)
|
||||
public void storeBlob() {
|
||||
mySvc.storeBlob(null, null, null);
|
||||
mySvc.storeBlob(null, null, null, null);
|
||||
}
|
||||
|
||||
@Test(expected = UnsupportedOperationException.class)
|
||||
|
@ -34,4 +34,13 @@ public class NullBinaryStorageSvcImplTest {
|
|||
mySvc.expungeBlob(null, null);
|
||||
}
|
||||
|
||||
@Test(expected = UnsupportedOperationException.class)
|
||||
public void fetchBlob() {
|
||||
mySvc.fetchBlob(null, null);
|
||||
}
|
||||
|
||||
@Test(expected = UnsupportedOperationException.class)
|
||||
public void newBlobId() {
|
||||
mySvc.newBlobId();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package ca.uhn.fhir.jpa.config;
|
||||
|
||||
import ca.uhn.fhir.i18n.HapiLocalizer;
|
||||
import ca.uhn.fhir.jpa.model.entity.ForcedId;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
|
||||
import org.hibernate.HibernateException;
|
||||
import org.hibernate.PersistentObjectException;
|
||||
import org.hibernate.StaleStateException;
|
||||
import org.hibernate.exception.ConstraintViolationException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class HapiFhirHibernateJpaDialectTest {
|
||||
|
||||
private HapiFhirHibernateJpaDialect mySvc;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
mySvc = new HapiFhirHibernateJpaDialect(new HapiLocalizer());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertHibernateAccessException() {
|
||||
DataAccessException outcome = mySvc.convertHibernateAccessException(new ConstraintViolationException("this is a message", new SQLException("reason"), "IDX_FOO"));
|
||||
assertThat(outcome.getMessage(), containsString("this is a message"));
|
||||
|
||||
try {
|
||||
mySvc.convertHibernateAccessException(new ConstraintViolationException("this is a message", new SQLException("reason"), ForcedId.IDX_FORCEDID_TYPE_FID));
|
||||
fail();
|
||||
} catch (ResourceVersionConflictException e) {
|
||||
assertThat(e.getMessage(), containsString("The operation has failed with a client-assigned ID constraint failure"));
|
||||
}
|
||||
|
||||
try {
|
||||
outcome = mySvc.convertHibernateAccessException(new StaleStateException("this is a message"));
|
||||
fail();
|
||||
} catch (ResourceVersionConflictException e) {
|
||||
assertThat(e.getMessage(), containsString("The operation has failed with a version constraint failure"));
|
||||
}
|
||||
|
||||
outcome = mySvc.convertHibernateAccessException(new HibernateException("this is a message"));
|
||||
assertThat(outcome.getMessage(), containsString("HibernateException: this is a message"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTranslate() {
|
||||
RuntimeException outcome = mySvc.translate(new PersistentObjectException("FOO"), "message");
|
||||
assertEquals("FOO", outcome.getMessage());
|
||||
|
||||
try {
|
||||
PersistenceException exception = new PersistenceException("a message", new ConstraintViolationException("this is a message", new SQLException("reason"), ForcedId.IDX_FORCEDID_TYPE_FID));
|
||||
mySvc.translate(exception, "a message");
|
||||
fail();
|
||||
} catch (ResourceVersionConflictException e) {
|
||||
assertThat(e.getMessage(), containsString("The operation has failed with a client-assigned ID constraint failure"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor;
|
||||
import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc;
|
||||
import ca.uhn.fhir.jpa.binstore.MemoryBinaryStorageSvcImpl;
|
||||
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.dao.DaoMethodOutcome;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Binary;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class BinaryStorageInterceptorR4Test extends BaseResourceProviderR4Test {
|
||||
|
||||
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};
|
||||
public static final byte[] SOME_BYTES_2 = {6, 7, 8, 7, 6, 5, 4, 3, 2, 1, 5, 5, 5, 6};
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(BinaryStorageInterceptorR4Test.class);
|
||||
|
||||
@Autowired
|
||||
private MemoryBinaryStorageSvcImpl myStorageSvc;
|
||||
@Autowired
|
||||
private IBinaryStorageSvc myBinaryStorageSvc;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
super.before();
|
||||
myStorageSvc.setMinimumBinarySize(10);
|
||||
myDaoConfig.setExpungeEnabled(true);
|
||||
myInterceptorRegistry.registerInterceptor(myBinaryStorageInterceptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
@After
|
||||
public void after() throws Exception {
|
||||
super.after();
|
||||
myStorageSvc.setMinimumBinarySize(0);
|
||||
myDaoConfig.setExpungeEnabled(new DaoConfig().isExpungeEnabled());
|
||||
myBinaryStorageInterceptor.setAutoDeExternalizeMaximumBytes(new BinaryStorageInterceptor().getAutoDeExternalizeMaximumBytes());
|
||||
|
||||
MemoryBinaryStorageSvcImpl binaryStorageSvc = (MemoryBinaryStorageSvcImpl) myBinaryStorageSvc;
|
||||
binaryStorageSvc.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateAndRetrieveBinary_ServerAssignedId_ExternalizedBinary() {
|
||||
|
||||
// Create a resource with a big enough binary
|
||||
Binary binary = new Binary();
|
||||
binary.setContentType("application/octet-stream");
|
||||
binary.setData(SOME_BYTES);
|
||||
DaoMethodOutcome outcome = myBinaryDao.create(binary, mySrd);
|
||||
|
||||
// Make sure it was externalized
|
||||
IIdType id = outcome.getId().toUnqualifiedVersionless();
|
||||
String encoded = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome.getResource());
|
||||
ourLog.info("Encoded: {}", encoded);
|
||||
assertThat(encoded, containsString(JpaConstants.EXT_EXTERNALIZED_BINARY_ID));
|
||||
assertThat(encoded, not(containsString("\"data\"")));
|
||||
|
||||
// Now read it back and make sure it was de-externalized
|
||||
Binary output = myBinaryDao.read(id, mySrd);
|
||||
assertEquals("application/octet-stream", output.getContentType());
|
||||
assertArrayEquals(SOME_BYTES, output.getData());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateAndRetrieveBinary_ServerAssignedId_NonExternalizedBinary() {
|
||||
|
||||
// Create a resource with a small binary
|
||||
Binary binary = new Binary();
|
||||
binary.setContentType("application/octet-stream");
|
||||
binary.setData(FEW_BYTES);
|
||||
DaoMethodOutcome outcome = myBinaryDao.create(binary, mySrd);
|
||||
|
||||
// Make sure it was externalized
|
||||
IIdType id = outcome.getId().toUnqualifiedVersionless();
|
||||
String encoded = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome.getResource());
|
||||
ourLog.info("Encoded: {}", encoded);
|
||||
assertThat(encoded, containsString("\"data\": \"BAMCAQ==\""));
|
||||
assertThat(encoded, not(containsString(JpaConstants.EXT_EXTERNALIZED_BINARY_ID)));
|
||||
|
||||
// Now read it back and make sure it was de-externalized
|
||||
Binary output = myBinaryDao.read(id, mySrd);
|
||||
assertEquals("application/octet-stream", output.getContentType());
|
||||
assertArrayEquals(FEW_BYTES, output.getData());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateAndRetrieveBinary_ClientAssignedId_ExternalizedBinary() {
|
||||
|
||||
// Create a resource with a big enough binary
|
||||
Binary binary = new Binary();
|
||||
binary.setId("FOO");
|
||||
binary.setContentType("application/octet-stream");
|
||||
binary.setData(SOME_BYTES);
|
||||
DaoMethodOutcome outcome = myBinaryDao.update(binary, mySrd);
|
||||
|
||||
// Make sure it was externalized
|
||||
IIdType id = outcome.getId().toUnqualifiedVersionless();
|
||||
String encoded = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome.getResource());
|
||||
ourLog.info("Encoded: {}", encoded);
|
||||
assertThat(encoded, containsString(JpaConstants.EXT_EXTERNALIZED_BINARY_ID));
|
||||
assertThat(encoded, not(containsString("\"data\"")));
|
||||
|
||||
// Now read it back and make sure it was de-externalized
|
||||
Binary output = myBinaryDao.read(id, mySrd);
|
||||
assertEquals("application/octet-stream", output.getContentType());
|
||||
assertArrayEquals(SOME_BYTES, output.getData());
|
||||
assertNotNull(output.getDataElement().getExtensionByUrl(JpaConstants.EXT_EXTERNALIZED_BINARY_ID).getValue());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpdateAndRetrieveBinary_ServerAssignedId_ExternalizedBinary() {
|
||||
|
||||
// Create a resource with a big enough binary
|
||||
Binary binary = new Binary();
|
||||
binary.setContentType("application/octet-stream");
|
||||
binary.setData(SOME_BYTES);
|
||||
DaoMethodOutcome outcome = myBinaryDao.create(binary, mySrd);
|
||||
|
||||
// Make sure it was externalized
|
||||
IIdType id = outcome.getId().toUnqualifiedVersionless();
|
||||
String encoded = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome.getResource());
|
||||
ourLog.info("Encoded: {}", encoded);
|
||||
assertThat(encoded, containsString(JpaConstants.EXT_EXTERNALIZED_BINARY_ID));
|
||||
assertThat(encoded, not(containsString("\"data\"")));
|
||||
|
||||
// Now update
|
||||
binary = new Binary();
|
||||
binary.setId(id.toUnqualifiedVersionless());
|
||||
binary.setContentType("application/octet-stream");
|
||||
binary.setData(SOME_BYTES_2);
|
||||
outcome = myBinaryDao.update(binary, mySrd);
|
||||
assertEquals("2", outcome.getId().getVersionIdPart());
|
||||
|
||||
// Now read it back the first version
|
||||
Binary output = myBinaryDao.read(id.withVersion("1"), mySrd);
|
||||
assertEquals("application/octet-stream", output.getContentType());
|
||||
assertArrayEquals(SOME_BYTES, output.getData());
|
||||
|
||||
// Now read back the second version
|
||||
output = myBinaryDao.read(id.withVersion("2"), mySrd);
|
||||
assertEquals("application/octet-stream", output.getContentType());
|
||||
assertArrayEquals(SOME_BYTES_2, output.getData());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testRetrieveBinaryAboveRetrievalThreshold() {
|
||||
myBinaryStorageInterceptor.setAutoDeExternalizeMaximumBytes(5);
|
||||
|
||||
// Create a resource with a big enough binary
|
||||
Binary binary = new Binary();
|
||||
binary.setContentType("application/octet-stream");
|
||||
binary.setData(SOME_BYTES);
|
||||
DaoMethodOutcome outcome = myBinaryDao.create(binary, mySrd);
|
||||
|
||||
// Make sure it was externalized
|
||||
IIdType id = outcome.getId().toUnqualifiedVersionless();
|
||||
String encoded = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome.getResource());
|
||||
ourLog.info("Encoded: {}", encoded);
|
||||
assertThat(encoded, containsString(JpaConstants.EXT_EXTERNALIZED_BINARY_ID));
|
||||
assertThat(encoded, not(containsString("\"data\"")));
|
||||
|
||||
// Now read it back and make sure it was de-externalized
|
||||
Binary output = myBinaryDao.read(id, mySrd);
|
||||
assertEquals("application/octet-stream", output.getContentType());
|
||||
assertEquals(null, output.getData());
|
||||
assertNotNull(output.getDataElement().getExtensionByUrl(JpaConstants.EXT_EXTERNALIZED_BINARY_ID).getValue());
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package ca.uhn.fhir.jpa.term;
|
||||
|
||||
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.dao.data.ITermConceptDao;
|
||||
import ca.uhn.fhir.jpa.entity.TermConcept;
|
||||
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink;
|
||||
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class TermDeferredStorageSvcImplTest {
|
||||
|
||||
|
||||
@Mock
|
||||
private PlatformTransactionManager myTxManager;
|
||||
@Mock
|
||||
private ITermCodeSystemStorageSvc myTermConceptStorageSvc;
|
||||
@Mock
|
||||
private ITermConceptDao myConceptDao;
|
||||
|
||||
@Test
|
||||
public void testSaveDeferredWithExecutionSuspended() {
|
||||
TermDeferredStorageSvcImpl svc = new TermDeferredStorageSvcImpl();
|
||||
svc.setProcessDeferred(false);
|
||||
svc.saveDeferred();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSaveDeferred_Concept() {
|
||||
TermConcept concept = new TermConcept();
|
||||
concept.setCode("CODE_A");
|
||||
|
||||
TermDeferredStorageSvcImpl svc = new TermDeferredStorageSvcImpl();
|
||||
svc.setTransactionManagerForUnitTest(myTxManager);
|
||||
svc.setCodeSystemStorageSvcForUnitTest(myTermConceptStorageSvc);
|
||||
svc.setDaoConfigForUnitTest(new DaoConfig());
|
||||
svc.setProcessDeferred(true);
|
||||
svc.addConceptToStorageQueue(concept);
|
||||
svc.saveDeferred();
|
||||
|
||||
verify(myTermConceptStorageSvc, times(1)).saveConcept(same(concept));
|
||||
verifyNoMoreInteractions(myTermConceptStorageSvc);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveDeferred_ConceptParentChildLink_ConceptsMissing() {
|
||||
TermConceptParentChildLink conceptLink = new TermConceptParentChildLink();
|
||||
conceptLink.setChild(new TermConcept().setId(111L));
|
||||
conceptLink.setParent(new TermConcept().setId(222L));
|
||||
|
||||
TermDeferredStorageSvcImpl svc = new TermDeferredStorageSvcImpl();
|
||||
svc.setTransactionManagerForUnitTest(myTxManager);
|
||||
svc.setCodeSystemStorageSvcForUnitTest(myTermConceptStorageSvc);
|
||||
svc.setConceptDaoForUnitTest(myConceptDao);
|
||||
svc.setDaoConfigForUnitTest(new DaoConfig());
|
||||
svc.setProcessDeferred(true);
|
||||
svc.addConceptLinkToStorageQueue(conceptLink);
|
||||
svc.saveDeferred();
|
||||
|
||||
verifyNoMoreInteractions(myTermConceptStorageSvc);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,16 +1,22 @@
|
|||
package ca.uhn.fhir.jpa.searchparam.registry;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class BaseSearchParamRegistryTest {
|
||||
|
@ -19,6 +25,13 @@ public class BaseSearchParamRegistryTest {
|
|||
private ISchedulerService mySchedulerService;
|
||||
@Mock
|
||||
private ISearchParamProvider mySearchParamProvider;
|
||||
private int myAnswerCount = 0;
|
||||
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
myAnswerCount = 0;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefreshAfterExpiry() {
|
||||
|
@ -44,7 +57,7 @@ public class BaseSearchParamRegistryTest {
|
|||
SearchParamRegistryR4 registry = new SearchParamRegistryR4();
|
||||
|
||||
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider());
|
||||
when(mySearchParamProvider.refreshCache(any(), anyLong())).thenAnswer(t->{
|
||||
when(mySearchParamProvider.refreshCache(any(), anyLong())).thenAnswer(t -> {
|
||||
registry.doRefresh(t.getArgument(1, Long.class));
|
||||
return 0;
|
||||
});
|
||||
|
@ -59,4 +72,32 @@ public class BaseSearchParamRegistryTest {
|
|||
assertFalse(registry.refreshCacheIfNecessary());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetActiveUniqueSearchParams_Empty() {
|
||||
SearchParamRegistryR4 registry = new SearchParamRegistryR4();
|
||||
assertThat(registry.getActiveUniqueSearchParams("Patient"), Matchers.empty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetActiveSearchParams() {
|
||||
SearchParamRegistryR4 registry = new SearchParamRegistryR4();
|
||||
registry.setFhirContextForUnitTest(FhirContext.forR4());
|
||||
registry.postConstruct();
|
||||
|
||||
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider());
|
||||
when(mySearchParamProvider.refreshCache(any(), anyLong())).thenAnswer(t -> {
|
||||
if (myAnswerCount == 0) {
|
||||
myAnswerCount++;
|
||||
throw new InternalErrorException("this is an error!");
|
||||
}
|
||||
|
||||
registry.doRefresh(0);
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
registry.setSearchParamProviderForUnitTest(mySearchParamProvider);
|
||||
Map<String, RuntimeSearchParam> outcome = registry.getActiveSearchParams("Patient");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -61,6 +61,14 @@
|
|||
<code>upload-terminology</code> command has been modified to support this new functionality.
|
||||
]]>
|
||||
</action>
|
||||
<action type="fix">
|
||||
<![CDATA[
|
||||
<b>New Feature</b>:
|
||||
When using Externalized Binary Storage in the JPA server, the system will now automatically
|
||||
externalize Binary and Attachment payloads, meaning that these will automatically not be
|
||||
stored in the RDBMS.
|
||||
]]>
|
||||
</action>
|
||||
<action type="add" issue="1489">
|
||||
<![CDATA[
|
||||
<b>Performance Improvement</b>:
|
||||
|
|
Loading…
Reference in New Issue