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}