001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.binary.svc; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.HookParams; 025import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 026import ca.uhn.fhir.interceptor.api.Pointcut; 027import ca.uhn.fhir.jpa.binary.api.IBinaryStorageSvc; 028import ca.uhn.fhir.jpa.util.RandomTextUtils; 029import ca.uhn.fhir.rest.api.server.RequestDetails; 030import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 031import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException; 032import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 033import ca.uhn.fhir.util.BinaryUtil; 034import ca.uhn.fhir.util.HapiExtensions; 035import com.google.common.annotations.VisibleForTesting; 036import com.google.common.hash.HashFunction; 037import com.google.common.hash.Hashing; 038import com.google.common.hash.HashingInputStream; 039import com.google.common.io.ByteStreams; 040import jakarta.annotation.Nonnull; 041import jakarta.annotation.Nullable; 042import org.apache.commons.io.input.CountingInputStream; 043import org.apache.commons.lang3.StringUtils; 044import org.apache.commons.lang3.Validate; 045import org.hl7.fhir.instance.model.api.IBaseBinary; 046import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 047import org.hl7.fhir.instance.model.api.IBaseResource; 048import org.hl7.fhir.instance.model.api.IIdType; 049import org.hl7.fhir.instance.model.api.IPrimitiveType; 050import org.springframework.beans.factory.annotation.Autowired; 051 052import java.io.IOException; 053import java.io.InputStream; 054import java.util.Optional; 055 056import static org.apache.commons.lang3.StringUtils.isNotBlank; 057 058public abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc { 059 public static long DEFAULT_MAXIMUM_BINARY_SIZE = Long.MAX_VALUE - 1; 060 public static String BLOB_ID_PREFIX_APPLIED = "blob-id-prefix-applied"; 061 062 private final int ID_LENGTH = 100; 063 private long myMaximumBinarySize = DEFAULT_MAXIMUM_BINARY_SIZE; 064 private int myMinimumBinarySize; 065 066 @Autowired 067 private FhirContext myFhirContext; 068 069 @Autowired 070 private IInterceptorBroadcaster myInterceptorBroadcaster; 071 072 public BaseBinaryStorageSvcImpl() { 073 super(); 074 } 075 076 @Override 077 public long getMaximumBinarySize() { 078 return myMaximumBinarySize; 079 } 080 081 @Override 082 public void setMaximumBinarySize(long theMaximumBinarySize) { 083 Validate.inclusiveBetween(1, DEFAULT_MAXIMUM_BINARY_SIZE, theMaximumBinarySize); 084 myMaximumBinarySize = theMaximumBinarySize; 085 } 086 087 @Override 088 public int getMinimumBinarySize() { 089 return myMinimumBinarySize; 090 } 091 092 @Override 093 public void setMinimumBinarySize(int theMinimumBinarySize) { 094 myMinimumBinarySize = theMinimumBinarySize; 095 } 096 097 @Override 098 public String newBlobId() { 099 return RandomTextUtils.newSecureRandomAlphaNumericString(ID_LENGTH); 100 } 101 102 /** 103 * Default implementation is to return true for any Blob ID. 104 */ 105 @Override 106 public boolean isValidBlobId(String theNewBlobId) { 107 return true; 108 } 109 110 @Override 111 public boolean shouldStoreBlob(long theSize, IIdType theResourceId, String theContentType) { 112 return theSize >= getMinimumBinarySize(); 113 } 114 115 @SuppressWarnings("UnstableApiUsage") 116 @Nonnull 117 protected HashingInputStream createHashingInputStream(InputStream theInputStream) { 118 HashFunction hash = Hashing.sha256(); 119 return new HashingInputStream(hash, theInputStream); 120 } 121 122 @Nonnull 123 protected CountingInputStream createCountingInputStream(InputStream theInputStream) { 124 InputStream is = ByteStreams.limit(theInputStream, getMaximumBinarySize() + 1L); 125 return new CountingInputStream(is) { 126 @Override 127 public long getByteCount() { 128 long retVal = super.getByteCount(); 129 if (retVal > getMaximumBinarySize()) { 130 throw new PayloadTooLargeException( 131 Msg.code(1343) + "Binary size exceeds maximum: " + getMaximumBinarySize()); 132 } 133 return retVal; 134 } 135 }; 136 } 137 138 @Deprecated( 139 since = 140 "6.6.0 - Maintained for interface backwards compatibility. Note that invokes interceptor pointcut with empty parameters", 141 forRemoval = true) 142 protected String provideIdForNewBlob(String theBlobIdOrNull) { 143 return isNotBlank(theBlobIdOrNull) ? theBlobIdOrNull : newBlobId(); 144 } 145 146 @Nonnull 147 protected String provideIdForNewBlob( 148 String theBlobIdOrNull, byte[] theBytes, RequestDetails theRequestDetails, String theContentType) { 149 String blobId = isNotBlank(theBlobIdOrNull) ? theBlobIdOrNull : newBlobId(); 150 151 // make sure another pointcut didn't already apply a prefix to the blobId 152 if (isBlobIdPrefixApplied(theRequestDetails)) { 153 return blobId; 154 } 155 156 String blobPrefixFromHooksOrNull = callBlobIdPointcut(theBytes, theRequestDetails, theContentType); 157 String blobIdPrefixFromHooks = blobPrefixFromHooksOrNull == null ? "" : blobPrefixFromHooksOrNull; 158 return blobIdPrefixFromHooks + blobId; 159 } 160 161 protected boolean isBlobIdPrefixApplied(RequestDetails theRequestDetails) { 162 return theRequestDetails.getUserData().get(BLOB_ID_PREFIX_APPLIED) == Boolean.TRUE; 163 } 164 165 public static void setBlobIdPrefixApplied(RequestDetails theRequestDetails) { 166 theRequestDetails.getUserData().put(BLOB_ID_PREFIX_APPLIED, true); 167 } 168 169 /** 170 * Invokes STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX pointcut if present 171 * @return null if pointcut is not present 172 */ 173 @Nullable 174 private String callBlobIdPointcut(byte[] theBytes, RequestDetails theRequestDetails, String theContentType) { 175 // Interceptor call: STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX 176 IBaseBinary binary = 177 BinaryUtil.newBinary(myFhirContext).setContent(theBytes).setContentType(theContentType); 178 179 HookParams hookParams = 180 new HookParams().add(RequestDetails.class, theRequestDetails).add(IBaseResource.class, binary); 181 182 setBlobIdPrefixApplied(theRequestDetails); 183 184 return (String) CompositeInterceptorBroadcaster.doCallHooksAndReturnObject( 185 myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX, hookParams); 186 } 187 188 @Override 189 public byte[] fetchDataBlobFromBinary(IBaseBinary theBaseBinary) throws IOException { 190 IPrimitiveType<byte[]> dataElement = BinaryUtil.getOrCreateData(myFhirContext, theBaseBinary); 191 byte[] value = dataElement.getValue(); 192 if (value == null) { 193 Optional<String> attachmentId = getAttachmentId((IBaseHasExtensions) dataElement); 194 if (attachmentId.isPresent()) { 195 value = fetchBlob(theBaseBinary.getIdElement(), attachmentId.get()); 196 } else { 197 throw new InternalErrorException( 198 Msg.code(1344) + "Unable to load binary blob data for " + theBaseBinary.getIdElement()); 199 } 200 } 201 return value; 202 } 203 204 @SuppressWarnings("unchecked") 205 private Optional<String> getAttachmentId(IBaseHasExtensions theBaseBinary) { 206 return theBaseBinary.getExtension().stream() 207 .filter(t -> HapiExtensions.EXT_EXTERNALIZED_BINARY_ID.equals(t.getUrl())) 208 .filter(t -> t.getValue() instanceof IPrimitiveType) 209 .map(t -> (IPrimitiveType<String>) t.getValue()) 210 .map(IPrimitiveType::getValue) 211 .filter(StringUtils::isNotBlank) 212 .findFirst(); 213 } 214 215 @VisibleForTesting 216 public void setInterceptorBroadcasterForTests(IInterceptorBroadcaster theInterceptorBroadcaster) { 217 myInterceptorBroadcaster = theInterceptorBroadcaster; 218 } 219 220 @VisibleForTesting 221 public void setFhirContextForTests(FhirContext theFhirContext) { 222 myFhirContext = theFhirContext; 223 } 224}