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}