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