001/*- 002 * #%L 003 * HAPI FHIR JPA Server 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.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.hash.HashingInputStream; 030import com.google.common.io.ByteStreams; 031import org.apache.commons.io.IOUtils; 032import org.apache.commons.io.input.CountingInputStream; 033import org.hibernate.LobHelper; 034import org.hibernate.Session; 035import org.hl7.fhir.instance.model.api.IIdType; 036import org.springframework.beans.factory.annotation.Autowired; 037import org.springframework.transaction.annotation.Propagation; 038import org.springframework.transaction.annotation.Transactional; 039 040import java.io.IOException; 041import java.io.InputStream; 042import java.io.OutputStream; 043import java.sql.Blob; 044import java.sql.SQLException; 045import java.util.Date; 046import java.util.Optional; 047import javax.annotation.Nonnull; 048import javax.persistence.EntityManager; 049import javax.persistence.PersistenceContext; 050import javax.persistence.PersistenceContextType; 051 052@Transactional 053public class DatabaseBlobBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl { 054 055 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 056 private EntityManager myEntityManager; 057 058 @Autowired 059 private IBinaryStorageEntityDao myBinaryStorageEntityDao; 060 061 @Nonnull 062 @Override 063 @Transactional(propagation = Propagation.REQUIRED) 064 public StoredDetails storeBlob( 065 IIdType theResourceId, 066 String theBlobIdOrNull, 067 String theContentType, 068 InputStream theInputStream, 069 RequestDetails theRequestDetails) 070 throws IOException { 071 072 /* 073 * Note on transactionality: This method used to have a propagation value of SUPPORTS and then do the actual 074 * write in a new transaction.. I don't actually get why that was the original design, but it causes 075 * connection pool deadlocks under load! 076 */ 077 078 Date publishedDate = new Date(); 079 080 HashingInputStream hashingInputStream = createHashingInputStream(theInputStream); 081 CountingInputStream countingInputStream = createCountingInputStream(hashingInputStream); 082 083 BinaryStorageEntity entity = new BinaryStorageEntity(); 084 entity.setResourceId(theResourceId.toUnqualifiedVersionless().getValue()); 085 entity.setBlobContentType(theContentType); 086 entity.setPublished(publishedDate); 087 088 Session session = (Session) myEntityManager.getDelegate(); 089 LobHelper lobHelper = session.getLobHelper(); 090 byte[] loadedStream = IOUtils.toByteArray(countingInputStream); 091 String id = super.provideIdForNewBlob(theBlobIdOrNull, loadedStream, theRequestDetails, theContentType); 092 entity.setBlobId(id); 093 Blob dataBlob = lobHelper.createBlob(loadedStream); 094 entity.setBlob(dataBlob); 095 096 // Update the entity with the final byte count and hash 097 long bytes = countingInputStream.getByteCount(); 098 String hash = hashingInputStream.hash().toString(); 099 entity.setSize(bytes); 100 entity.setHash(hash); 101 102 // Save the entity 103 myEntityManager.persist(entity); 104 105 return new StoredDetails() 106 .setBlobId(id) 107 .setBytes(bytes) 108 .setPublished(publishedDate) 109 .setHash(hash) 110 .setContentType(theContentType); 111 } 112 113 @Override 114 public StoredDetails fetchBlobDetails(IIdType theResourceId, String theBlobId) { 115 116 Optional<BinaryStorageEntity> entityOpt = myBinaryStorageEntityDao.findByIdAndResourceId( 117 theBlobId, theResourceId.toUnqualifiedVersionless().getValue()); 118 if (entityOpt.isEmpty()) { 119 return null; 120 } 121 122 BinaryStorageEntity entity = entityOpt.get(); 123 return new StoredDetails() 124 .setBlobId(theBlobId) 125 .setContentType(entity.getBlobContentType()) 126 .setHash(entity.getHash()) 127 .setPublished(entity.getPublished()) 128 .setBytes(entity.getSize()); 129 } 130 131 @Override 132 public boolean writeBlob(IIdType theResourceId, String theBlobId, OutputStream theOutputStream) throws IOException { 133 Optional<BinaryStorageEntity> entityOpt = myBinaryStorageEntityDao.findByIdAndResourceId( 134 theBlobId, theResourceId.toUnqualifiedVersionless().getValue()); 135 if (entityOpt.isEmpty()) { 136 return false; 137 } 138 139 copyBlobToOutputStream(theOutputStream, entityOpt.get()); 140 141 return true; 142 } 143 144 @Override 145 public void expungeBlob(IIdType theResourceId, String theBlobId) { 146 Optional<BinaryStorageEntity> entityOpt = myBinaryStorageEntityDao.findByIdAndResourceId( 147 theBlobId, theResourceId.toUnqualifiedVersionless().getValue()); 148 entityOpt.ifPresent( 149 theBinaryStorageEntity -> myBinaryStorageEntityDao.deleteByPid(theBinaryStorageEntity.getBlobId())); 150 } 151 152 @Override 153 public byte[] fetchBlob(IIdType theResourceId, String theBlobId) throws IOException { 154 BinaryStorageEntity entityOpt = myBinaryStorageEntityDao 155 .findByIdAndResourceId( 156 theBlobId, theResourceId.toUnqualifiedVersionless().getValue()) 157 .orElseThrow(() -> new ResourceNotFoundException( 158 "Unknown blob ID: " + theBlobId + " for resource ID " + theResourceId)); 159 160 return copyBlobToByteArray(entityOpt); 161 } 162 163 void copyBlobToOutputStream(OutputStream theOutputStream, BinaryStorageEntity theEntity) throws IOException { 164 try (InputStream inputStream = theEntity.getBlob().getBinaryStream()) { 165 IOUtils.copy(inputStream, theOutputStream); 166 } catch (SQLException e) { 167 throw new IOException(Msg.code(1341) + e); 168 } 169 } 170 171 byte[] copyBlobToByteArray(BinaryStorageEntity theEntity) throws IOException { 172 try { 173 return ByteStreams.toByteArray(theEntity.getBlob().getBinaryStream()); 174 } catch (SQLException e) { 175 throw new IOException(Msg.code(1342) + e); 176 } 177 } 178}