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 IInterceptorBroadcaster compositeBroadcaster = 178 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); 179 180 // TODO: to be removed when pointcut STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX has exceeded the grace period. 181 // Deprecated in 7.2.0. 182 boolean hasStorageBinaryAssignBlobIdPrefixHooks = 183 compositeBroadcaster.hasHooks(Pointcut.STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX); 184 185 boolean hasStorageBinaryAssignBinaryContentIdPrefixHooks = 186 compositeBroadcaster.hasHooks(Pointcut.STORAGE_BINARY_ASSIGN_BINARY_CONTENT_ID_PREFIX); 187 188 if (!(hasStorageBinaryAssignBlobIdPrefixHooks || hasStorageBinaryAssignBinaryContentIdPrefixHooks)) { 189 return null; 190 } 191 192 IBaseBinary binary = 193 BinaryUtil.newBinary(myFhirContext).setContent(theBytes).setContentType(theContentType); 194 195 HookParams hookParams = 196 new HookParams().add(RequestDetails.class, theRequestDetails).add(IBaseResource.class, binary); 197 198 setBinaryContentIdPrefixApplied(theRequestDetails); 199 200 Pointcut pointcutToInvoke = Pointcut.STORAGE_BINARY_ASSIGN_BINARY_CONTENT_ID_PREFIX; 201 202 // TODO: to be removed when pointcut STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX has exceeded the grace period 203 if (hasStorageBinaryAssignBlobIdPrefixHooks) { 204 pointcutToInvoke = Pointcut.STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX; 205 } 206 207 return (String) compositeBroadcaster.callHooksAndReturnObject(pointcutToInvoke, hookParams); 208 } 209 210 @Override 211 public byte[] fetchDataByteArrayFromBinary(IBaseBinary theBaseBinary) throws IOException { 212 IPrimitiveType<byte[]> dataElement = BinaryUtil.getOrCreateData(myFhirContext, theBaseBinary); 213 byte[] value = dataElement.getValue(); 214 if (value == null) { 215 Optional<String> attachmentId = getAttachmentId((IBaseHasExtensions) dataElement); 216 if (attachmentId.isPresent()) { 217 value = fetchBinaryContent(theBaseBinary.getIdElement(), attachmentId.get()); 218 } else { 219 throw new InternalErrorException( 220 Msg.code(1344) + "Unable to load binary content data for " + theBaseBinary.getIdElement()); 221 } 222 } 223 return value; 224 } 225 226 @SuppressWarnings("unchecked") 227 private Optional<String> getAttachmentId(IBaseHasExtensions theBaseBinary) { 228 return theBaseBinary.getExtension().stream() 229 .filter(t -> HapiExtensions.EXT_EXTERNALIZED_BINARY_ID.equals(t.getUrl())) 230 .filter(t -> t.getValue() instanceof IPrimitiveType) 231 .map(t -> (IPrimitiveType<String>) t.getValue()) 232 .map(IPrimitiveType::getValue) 233 .filter(StringUtils::isNotBlank) 234 .findFirst(); 235 } 236 237 @VisibleForTesting 238 public void setInterceptorBroadcasterForTests(IInterceptorBroadcaster theInterceptorBroadcaster) { 239 myInterceptorBroadcaster = theInterceptorBroadcaster; 240 } 241 242 @VisibleForTesting 243 public void setFhirContextForTests(FhirContext theFhirContext) { 244 myFhirContext = theFhirContext; 245 } 246}