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.util.Arrays.asList;
009import static java.util.Collections.singleton;
010import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER;
011import static org.fcrepo.kernel.api.RdfLexicon.CONTAINER;
012import static org.fcrepo.kernel.api.RdfLexicon.CREATED_BY;
013import static org.fcrepo.kernel.api.RdfLexicon.CREATED_DATE;
014import static org.fcrepo.kernel.api.RdfLexicon.DEFAULT_INTERACTION_MODEL;
015import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_RESOURCE;
016import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_BY;
017import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_DATE;
018import static org.fcrepo.kernel.api.RdfLexicon.MEMENTO_TYPE;
019import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE;
020import static org.fcrepo.kernel.api.RdfLexicon.RESOURCE;
021import static org.junit.Assert.assertEquals;
022import static org.junit.Assert.assertNotEquals;
023import static org.junit.Assert.assertTrue;
024import static org.mockito.Mockito.times;
025import static org.mockito.Mockito.verify;
026import static org.mockito.Mockito.when;
027import static org.springframework.test.util.ReflectionTestUtils.setField;
028
029import java.io.File;
030import java.net.URI;
031import java.nio.charset.StandardCharsets;
032import java.nio.file.Files;
033import java.time.Instant;
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.Calendar;
037import java.util.Collection;
038import java.util.Date;
039import java.util.List;
040import java.util.UUID;
041
042import javax.inject.Inject;
043
044import org.fcrepo.config.FedoraPropsConfig;
045import org.fcrepo.config.ServerManagedPropsMode;
046import org.fcrepo.kernel.api.ContainmentIndex;
047import org.fcrepo.kernel.api.Transaction;
048import org.fcrepo.kernel.api.cache.UserTypesCache;
049import org.fcrepo.kernel.api.exception.InteractionModelViolationException;
050import org.fcrepo.kernel.api.exception.RequestWithAclLinkHeaderException;
051import org.fcrepo.kernel.api.identifiers.FedoraId;
052import org.fcrepo.kernel.api.models.ExternalContent;
053import org.fcrepo.kernel.api.models.FedoraResource;
054import org.fcrepo.kernel.api.models.ResourceHeaders;
055import org.fcrepo.kernel.api.observer.EventAccumulator;
056import org.fcrepo.kernel.api.operations.CreateRdfSourceOperation;
057import org.fcrepo.kernel.api.operations.NonRdfSourceOperation;
058import org.fcrepo.kernel.api.operations.NonRdfSourceOperationFactory;
059import org.fcrepo.kernel.api.operations.RdfSourceOperation;
060import org.fcrepo.kernel.api.operations.RdfSourceOperationFactory;
061import org.fcrepo.kernel.api.operations.ResourceOperation;
062import org.fcrepo.kernel.api.services.MembershipService;
063import org.fcrepo.kernel.api.services.ReferenceService;
064import org.fcrepo.kernel.impl.TestTransactionHelper;
065import org.fcrepo.kernel.impl.operations.CreateNonRdfSourceOperation;
066import org.fcrepo.kernel.impl.operations.NonRdfSourceOperationFactoryImpl;
067import org.fcrepo.kernel.impl.operations.RdfSourceOperationFactoryImpl;
068import org.fcrepo.persistence.api.PersistentStorageSession;
069import org.fcrepo.persistence.api.PersistentStorageSessionManager;
070import org.fcrepo.persistence.api.exceptions.PersistentItemNotFoundException;
071import org.fcrepo.search.api.SearchIndex;
072
073import org.apache.commons.io.FileUtils;
074import org.apache.jena.datatypes.xsd.XSDDateTime;
075import org.apache.jena.rdf.model.Model;
076import org.apache.jena.rdf.model.ModelFactory;
077import org.flywaydb.test.annotation.FlywayTest;
078import org.junit.Before;
079import org.junit.Rule;
080import org.junit.Test;
081import org.junit.rules.TemporaryFolder;
082import org.junit.runner.RunWith;
083import org.mockito.ArgumentCaptor;
084import org.mockito.ArgumentMatchers;
085import org.mockito.Captor;
086import org.mockito.InjectMocks;
087import org.mockito.Mock;
088import org.mockito.MockitoAnnotations;
089import org.springframework.test.context.ContextConfiguration;
090import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
091
092/**
093 * @author bseeger
094 */
095@RunWith(SpringJUnit4ClassRunner.class)
096@ContextConfiguration("/containmentIndexTest.xml")
097public class CreateResourceServiceImplTest {
098
099    private static final List<String> STRING_TYPES_NOT_VALID = Arrays.asList(MEMENTO_TYPE, RESOURCE.toString(),
100            FEDORA_RESOURCE.toString());
101
102    private static final List<String> STRING_TYPES_VALID = Arrays.asList(MEMENTO_TYPE, CONTAINER.toString(),
103            BASIC_CONTAINER.toString());
104
105    private final String defaultInteractionModel = DEFAULT_INTERACTION_MODEL.toString();
106
107    @Rule
108    public TemporaryFolder tempFolder = new TemporaryFolder();
109
110    @Mock
111    private PersistentStorageSessionManager psManager;
112
113    private RdfSourceOperationFactory rdfSourceOperationFactory;
114
115    private NonRdfSourceOperationFactory nonRdfSourceOperationFactory;
116
117    @Inject
118    private ContainmentIndex containmentIndex;
119
120    @Mock
121    private SearchIndex searchIndex;
122
123    @Mock
124    private PersistentStorageSession psSession;
125
126    @Mock
127    private ResourceHeaders resourceHeaders;
128
129    @Mock
130    private ExternalContent extContent;
131
132    @Mock
133    private FedoraResource fedoraResource;
134
135    private Transaction transaction;
136
137    @Mock
138    private ReferenceService referenceService;
139
140    @Mock
141    private MembershipService membershipService;
142
143    @Captor
144    private ArgumentCaptor<ResourceOperation> operationCaptor;
145
146    @Mock
147    private EventAccumulator eventAccumulator;
148
149    @Mock
150    private UserTypesCache userTypesCache;
151
152    @InjectMocks
153    private CreateResourceServiceImpl createResourceService;
154
155    private static final String TX_ID = "tx1234";
156
157    private static final String USER_PRINCIPAL = "fedoraUser";
158
159    private static final String FILENAME = "file.html";
160
161    private static final String CONTENT_TYPE = "text/html";
162
163    private static final Long CONTENT_SIZE = 100L;
164
165    private static final String EXTERNAL_CONTENT_TYPE = "text/plain";
166
167    private final Model model = ModelFactory.createDefaultModel();
168
169    private static final Collection<URI> DIGESTS = singleton(URI.create("urn:sha1:12345abced"));
170
171    private static final List<FedoraId> cleanupList = new ArrayList<>() ;
172
173    private final FedoraId rootId = FedoraId.getRepositoryRootId();
174
175    private FedoraPropsConfig propsConfig = new FedoraPropsConfig();
176
177    @Before
178    @FlywayTest
179    public void setUp() {
180        MockitoAnnotations.openMocks(this);
181        rdfSourceOperationFactory = new RdfSourceOperationFactoryImpl();
182        setField(createResourceService, "rdfSourceOperationFactory", rdfSourceOperationFactory);
183        nonRdfSourceOperationFactory = new NonRdfSourceOperationFactoryImpl();
184        setField(createResourceService, "nonRdfSourceOperationFactory", nonRdfSourceOperationFactory);
185        setField(createResourceService, "containmentIndex", containmentIndex);
186        setField(createResourceService, "searchIndex", searchIndex);
187        setField(createResourceService, "eventAccumulator", eventAccumulator);
188        setField(createResourceService, "referenceService", referenceService);
189        setField(createResourceService, "membershipService", membershipService);
190        setField(createResourceService, "fedoraPropsConfig", propsConfig);
191        setField(createResourceService, "userTypesCache", userTypesCache);
192        propsConfig.setServerManagedPropsMode(ServerManagedPropsMode.STRICT);
193        when(psManager.getSession(ArgumentMatchers.any())).thenReturn(psSession);
194        transaction = TestTransactionHelper.mockTransaction(TX_ID, false);
195    }
196
197    /**
198     * Test trying to add a child to a non-existant parent.
199     * We recursive to repository root for a parent, so this is now just creating a ghost node?
200     */
201    @Test
202    public void testNoParentRdf() throws Exception {
203        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
204        final FedoraId childId = fedoraId.resolve("child");
205        when(psSession.getHeaders(fedoraId, null)).thenThrow(PersistentItemNotFoundException.class);
206        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, model);
207        verify(transaction).lockResourceAndGhostNodes(childId);
208    }
209
210    /**
211     * Test creating a new container directly inside an AG.
212     */
213    @Test
214    public void testParentAg() {
215        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
216        final FedoraId childId = fedoraId.resolve("child");
217        containmentIndex.addContainedBy(transaction, FedoraId.getRepositoryRootId(), fedoraId);
218        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
219        when(resourceHeaders.isArchivalGroup()).thenReturn(true);
220        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, model);
221        verify(transaction).lockResource(fedoraId);
222        verify(transaction).lockResourceAndGhostNodes(childId);
223    }
224
225    /**
226     * Test creating a new container inside a container inside an AG.
227     */
228    @Test
229    public void testParentContainerInAg() {
230        final FedoraId agId = FedoraId.create(UUID.randomUUID().toString());
231        final FedoraId fedoraId = agId.resolve(UUID.randomUUID().toString());
232        final FedoraId childId = fedoraId.resolve("child");
233        containmentIndex.addContainedBy(transaction, agId, fedoraId);
234        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
235        when(resourceHeaders.isArchivalGroup()).thenReturn(false);
236        when(resourceHeaders.getArchivalGroupId()).thenReturn(agId);
237        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, model);
238        verify(transaction).lockResource(agId);
239        verify(transaction).lockResourceAndGhostNodes(childId);
240    }
241
242    /**
243     * Test creating a RDFSource with a NonRDFSource parent.
244     */
245    @Test(expected = InteractionModelViolationException.class)
246    public void testParentIsBinaryRdf() throws Exception {
247        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
248        final FedoraId childId = fedoraId.resolve("child");
249        containmentIndex.addContainedBy(transaction, rootId, fedoraId);
250        when(resourceHeaders.getInteractionModel()).thenReturn(NON_RDF_SOURCE.toString());
251        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
252        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, model);
253    }
254
255    /**
256     * Test creating a NonRDFSource with a NonRDFSource parent.
257     */
258    @Test(expected = InteractionModelViolationException.class)
259    public void testParentIsBinary() throws Exception {
260        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
261        final FedoraId childId = fedoraId.resolve("child");
262        containmentIndex.addContainedBy(transaction, rootId, fedoraId);
263        when(resourceHeaders.getInteractionModel()).thenReturn(NON_RDF_SOURCE.toString());
264        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
265        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, FILENAME, CONTENT_SIZE, null,
266                DIGESTS, null, null);
267    }
268
269    /**
270     * Test creating an external NonRDFSource with a NonRDFSource parent.
271     * TODO: put/post to a binary parent is tested above, might be a duplicate.
272     */
273    @Test(expected = InteractionModelViolationException.class)
274    public void testParentIsExternal() throws Exception {
275        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
276        final FedoraId childId = fedoraId.resolve("child");
277        containmentIndex.addContainedBy(transaction, rootId, fedoraId);
278        when(resourceHeaders.getInteractionModel()).thenReturn(NON_RDF_SOURCE.toString());
279        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
280        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, FILENAME, CONTENT_SIZE, null,
281                DIGESTS, null, extContent);
282    }
283
284    /**
285     * Test creating a RDFSource with a RDFSource parent.
286     */
287    @Test
288    public void testParentIsRdf() throws Exception {
289        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
290        final FedoraId childId = fedoraId.resolve("child");
291        containmentIndex.addContainedBy(transaction, rootId, fedoraId);
292        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
293        when(resourceHeaders.getInteractionModel()).thenReturn(BASIC_CONTAINER.toString());
294        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, model);
295        cleanupList.add(fedoraId);
296        verify(psSession).persist(operationCaptor.capture());
297        final FedoraId persistedId = operationCaptor.getValue().getResourceId();
298        assertNotEquals(fedoraId, persistedId);
299        assertTrue(persistedId.getFullId().startsWith(fedoraId.getFullId()));
300        assertEquals(1, containmentIndex.getContains(transaction, fedoraId).count());
301
302        verify(transaction).lockResourceAndGhostNodes(childId);
303    }
304
305    /**
306     * Test creating a NonRDFSource with a RDFSource parent.
307     */
308    @Test
309    public void testParentIsRdfBinary() throws Exception {
310        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
311        final FedoraId childId = fedoraId.resolve("child");
312        containmentIndex.addContainedBy(transaction, rootId, fedoraId);
313        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
314        when(resourceHeaders.getInteractionModel()).thenReturn(BASIC_CONTAINER.toString());
315        createResourceService.perform(transaction, USER_PRINCIPAL, childId, CONTENT_TYPE,
316                FILENAME, CONTENT_SIZE, null, DIGESTS, null, null);
317        cleanupList.add(fedoraId);
318        verify(psSession, times(2)).persist(operationCaptor.capture());
319        final List<ResourceOperation> operations = operationCaptor.getAllValues();
320        final var operation = getOperation(operations, CreateNonRdfSourceOperation.class);
321        final FedoraId persistedId = operation.getResourceId();
322        assertNotEquals(fedoraId, persistedId);
323        assertTrue(persistedId.getFullId().startsWith(fedoraId.getFullId()));
324        assertBinaryPropertiesPresent(operation);
325        assertEquals(fedoraId, operation.getParentId());
326        assertEquals(1, containmentIndex.getContains(transaction, fedoraId).count());
327        verify(transaction).lockResourceAndGhostNodes(childId);
328    }
329
330    /**
331     * Test setting some system properties only accessible in relaxed mode.
332     */
333    @Test
334    public void testRdfSetRelaxedProperties_Post() throws Exception {
335        final var createdDate = Instant.parse("2019-11-12T10:00:30.0Z");
336        final var calendar = Calendar.getInstance();
337        calendar.setTime(Date.from(createdDate));
338        final var createdDateXsd = new XSDDateTime(calendar);
339        final var lastModifiedDate = Instant.parse("2019-11-12T14:11:05.0Z");
340        calendar.setTime(Date.from(lastModifiedDate));
341        final var lastModifiedDateXsd = new XSDDateTime(calendar);
342        final String relaxedUser = "relaxedUser";
343        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
344        final FedoraId childId = fedoraId.resolve("testSlug");
345        containmentIndex.addContainedBy(transaction, rootId, fedoraId);
346
347        final var resc = model.getResource(childId.getFullId());
348        resc.addLiteral(LAST_MODIFIED_DATE, lastModifiedDateXsd);
349        resc.addLiteral(LAST_MODIFIED_BY, relaxedUser);
350        resc.addLiteral(CREATED_DATE, createdDateXsd);
351        resc.addLiteral(CREATED_BY, relaxedUser);
352
353        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
354        when(psSession.getHeaders(childId, null)).thenReturn(resourceHeaders);
355
356        when(resourceHeaders.getInteractionModel()).thenReturn(BASIC_CONTAINER.toString());
357        propsConfig.setServerManagedPropsMode(ServerManagedPropsMode.RELAXED);
358        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, model);
359        cleanupList.add(fedoraId);
360
361        verify(psSession).persist(operationCaptor.capture());
362
363        final var operation = operationCaptor.getValue();
364        final FedoraId persistedId = operation.getResourceId();
365        assertNotEquals(fedoraId, persistedId);
366        assertTrue(persistedId.getFullId().startsWith(fedoraId.getFullId()));
367        assertEquals(persistedId, childId);
368
369        final var rdfOp = (RdfSourceOperation) operation;
370        assertEquals(relaxedUser, rdfOp.getCreatedBy());
371        assertEquals(relaxedUser, rdfOp.getLastModifiedBy());
372        assertEquals(createdDate, rdfOp.getCreatedDate());
373        assertEquals(lastModifiedDate, rdfOp.getLastModifiedDate());
374        assertEquals(1, containmentIndex.getContains(transaction, fedoraId).count());
375        verify(transaction).lockResourceAndGhostNodes(childId);
376    }
377
378    /**
379     * This test now seems to ensure that the createResourceService will overwrite an existing object
380     * TODO: Review expectations
381     */
382    @Test
383    public void testWithBinary() throws Exception {
384        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
385        final FedoraId childId = fedoraId.resolve("testSlug");
386        containmentIndex.addContainedBy(transaction, FedoraId.getRepositoryRootId(), fedoraId);
387        containmentIndex.commitTransaction(transaction);
388        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
389        when(psSession.getHeaders(childId, null)).thenReturn(resourceHeaders);
390        when(resourceHeaders.getInteractionModel()).thenReturn(BASIC_CONTAINER.toString());
391        createResourceService.perform(transaction, USER_PRINCIPAL, childId,
392                CONTENT_TYPE, FILENAME, CONTENT_SIZE, null, DIGESTS, null, null);
393        cleanupList.add(fedoraId);
394        verify(psSession, times(2)).persist(operationCaptor.capture());
395        final List<ResourceOperation> operations = operationCaptor.getAllValues();
396        final var operation = getOperation(operations, CreateNonRdfSourceOperation.class);
397        final FedoraId persistedId = operation.getResourceId();
398        assertNotEquals(fedoraId, persistedId);
399        assertEquals(childId, persistedId);
400        assertTrue(persistedId.getFullId().startsWith(fedoraId.getFullId()));
401        assertBinaryPropertiesPresent(operation);
402        assertEquals(fedoraId, operation.getParentId());
403
404        final var descOperation = getOperation(operations, CreateRdfSourceOperation.class);
405        assertEquals(persistedId.asDescription(), descOperation.getResourceId());
406        assertEquals(1, containmentIndex.getContains(transaction, fedoraId).count());
407        verify(transaction).lockResourceAndGhostNodes(childId);
408    }
409
410    @Test
411    public void testSendingValidInteractionModel() {
412        // If you provide a valid interaction model, you should always get it back.
413        final String expected = BASIC_CONTAINER.toString();
414        final String model1 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, false, false, false);
415        assertEquals(expected, model1);
416        final String model2 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, false, false, true);
417        assertEquals(expected, model2);
418        final String model3 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, false, true, false);
419        assertEquals(expected, model3);
420        final String model4 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, false, true, true);
421        assertEquals(expected, model4);
422        final String model5 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, true, false, false);
423        assertEquals(expected, model5);
424        final String model6 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, true, false, true);
425        assertEquals(expected, model6);
426        final String model7 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, true, true, false);
427        assertEquals(expected, model7);
428        final String model8 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, true, true, true);
429        assertEquals(expected, model8);
430    }
431
432    @Test
433    public void testSendingInvalidInteractionModelIsNotRdf() {
434        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, false, false,
435                false);
436        assertEquals(defaultInteractionModel, model);
437    }
438
439    @Test
440    public void testNotRdfNoContentIsExternal() {
441        final String expected = NON_RDF_SOURCE.toString();
442        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, false, false,
443                true);
444        assertEquals(expected, model);
445    }
446
447    @Test
448    public void testNotRdfContentPresentNotExternal() {
449        final String expected = NON_RDF_SOURCE.toString();
450        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, false, true,
451                false);
452        assertEquals(expected, model);
453    }
454
455    @Test
456    public void testNotRdfContentPresentIsExternal() {
457        final String expected = NON_RDF_SOURCE.toString();
458        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, false, true, true);
459        assertEquals(expected, model);
460    }
461
462    @Test
463    public void testIsRdfNoContentNotExternal() {
464        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, true, false,
465                false);
466        assertEquals(defaultInteractionModel, model);
467    }
468
469    @Test
470    public void testIsRdfNoContentIsExternal() {
471        final String expected = NON_RDF_SOURCE.toString();
472        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, true, false, true);
473        assertEquals(expected, model);
474    }
475
476    @Test
477    public void testIsRdfHasContentNotExternal() {
478        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, true, true, false);
479        assertEquals(defaultInteractionModel, model);
480    }
481
482    @Test
483    public void testIsRdfHasContentIsExternal() {
484        final String expected = NON_RDF_SOURCE.toString();
485        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, true, true, true);
486        assertEquals(expected, model);
487    }
488
489    @Test(expected = RequestWithAclLinkHeaderException.class)
490    public void testCheckAclLinkHeaderFailDblQ() throws Exception {
491        final List<String> links = Arrays.asList("<" + NON_RDF_SOURCE.toString() + ">; rel=\"type\"",
492                "<http" + "://example.org/some/location/image.tiff>; " + "rel=\"http://fedora" +
493                        ".info/definitions/fcrepo#ExternalContent\"; " + "handling=\"proxy\"; type=\"image/tiff\"",
494                "<http" + "://example.org/some/otherlocation>; rel=\"acl\"");
495        createResourceService.checkAclLinkHeader(links);
496    }
497
498    @Test(expected = RequestWithAclLinkHeaderException.class)
499    public void testCheckAclLinkHeaderFailSingleQ() throws Exception {
500        final List<String> links = Arrays.asList("<" + NON_RDF_SOURCE.toString() + ">; rel=\"type\"",
501                "<http" + "://example.org/some/location/image.tiff>; " + "rel=\"http://fedora" +
502                        ".info/definitions/fcrepo#ExternalContent\"; " + "handling=\"proxy\"; type=\"image/tiff\"",
503                "<http" + "://example.org/some/otherlocation>; rel='acl'");
504        createResourceService.checkAclLinkHeader(links);
505    }
506
507    @Test(expected = RequestWithAclLinkHeaderException.class)
508    public void testCheckAclLinkHeaderFailNoQ() throws Exception {
509        final List<String> links = Arrays.asList("<" + NON_RDF_SOURCE.toString() + ">; rel=\"type\"",
510                "<http" + "://example.org/some/location/image.tiff>; " + "rel=\"http://fedora" +
511                        ".info/definitions/fcrepo#ExternalContent\"; " + "handling=\"proxy\"; type=\"image/tiff\"",
512                "<http" + "://example.org/some/otherlocation>; rel=acl");
513        createResourceService.checkAclLinkHeader(links);
514    }
515
516    @Test
517    public void testCheckAclLinkHeaderSuccess() throws Exception {
518        final List<String> links = Arrays.asList("<" + NON_RDF_SOURCE.toString() + ">; rel=\"type\"",
519                "<http" + "://example.org/some/location/image.tiff>; " + "rel=\"http://fedora" +
520                        ".info/definitions/fcrepo#ExternalContent\"; " + "handling=\"proxy\"; type=\"image/tiff\"");
521        createResourceService.checkAclLinkHeader(links);
522    }
523
524    @Test
525    public void testCopyExternalBinary() throws Exception {
526        final var realDigests = asList(URI.create("urn:sha1:94e66df8cd09d410c62d9e0dc59d3a884e458e05"));
527
528        tempFolder.create();
529        final File externalFile = tempFolder.newFile();
530        final String contentString = "some content";
531        FileUtils.write(externalFile, contentString, StandardCharsets.UTF_8);
532        final URI uri = externalFile.toURI();
533        when(extContent.fetchExternalContent()).thenReturn(Files.newInputStream(externalFile.toPath()));
534        when(extContent.getURI()).thenReturn(uri);
535        when(extContent.isCopy()).thenReturn(true);
536        when(extContent.getHandling()).thenReturn(ExternalContent.COPY);
537        when(extContent.getContentType()).thenReturn("text/plain");
538
539        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
540        final FedoraId childId = fedoraId.resolve("child");
541        containmentIndex.addContainedBy(transaction, FedoraId.getRepositoryRootId(), fedoraId);
542        containmentIndex.commitTransaction(transaction);
543
544        when(resourceHeaders.getArchivalGroupId()).thenReturn(null);
545        when(resourceHeaders.isArchivalGroup()).thenReturn(false);
546        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
547
548        createResourceService.perform(transaction, USER_PRINCIPAL, childId,
549                CONTENT_TYPE, FILENAME, contentString.length(), null, realDigests, null, extContent);
550
551        verify(psSession, times(2)).persist(operationCaptor.capture());
552        final List<ResourceOperation> operations = operationCaptor.getAllValues();
553        final var operation = getOperation(operations, CreateNonRdfSourceOperation.class);
554        final FedoraId persistedId = operation.getResourceId();
555        assertNotEquals(fedoraId, persistedId);
556        assertTrue(persistedId.getFullId().startsWith(fedoraId.getFullId()));
557        assertBinaryPropertiesPresent(operation, "text/plain", FILENAME, contentString.length(), realDigests);
558        verify(transaction).lockResourceAndGhostNodes(childId);
559    }
560
561    @Test
562    public void testProxyExternalBinary() throws Exception {
563        final var realDigests = asList(URI.create("urn:sha1:94e66df8cd09d410c62d9e0dc59d3a884e458e05"));
564
565        tempFolder.create();
566        final File externalFile = tempFolder.newFile();
567        final String contentString = "some content";
568        FileUtils.write(externalFile, contentString, StandardCharsets.UTF_8);
569        final URI uri = externalFile.toURI();
570        when(extContent.fetchExternalContent()).thenReturn(Files.newInputStream(externalFile.toPath()));
571        when(extContent.getURI()).thenReturn(uri);
572        when(extContent.getHandling()).thenReturn(ExternalContent.PROXY);
573        when(extContent.getContentType()).thenReturn(EXTERNAL_CONTENT_TYPE);
574
575        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
576        final FedoraId childId = fedoraId.resolve("child");
577        containmentIndex.addContainedBy(transaction, FedoraId.getRepositoryRootId(), fedoraId);
578        containmentIndex.commitTransaction(transaction);
579
580        when(resourceHeaders.getArchivalGroupId()).thenReturn(null);
581        when(resourceHeaders.isArchivalGroup()).thenReturn(false);
582        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
583
584        createResourceService.perform(transaction, USER_PRINCIPAL, childId,
585                CONTENT_TYPE, FILENAME, contentString.length(), null, realDigests, null, extContent);
586
587        verify(psSession, times(2)).persist(operationCaptor.capture());
588        final List<ResourceOperation> operations = operationCaptor.getAllValues();
589        final var operation = getOperation(operations, CreateNonRdfSourceOperation.class);
590        final FedoraId persistedId = operation.getResourceId();
591        assertNotEquals(fedoraId, persistedId);
592        assertTrue(persistedId.getFullId().startsWith(fedoraId.getFullId()));
593        assertBinaryPropertiesPresent(operation, EXTERNAL_CONTENT_TYPE, FILENAME, contentString.length(),
594                realDigests);
595
596        assertExternalBinaryPropertiesPresent(operation, uri, ExternalContent.PROXY);
597        verify(transaction).lockResourceAndGhostNodes(childId);
598    }
599
600    private void assertBinaryPropertiesPresent(final ResourceOperation operation, final String exMimetype,
601            final String exFilename, final long exContentSize, final Collection<URI> exDigests) {
602        final var nonRdfOperation = (NonRdfSourceOperation) operation;
603        assertEquals(exContentSize, nonRdfOperation.getContentSize());
604        assertEquals(exFilename, nonRdfOperation.getFilename());
605        assertEquals(exMimetype, nonRdfOperation.getMimeType());
606        assertTrue(exDigests.containsAll(nonRdfOperation.getContentDigests()));
607    }
608
609    private void assertBinaryPropertiesPresent(final ResourceOperation operation) {
610        assertBinaryPropertiesPresent(operation, CONTENT_TYPE, FILENAME, CONTENT_SIZE, DIGESTS);
611    }
612
613    private void assertExternalBinaryPropertiesPresent(final ResourceOperation operation, final URI exExternalUri,
614            final String exHandling) {
615        final var nonRdfOperation = (NonRdfSourceOperation) operation;
616        assertEquals(exExternalUri, nonRdfOperation.getContentUri());
617        assertEquals(exHandling, nonRdfOperation.getExternalHandling());
618    }
619
620    private <T extends ResourceOperation> T getOperation(final List<ResourceOperation> operations,
621            final Class<T> clazz) {
622        return clazz.cast(operations.stream()
623                .filter(o -> (clazz.isInstance(o)))
624                .findFirst()
625                .get());
626    }
627
628}