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