001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.kernel.impl.services; 007 008import static java.nio.charset.StandardCharsets.UTF_8; 009import static java.util.Arrays.asList; 010import static org.apache.commons.io.IOUtils.toInputStream; 011import static org.fcrepo.kernel.api.models.ExternalContent.COPY; 012import static org.junit.Assert.assertEquals; 013import static org.junit.Assert.assertNull; 014import static org.mockito.ArgumentMatchers.any; 015import static org.mockito.Mockito.doThrow; 016import static org.mockito.Mockito.verify; 017import static org.mockito.Mockito.when; 018import static org.springframework.test.util.ReflectionTestUtils.setField; 019 020import java.io.File; 021import java.net.URI; 022import java.nio.charset.StandardCharsets; 023import java.nio.file.Files; 024import java.util.Collection; 025 026import org.fcrepo.kernel.api.Transaction; 027import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 028import org.fcrepo.kernel.api.identifiers.FedoraId; 029import org.fcrepo.kernel.api.models.ExternalContent; 030import org.fcrepo.kernel.api.models.ResourceHeaders; 031import org.fcrepo.kernel.api.observer.EventAccumulator; 032import org.fcrepo.kernel.api.operations.NonRdfSourceOperation; 033import org.fcrepo.kernel.api.operations.NonRdfSourceOperationFactory; 034import org.fcrepo.kernel.api.operations.ResourceOperation; 035import org.fcrepo.kernel.impl.TestTransactionHelper; 036import org.fcrepo.kernel.impl.operations.NonRdfSourceOperationFactoryImpl; 037import org.fcrepo.kernel.impl.operations.UpdateNonRdfSourceOperation; 038import org.fcrepo.persistence.api.PersistentStorageSession; 039import org.fcrepo.persistence.api.PersistentStorageSessionManager; 040import org.fcrepo.persistence.api.exceptions.PersistentStorageException; 041import org.fcrepo.search.api.SearchIndex; 042 043import org.apache.commons.io.FileUtils; 044import org.apache.commons.io.IOUtils; 045import org.junit.Before; 046import org.junit.Rule; 047import org.junit.Test; 048import org.junit.rules.TemporaryFolder; 049import org.junit.runner.RunWith; 050import org.mockito.ArgumentCaptor; 051import org.mockito.Captor; 052import org.mockito.InjectMocks; 053import org.mockito.Mock; 054import org.mockito.junit.MockitoJUnitRunner; 055 056/** 057 * @author bbpennel 058 */ 059@RunWith(MockitoJUnitRunner.Silent.class) 060public class ReplaceBinariesServiceImplTest { 061 062 @Rule 063 public TemporaryFolder tempFolder = new TemporaryFolder(); 064 065 private static final String USER_PRINCIPAL = "fedoraUser"; 066 067 private static final FedoraId FEDORA_ID = FedoraId.create("info:fedora/resource1"); 068 069 private static final String TX_ID = "tx-1234"; 070 071 private final String MIME_TYPE = "text/plain"; 072 073 private final String FILENAME = "someFile.txt"; 074 075 private final Long FILESIZE = 123L; 076 077 private final Collection<URI> DIGESTS = asList(URI.create("urn:sha1:1234abcd"), URI.create("urn:md5:zyxw9876")); 078 079 @Mock 080 private EventAccumulator eventAccumulator; 081 082 private Transaction tx; 083 084 @Mock 085 private PersistentStorageSession pSession; 086 087 @Mock 088 private SearchIndex searchIndex; 089 090 @Mock 091 private PersistentStorageSessionManager psManager; 092 093 @Mock 094 private ExternalContent externalContent; 095 096 @Mock 097 private ResourceHeaders headers; 098 099 private NonRdfSourceOperationFactory factory; 100 101 @InjectMocks 102 private ReplaceBinariesServiceImpl service; 103 104 @Captor 105 private ArgumentCaptor<UpdateNonRdfSourceOperation> operationCaptor; 106 107 @Before 108 public void setup() { 109 factory = new NonRdfSourceOperationFactoryImpl(); 110 setField(service, "factory", factory); 111 setField(service, "eventAccumulator", eventAccumulator); 112 when(psManager.getSession(any(Transaction.class))).thenReturn(pSession); 113 tx = TestTransactionHelper.mockTransaction(TX_ID, false); 114 setField(service, "searchIndex", searchIndex); 115 when(tx.getId()).thenReturn(TX_ID); 116 when(pSession.getHeaders(FEDORA_ID, null)).thenReturn(headers); 117 } 118 119 @Test 120 public void replaceInternalBinary() throws Exception { 121 final String contentString = "This is some test data"; 122 final var stream = toInputStream(contentString, UTF_8); 123 124 service.perform(tx, USER_PRINCIPAL, FEDORA_ID, FILENAME, MIME_TYPE, DIGESTS, stream, FILESIZE, 125 null); 126 verify(pSession).persist(operationCaptor.capture()); 127 final NonRdfSourceOperation op = operationCaptor.getValue(); 128 129 assertEquals(FEDORA_ID, operationCaptor.getValue().getResourceId()); 130 assertEquals(contentString, IOUtils.toString(op.getContentStream(), UTF_8)); 131 assertPropertiesPopulated(op); 132 133 verify(tx).lockResource(FEDORA_ID); 134 verify(tx).lockResource(FEDORA_ID.asDescription()); 135 } 136 137 @Test 138 public void replaceInternalBinaryInAg() throws Exception { 139 final String contentString = "This is some test data"; 140 final var stream = toInputStream(contentString, UTF_8); 141 142 final var agId = FedoraId.create("ag"); 143 final var binId = agId.resolve("bin"); 144 145 when(pSession.getHeaders(binId, null)).thenReturn(headers); 146 when(headers.getArchivalGroupId()).thenReturn(agId); 147 148 service.perform(tx, USER_PRINCIPAL, binId, FILENAME, MIME_TYPE, DIGESTS, stream, FILESIZE, 149 null); 150 verify(pSession).persist(operationCaptor.capture()); 151 final NonRdfSourceOperation op = operationCaptor.getValue(); 152 153 assertEquals(binId, operationCaptor.getValue().getResourceId()); 154 assertEquals(contentString, IOUtils.toString(op.getContentStream(), UTF_8)); 155 assertPropertiesPopulated(op); 156 157 verify(tx).lockResource(agId); 158 verify(tx).lockResource(binId); 159 verify(tx).lockResource(binId.asDescription()); 160 } 161 162 @Test 163 public void replaceExternalBinary() throws Exception { 164 final var realDigests = asList(URI.create("urn:sha1:94e66df8cd09d410c62d9e0dc59d3a884e458e05"), 165 URI.create("urn:md5:9893532233caff98cd083a116b013c0b")); 166 167 tempFolder.create(); 168 final File externalFile = tempFolder.newFile(); 169 FileUtils.write(externalFile, "some content", StandardCharsets.UTF_8); 170 final URI uri = externalFile.toURI(); 171 when(externalContent.fetchExternalContent()).thenReturn(Files.newInputStream(externalFile.toPath())); 172 when(externalContent.getURI()).thenReturn(uri); 173 when(externalContent.getHandling()).thenReturn(ExternalContent.PROXY); 174 175 service.perform(tx, USER_PRINCIPAL, FEDORA_ID, FILENAME, MIME_TYPE, realDigests, null, FILESIZE, 176 externalContent); 177 verify(pSession).persist(operationCaptor.capture()); 178 final NonRdfSourceOperation op = operationCaptor.getValue(); 179 180 assertEquals(FEDORA_ID, operationCaptor.getValue().getResourceId()); 181 assertEquals(uri, op.getContentUri()); 182 assertEquals(ExternalContent.PROXY, op.getExternalHandling()); 183 assertPropertiesPopulated(op, MIME_TYPE, FILENAME, FILESIZE, realDigests); 184 185 assertNull(op.getContentStream()); 186 } 187 188 // Check that the content type from the external content link is given preference 189 @Test 190 public void replaceExternalBinary_WithExternalContentType() throws Exception { 191 final var realDigests = asList(URI.create("urn:sha1:94e66df8cd09d410c62d9e0dc59d3a884e458e05"), 192 URI.create("urn:md5:9893532233caff98cd083a116b013c0b")); 193 194 tempFolder.create(); 195 final String contentString = "some content"; 196 final File externalFile = tempFolder.newFile(); 197 FileUtils.write(externalFile, contentString, StandardCharsets.UTF_8); 198 final URI uri = externalFile.toURI(); 199 when(externalContent.fetchExternalContent()).thenReturn(Files.newInputStream(externalFile.toPath())); 200 when(externalContent.getContentSize()).thenReturn((long) contentString.length()); 201 when(externalContent.getURI()).thenReturn(uri); 202 when(externalContent.getHandling()).thenReturn(COPY); 203 when(externalContent.getContentType()).thenReturn(MIME_TYPE); 204 205 service.perform(tx, USER_PRINCIPAL, FEDORA_ID, FILENAME, "application/octet-stream", 206 realDigests, null, -1L, externalContent); 207 verify(pSession).persist(operationCaptor.capture()); 208 final NonRdfSourceOperation op = operationCaptor.getValue(); 209 210 assertEquals(FEDORA_ID, operationCaptor.getValue().getResourceId()); 211 assertEquals(uri, op.getContentUri()); 212 assertEquals(COPY, op.getExternalHandling()); 213 assertPropertiesPopulated(op, MIME_TYPE, FILENAME, (long) contentString.length(), realDigests); 214 215 assertNull(op.getContentStream()); 216 } 217 218 @Test(expected = RepositoryRuntimeException.class) 219 public void replaceBinary_PersistFailure() throws Exception { 220 doThrow(new PersistentStorageException("Boom")).when(pSession) 221 .persist(any(ResourceOperation.class)); 222 223 final var stream = toInputStream("Some content", UTF_8); 224 225 service.perform(tx, USER_PRINCIPAL, FEDORA_ID, FILENAME, MIME_TYPE, DIGESTS, stream, FILESIZE, 226 null); 227 } 228 229 @Test 230 public void copyExternalBinary() throws Exception { 231 final var realDigests = asList(URI.create("urn:sha1:94e66df8cd09d410c62d9e0dc59d3a884e458e05")); 232 233 tempFolder.create(); 234 final File externalFile = tempFolder.newFile(); 235 final String contentString = "some content"; 236 FileUtils.write(externalFile, contentString, StandardCharsets.UTF_8); 237 final URI uri = externalFile.toURI(); 238 when(externalContent.fetchExternalContent()).thenReturn(Files.newInputStream(externalFile.toPath())); 239 when(externalContent.getURI()).thenReturn(uri); 240 when(externalContent.isCopy()).thenReturn(true); 241 when(externalContent.getHandling()).thenReturn(ExternalContent.COPY); 242 when(externalContent.getContentType()).thenReturn("text/xml"); 243 244 service.perform(tx, USER_PRINCIPAL, FEDORA_ID, FILENAME, MIME_TYPE, realDigests, null, 245 (long) contentString.length(), externalContent); 246 verify(pSession).persist(operationCaptor.capture()); 247 final NonRdfSourceOperation op = operationCaptor.getValue(); 248 249 assertEquals(FEDORA_ID, operationCaptor.getValue().getResourceId()); 250 assertNull(op.getContentUri()); 251 assertNull(op.getExternalHandling()); 252 assertPropertiesPopulated(op, "text/xml", FILENAME, (long) contentString.length(), realDigests); 253 254 assertEquals(contentString, IOUtils.toString(op.getContentStream(), UTF_8)); 255 } 256 257 private void assertPropertiesPopulated(final NonRdfSourceOperation op, final String exMimetype, 258 final String exFilename, final long exContentSize, final Collection<URI> exDigests) { 259 assertEquals(exMimetype, op.getMimeType()); 260 assertEquals(exFilename, op.getFilename()); 261 assertEquals(exContentSize, op.getContentSize()); 262 assertEquals(exDigests, op.getContentDigests()); 263 } 264 265 private void assertPropertiesPopulated(final NonRdfSourceOperation op) { 266 assertPropertiesPopulated(op, MIME_TYPE, FILENAME, FILESIZE, DIGESTS); 267 } 268}