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.models;
019
020import org.apache.jena.rdf.model.Resource;
021import org.apache.jena.rdf.model.ResourceFactory;
022import org.fcrepo.kernel.api.ContainmentIndex;
023import org.fcrepo.kernel.api.ReadOnlyTransaction;
024import org.fcrepo.kernel.api.Transaction;
025import org.fcrepo.kernel.api.exception.PathNotFoundException;
026import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
027import org.fcrepo.kernel.api.exception.ResourceTypeException;
028import org.fcrepo.kernel.api.identifiers.FedoraId;
029import org.fcrepo.kernel.api.models.Binary;
030import org.fcrepo.kernel.api.models.Container;
031import org.fcrepo.kernel.api.models.FedoraResource;
032import org.fcrepo.kernel.impl.TestTransactionHelper;
033import org.fcrepo.persistence.api.PersistentStorageSession;
034import org.fcrepo.persistence.api.PersistentStorageSessionManager;
035import org.fcrepo.persistence.api.exceptions.PersistentItemNotFoundException;
036import org.fcrepo.persistence.api.exceptions.PersistentStorageException;
037import org.fcrepo.persistence.common.ResourceHeadersImpl;
038import org.junit.After;
039import org.junit.Before;
040import org.junit.Test;
041import org.junit.runner.RunWith;
042import org.mockito.InjectMocks;
043import org.mockito.Mock;
044import org.mockito.MockitoAnnotations;
045import org.springframework.test.context.ContextConfiguration;
046import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
047
048import javax.inject.Inject;
049import java.net.URI;
050import java.time.Instant;
051import java.util.Collection;
052import java.util.UUID;
053import java.util.stream.Collectors;
054
055import static java.util.Arrays.asList;
056import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_ID_PREFIX;
057import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER;
058import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER;
059import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI;
060import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER;
061import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE;
062import static org.fcrepo.kernel.api.models.ExternalContent.PROXY;
063import static org.junit.Assert.assertEquals;
064import static org.junit.Assert.assertTrue;
065import static org.mockito.ArgumentMatchers.eq;
066import static org.mockito.ArgumentMatchers.nullable;
067import static org.mockito.Mockito.verify;
068import static org.mockito.Mockito.when;
069import static org.springframework.test.util.ReflectionTestUtils.setField;
070
071/**
072 * @author bbpennel
073 *
074 */
075@RunWith(SpringJUnit4ClassRunner.class)
076@ContextConfiguration("/containmentIndexTest.xml")
077public class ResourceFactoryImplTest {
078
079    private final static Instant CREATED_DATE = Instant.parse("2019-11-12T10:00:30.0Z");
080
081    private final static String CREATED_BY = "user1";
082
083    private final static Instant LAST_MODIFIED_DATE = Instant.parse("2019-11-12T14:11:05.0Z");
084
085    private final static String LAST_MODIFIED_BY = "user2";
086
087    private final static String STATE_TOKEN = "stately_value";
088
089    private final static long CONTENT_SIZE = 100L;
090
091    private final static String MIME_TYPE = "text/plain";
092
093    private final static String FILENAME = "testfile.txt";
094
095    private final static URI DIGEST = URI.create("sha:12345");
096
097    private final static Collection<URI> DIGESTS = asList(DIGEST);
098
099    @Mock
100    private PersistentStorageSessionManager sessionManager;
101
102    @Mock
103    private PersistentStorageSession psSession;
104
105    private ResourceHeadersImpl resourceHeaders;
106
107    private String fedoraIdStr;
108
109    private String sessionId;
110
111    private final FedoraId rootId = FedoraId.getRepositoryRootId();
112
113    private FedoraId fedoraId;
114
115    private String fedoraMementoIdStr;
116
117    @Mock
118    private Transaction mockTx;
119
120    @Mock
121    private FedoraResource mockResource;
122
123    @Inject
124    private ContainmentIndex containmentIndex;
125
126    @InjectMocks
127    private ResourceFactoryImpl factory;
128
129    @Before
130    public void setup() throws Exception {
131        MockitoAnnotations.openMocks(this);
132        fedoraIdStr = FEDORA_ID_PREFIX + "/" + UUID.randomUUID().toString();
133        fedoraId = FedoraId.create(fedoraIdStr);
134        fedoraMementoIdStr = fedoraIdStr + "/fcr:versions/20000102120000";
135
136        sessionId = UUID.randomUUID().toString();
137        mockTx = TestTransactionHelper.mockTransaction(sessionId, false);
138
139        factory = new ResourceFactoryImpl();
140
141        setField(factory, "persistentStorageSessionManager", sessionManager);
142        setField(factory, "containmentIndex", containmentIndex);
143
144        resourceHeaders = new ResourceHeadersImpl();
145        resourceHeaders.setId(fedoraId);
146
147        when(sessionManager.getSession(mockTx)).thenReturn(psSession);
148        when(sessionManager.getReadOnlySession()).thenReturn(psSession);
149
150        when(psSession.getHeaders(eq(fedoraId), nullable(Instant.class))).thenReturn(resourceHeaders);
151    }
152
153    @After
154    public void cleanUp() {
155        when(mockResource.getFedoraId()).thenReturn(rootId);
156        containmentIndex.reset();
157    }
158
159    @Test(expected = PathNotFoundException.class)
160    public void getResource_ObjectNotFound() throws Exception {
161        when(psSession.getHeaders(fedoraId, null)).thenThrow(PersistentItemNotFoundException.class);
162
163        factory.getResource(mockTx, fedoraId);
164    }
165
166    @Test(expected = ResourceTypeException.class)
167    public void getResource_NoInteractionModel() throws Exception {
168        resourceHeaders.setInteractionModel(null);
169
170        factory.getResource(mockTx, fedoraId);
171    }
172
173    @Test(expected = ResourceTypeException.class)
174    public void getResource_UnknownInteractionModel() throws Exception {
175        resourceHeaders.setInteractionModel("http://example.com/mystery_stroop");
176
177        factory.getResource(mockTx, fedoraId);
178    }
179
180    @Test
181    public void getResource_BasicContainer() throws Exception {
182        populateHeaders(resourceHeaders, BASIC_CONTAINER);
183
184        // Need to make a short lived transaction or the verify will fail.
185        final var shortTx = TestTransactionHelper.mockTransaction(sessionId, true);
186        final var resc = factory.getResource(ReadOnlyTransaction.INSTANCE, fedoraId);
187
188        assertTrue("Factory must return a container", resc instanceof Container);
189        assertEquals(fedoraIdStr, resc.getId());
190        assertStateFieldsMatches(resc);
191
192        verify(sessionManager).getReadOnlySession();
193    }
194
195    @Test
196    public void getResource_BasicContainer_WithParent() throws Exception {
197        populateHeaders(resourceHeaders, BASIC_CONTAINER);
198
199        final var parentId = FedoraId.create(FEDORA_ID_PREFIX + UUID.randomUUID().toString());
200        resourceHeaders.setParent(parentId);
201
202        final var parentHeaders = new ResourceHeadersImpl();
203        parentHeaders.setId(parentId);
204        populateHeaders(parentHeaders, DIRECT_CONTAINER);
205
206        when(psSession.getHeaders(parentId, null)).thenReturn(parentHeaders);
207
208        final var resc = factory.getResource(mockTx, fedoraId);
209
210        assertTrue("Factory must return a container", resc instanceof Container);
211        assertEquals(fedoraIdStr, resc.getId());
212        assertStateFieldsMatches(resc);
213
214        final var parentResc = resc.getParent();
215        assertTrue("Parent must be a container", parentResc instanceof Container);
216        assertEquals(parentId, parentResc.getFedoraId());
217        assertStateFieldsMatches(parentResc);
218    }
219
220    @Test
221    public void getResource_BasicContainer_InTransaction() throws Exception {
222        populateHeaders(resourceHeaders, BASIC_CONTAINER);
223
224        final var resc = factory.getResource(mockTx, fedoraId);
225
226        assertTrue("Factory must return a container", resc instanceof Container);
227        assertEquals(fedoraIdStr, resc.getId());
228        assertStateFieldsMatches(resc);
229
230        verify(sessionManager).getSession(mockTx);
231    }
232
233    @Test
234    public void getResource_BasicContainer_Cast_InTransaction() throws Exception {
235        populateHeaders(resourceHeaders, BASIC_CONTAINER);
236
237        final var resc = factory.getResource(mockTx, fedoraId, Container.class);
238
239        assertTrue("Factory must return a container", resc instanceof Container);
240        assertEquals(fedoraIdStr, resc.getId());
241        assertStateFieldsMatches(resc);
242
243        verify(sessionManager).getSession(mockTx);
244    }
245
246    @Test
247    public void getResource_DirectContainer() throws Exception {
248        populateHeaders(resourceHeaders, DIRECT_CONTAINER);
249
250        final var resc = factory.getResource(mockTx, fedoraId);
251
252        assertTrue("Factory must return a container", resc instanceof Container);
253        assertEquals(fedoraIdStr, resc.getId());
254        assertStateFieldsMatches(resc);
255    }
256
257    @Test
258    public void getResource_IndirectContainer() throws Exception {
259        populateHeaders(resourceHeaders, INDIRECT_CONTAINER);
260
261        final var resc = factory.getResource(mockTx, fedoraId);
262
263        assertTrue("Factory must return a container", resc instanceof Container);
264        assertEquals(fedoraIdStr, resc.getId());
265        assertStateFieldsMatches(resc);
266    }
267
268    @Test
269    public void getResource_Binary() throws Exception {
270        populateHeaders(resourceHeaders, NON_RDF_SOURCE);
271        populateInternalBinaryHeaders(resourceHeaders);
272
273        final var resc = factory.getResource(mockTx, fedoraId);
274
275        assertTrue("Factory must return a binary", resc instanceof Binary);
276        assertEquals(fedoraIdStr, resc.getId());
277        assertStateFieldsMatches(resc);
278        assertBinaryFieldsMatch(resc);
279    }
280
281    @Test(expected = RepositoryRuntimeException.class)
282    public void getResource_Binary_StorageException() throws Exception {
283        when(psSession.getHeaders(fedoraId, null)).thenThrow(new PersistentStorageException("Boom"));
284
285        populateHeaders(resourceHeaders, NON_RDF_SOURCE);
286        populateInternalBinaryHeaders(resourceHeaders);
287
288        factory.getResource(mockTx, fedoraId);
289    }
290
291    @Test
292    public void getResource_ExternalBinary() throws Exception {
293        populateHeaders(resourceHeaders, NON_RDF_SOURCE);
294        populateInternalBinaryHeaders(resourceHeaders);
295        final String externalUrl = "http://example.com/stuff";
296        resourceHeaders.setExternalUrl(externalUrl);
297        resourceHeaders.setExternalHandling(PROXY);
298
299        final var resc = factory.getResource(mockTx, fedoraId);
300
301        assertTrue("Factory must return a container", resc instanceof Binary);
302        assertEquals(fedoraIdStr, resc.getId());
303        assertStateFieldsMatches(resc);
304        assertBinaryFieldsMatch(resc);
305        final var binary = (Binary) resc;
306        assertEquals(externalUrl, binary.getExternalURL());
307        assertTrue(binary.isProxy());
308    }
309
310    @Test
311    public void getNonRdfSourceDescription() throws Exception {
312        final FedoraId descFedoraId = FedoraId.create(FEDORA_ID_PREFIX + "/object1/fcr:metadata");
313
314        final var headers = new ResourceHeadersImpl();
315        headers.setId(descFedoraId);
316        populateHeaders(headers, ResourceFactory.createResource(FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI));
317        when(psSession.getHeaders(descFedoraId, null)).thenReturn(headers);
318
319        final var resc = factory.getResource(mockTx, descFedoraId);
320        assertTrue("Factory must return a NonRdfSourceDescripton", resc instanceof NonRdfSourceDescriptionImpl);
321    }
322
323    @Test
324    public void getChildren_NoChildren() throws Exception {
325        populateHeaders(resourceHeaders, BASIC_CONTAINER);
326
327        final var childrenStream = factory.getChildren(mockTx, fedoraId);
328
329        assertEquals(0, childrenStream.count());
330    }
331
332    @Test
333    public void getChildren_DoesNotExist() throws Exception {
334        final var childrenStream = factory.getChildren(mockTx, fedoraId);
335        assertEquals(0, childrenStream.count());
336    }
337
338    @Test
339    public void getChildren_WithChildren() throws Exception {
340        populateHeaders(resourceHeaders, BASIC_CONTAINER);
341
342        final var child1Id = FedoraId.create(UUID.randomUUID().toString());
343        final var child1Headers = new ResourceHeadersImpl();
344        child1Headers.setId(child1Id);
345        populateHeaders(child1Headers, BASIC_CONTAINER);
346        when(psSession.getHeaders(child1Id, null)).thenReturn(child1Headers);
347
348        final var childNestedId = FedoraId.create(UUID.randomUUID().toString());
349        final var childNestedHeaders = new ResourceHeadersImpl();
350        childNestedHeaders.setId(childNestedId);
351        populateHeaders(childNestedHeaders, BASIC_CONTAINER);
352        when(psSession.getHeaders(childNestedId, null)).thenReturn(childNestedHeaders);
353
354        final var child2Id = FedoraId.create(UUID.randomUUID().toString());
355        final var child2Headers = new ResourceHeadersImpl();
356        child2Headers.setId(child2Id);
357        populateHeaders(child2Headers, NON_RDF_SOURCE);
358        populateInternalBinaryHeaders(child2Headers);
359        when(psSession.getHeaders(child2Id, null)).thenReturn(child2Headers);
360
361        containmentIndex.addContainedBy(mockTx, rootId, fedoraId);
362        containmentIndex.addContainedBy(mockTx, fedoraId, child1Id);
363        containmentIndex.addContainedBy(mockTx, child1Id, childNestedId);
364        containmentIndex.addContainedBy(mockTx, fedoraId, child2Id);
365        containmentIndex.commitTransaction(mockTx);
366
367        final var childrenStream = factory.getChildren(mockTx, fedoraId);
368        final var childrenList = childrenStream.collect(Collectors.toList());
369
370        assertEquals(2, childrenList.size());
371
372        final var child1 = childrenList.stream().filter(c -> c.getFedoraId().equals(child1Id)).findFirst();
373        assertTrue(child1.isPresent());
374        assertTrue(child1.get() instanceof Container);
375
376        final var child2 = childrenList.stream().filter(c -> c.getFedoraId().equals(child2Id)).findFirst();
377        assertTrue(child2.isPresent());
378        assertTrue(child2.get() instanceof Binary);
379    }
380
381    private void assertStateFieldsMatches(final FedoraResource resc) {
382        assertEquals(CREATED_DATE, resc.getCreatedDate());
383        assertEquals(CREATED_BY, resc.getCreatedBy());
384        assertEquals(LAST_MODIFIED_DATE, resc.getLastModifiedDate());
385        assertEquals(LAST_MODIFIED_BY, resc.getLastModifiedBy());
386        assertEquals(STATE_TOKEN, resc.getStateToken());
387        assertEquals(STATE_TOKEN, resc.getEtagValue());
388    }
389
390    private static void populateHeaders(final ResourceHeadersImpl headers, final Resource ixModel) {
391        headers.setInteractionModel(ixModel.getURI());
392        headers.setCreatedBy(CREATED_BY);
393        headers.setCreatedDate(CREATED_DATE);
394        headers.setLastModifiedBy(LAST_MODIFIED_BY);
395        headers.setLastModifiedDate(LAST_MODIFIED_DATE);
396        headers.setStateToken(STATE_TOKEN);
397    }
398
399    private void assertBinaryFieldsMatch(final FedoraResource resc) {
400        final Binary binary = (Binary) resc;
401        assertEquals(CONTENT_SIZE, binary.getContentSize());
402        assertEquals(MIME_TYPE, binary.getMimeType());
403        assertEquals(DIGEST, binary.getContentDigests().stream().findFirst().get());
404        assertEquals(FILENAME, binary.getFilename());
405    }
406
407    private static void populateInternalBinaryHeaders(final ResourceHeadersImpl headers) {
408        headers.setContentSize(CONTENT_SIZE);
409        headers.setMimeType(MIME_TYPE);
410        headers.setDigests(DIGESTS);
411        headers.setFilename(FILENAME);
412    }
413
414}