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}