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}