001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.kernel.impl.services;
019
020import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel;
021import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA;
022import static org.fcrepo.kernel.api.RdfCollectors.toModel;
023import static org.fcrepo.kernel.api.rdf.DefaultRdfStream.fromModel;
024import static org.junit.Assert.assertEquals;
025import static org.junit.Assert.assertTrue;
026import static org.junit.Assert.fail;
027import static org.mockito.Mockito.when;
028
029import java.util.List;
030import java.util.UUID;
031import java.util.stream.Collectors;
032
033import javax.inject.Inject;
034
035import org.fcrepo.kernel.api.RdfStream;
036import org.fcrepo.kernel.api.Transaction;
037import org.fcrepo.kernel.api.identifiers.FedoraId;
038import org.fcrepo.kernel.api.models.Binary;
039import org.fcrepo.kernel.api.models.FedoraResource;
040import org.fcrepo.kernel.api.models.NonRdfSourceDescription;
041import org.fcrepo.kernel.api.services.ReferenceService;
042import org.fcrepo.kernel.impl.TestTransactionHelper;
043
044import org.apache.jena.graph.Triple;
045import org.apache.jena.rdf.model.Model;
046import org.apache.jena.rdf.model.Property;
047import org.apache.jena.rdf.model.Resource;
048import org.apache.jena.rdf.model.ResourceFactory;
049import org.flywaydb.test.FlywayTestExecutionListener;
050import org.flywaydb.test.annotation.FlywayTest;
051import org.junit.Before;
052import org.junit.Test;
053import org.junit.runner.RunWith;
054import org.mockito.Mock;
055import org.mockito.MockitoAnnotations;
056import org.springframework.test.context.ContextConfiguration;
057import org.springframework.test.context.TestExecutionListeners;
058import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
059import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
060
061/**
062 * Reference Service Tests
063 * @author whikloj
064 */
065@RunWith(SpringJUnit4ClassRunner.class)
066@ContextConfiguration("/containmentIndexTest.xml")
067@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, FlywayTestExecutionListener.class })
068public class ReferenceServiceImplTest {
069
070    @Inject
071    private ReferenceService referenceService;
072
073    @Mock
074    private FedoraResource targetResource;
075
076    @Mock
077    private Binary binaryResource;
078
079    @Mock
080    private NonRdfSourceDescription binaryDescriptionResource;
081
082    @Mock
083    private Transaction transaction;
084
085    @Mock
086    private Transaction shortLivedTx;
087
088    private FedoraId subject1Id;
089
090    private FedoraId subject2Id;
091
092    private Resource subject1;
093
094    private Resource subject2;
095
096    private Resource target;
097
098    private static final Property referenceProp = ResourceFactory.createProperty("http://example.org/pointer");
099
100    private static final String TEST_USER = "someUser";
101
102    @Before
103    @FlywayTest
104    public void setUp() {
105        MockitoAnnotations.openMocks(this);
106        final String transactionId = UUID.randomUUID().toString();
107        transaction = TestTransactionHelper.mockTransaction(transactionId, false);
108        shortLivedTx = TestTransactionHelper.mockTransaction(UUID.randomUUID().toString(), true);
109        subject1Id = FedoraId.create(UUID.randomUUID().toString());
110        subject2Id = FedoraId.create(UUID.randomUUID().toString());
111        final FedoraId targetId = FedoraId.create(UUID.randomUUID().toString());
112        when(targetResource.getFedoraId()).thenReturn(targetId);
113        subject1 =  ResourceFactory.createResource(subject1Id.getFullId());
114        subject2 = ResourceFactory.createResource(subject2Id.getFullId());
115        target = ResourceFactory.createResource(targetId.getFullId());
116    }
117
118    @Test
119    public void testAddAReference() {
120        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
121
122        final Model model = createDefaultModel();
123        model.add(subject1, referenceProp, target);
124        final RdfStream stream = fromModel(subject1.asNode(), model);
125        referenceService.updateReferences(transaction, subject1Id, TEST_USER, stream);
126        referenceService.commitTransaction(transaction);
127
128        final List<Triple> refs = referenceService.getInboundReferences(shortLivedTx, targetResource)
129                .collect(Collectors.toList());
130
131        assertEquals(1, refs.size());
132        assertEquals(subject1Id.getFullId(), refs.get(0).getSubject().getURI());
133        assertEquals(referenceProp.getURI(), refs.get(0).getPredicate().getURI());
134    }
135
136    @Test
137    public void testAddNoReference() {
138        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
139        final Model model = createDefaultModel();
140        model.add(subject1, referenceProp, "http://some/text/uri");
141        final RdfStream stream = fromModel(subject1.asNode(), model);
142        referenceService.updateReferences(transaction, subject1Id, TEST_USER, stream);
143        referenceService.commitTransaction(transaction);
144        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
145    }
146
147    @Test
148    public void testAdd2References() {
149        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
150        final Model model = createDefaultModel();
151        model.add(subject1, referenceProp, target);
152        final RdfStream stream = fromModel(subject1.asNode(), model);
153        referenceService.updateReferences(transaction, subject1Id, TEST_USER, stream);
154        referenceService.commitTransaction(transaction);
155        final List<Triple> refs = referenceService.getInboundReferences(shortLivedTx, targetResource).collect(
156                Collectors.toList());
157        assertEquals(1, refs.size());
158        assertEquals(subject1Id.getFullId(), refs.get(0).getSubject().getURI());
159        // Now make another object reference the target.
160        model.remove(subject1, referenceProp, target);
161        model.add(subject2, referenceProp, target);
162        final RdfStream stream2 = fromModel(subject2.asNode(),
163                model);
164        referenceService.updateReferences(transaction, subject2Id, TEST_USER, stream2);
165        referenceService.commitTransaction(transaction);
166        final List<Triple> refs2 = referenceService.getInboundReferences(shortLivedTx, targetResource).collect(
167                Collectors.toList());
168        assertEquals(2, refs2.size());
169        for (final var r : refs2) {
170            if (!(subject2Id.getFullId().equals(r.getSubject().getURI()) ||
171                    subject1Id.getFullId().equals(r.getSubject().getURI()))) {
172                fail("Missing expected subject in reference");
173            }
174        }
175    }
176
177    @Test
178    public void testAddExternalReference() {
179        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
180
181        final Model model = createDefaultModel();
182        final Resource external = ResourceFactory.createResource("http://someother.org/pointer");
183        model.add(external, referenceProp, target);
184        final RdfStream stream = fromModel(external.asNode(), model);
185
186        referenceService.updateReferences(transaction, subject1Id, TEST_USER, stream);
187        referenceService.commitTransaction(transaction);
188        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
189
190        model.remove(external, referenceProp, target);
191        model.add(subject1, referenceProp, target);
192        final RdfStream stream2 = fromModel(subject1.asNode(), model);
193        referenceService.updateReferences(transaction, subject1Id, TEST_USER, stream2);
194        referenceService.commitTransaction(transaction);
195        assertEquals(1, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
196
197    }
198
199    @Test
200    public void testRollback() {
201        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
202
203        final Model model = createDefaultModel();
204        model.add(subject1, referenceProp, target);
205        final RdfStream stream = fromModel(subject1.asNode(), model);
206        referenceService.updateReferences(transaction, subject1Id, TEST_USER, stream);
207        // Still nothing outside the transaction.
208        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
209        // One inside the transaction
210        assertEquals(1, referenceService.getInboundReferences(transaction, targetResource).count());
211        referenceService.rollbackTransaction(transaction);
212        // Still nothing outside or inside the transaction.
213        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
214        assertEquals(0, referenceService.getInboundReferences(transaction, targetResource).count());
215    }
216
217    @Test
218    public void testCommit() {
219        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
220
221        final Model model = createDefaultModel();
222        model.add(subject1, referenceProp, target);
223        final RdfStream stream = fromModel(subject1.asNode(), model);
224        referenceService.updateReferences(transaction, subject1Id, TEST_USER, stream);
225        // Still nothing outside the transaction.
226        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
227        // One inside the transaction
228        assertEquals(1, referenceService.getInboundReferences(transaction, targetResource).count());
229        referenceService.commitTransaction(transaction);
230        // Now 1 outside or inside the transaction.
231        assertEquals(1, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
232        assertEquals(1, referenceService.getInboundReferences(transaction, targetResource).count());
233    }
234
235    @Test
236    public void commitTransactionNotExist() {
237        final String txID = UUID.randomUUID().toString();
238        when(transaction.getId()).thenReturn(txID);
239        referenceService.commitTransaction(transaction);
240    }
241
242    @Test
243    public void ensureNoCrossTransactionLeakage() {
244        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
245
246        final Model model = createDefaultModel();
247        model.add(subject1, referenceProp, target);
248        final RdfStream stream = fromModel(subject1.asNode(), model);
249        referenceService.updateReferences(transaction, subject1Id, TEST_USER, stream);
250
251        final String transaction2Id = UUID.randomUUID().toString();
252        final Transaction transaction2 = TestTransactionHelper.mockTransaction(transaction2Id, false);
253        // Make both of these long-running.
254        when(transaction.isShortLived()).thenReturn(false);
255
256        // Still not public.
257        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
258        // Available to current transaction
259        assertEquals(1, referenceService.getInboundReferences(transaction, targetResource).count());
260        // But not to other transactions.
261        assertEquals(0, referenceService.getInboundReferences(transaction2, targetResource).count());
262        referenceService.commitTransaction(transaction);
263        // Now all return the one.
264        assertEquals(1, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
265        assertEquals(1, referenceService.getInboundReferences(transaction, targetResource).count());
266        assertEquals(1, referenceService.getInboundReferences(transaction2, targetResource).count());
267    }
268
269    @Test
270    public void testAddAndRemove() {
271        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
272
273        // Add a reference.
274        final Model model = createDefaultModel();
275        model.add(subject1, referenceProp, target);
276        final RdfStream stream = fromModel(subject1.asNode(), model);
277        referenceService.updateReferences(transaction, subject1Id, TEST_USER, stream);
278        referenceService.commitTransaction(transaction);
279        assertEquals(1, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
280
281        // Change the RDF to remove the reference.
282        final String transaction2Id = UUID.randomUUID().toString();
283        final Transaction transaction2 = TestTransactionHelper.mockTransaction(transaction2Id, false);
284        model.add(subject1, ResourceFactory.createProperty("http://someother/description"), "Some text");
285        model.remove(subject1, referenceProp, target);
286        final RdfStream stream2 = fromModel(subject1.asNode(), model);
287        referenceService.updateReferences(transaction2, subject1Id, TEST_USER, stream2);
288        // Reference still available outside transaction.
289        assertEquals(1, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
290        // But gone inside the transaction
291        assertEquals(0, referenceService.getInboundReferences(transaction2, targetResource).count());
292        referenceService.commitTransaction(transaction2);
293        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
294    }
295
296    @Test
297    public void testBinaryDescriptionListAllReferences() {
298        final FedoraId binaryId = FedoraId.create(UUID.randomUUID().toString());
299        final FedoraId descriptionId = binaryId.resolve(FCR_METADATA);
300        final Resource binaryUri = ResourceFactory.createResource(binaryId.getFullId());
301        final Resource binaryDescUri = ResourceFactory.createResource(descriptionId.getFullId());
302        when(binaryResource.getFedoraId()).thenReturn(binaryId);
303        when(binaryDescriptionResource.getFedoraId()).thenReturn(descriptionId);
304        when(binaryDescriptionResource.getDescribedResource()).thenReturn(binaryResource);
305        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, binaryDescriptionResource).count());
306
307        // Add a reference to binary
308        final Model model = createDefaultModel();
309        model.add(subject1, referenceProp, binaryUri);
310        final RdfStream stream = fromModel(subject1.asNode(), model);
311        referenceService.updateReferences(transaction, subject1Id, TEST_USER, stream);
312        // Check before committing
313        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, binaryDescriptionResource).count());
314        assertEquals(1, referenceService.getInboundReferences(transaction, binaryDescriptionResource)
315                .count());
316        // Commit
317        referenceService.commitTransaction(transaction);
318        assertEquals(1, referenceService.getInboundReferences(shortLivedTx, binaryDescriptionResource).count());
319
320        // Add a second to the description.
321        final Model model2 = createDefaultModel();
322        model2.add(subject2, referenceProp, binaryDescUri);
323        final RdfStream stream2 = fromModel(subject2.asNode(), model2);
324        referenceService.updateReferences(transaction, subject2Id, TEST_USER, stream2);
325        // One reference outside the transaction
326        assertEquals(1, referenceService.getInboundReferences(shortLivedTx, binaryDescriptionResource).count());
327        // Two inside
328        assertEquals(2, referenceService.getInboundReferences(transaction, binaryDescriptionResource)
329                .count());
330        // Commit the transaction
331        referenceService.commitTransaction(transaction);
332
333        // Verify both the reference to the binary and the description are returned.
334        final Model rdfModel = referenceService.getInboundReferences(shortLivedTx, binaryDescriptionResource).collect(
335                toModel());
336        assertTrue(rdfModel.contains(subject1, referenceProp, binaryUri));
337        assertTrue(rdfModel.contains(subject2, referenceProp, binaryDescUri));
338    }
339
340    @Test
341    public void testReferencesFromTwoSources() throws Exception {
342        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
343
344        // Add a reference.
345        final Model model = createDefaultModel();
346        model.add(subject1, referenceProp, target);
347        final RdfStream stream = fromModel(subject1.asNode(), model);
348        referenceService.updateReferences(transaction, subject1Id, TEST_USER, stream);
349        referenceService.commitTransaction(transaction);
350        assertEquals(1, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
351
352        // Add the same reference from another resource.
353        final RdfStream stream2 = fromModel(subject1.asNode(), model);
354        referenceService.updateReferences(transaction, subject2Id, TEST_USER, stream2);
355        referenceService.commitTransaction(transaction);
356        assertEquals(2, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
357        final Triple expectedTriple = Triple.create(subject1.asNode(), referenceProp.asNode(), target.asNode());
358        assertTrue(referenceService.getInboundReferences(shortLivedTx, targetResource)
359                .allMatch(t -> t.equals(expectedTriple)));
360        // Delete the first resource and see the second reference remains.
361        referenceService.deleteAllReferences(transaction, subject1Id);
362        referenceService.commitTransaction(transaction);
363        assertEquals(1, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
364        assertTrue(referenceService.getInboundReferences(shortLivedTx, targetResource)
365                .allMatch(t -> t.equals(expectedTriple)));
366
367        // Delete the second resource and see all references gone.
368        referenceService.deleteAllReferences(transaction, subject2Id);
369        referenceService.commitTransaction(transaction);
370        assertEquals(0, referenceService.getInboundReferences(shortLivedTx, targetResource).count());
371    }
372}