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