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 BINARY_CONTENT_ID_PREFIX_APPLIED = "binary-content-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 newBinaryContentId() { 099 return RandomTextUtils.newSecureRandomAlphaNumericString(ID_LENGTH); 100 } 101 102 /** 103 * Default implementation is to return true for any binary content ID. 104 */ 105 @Override 106 public boolean isValidBinaryContentId(String theNewBinaryContentId) { 107 return true; 108 } 109 110 @Override 111 public boolean shouldStoreBinaryContent(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 provideIdForNewBinaryContent(String theBinaryContentIdOrNull) { 143 return isNotBlank(theBinaryContentIdOrNull) ? theBinaryContentIdOrNull : newBinaryContentId(); 144 } 145 146 @Nonnull 147 protected String provideIdForNewBinaryContent( 148 String theBinaryContentIdOrNull, byte[] theBytes, RequestDetails theRequestDetails, String theContentType) { 149 String binaryContentId = isNotBlank(theBinaryContentIdOrNull) ? theBinaryContentIdOrNull : newBinaryContentId(); 150 151 // make sure another pointcut didn't already apply a prefix to the binaryContentId 152 if (isBinaryContentIdPrefixApplied(theRequestDetails)) { 153 return binaryContentId; 154 } 155 156 String binaryContentIdPrefixFromHooksOrNull = 157 callBinaryContentIdPointcut(theBytes, theRequestDetails, theContentType); 158 String binaryContentIdPrefixFromHooks = StringUtils.defaultString(binaryContentIdPrefixFromHooksOrNull); 159 return binaryContentIdPrefixFromHooks + binaryContentId; 160 } 161 162 protected boolean isBinaryContentIdPrefixApplied(RequestDetails theRequestDetails) { 163 return theRequestDetails.getUserData().get(BINARY_CONTENT_ID_PREFIX_APPLIED) == Boolean.TRUE; 164 } 165 166 public static void setBinaryContentIdPrefixApplied(RequestDetails theRequestDetails) { 167 theRequestDetails.getUserData().put(BINARY_CONTENT_ID_PREFIX_APPLIED, true); 168 } 169 170 /** 171 * This invokes the {@link Pointcut#STORAGE_BINARY_ASSIGN_BINARY_CONTENT_ID_PREFIX} hook and returns the prefix to use for the binary content ID, or null if there are no implementers. 172 * @return A string, which will be used to prefix the binary content ID. May be null. 173 */ 174 @Nullable 175 private String callBinaryContentIdPointcut( 176 byte[] theBytes, RequestDetails theRequestDetails, String theContentType) { 177 // TODO: to be removed when pointcut STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX has exceeded the grace period. 178 // Deprecated in 7.2.0. 179 boolean hasStorageBinaryAssignBlobIdPrefixHooks = CompositeInterceptorBroadcaster.hasHooks( 180 Pointcut.STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX, myInterceptorBroadcaster, theRequestDetails); 181 182 boolean hasStorageBinaryAssignBinaryContentIdPrefixHooks = CompositeInterceptorBroadcaster.hasHooks( 183 Pointcut.STORAGE_BINARY_ASSIGN_BINARY_CONTENT_ID_PREFIX, myInterceptorBroadcaster, theRequestDetails); 184 185 if (!(hasStorageBinaryAssignBlobIdPrefixHooks || hasStorageBinaryAssignBinaryContentIdPrefixHooks)) { 186 return null; 187 } 188 189 IBaseBinary binary = 190 BinaryUtil.newBinary(myFhirContext).setContent(theBytes).setContentType(theContentType); 191 192 HookParams hookParams = 193 new HookParams().add(RequestDetails.class, theRequestDetails).add(IBaseResource.class, binary); 194 195 setBinaryContentIdPrefixApplied(theRequestDetails); 196 197 Pointcut pointcutToInvoke = Pointcut.STORAGE_BINARY_ASSIGN_BINARY_CONTENT_ID_PREFIX; 198 199 // TODO: to be removed when pointcut STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX has exceeded the grace period 200 if (hasStorageBinaryAssignBlobIdPrefixHooks) { 201 pointcutToInvoke = Pointcut.STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX; 202 } 203 204 return (String) CompositeInterceptorBroadcaster.doCallHooksAndReturnObject( 205 myInterceptorBroadcaster, theRequestDetails, pointcutToInvoke, hookParams); 206 } 207 208 @Override 209 public byte[] fetchDataByteArrayFromBinary(IBaseBinary theBaseBinary) throws IOException { 210 IPrimitiveType<byte[]> dataElement = BinaryUtil.getOrCreateData(myFhirContext, theBaseBinary); 211 byte[] value = dataElement.getValue(); 212 if (value == null) { 213 Optional<String> attachmentId = getAttachmentId((IBaseHasExtensions) dataElement); 214 if (attachmentId.isPresent()) { 215 value = fetchBinaryContent(theBaseBinary.getIdElement(), attachmentId.get()); 216 } else { 217 throw new InternalErrorException( 218 Msg.code(1344) + "Unable to load binary content data for " + theBaseBinary.getIdElement()); 219 } 220 } 221 return value; 222 } 223 224 @SuppressWarnings("unchecked") 225 private Optional<String> getAttachmentId(IBaseHasExtensions theBaseBinary) { 226 return theBaseBinary.getExtension().stream() 227 .filter(t -> HapiExtensions.EXT_EXTERNALIZED_BINARY_ID.equals(t.getUrl())) 228 .filter(t -> t.getValue() instanceof IPrimitiveType) 229 .map(t -> (IPrimitiveType<String>) t.getValue()) 230 .map(IPrimitiveType::getValue) 231 .filter(StringUtils::isNotBlank) 232 .findFirst(); 233 } 234 235 @VisibleForTesting 236 public void setInterceptorBroadcasterForTests(IInterceptorBroadcaster theInterceptorBroadcaster) { 237 myInterceptorBroadcaster = theInterceptorBroadcaster; 238 } 239 240 @VisibleForTesting 241 public void setFhirContextForTests(FhirContext theFhirContext) { 242 myFhirContext = theFhirContext; 243 } 244}