View Javadoc
1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements. See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership. The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.commons.rdf.api;
19  
20  import static org.junit.Assert.assertEquals;
21  import static org.junit.Assert.assertFalse;
22  import static org.junit.Assert.assertNotNull;
23  import static org.junit.Assert.assertTrue;
24  import static org.junit.Assert.fail;
25  
26  import java.util.ArrayList;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Optional;
32  import java.util.concurrent.ConcurrentHashMap;
33  import java.util.stream.Stream;
34  
35  import org.junit.Assume;
36  import org.junit.Before;
37  import org.junit.Test;
38  
39  /**
40   * Test Graph implementation
41   * <p>
42   * To add to your implementation's tests, create a subclass with a name ending
43   * in <code>Test</code> and provide {@link #createFactory()} which minimally
44   * must support {@link RDF#createGraph()} and {@link RDF#createIRI(String)}, but
45   * ideally support all operations.
46   * <p>
47   * This test uses try-with-resources blocks for calls to {@link Graph#stream()}
48   * and {@link Graph#iterate()}.
49   * 
50   * @see Graph
51   * @see RDF
52   */
53  public abstract class AbstractGraphTest {
54  
55      protected RDF factory;
56      protected Graph graph;
57      protected IRI alice;
58      protected IRI bob;
59      protected IRI name;
60      protected IRI knows;
61      protected IRI member;
62      protected BlankNode bnode1;
63      protected BlankNode bnode2;
64      protected Literal aliceName;
65      protected Literal bobName;
66      protected Literal secretClubName;
67      protected Literal companyName;
68      protected Triple bobNameTriple;
69  
70      /**
71       * 
72       * This method must be overridden by the implementing test to provide a
73       * factory for the test to create {@link Graph}, {@link IRI} etc.
74       * 
75       * @return {@link RDF} instance to be tested.
76       */
77      protected abstract RDF createFactory();
78  
79      @Before
80      public void createGraphAndAdd() {
81          factory = createFactory();
82          graph = factory.createGraph();
83          assertEquals(0, graph.size());
84  
85          alice = factory.createIRI("http://example.com/alice");
86          bob = factory.createIRI("http://example.com/bob");
87          name = factory.createIRI("http://xmlns.com/foaf/0.1/name");
88          knows = factory.createIRI("http://xmlns.com/foaf/0.1/knows");
89          member = factory.createIRI("http://xmlns.com/foaf/0.1/member");
90          try {
91              bnode1 = factory.createBlankNode("org1");
92              bnode2 = factory.createBlankNode("org2");
93          } catch (final UnsupportedOperationException ex) {
94              // leave as null
95          }
96  
97          try {
98              secretClubName = factory.createLiteral("The Secret Club");
99              companyName = factory.createLiteral("A company");
100             aliceName = factory.createLiteral("Alice");
101             bobName = factory.createLiteral("Bob", "en-US");
102         } catch (final UnsupportedOperationException ex) {
103             // leave as null
104         }
105 
106         if (aliceName != null) {
107             graph.add(alice, name, aliceName);
108         }
109         graph.add(alice, knows, bob);
110 
111         if (bnode1 != null) {
112             graph.add(alice, member, bnode1);
113         }
114 
115         if (bobName != null) {
116             try {
117                 bobNameTriple = factory.createTriple(bob, name, bobName);
118             } catch (final UnsupportedOperationException ex) {
119                 // leave as null
120             }
121             if (bobNameTriple != null) {
122                 graph.add(bobNameTriple);
123             }
124         }
125         if (bnode1 != null) {
126             graph.add(factory.createTriple(bob, member, bnode1));
127             graph.add(factory.createTriple(bob, member, bnode2));
128             if (secretClubName != null) {
129                 graph.add(bnode1, name, secretClubName);
130                 graph.add(bnode2, name, companyName);
131             }
132         }
133     }
134 
135     @Test
136     public void size() throws Exception {
137         assertTrue(graph.size() > 0);
138         Assume.assumeNotNull(bnode1, bnode2, aliceName, bobName, secretClubName, companyName, bobNameTriple);
139         // Can only reliably predict size if we could create all triples
140         assertEquals(8, graph.size());
141     }
142 
143     @Test
144     public void iterate() throws Exception {
145 
146         Assume.assumeTrue(graph.size() > 0);
147 
148         final List<Triple> triples = new ArrayList<>();
149         for (final Triple t : graph.iterate()) {
150             triples.add(t);
151         }
152         assertEquals(graph.size(), triples.size());
153         if (bobNameTriple != null) {
154             assertTrue(triples.contains(bobNameTriple));
155         }
156 
157         // aborted iteration
158         final Iterable<Triple> iterate = graph.iterate();
159         final Iterator<Triple> it = iterate.iterator();
160 
161         assertTrue(it.hasNext());
162         it.next();
163         closeIterable(iterate);
164 
165         // second iteration - should start from fresh and
166         // get the same count
167         long count = 0;
168         final Iterable<Triple> iterable = graph.iterate();
169         for (@SuppressWarnings("unused") final
170         Triple t : iterable) {
171             count++;
172         }
173         assertEquals(graph.size(), count);
174     }
175 
176     /**
177      * Special triple closing for RDF4J.
178      */
179     private void closeIterable(final Iterable<Triple> iterate) throws Exception {
180         if (iterate instanceof AutoCloseable) {
181             ((AutoCloseable) iterate).close();
182         }
183     }
184 
185     @Test
186     public void iterateFilter() throws Exception {
187         final List<RDFTerm> friends = new ArrayList<>();
188         final IRI alice = factory.createIRI("http://example.com/alice");
189         final IRI knows = factory.createIRI("http://xmlns.com/foaf/0.1/knows");
190         for (final Triple t : graph.iterate(alice, knows, null)) {
191             friends.add(t.getObject());
192         }
193         assertEquals(1, friends.size());
194         assertEquals(bob, friends.get(0));
195 
196         // .. can we iterate over zero hits?
197         final Iterable<Triple> iterate = graph.iterate(bob, knows, alice);
198         for (final Triple unexpected : iterate) {
199             fail("Unexpected triple " + unexpected);
200         }
201         // closeIterable(iterate);
202     }
203 
204     @Test
205     public void contains() throws Exception {
206         assertFalse(graph.contains(bob, knows, alice)); // or so he claims..
207 
208         assertTrue(graph.contains(alice, knows, bob));
209 
210         try (Stream<? extends Triple> stream = graph.stream()) {
211             final Optional<? extends Triple> first = stream.skip(4).findFirst();
212             Assume.assumeTrue(first.isPresent());
213             final Triple existingTriple = first.get();
214             assertTrue(graph.contains(existingTriple));
215         }
216 
217         final Triple nonExistingTriple = factory.createTriple(bob, knows, alice);
218         assertFalse(graph.contains(nonExistingTriple));
219 
220         Triple triple = null;
221         try {
222             triple = factory.createTriple(alice, knows, bob);
223         } catch (final UnsupportedOperationException ex) {
224         }
225         if (triple != null) {
226             // FIXME: Should not this always be true?
227             // assertTrue(graph.contains(triple));
228         }
229     }
230 
231     @Test
232     public void remove() throws Exception {
233         final long fullSize = graph.size();
234         graph.remove(alice, knows, bob);
235         final long shrunkSize = graph.size();
236         assertEquals(1, fullSize - shrunkSize);
237 
238         graph.remove(alice, knows, bob);
239         assertEquals(shrunkSize, graph.size()); // unchanged
240 
241         graph.add(alice, knows, bob);
242         graph.add(alice, knows, bob);
243         graph.add(alice, knows, bob);
244         // Undetermined size at this point -- but at least it
245         // should be bigger
246         assertTrue(graph.size() > shrunkSize);
247 
248         // and after a single remove they should all be gone
249         graph.remove(alice, knows, bob);
250         assertEquals(shrunkSize, graph.size());
251 
252         Triple otherTriple;
253         try (Stream<? extends Triple> stream = graph.stream()) {
254             final Optional<? extends Triple> anyTriple = stream.findAny();
255             Assume.assumeTrue(anyTriple.isPresent());
256             otherTriple = anyTriple.get();
257         }
258 
259         graph.remove(otherTriple);
260         assertEquals(shrunkSize - 1, graph.size());
261         graph.remove(otherTriple);
262         assertEquals(shrunkSize - 1, graph.size()); // no change
263 
264         // for some reason in rdf4j this causes duplicates!
265         graph.add(otherTriple);
266         // graph.stream().forEach(System.out::println);
267         // should have increased
268         assertTrue(graph.size() >= shrunkSize);
269     }
270 
271     @Test
272     public void clear() throws Exception {
273         graph.clear();
274         assertFalse(graph.contains(alice, knows, bob));
275         assertEquals(0, graph.size());
276         graph.clear(); // no-op
277         assertEquals(0, graph.size());
278     }
279 
280     @Test
281     public void getTriples() throws Exception {
282         long tripleCount;
283         try (Stream<? extends Triple> stream = graph.stream()) {
284             tripleCount = stream.count();
285         }
286         assertTrue(tripleCount > 0);
287 
288         try (Stream<? extends Triple> stream = graph.stream()) {
289             assertTrue(stream.allMatch(t -> graph.contains(t)));
290         }
291 
292         // Check exact count
293         Assume.assumeNotNull(bnode1, bnode2, aliceName, bobName, secretClubName, companyName, bobNameTriple);
294         assertEquals(8, tripleCount);
295     }
296 
297     @Test
298     public void getTriplesQuery() throws Exception {
299 
300         try (Stream<? extends Triple> stream = graph.stream(alice, null, null)) {
301             final long aliceCount = stream.count();
302             assertTrue(aliceCount > 0);
303             Assume.assumeNotNull(aliceName);
304             assertEquals(3, aliceCount);
305         }
306 
307         Assume.assumeNotNull(bnode1, bnode2, bobName, companyName, secretClubName);
308         try (Stream<? extends Triple> stream = graph.stream(null, name, null)) {
309             assertEquals(4, stream.count());
310         }
311         Assume.assumeNotNull(bnode1);
312         try (Stream<? extends Triple> stream = graph.stream(null, member, null)) {
313             assertEquals(3, stream.count());
314         }
315     }
316 
317     @Test
318     public void addBlankNodesFromMultipleGraphs() {
319 
320         try {
321             // Create two separate Graph instances
322             final Graph g1 = createGraph1();
323             final Graph g2 = createGraph2();
324 
325             // and add them to a new Graph g3
326             final Graph g3 = factory.createGraph();
327             addAllTriples(g1, g3);
328             addAllTriples(g2, g3);
329 
330             // Let's make a map to find all those blank nodes after insertion
331             // (The Graph implementation is not currently required to
332             // keep supporting those BlankNodes with contains() - see
333             // COMMONSRDF-15)
334 
335             final Map<String, BlankNodeOrIRI> whoIsWho = new ConcurrentHashMap<>();
336             // ConcurrentHashMap as we will try parallel forEach below,
337             // which should not give inconsistent results (it does with a
338             // HashMap!)
339 
340             // look up BlankNodes by name
341             final IRI name = factory.createIRI("http://xmlns.com/foaf/0.1/name");
342             try (Stream<? extends Triple> stream = g3.stream(null, name, null)) {
343                 stream.parallel().forEach(t -> whoIsWho.put(t.getObject().ntriplesString(), t.getSubject()));
344             }
345 
346             assertEquals(4, whoIsWho.size());
347             // and contains 4 unique values
348             assertEquals(4, new HashSet<>(whoIsWho.values()).size());
349 
350             final BlankNodeOrIRI b1Alice = whoIsWho.get("\"Alice\"");
351             assertNotNull(b1Alice);
352             final BlankNodeOrIRI b2Bob = whoIsWho.get("\"Bob\"");
353             assertNotNull(b2Bob);
354             final BlankNodeOrIRI b1Charlie = whoIsWho.get("\"Charlie\"");
355             assertNotNull(b1Charlie);
356             final BlankNodeOrIRI b2Dave = whoIsWho.get("\"Dave\"");
357             assertNotNull(b2Dave);
358 
359             // All blank nodes should differ
360             notEquals(b1Alice, b2Bob);
361             notEquals(b1Alice, b1Charlie);
362             notEquals(b1Alice, b2Dave);
363             notEquals(b2Bob, b1Charlie);
364             notEquals(b2Bob, b2Dave);
365             notEquals(b1Charlie, b2Dave);
366 
367             // And we should be able to query with them again
368             // as we got them back from g3
369             final IRI hasChild = factory.createIRI("http://example.com/hasChild");
370             assertTrue(g3.contains(b1Alice, hasChild, b2Bob));
371             assertTrue(g3.contains(b2Dave, hasChild, b1Charlie));
372             // But not
373             assertFalse(g3.contains(b1Alice, hasChild, b1Alice));
374             assertFalse(g3.contains(b1Alice, hasChild, b1Charlie));
375             assertFalse(g3.contains(b1Alice, hasChild, b2Dave));
376             // nor
377             assertFalse(g3.contains(b2Dave, hasChild, b1Alice));
378             assertFalse(g3.contains(b2Dave, hasChild, b1Alice));
379 
380             // and these don't have any children (as far as we know)
381             assertFalse(g3.contains(b2Bob, hasChild, null));
382             assertFalse(g3.contains(b1Charlie, hasChild, null));
383         } catch (final UnsupportedOperationException ex) {
384             Assume.assumeNoException(ex);
385         }
386     }
387 
388     private void notEquals(final BlankNodeOrIRI node1, final BlankNodeOrIRI node2) {
389         assertFalse(node1.equals(node2));
390         // in which case we should be able to assume
391         // (as they are in the same graph)
392         assertFalse(node1.ntriplesString().equals(node2.ntriplesString()));
393     }
394 
395     /**
396      * Add all triples from the source to the target.
397      * <p>
398      * The triples may be copied in any order. No special conversion or
399      * adaptation of {@link BlankNode}s are performed.
400      *
401      * @param source
402      *            Source Graph to copy triples from
403      * @param target
404      *            Target Graph where triples will be added
405      */
406     private void addAllTriples(final Graph source, final Graph target) {
407 
408         // unordered() as we don't need to preserve triple order
409         // sequential() as we don't (currently) require target Graph to be
410         // thread-safe
411 
412         try (Stream<? extends Triple> stream = source.stream()) {
413             stream.unordered().sequential().forEach(t -> target.add(t));
414         }
415     }
416 
417     /**
418      * Make a new graph with two BlankNodes - each with a different
419      * uniqueReference
420      */
421     private Graph createGraph1() {
422         final RDF factory1 = createFactory();
423 
424         final IRI name = factory1.createIRI("http://xmlns.com/foaf/0.1/name");
425         final Graph g1 = factory1.createGraph();
426         final BlankNode b1 = createOwnBlankNode("b1", "0240eaaa-d33e-4fc0-a4f1-169d6ced3680");
427         g1.add(b1, name, factory1.createLiteral("Alice"));
428 
429         final BlankNode b2 = createOwnBlankNode("b2", "9de7db45-0ce7-4b0f-a1ce-c9680ffcfd9f");
430         g1.add(b2, name, factory1.createLiteral("Bob"));
431 
432         final IRI hasChild = factory1.createIRI("http://example.com/hasChild");
433         g1.add(b1, hasChild, b2);
434 
435         return g1;
436     }
437 
438     /**
439      * Create a different implementation of BlankNode to be tested with
440      * graph.add(a,b,c); (the implementation may or may not then choose to
441      * translate such to its own instances)
442      * 
443      * @param name
444      * @return
445      */
446     private BlankNode createOwnBlankNode(final String name, final String uuid) {
447         return new BlankNode() {
448             @Override
449             public String ntriplesString() {
450                 return "_: " + name;
451             }
452 
453             @Override
454             public String uniqueReference() {
455                 return uuid;
456             }
457 
458             @Override
459             public int hashCode() {
460                 return uuid.hashCode();
461             }
462 
463             @Override
464             public boolean equals(final Object obj) {
465                 if (!(obj instanceof BlankNode)) {
466                     return false;
467                 }
468                 final BlankNode other = (BlankNode) obj;
469                 return uuid.equals(other.uniqueReference());
470             }
471         };
472     }
473 
474     private Graph createGraph2() {
475         final RDF factory2 = createFactory();
476         final IRI name = factory2.createIRI("http://xmlns.com/foaf/0.1/name");
477 
478         final Graph g2 = factory2.createGraph();
479 
480         final BlankNode b1 = createOwnBlankNode("b1", "bc8d3e45-a08f-421d-85b3-c25b373abf87");
481         g2.add(b1, name, factory2.createLiteral("Charlie"));
482 
483         final BlankNode b2 = createOwnBlankNode("b2", "2209097a-5078-4b03-801a-6a2d2f50d739");
484         g2.add(b2, name, factory2.createLiteral("Dave"));
485 
486         final IRI hasChild = factory2.createIRI("http://example.com/hasChild");
487         // NOTE: Opposite direction of loadGraph1
488         g2.add(b2, hasChild, b1);
489         return g2;
490     }
491 
492     /**
493      * An attempt to use the Java 8 streams to look up a more complicated query.
494      * <p>
495      * FYI, the equivalent SPARQL version (untested):
496      * 
497      * <pre>
498      *     SELECT ?orgName WHERE {
499      *             ?org foaf:name ?orgName .
500      *             ?alice foaf:member ?org .
501      *             ?bob foaf:member ?org .
502      *             ?alice foaf:knows ?bob .
503      *           FILTER NOT EXIST { ?bob foaf:knows ?alice }
504      *    }
505      * </pre>
506      *
507      * @throws Exception If test fails
508      */
509     @Test
510     public void whyJavaStreamsMightNotTakeOverFromSparql() throws Exception {
511         Assume.assumeNotNull(bnode1, bnode2, secretClubName);
512         // Find a secret organizations
513         try (Stream<? extends Triple> stream = graph.stream(null, knows, null)) {
514             assertEquals("\"The Secret Club\"",
515                     // Find One-way "knows"
516                     stream.filter(t -> !graph.contains((BlankNodeOrIRI) t.getObject(), knows, t.getSubject()))
517                             .map(knowsTriple -> {
518                                 try (Stream<? extends Triple> memberOf = graph
519                                         // and those they know, what are they
520                                         // member of?
521                                         .stream((BlankNodeOrIRI) knowsTriple.getObject(), member, null)) {
522                                     return memberOf
523                                             // keep those which first-guy is a
524                                             // member of
525                                             .filter(memberTriple -> graph.contains(knowsTriple.getSubject(), member,
526                                                     // First hit is good enough
527                                                     memberTriple.getObject()))
528                                             .findFirst().get().getObject();
529                                 }
530                             })
531                             // then look up the name of that org
532                             .map(org -> {
533                                 try (Stream<? extends Triple> orgName = graph.stream((BlankNodeOrIRI) org, name,
534                                         null)) {
535                                     return orgName.findFirst().get().getObject().ntriplesString();
536                                 }
537                             }).findFirst().get());
538         }
539     }
540 }