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:
James Agnew 2019-10-19 09:01:00 -04:00 committed by GitHub
parent 985cd49892
commit bfbc73caaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1179 additions and 265 deletions

View File

@ -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

View File

@ -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();
}
}

View File

@ -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; }
}

View File

@ -25,6 +25,8 @@ public interface IBaseBinary extends IBaseResource {
byte[] getContent();
IPrimitiveType<byte[]> getContentElement();
String getContentAsBase64();
String getContentType();

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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

View File

@ -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}$"));
}

View File

@ -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 {

View File

@ -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());

View File

@ -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();
}
}

View File

@ -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"));
}
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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>: