
001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2023 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.rest.api.server.RequestDetails; 029import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 030import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException; 031import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 032import ca.uhn.fhir.util.BinaryUtil; 033import ca.uhn.fhir.util.HapiExtensions; 034import com.google.common.annotations.VisibleForTesting; 035import com.google.common.hash.HashFunction; 036import com.google.common.hash.Hashing; 037import com.google.common.hash.HashingInputStream; 038import com.google.common.io.ByteStreams; 039import org.apache.commons.io.input.CountingInputStream; 040import org.apache.commons.lang3.StringUtils; 041import org.apache.commons.lang3.Validate; 042import org.hl7.fhir.instance.model.api.IBaseBinary; 043import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 044import org.hl7.fhir.instance.model.api.IBaseResource; 045import org.hl7.fhir.instance.model.api.IIdType; 046import org.hl7.fhir.instance.model.api.IPrimitiveType; 047import org.springframework.beans.factory.annotation.Autowired; 048 049import javax.annotation.Nonnull; 050import javax.annotation.Nullable; 051import java.io.IOException; 052import java.io.InputStream; 053import java.security.SecureRandom; 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 SecureRandom myRandom; 063 private final String CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 064 private final int ID_LENGTH = 100; 065 private long myMaximumBinarySize = DEFAULT_MAXIMUM_BINARY_SIZE; 066 private int myMinimumBinarySize; 067 068 @Autowired 069 private FhirContext myFhirContext; 070 @Autowired 071 private IInterceptorBroadcaster myInterceptorBroadcaster; 072 073 074 public BaseBinaryStorageSvcImpl() { 075 myRandom = new SecureRandom(); 076 } 077 078 @Override 079 public long getMaximumBinarySize() { 080 return myMaximumBinarySize; 081 } 082 083 @Override 084 public void setMaximumBinarySize(long theMaximumBinarySize) { 085 Validate.inclusiveBetween(1, DEFAULT_MAXIMUM_BINARY_SIZE, theMaximumBinarySize); 086 myMaximumBinarySize = theMaximumBinarySize; 087 } 088 089 @Override 090 public int getMinimumBinarySize() { 091 return myMinimumBinarySize; 092 } 093 094 @Override 095 public void setMinimumBinarySize(int theMinimumBinarySize) { 096 myMinimumBinarySize = theMinimumBinarySize; 097 } 098 099 @Override 100 public String newBlobId() { 101 StringBuilder b = new StringBuilder(); 102 for (int i = 0; i < ID_LENGTH; i++) { 103 int nextInt = Math.abs(myRandom.nextInt()); 104 b.append(CHARS.charAt(nextInt % CHARS.length())); 105 } 106 return b.toString(); 107 } 108 109 /** 110 * Default implementation is to return true for any Blob ID. 111 */ 112 @Override 113 public boolean isValidBlobId(String theNewBlobId) { 114 return true; 115 } 116 117 @Override 118 public boolean shouldStoreBlob(long theSize, IIdType theResourceId, String theContentType) { 119 return theSize >= getMinimumBinarySize(); 120 } 121 122 @SuppressWarnings("UnstableApiUsage") 123 @Nonnull 124 protected HashingInputStream createHashingInputStream(InputStream theInputStream) { 125 HashFunction hash = Hashing.sha256(); 126 return new HashingInputStream(hash, theInputStream); 127 } 128 129 @Nonnull 130 protected CountingInputStream createCountingInputStream(InputStream theInputStream) { 131 InputStream is = ByteStreams.limit(theInputStream, getMaximumBinarySize() + 1L); 132 return new CountingInputStream(is) { 133 @Override 134 public long getByteCount() { 135 long retVal = super.getByteCount(); 136 if (retVal > getMaximumBinarySize()) { 137 throw new PayloadTooLargeException(Msg.code(1343) + "Binary size exceeds maximum: " + getMaximumBinarySize()); 138 } 139 return retVal; 140 } 141 }; 142 } 143 144 @Deprecated(since = "6.6.0 - Maintained for interface backwards compatibility. Note that invokes interceptor pointcut with empty parameters", forRemoval = true) 145 protected String provideIdForNewBlob(String theBlobIdOrNull) { 146 return isNotBlank(theBlobIdOrNull) ? theBlobIdOrNull : newBlobId(); 147 } 148 149 @Nonnull 150 protected String provideIdForNewBlob(String theBlobIdOrNull, byte[] theBytes, RequestDetails theRequestDetails, String theContentType) { 151 String blobId = isNotBlank(theBlobIdOrNull) ? theBlobIdOrNull : newBlobId(); 152 153 // make sure another pointcut didn't already apply a prefix to the blobId 154 if (isBlobIdPrefixApplied(theRequestDetails)) { 155 return blobId; 156 } 157 158 String blobPrefixFromHooksOrNull = callBlobIdPointcut(theBytes, theRequestDetails, theContentType); 159 String blobIdPrefixFromHooks = blobPrefixFromHooksOrNull == null ? "" : blobPrefixFromHooksOrNull; 160 return blobIdPrefixFromHooks + blobId; 161 } 162 163 protected boolean isBlobIdPrefixApplied(RequestDetails theRequestDetails) { 164 return theRequestDetails.getUserData().get(BLOB_ID_PREFIX_APPLIED) == Boolean.TRUE; 165 } 166 167 public static void setBlobIdPrefixApplied(RequestDetails theRequestDetails) { 168 theRequestDetails.getUserData().put(BLOB_ID_PREFIX_APPLIED, true); 169 } 170 171 /** 172 * Invokes STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX pointcut if present 173 * @return null if pointcut is not present 174 */ 175 @Nullable 176 private String callBlobIdPointcut(byte[] theBytes, RequestDetails theRequestDetails, String theContentType) { 177 // Interceptor call: STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX 178 IBaseBinary binary = BinaryUtil.newBinary(myFhirContext) 179 .setContent(theBytes) 180 .setContentType(theContentType); 181 182 HookParams hookParams = new HookParams() 183 .add(RequestDetails.class, theRequestDetails) 184 .add(IBaseResource.class, binary); 185 186 setBlobIdPrefixApplied(theRequestDetails); 187 188 return (String) CompositeInterceptorBroadcaster.doCallHooksAndReturnObject( 189 myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX, hookParams); 190 } 191 192 @Override 193 public byte[] fetchDataBlobFromBinary(IBaseBinary theBaseBinary) throws IOException { 194 IPrimitiveType<byte[]> dataElement = BinaryUtil.getOrCreateData(myFhirContext, theBaseBinary); 195 byte[] value = dataElement.getValue(); 196 if (value == null) { 197 Optional<String> attachmentId = getAttachmentId((IBaseHasExtensions) dataElement); 198 if (attachmentId.isPresent()) { 199 value = fetchBlob(theBaseBinary.getIdElement(), attachmentId.get()); 200 } else { 201 throw new InternalErrorException(Msg.code(1344) + "Unable to load binary blob data for " + theBaseBinary.getIdElement()); 202 } 203 } 204 return value; 205 } 206 207 @SuppressWarnings("unchecked") 208 private Optional<String> getAttachmentId(IBaseHasExtensions theBaseBinary) { 209 return theBaseBinary 210 .getExtension() 211 .stream() 212 .filter(t -> HapiExtensions.EXT_EXTERNALIZED_BINARY_ID.equals(t.getUrl())) 213 .filter(t -> t.getValue() instanceof IPrimitiveType) 214 .map(t -> (IPrimitiveType<String>) t.getValue()) 215 .map(IPrimitiveType::getValue) 216 .filter(StringUtils::isNotBlank) 217 .findFirst(); 218 } 219 220 @VisibleForTesting 221 public void setInterceptorBroadcasterForTests(IInterceptorBroadcaster theInterceptorBroadcaster) { 222 myInterceptorBroadcaster = theInterceptorBroadcaster; 223 } 224 225 @VisibleForTesting 226 public void setFhirContextForTests(FhirContext theFhirContext) { 227 myFhirContext = theFhirContext; 228 } 229}