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}