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).lockResource(childId);
208    }
209
210    @Test
211    public void testParentAg() {
212        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
213        final FedoraId childId = fedoraId.resolve("child");
214        containmentIndex.addContainedBy(transaction, FedoraId.getRepositoryRootId(), fedoraId);
215        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
216        when(resourceHeaders.isArchivalGroup()).thenReturn(true);
217        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, model);
218        verify(transaction).lockResource(fedoraId);
219        verify(transaction).lockResource(childId);
220    }
221
222    @Test
223    public void testParentContainerInAg() {
224        final FedoraId agId = FedoraId.create(UUID.randomUUID().toString());
225        final FedoraId fedoraId = agId.resolve(UUID.randomUUID().toString());
226        final FedoraId childId = fedoraId.resolve("child");
227        containmentIndex.addContainedBy(transaction, agId, fedoraId);
228        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
229        when(resourceHeaders.isArchivalGroup()).thenReturn(false);
230        when(resourceHeaders.getArchivalGroupId()).thenReturn(agId);
231        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, model);
232        verify(transaction).lockResource(agId);
233        verify(transaction).lockResource(childId);
234    }
235
236    /**
237     * Test creating a RDFSource with a NonRDFSource parent.
238     */
239    @Test(expected = InteractionModelViolationException.class)
240    public void testParentIsBinaryRdf() throws Exception {
241        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
242        final FedoraId childId = fedoraId.resolve("child");
243        containmentIndex.addContainedBy(transaction, rootId, fedoraId);
244        when(resourceHeaders.getInteractionModel()).thenReturn(NON_RDF_SOURCE.toString());
245        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
246        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, model);
247    }
248
249    /**
250     * Test creating a NonRDFSource with a NonRDFSource parent.
251     */
252    @Test(expected = InteractionModelViolationException.class)
253    public void testParentIsBinary() throws Exception {
254        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
255        final FedoraId childId = fedoraId.resolve("child");
256        containmentIndex.addContainedBy(transaction, rootId, fedoraId);
257        when(resourceHeaders.getInteractionModel()).thenReturn(NON_RDF_SOURCE.toString());
258        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
259        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, FILENAME, CONTENT_SIZE, null,
260                DIGESTS, null, null);
261    }
262
263    /**
264     * Test creating an external NonRDFSource with a NonRDFSource parent.
265     * TODO: put/post to a binary parent is tested above, might be a duplicate.
266     */
267    @Test(expected = InteractionModelViolationException.class)
268    public void testParentIsExternal() throws Exception {
269        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
270        final FedoraId childId = fedoraId.resolve("child");
271        containmentIndex.addContainedBy(transaction, rootId, fedoraId);
272        when(resourceHeaders.getInteractionModel()).thenReturn(NON_RDF_SOURCE.toString());
273        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
274        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, FILENAME, CONTENT_SIZE, null,
275                DIGESTS, null, extContent);
276    }
277
278    /**
279     * Test creating a RDFSource with a RDFSource parent.
280     */
281    @Test
282    public void testParentIsRdf() throws Exception {
283        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
284        final FedoraId childId = fedoraId.resolve("child");
285        containmentIndex.addContainedBy(transaction, rootId, fedoraId);
286        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
287        when(resourceHeaders.getInteractionModel()).thenReturn(BASIC_CONTAINER.toString());
288        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, model);
289        cleanupList.add(fedoraId);
290        verify(psSession).persist(operationCaptor.capture());
291        final FedoraId persistedId = operationCaptor.getValue().getResourceId();
292        assertNotEquals(fedoraId, persistedId);
293        assertTrue(persistedId.getFullId().startsWith(fedoraId.getFullId()));
294        assertEquals(1, containmentIndex.getContains(transaction, fedoraId).count());
295
296        verify(transaction).lockResource(childId);
297    }
298
299    /**
300     * Test creating a NonRDFSource with a RDFSource parent.
301     */
302    @Test
303    public void testParentIsRdfBinary() throws Exception {
304        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
305        final FedoraId childId = fedoraId.resolve("child");
306        containmentIndex.addContainedBy(transaction, rootId, fedoraId);
307        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
308        when(resourceHeaders.getInteractionModel()).thenReturn(BASIC_CONTAINER.toString());
309        createResourceService.perform(transaction, USER_PRINCIPAL, childId, CONTENT_TYPE,
310                FILENAME, CONTENT_SIZE, null, DIGESTS, null, null);
311        cleanupList.add(fedoraId);
312        verify(psSession, times(2)).persist(operationCaptor.capture());
313        final List<ResourceOperation> operations = operationCaptor.getAllValues();
314        final var operation = getOperation(operations, CreateNonRdfSourceOperation.class);
315        final FedoraId persistedId = operation.getResourceId();
316        assertNotEquals(fedoraId, persistedId);
317        assertTrue(persistedId.getFullId().startsWith(fedoraId.getFullId()));
318        assertBinaryPropertiesPresent(operation);
319        assertEquals(fedoraId, operation.getParentId());
320        assertEquals(1, containmentIndex.getContains(transaction, fedoraId).count());
321        verify(transaction).lockResource(childId);
322    }
323
324    /**
325     * Test setting some system properties only accessible in relaxed mode.
326     */
327    @Test
328    public void testRdfSetRelaxedProperties_Post() throws Exception {
329        final var createdDate = Instant.parse("2019-11-12T10:00:30.0Z");
330        final var calendar = Calendar.getInstance();
331        calendar.setTime(Date.from(createdDate));
332        final var createdDateXsd = new XSDDateTime(calendar);
333        final var lastModifiedDate = Instant.parse("2019-11-12T14:11:05.0Z");
334        calendar.setTime(Date.from(lastModifiedDate));
335        final var lastModifiedDateXsd = new XSDDateTime(calendar);
336        final String relaxedUser = "relaxedUser";
337        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
338        final FedoraId childId = fedoraId.resolve("testSlug");
339        containmentIndex.addContainedBy(transaction, rootId, fedoraId);
340
341        final var resc = model.getResource(childId.getFullId());
342        resc.addLiteral(LAST_MODIFIED_DATE, lastModifiedDateXsd);
343        resc.addLiteral(LAST_MODIFIED_BY, relaxedUser);
344        resc.addLiteral(CREATED_DATE, createdDateXsd);
345        resc.addLiteral(CREATED_BY, relaxedUser);
346
347        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
348        when(psSession.getHeaders(childId, null)).thenReturn(resourceHeaders);
349
350        when(resourceHeaders.getInteractionModel()).thenReturn(BASIC_CONTAINER.toString());
351        propsConfig.setServerManagedPropsMode(ServerManagedPropsMode.RELAXED);
352        createResourceService.perform(transaction, USER_PRINCIPAL, childId, null, model);
353        cleanupList.add(fedoraId);
354
355        verify(psSession).persist(operationCaptor.capture());
356
357        final var operation = operationCaptor.getValue();
358        final FedoraId persistedId = operation.getResourceId();
359        assertNotEquals(fedoraId, persistedId);
360        assertTrue(persistedId.getFullId().startsWith(fedoraId.getFullId()));
361        assertEquals(persistedId, childId);
362
363        final var rdfOp = (RdfSourceOperation) operation;
364        assertEquals(relaxedUser, rdfOp.getCreatedBy());
365        assertEquals(relaxedUser, rdfOp.getLastModifiedBy());
366        assertEquals(createdDate, rdfOp.getCreatedDate());
367        assertEquals(lastModifiedDate, rdfOp.getLastModifiedDate());
368        assertEquals(1, containmentIndex.getContains(transaction, fedoraId).count());
369        verify(transaction).lockResource(childId);
370    }
371
372    /**
373     * This test now seems to ensure that the createResourceService will overwrite an existing object
374     * TODO: Review expectations
375     */
376    @Test
377    public void testWithBinary() throws Exception {
378        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
379        final FedoraId childId = fedoraId.resolve("testSlug");
380        containmentIndex.addContainedBy(transaction, FedoraId.getRepositoryRootId(), fedoraId);
381        containmentIndex.commitTransaction(transaction);
382        when(psSession.getHeaders(fedoraId, null)).thenReturn(resourceHeaders);
383        when(psSession.getHeaders(childId, null)).thenReturn(resourceHeaders);
384        when(resourceHeaders.getInteractionModel()).thenReturn(BASIC_CONTAINER.toString());
385        createResourceService.perform(transaction, USER_PRINCIPAL, childId,
386                CONTENT_TYPE, FILENAME, CONTENT_SIZE, null, DIGESTS, null, null);
387        cleanupList.add(fedoraId);
388        verify(psSession, times(2)).persist(operationCaptor.capture());
389        final List<ResourceOperation> operations = operationCaptor.getAllValues();
390        final var operation = getOperation(operations, CreateNonRdfSourceOperation.class);
391        final FedoraId persistedId = operation.getResourceId();
392        assertNotEquals(fedoraId, persistedId);
393        assertEquals(childId, persistedId);
394        assertTrue(persistedId.getFullId().startsWith(fedoraId.getFullId()));
395        assertBinaryPropertiesPresent(operation);
396        assertEquals(fedoraId, operation.getParentId());
397
398        final var descOperation = getOperation(operations, CreateRdfSourceOperation.class);
399        assertEquals(persistedId.asDescription(), descOperation.getResourceId());
400        assertEquals(1, containmentIndex.getContains(transaction, fedoraId).count());
401        verify(transaction).lockResource(childId);
402    }
403
404    @Test
405    public void testSendingValidInteractionModel() {
406        // If you provide a valid interaction model, you should always get it back.
407        final String expected = BASIC_CONTAINER.toString();
408        final String model1 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, false, false, false);
409        assertEquals(expected, model1);
410        final String model2 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, false, false, true);
411        assertEquals(expected, model2);
412        final String model3 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, false, true, false);
413        assertEquals(expected, model3);
414        final String model4 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, false, true, true);
415        assertEquals(expected, model4);
416        final String model5 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, true, false, false);
417        assertEquals(expected, model5);
418        final String model6 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, true, false, true);
419        assertEquals(expected, model6);
420        final String model7 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, true, true, false);
421        assertEquals(expected, model7);
422        final String model8 = createResourceService.determineInteractionModel(STRING_TYPES_VALID, true, true, true);
423        assertEquals(expected, model8);
424    }
425
426    @Test
427    public void testSendingInvalidInteractionModelIsNotRdf() {
428        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, false, false,
429                false);
430        assertEquals(defaultInteractionModel, model);
431    }
432
433    @Test
434    public void testNotRdfNoContentIsExternal() {
435        final String expected = NON_RDF_SOURCE.toString();
436        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, false, false,
437                true);
438        assertEquals(expected, model);
439    }
440
441    @Test
442    public void testNotRdfContentPresentNotExternal() {
443        final String expected = NON_RDF_SOURCE.toString();
444        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, false, true,
445                false);
446        assertEquals(expected, model);
447    }
448
449    @Test
450    public void testNotRdfContentPresentIsExternal() {
451        final String expected = NON_RDF_SOURCE.toString();
452        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, false, true, true);
453        assertEquals(expected, model);
454    }
455
456    @Test
457    public void testIsRdfNoContentNotExternal() {
458        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, true, false,
459                false);
460        assertEquals(defaultInteractionModel, model);
461    }
462
463    @Test
464    public void testIsRdfNoContentIsExternal() {
465        final String expected = NON_RDF_SOURCE.toString();
466        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, true, false, true);
467        assertEquals(expected, model);
468    }
469
470    @Test
471    public void testIsRdfHasContentNotExternal() {
472        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, true, true, false);
473        assertEquals(defaultInteractionModel, model);
474    }
475
476    @Test
477    public void testIsRdfHasContentIsExternal() {
478        final String expected = NON_RDF_SOURCE.toString();
479        final String model = createResourceService.determineInteractionModel(STRING_TYPES_NOT_VALID, true, true, true);
480        assertEquals(expected, model);
481    }
482
483    @Test(expected = RequestWithAclLinkHeaderException.class)
484    public void testCheckAclLinkHeaderFailDblQ() throws Exception {
485        final List<String> links = Arrays.asList("<" + NON_RDF_SOURCE.toString() + ">; rel=\"type\"",
486                "<http" + "://example.org/some/location/image.tiff>; " + "rel=\"http://fedora" +
487                        ".info/definitions/fcrepo#ExternalContent\"; " + "handling=\"proxy\"; type=\"image/tiff\"",
488                "<http" + "://example.org/some/otherlocation>; rel=\"acl\"");
489        createResourceService.checkAclLinkHeader(links);
490    }
491
492    @Test(expected = RequestWithAclLinkHeaderException.class)
493    public void testCheckAclLinkHeaderFailSingleQ() throws Exception {
494        final List<String> links = Arrays.asList("<" + NON_RDF_SOURCE.toString() + ">; rel=\"type\"",
495                "<http" + "://example.org/some/location/image.tiff>; " + "rel=\"http://fedora" +
496                        ".info/definitions/fcrepo#ExternalContent\"; " + "handling=\"proxy\"; type=\"image/tiff\"",
497                "<http" + "://example.org/some/otherlocation>; rel='acl'");
498        createResourceService.checkAclLinkHeader(links);
499    }
500
501    @Test(expected = RequestWithAclLinkHeaderException.class)
502    public void testCheckAclLinkHeaderFailNoQ() throws Exception {
503        final List<String> links = Arrays.asList("<" + NON_RDF_SOURCE.toString() + ">; rel=\"type\"",
504                "<http" + "://example.org/some/location/image.tiff>; " + "rel=\"http://fedora" +
505                        ".info/definitions/fcrepo#ExternalContent\"; " + "handling=\"proxy\"; type=\"image/tiff\"",
506                "<http" + "://example.org/some/otherlocation>; rel=acl");
507        createResourceService.checkAclLinkHeader(links);
508    }
509
510    @Test
511    public void testCheckAclLinkHeaderSuccess() throws Exception {
512        final List<String> links = Arrays.asList("<" + NON_RDF_SOURCE.toString() + ">; rel=\"type\"",
513                "<http" + "://example.org/some/location/image.tiff>; " + "rel=\"http://fedora" +
514                        ".info/definitions/fcrepo#ExternalContent\"; " + "handling=\"proxy\"; type=\"image/tiff\"");
515        createResourceService.checkAclLinkHeader(links);
516    }
517
518    @Test
519    public void testCopyExternalBinary() throws Exception {
520        final var realDigests = asList(URI.create("urn:sha1:94e66df8cd09d410c62d9e0dc59d3a884e458e05"));
521
522        tempFolder.create();
523        final File externalFile = tempFolder.newFile();
524        final String contentString = "some content";
525        FileUtils.write(externalFile, contentString, StandardCharsets.UTF_8);
526        final URI uri = externalFile.toURI();
527        when(extContent.fetchExternalContent()).thenReturn(Files.newInputStream(externalFile.toPath()));
528        when(extContent.getURI()).thenReturn(uri);
529        when(extContent.isCopy()).thenReturn(true);
530        when(extContent.getHandling()).thenReturn(ExternalContent.COPY);
531        when(extContent.getContentType()).thenReturn("text/plain");
532
533        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
534        final FedoraId childId = fedoraId.resolve("child");
535
536        createResourceService.perform(transaction, USER_PRINCIPAL, childId,
537                CONTENT_TYPE, FILENAME, contentString.length(), null, realDigests, null, extContent);
538
539        verify(psSession, times(2)).persist(operationCaptor.capture());
540        final List<ResourceOperation> operations = operationCaptor.getAllValues();
541        final var operation = getOperation(operations, CreateNonRdfSourceOperation.class);
542        final FedoraId persistedId = operation.getResourceId();
543        assertNotEquals(fedoraId, persistedId);
544        assertTrue(persistedId.getFullId().startsWith(fedoraId.getFullId()));
545        assertBinaryPropertiesPresent(operation, "text/plain", FILENAME, contentString.length(), realDigests);
546        verify(transaction).lockResource(childId);
547    }
548
549    @Test
550    public void testProxyExternalBinary() throws Exception {
551        final var realDigests = asList(URI.create("urn:sha1:94e66df8cd09d410c62d9e0dc59d3a884e458e05"));
552
553        tempFolder.create();
554        final File externalFile = tempFolder.newFile();
555        final String contentString = "some content";
556        FileUtils.write(externalFile, contentString, StandardCharsets.UTF_8);
557        final URI uri = externalFile.toURI();
558        when(extContent.fetchExternalContent()).thenReturn(Files.newInputStream(externalFile.toPath()));
559        when(extContent.getURI()).thenReturn(uri);
560        when(extContent.getHandling()).thenReturn(ExternalContent.PROXY);
561        when(extContent.getContentType()).thenReturn(EXTERNAL_CONTENT_TYPE);
562
563        final FedoraId fedoraId = FedoraId.create(UUID.randomUUID().toString());
564        final FedoraId childId = fedoraId.resolve("child");
565
566        createResourceService.perform(transaction, USER_PRINCIPAL, childId,
567                CONTENT_TYPE, FILENAME, contentString.length(), null, realDigests, null, extContent);
568
569        verify(psSession, times(2)).persist(operationCaptor.capture());
570        final List<ResourceOperation> operations = operationCaptor.getAllValues();
571        final var operation = getOperation(operations, CreateNonRdfSourceOperation.class);
572        final FedoraId persistedId = operation.getResourceId();
573        assertNotEquals(fedoraId, persistedId);
574        assertTrue(persistedId.getFullId().startsWith(fedoraId.getFullId()));
575        assertBinaryPropertiesPresent(operation, EXTERNAL_CONTENT_TYPE, FILENAME, contentString.length(),
576                realDigests);
577
578        assertExternalBinaryPropertiesPresent(operation, uri, ExternalContent.PROXY);
579        verify(transaction).lockResource(childId);
580    }
581
582    private void assertBinaryPropertiesPresent(final ResourceOperation operation, final String exMimetype,
583            final String exFilename, final long exContentSize, final Collection<URI> exDigests) {
584        final var nonRdfOperation = (NonRdfSourceOperation) operation;
585        assertEquals(exContentSize, nonRdfOperation.getContentSize());
586        assertEquals(exFilename, nonRdfOperation.getFilename());
587        assertEquals(exMimetype, nonRdfOperation.getMimeType());
588        assertTrue(exDigests.containsAll(nonRdfOperation.getContentDigests()));
589    }
590
591    private void assertBinaryPropertiesPresent(final ResourceOperation operation) {
592        assertBinaryPropertiesPresent(operation, CONTENT_TYPE, FILENAME, CONTENT_SIZE, DIGESTS);
593    }
594
595    private void assertExternalBinaryPropertiesPresent(final ResourceOperation operation, final URI exExternalUri,
596            final String exHandling) {
597        final var nonRdfOperation = (NonRdfSourceOperation) operation;
598        assertEquals(exExternalUri, nonRdfOperation.getContentUri());
599        assertEquals(exHandling, nonRdfOperation.getExternalHandling());
600    }
601
602    private <T extends ResourceOperation> T getOperation(final List<ResourceOperation> operations,
603            final Class<T> clazz) {
604        return clazz.cast(operations.stream()
605                .filter(o -> (clazz.isInstance(o)))
606                .findFirst()
607                .get());
608    }
609
610}