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}