001/*-
002 * #%L
003 * HAPI FHIR JPA Server
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.binstore;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.jpa.binary.api.StoredDetails;
024import ca.uhn.fhir.jpa.binary.svc.BaseBinaryStorageSvcImpl;
025import ca.uhn.fhir.jpa.dao.data.IBinaryStorageEntityDao;
026import ca.uhn.fhir.jpa.model.entity.BinaryStorageEntity;
027import ca.uhn.fhir.rest.api.server.RequestDetails;
028import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
029import com.google.common.annotations.VisibleForTesting;
030import com.google.common.hash.HashingInputStream;
031import com.google.common.io.ByteStreams;
032import jakarta.annotation.Nonnull;
033import jakarta.persistence.EntityManager;
034import jakarta.persistence.PersistenceContext;
035import jakarta.persistence.PersistenceContextType;
036import org.apache.commons.io.IOUtils;
037import org.apache.commons.io.input.CountingInputStream;
038import org.hibernate.LobHelper;
039import org.hibernate.Session;
040import org.hl7.fhir.instance.model.api.IIdType;
041import org.springframework.beans.factory.annotation.Autowired;
042import org.springframework.transaction.annotation.Propagation;
043import org.springframework.transaction.annotation.Transactional;
044
045import java.io.ByteArrayInputStream;
046import java.io.IOException;
047import java.io.InputStream;
048import java.io.OutputStream;
049import java.sql.Blob;
050import java.sql.SQLException;
051import java.util.Date;
052import java.util.Optional;
053
054@Transactional
055public class DatabaseBinaryContentStorageSvcImpl extends BaseBinaryStorageSvcImpl {
056
057        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
058        private EntityManager myEntityManager;
059
060        @Autowired
061        private IBinaryStorageEntityDao myBinaryStorageEntityDao;
062
063        private boolean mySupportLegacyLobServer = false;
064
065        @Nonnull
066        @Override
067        @Transactional(propagation = Propagation.REQUIRED)
068        public StoredDetails storeBinaryContent(
069                        IIdType theResourceId,
070                        String theBinaryContentIdOrNull,
071                        String theContentType,
072                        InputStream theInputStream,
073                        RequestDetails theRequestDetails)
074                        throws IOException {
075
076                /*
077                 * Note on transactionality: This method used to have a propagation value of SUPPORTS and then do the actual
078                 * write in a new transaction.. I don't actually get why that was the original design, but it causes
079                 * connection pool deadlocks under load!
080                 */
081
082                Date publishedDate = new Date();
083
084                HashingInputStream hashingInputStream = createHashingInputStream(theInputStream);
085                CountingInputStream countingInputStream = createCountingInputStream(hashingInputStream);
086
087                BinaryStorageEntity entity = new BinaryStorageEntity();
088                entity.setResourceId(theResourceId.toUnqualifiedVersionless().getValue());
089                entity.setContentType(theContentType);
090                entity.setPublished(publishedDate);
091
092                Session session = (Session) myEntityManager.getDelegate();
093                LobHelper lobHelper = session.getLobHelper();
094
095                byte[] loadedStream = IOUtils.toByteArray(countingInputStream);
096                String id = super.provideIdForNewBinaryContent(
097                                theBinaryContentIdOrNull, loadedStream, theRequestDetails, theContentType);
098
099                entity.setContentId(id);
100                entity.setStorageContentBin(loadedStream);
101
102                if (mySupportLegacyLobServer) {
103                        Blob dataBlob = lobHelper.createBlob(loadedStream);
104                        entity.setBlob(dataBlob);
105                }
106
107                // Update the entity with the final byte count and hash
108                long bytes = countingInputStream.getByteCount();
109                String hash = hashingInputStream.hash().toString();
110                entity.setSize(bytes);
111                entity.setHash(hash);
112
113                // Save the entity
114                myEntityManager.persist(entity);
115
116                return new StoredDetails()
117                                .setBinaryContentId(id)
118                                .setBytes(bytes)
119                                .setPublished(publishedDate)
120                                .setHash(hash)
121                                .setContentType(theContentType);
122        }
123
124        @Override
125        public StoredDetails fetchBinaryContentDetails(IIdType theResourceId, String theBinaryContentId) {
126
127                Optional<BinaryStorageEntity> entityOpt = myBinaryStorageEntityDao.findByIdAndResourceId(
128                                theBinaryContentId, theResourceId.toUnqualifiedVersionless().getValue());
129                if (entityOpt.isEmpty()) {
130                        return null;
131                }
132
133                BinaryStorageEntity entity = entityOpt.get();
134                return new StoredDetails()
135                                .setBinaryContentId(theBinaryContentId)
136                                .setContentType(entity.getContentType())
137                                .setHash(entity.getHash())
138                                .setPublished(entity.getPublished())
139                                .setBytes(entity.getSize());
140        }
141
142        @Override
143        public boolean writeBinaryContent(IIdType theResourceId, String theBinaryContentId, OutputStream theOutputStream)
144                        throws IOException {
145                Optional<BinaryStorageEntity> entityOpt = myBinaryStorageEntityDao.findByIdAndResourceId(
146                                theBinaryContentId, theResourceId.toUnqualifiedVersionless().getValue());
147                if (entityOpt.isEmpty()) {
148                        return false;
149                }
150
151                copyBinaryContentToOutputStream(theOutputStream, entityOpt.get());
152
153                return true;
154        }
155
156        @Override
157        public void expungeBinaryContent(IIdType theResourceId, String theBinaryContentId) {
158                Optional<BinaryStorageEntity> entityOpt = myBinaryStorageEntityDao.findByIdAndResourceId(
159                                theBinaryContentId, theResourceId.toUnqualifiedVersionless().getValue());
160                entityOpt.ifPresent(
161                                theBinaryStorageEntity -> myBinaryStorageEntityDao.deleteByPid(theBinaryStorageEntity.getContentId()));
162        }
163
164        @Override
165        public byte[] fetchBinaryContent(IIdType theResourceId, String theBinaryContentId) throws IOException {
166                BinaryStorageEntity entityOpt = myBinaryStorageEntityDao
167                                .findByIdAndResourceId(
168                                                theBinaryContentId,
169                                                theResourceId.toUnqualifiedVersionless().getValue())
170                                .orElseThrow(() -> new ResourceNotFoundException(
171                                                "Unknown BinaryContent ID: " + theBinaryContentId + " for resource ID " + theResourceId));
172
173                return copyBinaryContentToByteArray(entityOpt);
174        }
175
176        public DatabaseBinaryContentStorageSvcImpl setSupportLegacyLobServer(boolean theSupportLegacyLobServer) {
177                mySupportLegacyLobServer = theSupportLegacyLobServer;
178                return this;
179        }
180
181        void copyBinaryContentToOutputStream(OutputStream theOutputStream, BinaryStorageEntity theEntity)
182                        throws IOException {
183
184                try (InputStream inputStream = getBinaryContent(theEntity)) {
185                        IOUtils.copy(inputStream, theOutputStream);
186                } catch (SQLException e) {
187                        throw new IOException(Msg.code(1341) + e);
188                }
189        }
190
191        byte[] copyBinaryContentToByteArray(BinaryStorageEntity theEntity) throws IOException {
192                byte[] retVal;
193
194                try (InputStream inputStream = getBinaryContent(theEntity)) {
195                        retVal = ByteStreams.toByteArray(inputStream);
196                } catch (SQLException e) {
197                        throw new IOException(Msg.code(1342) + e);
198                }
199
200                return retVal;
201        }
202
203        /**
204         *
205         * The caller is responsible for closing the returned stream.
206         *
207         * @param theEntity
208         * @return
209         * @throws SQLException
210         */
211        private InputStream getBinaryContent(BinaryStorageEntity theEntity) throws SQLException {
212                InputStream retVal;
213
214                if (theEntity.hasStorageContent()) {
215                        retVal = new ByteArrayInputStream(theEntity.getStorageContentBin());
216                } else if (theEntity.hasBlob()) {
217                        retVal = theEntity.getBlob().getBinaryStream();
218                } else {
219                        retVal = new ByteArrayInputStream(new byte[0]);
220                }
221
222                return retVal;
223        }
224
225        @VisibleForTesting
226        public DatabaseBinaryContentStorageSvcImpl setEntityManagerForTesting(EntityManager theEntityManager) {
227                myEntityManager = theEntityManager;
228                return this;
229        }
230}