blob: 898740f5486f854a9bca5676a3c497b8846622df [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.rdf.api;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
/**
* Test Graph implementation
* <p>
* To add to your implementation's tests, create a subclass with a name ending
* in <code>Test</code> and provide {@link #createFactory()} which minimally
* must support {@link RDF#createGraph()} and {@link RDF#createIRI(String)}, but
* ideally support all operations.
* <p>
* This test uses try-with-resources blocks for calls to {@link Graph#stream()}
* and {@link Graph#iterate()}.
*
* @see Graph
* @see RDF
*/
public abstract class AbstractGraphTest {
protected RDF factory;
protected Graph graph;
protected IRI alice;
protected IRI bob;
protected IRI name;
protected IRI knows;
protected IRI member;
protected BlankNode bnode1;
protected BlankNode bnode2;
protected Literal aliceName;
protected Literal bobName;
protected Literal secretClubName;
protected Literal companyName;
protected Triple bobNameTriple;
/**
*
* This method must be overridden by the implementing test to provide a
* factory for the test to create {@link Graph}, {@link IRI} etc.
*
* @return {@link RDF} instance to be tested.
*/
protected abstract RDF createFactory();
@Before
public void createGraphAndAdd() {
factory = createFactory();
graph = factory.createGraph();
assertEquals(0, graph.size());
alice = factory.createIRI("http://example.com/alice");
bob = factory.createIRI("http://example.com/bob");
name = factory.createIRI("http://xmlns.com/foaf/0.1/name");
knows = factory.createIRI("http://xmlns.com/foaf/0.1/knows");
member = factory.createIRI("http://xmlns.com/foaf/0.1/member");
try {
bnode1 = factory.createBlankNode("org1");
bnode2 = factory.createBlankNode("org2");
} catch (final UnsupportedOperationException ex) {
// leave as null
}
try {
secretClubName = factory.createLiteral("The Secret Club");
companyName = factory.createLiteral("A company");
aliceName = factory.createLiteral("Alice");
bobName = factory.createLiteral("Bob", "en-US");
} catch (final UnsupportedOperationException ex) {
// leave as null
}
if (aliceName != null) {
graph.add(alice, name, aliceName);
}
graph.add(alice, knows, bob);
if (bnode1 != null) {
graph.add(alice, member, bnode1);
}
if (bobName != null) {
try {
bobNameTriple = factory.createTriple(bob, name, bobName);
} catch (final UnsupportedOperationException ex) {
// leave as null
}
if (bobNameTriple != null) {
graph.add(bobNameTriple);
}
}
if (bnode1 != null) {
graph.add(factory.createTriple(bob, member, bnode1));
graph.add(factory.createTriple(bob, member, bnode2));
if (secretClubName != null) {
graph.add(bnode1, name, secretClubName);
graph.add(bnode2, name, companyName);
}
}
}
@Test
public void size() throws Exception {
assertFalse(graph.isEmpty());
Assume.assumeNotNull(bnode1, bnode2, aliceName, bobName, secretClubName, companyName, bobNameTriple);
// Can only reliably predict size if we could create all triples
assertEquals(8, graph.size());
}
@Test
public void iterate() throws Exception {
Assume.assumeFalse(graph.isEmpty());
final List<Triple> triples = new ArrayList<>();
for (final Triple t : graph.iterate()) {
triples.add(t);
}
assertEquals(graph.size(), triples.size());
if (bobNameTriple != null) {
assertTrue(triples.contains(bobNameTriple));
}
// aborted iteration
final Iterable<Triple> iterate = graph.iterate();
final Iterator<Triple> it = iterate.iterator();
assertTrue(it.hasNext());
it.next();
closeIterable(iterate);
// second iteration - should start from fresh and
// get the same count
long count = 0;
final Iterable<Triple> iterable = graph.iterate();
for (@SuppressWarnings("unused") final
Triple t : iterable) {
count++;
}
assertEquals(graph.size(), count);
}
/**
* Special triple closing for RDF4J.
*/
private void closeIterable(final Iterable<Triple> iterate) throws Exception {
if (iterate instanceof AutoCloseable) {
((AutoCloseable) iterate).close();
}
}
@Test
public void iterateFilter() throws Exception {
final List<RDFTerm> friends = new ArrayList<>();
final IRI alice = factory.createIRI("http://example.com/alice");
final IRI knows = factory.createIRI("http://xmlns.com/foaf/0.1/knows");
for (final Triple t : graph.iterate(alice, knows, null)) {
friends.add(t.getObject());
}
assertEquals(1, friends.size());
assertEquals(bob, friends.get(0));
// .. can we iterate over zero hits?
final Iterable<Triple> iterate = graph.iterate(bob, knows, alice);
for (final Triple unexpected : iterate) {
fail("Unexpected triple " + unexpected);
}
// closeIterable(iterate);
}
@Test
public void contains() throws Exception {
assertFalse(graph.contains(bob, knows, alice)); // or so he claims..
assertTrue(graph.contains(alice, knows, bob));
try (Stream<? extends Triple> stream = graph.stream()) {
final Optional<? extends Triple> first = stream.skip(4).findFirst();
Assume.assumeTrue(first.isPresent());
final Triple existingTriple = first.get();
assertTrue(graph.contains(existingTriple));
}
final Triple nonExistingTriple = factory.createTriple(bob, knows, alice);
assertFalse(graph.contains(nonExistingTriple));
Triple triple = null;
try {
triple = factory.createTriple(alice, knows, bob);
} catch (final UnsupportedOperationException ex) {
}
if (triple != null) {
// FIXME: Should not this always be true?
// assertTrue(graph.contains(triple));
}
}
@Test
public void remove() throws Exception {
final long fullSize = graph.size();
graph.remove(alice, knows, bob);
final long shrunkSize = graph.size();
assertEquals(1, fullSize - shrunkSize);
graph.remove(alice, knows, bob);
assertEquals(shrunkSize, graph.size()); // unchanged
graph.add(alice, knows, bob);
graph.add(alice, knows, bob);
graph.add(alice, knows, bob);
// Undetermined size at this point -- but at least it
// should be bigger
assertTrue(graph.size() > shrunkSize);
// and after a single remove they should all be gone
graph.remove(alice, knows, bob);
assertEquals(shrunkSize, graph.size());
Triple otherTriple;
try (Stream<? extends Triple> stream = graph.stream()) {
final Optional<? extends Triple> anyTriple = stream.findAny();
Assume.assumeTrue(anyTriple.isPresent());
otherTriple = anyTriple.get();
}
graph.remove(otherTriple);
assertEquals(shrunkSize - 1, graph.size());
graph.remove(otherTriple);
assertEquals(shrunkSize - 1, graph.size()); // no change
// for some reason in rdf4j this causes duplicates!
graph.add(otherTriple);
// graph.stream().forEach(System.out::println);
// should have increased
assertTrue(graph.size() >= shrunkSize);
}
@Test
public void clear() throws Exception {
graph.clear();
assertFalse(graph.contains(alice, knows, bob));
assertEquals(0, graph.size());
graph.clear(); // no-op
assertEquals(0, graph.size());
}
@Test
public void getTriples() throws Exception {
long tripleCount;
try (Stream<? extends Triple> stream = graph.stream()) {
tripleCount = stream.count();
}
assertTrue(tripleCount > 0);
try (Stream<? extends Triple> stream = graph.stream()) {
assertTrue(stream.allMatch(t -> graph.contains(t)));
}
// Check exact count
Assume.assumeNotNull(bnode1, bnode2, aliceName, bobName, secretClubName, companyName, bobNameTriple);
assertEquals(8, tripleCount);
}
@Test
public void getTriplesQuery() throws Exception {
try (Stream<? extends Triple> stream = graph.stream(alice, null, null)) {
final long aliceCount = stream.count();
assertTrue(aliceCount > 0);
Assume.assumeNotNull(aliceName);
assertEquals(3, aliceCount);
}
Assume.assumeNotNull(bnode1, bnode2, bobName, companyName, secretClubName);
try (Stream<? extends Triple> stream = graph.stream(null, name, null)) {
assertEquals(4, stream.count());
}
Assume.assumeNotNull(bnode1);
try (Stream<? extends Triple> stream = graph.stream(null, member, null)) {
assertEquals(3, stream.count());
}
}
@Test
public void addBlankNodesFromMultipleGraphs() throws Exception {
// Create two separate Graph instances
// and add them to a new Graph g3
try (final Graph g1 = createGraph1(); final Graph g2 = createGraph2(); final Graph g3 = factory.createGraph()) {
addAllTriples(g1, g3);
addAllTriples(g2, g3);
// Let's make a map to find all those blank nodes after insertion
// (The Graph implementation is not currently required to
// keep supporting those BlankNodes with contains() - see
// COMMONSRDF-15)
final Map<String, BlankNodeOrIRI> whoIsWho = new ConcurrentHashMap<>();
// ConcurrentHashMap as we will try parallel forEach below,
// which should not give inconsistent results (it does with a
// HashMap!)
// look up BlankNodes by name
final IRI name = factory.createIRI("http://xmlns.com/foaf/0.1/name");
try (Stream<? extends Triple> stream = g3.stream(null, name, null)) {
stream.parallel().forEach(t -> whoIsWho.put(t.getObject().ntriplesString(), t.getSubject()));
}
assertEquals(4, whoIsWho.size());
// and contains 4 unique values
assertEquals(4, new HashSet<>(whoIsWho.values()).size());
final BlankNodeOrIRI b1Alice = whoIsWho.get("\"Alice\"");
assertNotNull(b1Alice);
final BlankNodeOrIRI b2Bob = whoIsWho.get("\"Bob\"");
assertNotNull(b2Bob);
final BlankNodeOrIRI b1Charlie = whoIsWho.get("\"Charlie\"");
assertNotNull(b1Charlie);
final BlankNodeOrIRI b2Dave = whoIsWho.get("\"Dave\"");
assertNotNull(b2Dave);
// All blank nodes should differ
notEquals(b1Alice, b2Bob);
notEquals(b1Alice, b1Charlie);
notEquals(b1Alice, b2Dave);
notEquals(b2Bob, b1Charlie);
notEquals(b2Bob, b2Dave);
notEquals(b1Charlie, b2Dave);
// And we should be able to query with them again
// as we got them back from g3
final IRI hasChild = factory.createIRI("http://example.com/hasChild");
assertTrue(g3.contains(b1Alice, hasChild, b2Bob));
assertTrue(g3.contains(b2Dave, hasChild, b1Charlie));
// But not
assertFalse(g3.contains(b1Alice, hasChild, b1Alice));
assertFalse(g3.contains(b1Alice, hasChild, b1Charlie));
assertFalse(g3.contains(b1Alice, hasChild, b2Dave));
// nor
assertFalse(g3.contains(b2Dave, hasChild, b1Alice));
assertFalse(g3.contains(b2Dave, hasChild, b1Alice));
// and these don't have any children (as far as we know)
assertFalse(g3.contains(b2Bob, hasChild, null));
assertFalse(g3.contains(b1Charlie, hasChild, null));
} catch (final UnsupportedOperationException ex) {
Assume.assumeNoException(ex);
}
}
@Test
public void containsLanguageTagsCaseInsensitive() throws Exception {
// COMMONSRDF-51: Ensure we can add/contains/remove with any casing
// of literal language tag
final Literal lower = factory.createLiteral("Hello", "en-gb");
final Literal upper = factory.createLiteral("Hello", "EN-GB");
final Literal mixed = factory.createLiteral("Hello", "en-GB");
final IRI example1 = factory.createIRI("http://example.com/s1");
final IRI greeting = factory.createIRI("http://example.com/greeting");
try (final Graph graph = factory.createGraph()) {
graph.add(example1, greeting, upper);
// any kind of Triple should match
assertTrue(graph.contains(factory.createTriple(example1, greeting, upper)));
assertTrue(graph.contains(factory.createTriple(example1, greeting, lower)));
assertTrue(graph.contains(factory.createTriple(example1, greeting, mixed)));
// or as patterns
assertTrue(graph.contains(null, null, upper));
assertTrue(graph.contains(null, null, lower));
assertTrue(graph.contains(null, null, mixed));
}
}
@Test
public void containsLanguageTagsCaseInsensitiveTurkish() throws Exception {
// COMMONSRDF-51: Special test for Turkish issue where
// "i".toLowerCase() != "i"
// See also:
// https://garygregory.wordpress.com/2015/11/03/java-lowercase-conversion-turkey/
// This is similar to the test in AbstractRDFTest, but on a graph
final Locale defaultLocale = Locale.getDefault();
try (final Graph g = factory.createGraph()) {
Locale.setDefault(Locale.ROOT);
final Literal lowerROOT = factory.createLiteral("moi", "fi");
final Literal upperROOT = factory.createLiteral("moi", "FI");
final Literal mixedROOT = factory.createLiteral("moi", "fI");
final IRI exampleROOT = factory.createIRI("http://example.com/s1");
final IRI greeting = factory.createIRI("http://example.com/greeting");
g.add(exampleROOT, greeting, mixedROOT);
final Locale turkish = Locale.forLanguageTag("TR");
Locale.setDefault(turkish);
// If the below assertion fails, then the Turkish
// locale no longer have this peculiarity that
// we want to test.
Assume.assumeFalse("FI".toLowerCase().equals("fi"));
// Below is pretty much the same as in
// containsLanguageTagsCaseInsensitive()
final Literal lower = factory.createLiteral("moi", "fi");
final Literal upper = factory.createLiteral("moi", "FI");
final Literal mixed = factory.createLiteral("moi", "fI");
final IRI exampleTR = factory.createIRI("http://example.com/s2");
g.add(exampleTR, greeting, upper);
assertTrue(g.contains(factory.createTriple(exampleTR, greeting, upper)));
assertTrue(g.contains(factory.createTriple(exampleTR, greeting, upperROOT)));
assertTrue(g.contains(factory.createTriple(exampleTR, greeting, lower)));
assertTrue(g.contains(factory.createTriple(exampleTR, greeting, lowerROOT)));
assertTrue(g.contains(factory.createTriple(exampleTR, greeting, mixed)));
assertTrue(g.contains(factory.createTriple(exampleTR, greeting, mixedROOT)));
assertTrue(g.contains(exampleTR, null, upper));
assertTrue(g.contains(exampleTR, null, upperROOT));
assertTrue(g.contains(exampleTR, null, lower));
assertTrue(g.contains(exampleTR, null, lowerROOT));
assertTrue(g.contains(exampleTR, null, mixed));
assertTrue(g.contains(exampleTR, null, mixedROOT));
// What about the triple we added while in ROOT locale?
assertTrue(g.contains(factory.createTriple(exampleROOT, greeting, upper)));
assertTrue(g.contains(factory.createTriple(exampleROOT, greeting, lower)));
assertTrue(g.contains(factory.createTriple(exampleROOT, greeting, mixed)));
assertTrue(g.contains(exampleROOT, null, upper));
assertTrue(g.contains(exampleROOT, null, lower));
assertTrue(g.contains(exampleROOT, null, mixed));
} finally {
Locale.setDefault(defaultLocale);
}
}
@Test
public void removeLanguageTagsCaseInsensitive() throws Exception {
// COMMONSRDF-51: Ensure we can remove with any casing
// of literal language tag
final Literal lower = factory.createLiteral("Hello", "en-gb");
final Literal upper = factory.createLiteral("Hello", "EN-GB");
final Literal mixed = factory.createLiteral("Hello", "en-GB");
final IRI example1 = factory.createIRI("http://example.com/s1");
final IRI greeting = factory.createIRI("http://example.com/greeting");
try (final Graph graph = factory.createGraph()) {
graph.add(example1, greeting, upper);
// Remove should also honor any case
graph.remove(example1, null, mixed);
assertFalse(graph.contains(null, greeting, null));
graph.add(example1, greeting, lower);
graph.remove(example1, null, upper);
// Check with Triple
graph.add(factory.createTriple(example1, greeting, mixed));
graph.remove(factory.createTriple(example1, greeting, upper));
assertFalse(graph.contains(null, greeting, null));
}
}
private static Optional<? extends Triple> closableFindAny(final Stream<? extends Triple> stream) {
try (Stream<? extends Triple> s = stream) {
return s.findAny();
}
}
@Test
public void streamLanguageTagsCaseInsensitive() throws Exception {
// COMMONSRDF-51: Ensure we can add/contains/remove with any casing
// of literal language tag
final Literal lower = factory.createLiteral("Hello", "en-gb");
final Literal upper = factory.createLiteral("Hello", "EN-GB");
final Literal mixed = factory.createLiteral("Hello", "en-GB");
final IRI example1 = factory.createIRI("http://example.com/s1");
final IRI greeting = factory.createIRI("http://example.com/greeting");
try (final Graph graph = factory.createGraph()) {
graph.add(example1, greeting, upper);
// or as patterns
assertTrue(closableFindAny(graph.stream(null, null, upper)).isPresent());
assertTrue(closableFindAny(graph.stream(null, null, lower)).isPresent());
assertTrue(closableFindAny(graph.stream(null, null, mixed)).isPresent());
// Check the triples returned equal a new triple
final Triple t = closableFindAny(graph.stream(null, null, lower)).get();
assertEquals(t, factory.createTriple(example1, greeting, mixed));
}
}
private void notEquals(final BlankNodeOrIRI node1, final BlankNodeOrIRI node2) {
assertFalse(node1.equals(node2));
// in which case we should be able to assume
// (as they are in the same graph)
assertFalse(node1.ntriplesString().equals(node2.ntriplesString()));
}
/**
* Add all triples from the source to the target.
* <p>
* The triples may be copied in any order. No special conversion or
* adaptation of {@link BlankNode}s are performed.
*
* @param source
* Source Graph to copy triples from
* @param target
* Target Graph where triples will be added
*/
private void addAllTriples(final Graph source, final Graph target) {
// unordered() as we don't need to preserve triple order
// sequential() as we don't (currently) require target Graph to be
// thread-safe
try (Stream<? extends Triple> stream = source.stream()) {
stream.unordered().sequential().forEach(t -> target.add(t));
}
}
/**
* Make a new graph with two BlankNodes - each with a different
* uniqueReference
*/
private Graph createGraph1() {
final RDF factory1 = createFactory();
final IRI name = factory1.createIRI("http://xmlns.com/foaf/0.1/name");
final Graph g1 = factory1.createGraph();
final BlankNode b1 = createOwnBlankNode("b1", "0240eaaa-d33e-4fc0-a4f1-169d6ced3680");
g1.add(b1, name, factory1.createLiteral("Alice"));
final BlankNode b2 = createOwnBlankNode("b2", "9de7db45-0ce7-4b0f-a1ce-c9680ffcfd9f");
g1.add(b2, name, factory1.createLiteral("Bob"));
final IRI hasChild = factory1.createIRI("http://example.com/hasChild");
g1.add(b1, hasChild, b2);
return g1;
}
/**
* Create a different implementation of BlankNode to be tested with
* graph.add(a,b,c); (the implementation may or may not then choose to
* translate such to its own instances)
*
* @param name
* @return
*/
private BlankNode createOwnBlankNode(final String name, final String uuid) {
return new BlankNode() {
@Override
public String ntriplesString() {
return "_: " + name;
}
@Override
public String uniqueReference() {
return uuid;
}
@Override
public int hashCode() {
return uuid.hashCode();
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof BlankNode)) {
return false;
}
final BlankNode other = (BlankNode) obj;
return uuid.equals(other.uniqueReference());
}
};
}
private Graph createGraph2() {
final RDF factory2 = createFactory();
final IRI name = factory2.createIRI("http://xmlns.com/foaf/0.1/name");
final Graph g2 = factory2.createGraph();
final BlankNode b1 = createOwnBlankNode("b1", "bc8d3e45-a08f-421d-85b3-c25b373abf87");
g2.add(b1, name, factory2.createLiteral("Charlie"));
final BlankNode b2 = createOwnBlankNode("b2", "2209097a-5078-4b03-801a-6a2d2f50d739");
g2.add(b2, name, factory2.createLiteral("Dave"));
final IRI hasChild = factory2.createIRI("http://example.com/hasChild");
// NOTE: Opposite direction of loadGraph1
g2.add(b2, hasChild, b1);
return g2;
}
/**
* An attempt to use the Java 8 streams to look up a more complicated query.
* <p>
* FYI, the equivalent SPARQL version (untested):
*
* <pre>
* SELECT ?orgName WHERE {
* ?org foaf:name ?orgName .
* ?alice foaf:member ?org .
* ?bob foaf:member ?org .
* ?alice foaf:knows ?bob .
* FILTER NOT EXIST { ?bob foaf:knows ?alice }
* }
* </pre>
*
* @throws Exception If test fails
*/
@Test
public void whyJavaStreamsMightNotTakeOverFromSparql() throws Exception {
Assume.assumeNotNull(bnode1, bnode2, secretClubName);
// Find a secret organizations
try (Stream<? extends Triple> stream = graph.stream(null, knows, null)) {
assertEquals("\"The Secret Club\"",
// Find One-way "knows"
stream.filter(t -> !graph.contains((BlankNodeOrIRI) t.getObject(), knows, t.getSubject()))
.map(knowsTriple -> {
try (Stream<? extends Triple> memberOf = graph
// and those they know, what are they
// member of?
.stream((BlankNodeOrIRI) knowsTriple.getObject(), member, null)) {
return memberOf
// keep those which first-guy is a
// member of
.filter(memberTriple -> graph.contains(knowsTriple.getSubject(), member,
// First hit is good enough
memberTriple.getObject()))
.findFirst().get().getObject();
}
})
// then look up the name of that org
.map(org -> {
try (Stream<? extends Triple> orgName = graph.stream((BlankNodeOrIRI) org, name,
null)) {
return orgName.findFirst().get().getObject().ntriplesString();
}
}).findFirst().get());
}
}
}