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}