This is an automated email from the ASF dual-hosted git repository. afs pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/jena.git
commit 6d5d2c76c37c367b30dcf037a8f7949cd1dbe622 Author: Andy Seaborne <[email protected]> AuthorDate: Sun May 3 20:49:28 2026 +0100 GList and tests --- .../main/java/org/apache/jena/system/GList.java | 501 +++++++++++++++++++++ .../java/org/apache/jena/system/TestGList.java | 469 +++++++++++++++++++ 2 files changed, 970 insertions(+) diff --git a/jena-arq/src/main/java/org/apache/jena/system/GList.java b/jena-arq/src/main/java/org/apache/jena/system/GList.java new file mode 100644 index 0000000000..6880e2b2f4 --- /dev/null +++ b/jena-arq/src/main/java/org/apache/jena/system/GList.java @@ -0,0 +1,501 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.system; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.Triple; +import org.apache.jena.sparql.util.graph.GNode; +import org.apache.jena.sparql.util.graph.GraphList; +import org.apache.jena.sys.JenaSystem; +import org.apache.jena.util.iterator.ExtendedIterator; +import org.apache.jena.vocabulary.RDF; + +/** + * Operations on RDF Collections (RDF Lists). + * <p> + * Operations may throw {@link RDFDataException} if the list is not well-formed. + * <p> + * Operations check for exactly {@code rdf:first}, and exactly one {@code rdf:rest}, + * but do not check for cycles unless noted in their javadoc. + * <p> + * To get a list of all the items in a list, use {@link #members} operations, which + * does check for cycles, or if the list is known to be well-formed, one of the {@link #elements} + * operations, which do not check for cycles. + * <p> + * The {@link #contains} operation does not check for cycles so that repeated use on + * a list does not perform duplicate checking work repeatedly. + * <p> + * If a list is potentially mal-formed by having a cycle, check first with + * {@link #isWellformedList(Graph, Node)} or + * {@link #isWellformedListEx(Graph, Node)}. + * <p> + * List validation only needs to be performed once. + * <p> + * List arising from parsing Turtle or TriG syntax for lists will be cycle-free. + * <p> + * <b>This class is <em>not</em> public API</b>. + * + * @see GraphList - uses a findable abstaction ({@link GNode}to work on a graph or lists of triples. + */ +public class GList { + + static { JenaSystem.init(); } + + private static final Node CAR = RDF.Nodes.first; + private static final Node CDR = RDF.Nodes.rest; + private static final Node NIL = RDF.Nodes.nil; + private static final Node RDF_TYPE = RDF.Nodes.type; + private static final long BAD_LIST = -1; + private static final Function<String, RuntimeException> stdExceptionMaker = RDFDataException::new; + + // forEach (unchecked). + +// occurs(GNode, Node) +// contains(GNode, Node) +// get(GNode, int) +// index(GNode, Node) +// indexes(GNode, Node) +// triples(GNode, Collection<Triple>) +// allTriples(GNode) +// allTriples(GNode, Collection<Triple>) +// findAllLists(Graph) +// listToTriples(List<Node>, BasicPattern) + + + /** + * Check for a valid list; throw a {@link RDFDataException} if the list is bad in some way. + * The {@link RDFDataException} message indicates the problem with the list. + * @param graph + * @param list + */ + public static void isWellformedListEx(Graph graph, Node list) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + forEachMember(graph, list, false, null, stdExceptionMaker); + } + + /** Check for a valid list; throw a {@link RDFDataException} if the list is bad in some way. + * The {@link RDFDataException} message indicates the problem with the list. + * @param graph + * @param list + * @param closedCells - whether to check that only {@code rdf:first} and {@code rdf:next} are used on a list cell + */ + public static void isWellformedListEx(Graph graph, Node list, boolean closedCells) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + forEachMember(graph, list, closedCells, null, stdExceptionMaker); + } + + /** + * Check for a valid list; return true if well-formed else return false. + * Use {@link #isWellformedListEx(Graph, Node)} to get an except with an + * informational message about the problem detected. + * + * @param graph + * @param list + */ + public static boolean isWellformedList(Graph graph, Node list) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + return forEachMember(graph, list, false, null, null) != BAD_LIST; + } + + /** Check for a valid list; return true if well-formed else return false. + * Use {@link #isWellformedListEx(Graph, Node, boolean)} to get an except with an + * informational message about the problem detected. + * @param graph + * @param list + * @param closedCells - whether to check that only rdf:first and rdf:next are used on a list cell + */ + public static boolean isWellformedList(Graph graph, Node list, boolean closedCells) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + return forEachMember(graph, list, closedCells, null, null) != BAD_LIST; + } + + /** + * Return the members of a well-formed RDF list. + * @throws RDFDataException if the list is not well-formed, including if it has a cycle. + */ + public static List<Node> members(Graph graph, Node list) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + if ( list.equals(NIL) ) + return List.of(); + List<Node> acc = new ArrayList<>(); + forEachMember(graph, list, false, acc::add, stdExceptionMaker); + return acc; + } + + /** + * Accumulate the members of a well-formed RDF list. + * @throws RDFDataException if the list is not well-formed, including if it has a cycle. + */ + public static void members(Graph graph, Node list, Collection<Node> acc) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + if ( list.equals(NIL) ) + return; + forEachMember(graph, list, false, acc::add, stdExceptionMaker); + } + + /** + * Return the elements of a well-formed RDF list. + * This operation does not check for cycles. + * {@link #members} is has the same result and incorporates a well-formness cycle check. + */ + public static List<Node> elements(Graph graph, Node list) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + + if ( list.equals(NIL) ) + return List.of(); + List<Node> acc = new ArrayList<>(); + elements(graph, list, acc); + return acc; + } + + /** + * Accumulate the elements of a well-formed RDF list. + * This operation does not check for cycles. + */ + public static void elements(Graph graph, Node list, Collection<Node> acc) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + Objects.requireNonNull(acc); + if ( list.equals(NIL) ) + return; + Node cell = list; + while ( ! listEnd(graph, cell) ) { + Node elt = G.getOneSP(graph, cell, CAR); + Node next = G.getOneSP(graph, cell, CDR); + acc.add(elt); + cell = next; + } + } + + /** + * Run an action on each element of a list, in order. + * @see #iterator(Graph, Node) + */ + public static void forEach(Graph graph, Node list, Consumer<Node> action) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + Objects.requireNonNull(action); + if ( list.equals(NIL) ) + return; + // No cycle checking. + long countElts = 0; + Node cell = list; + while(!listEnd(graph, cell) ) { + Node elt = G.getOneSP(graph, cell, CAR); + Node next = G.getOneSP(graph, cell, CDR); + cell = next; + action.accept(elt); + } + } + + /** + * Return the length of a well-formed list. + * This operations assumes the list is well-formed. + * See {@link #isWellformedList(Graph, Node)} for check a list. + */ + public static long listLength(Graph graph, Node list) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + if ( list.equals(NIL) ) + return 0; + // No cycle checking. + long countElts = 0; + Node cell = list; + while(!listEnd(graph, cell) ) { + // Check integrity. + G.hasOneSP(graph, cell, CDR); + Node next = G.getOneSP(graph, cell, CDR); + cell = next; + countElts++; + } + return countElts; + } + + // -------- + + /** + * Return an iterator over the list. + * The iterator does not check for well-formed lists. + * Ensure the list is well-formed - {@link #isWellformedListEx(Graph, Node)}. + * + * @see #forEach + */ + public static Iterator<Node> iterator(Graph graph, Node list) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + return new RDFListIterator(graph, list); + } + + /** + * Return the first index of an occurrence of a node in a list. + * Return -1 for not in list. + * Indexes start at 0. + * @param graph + * @param list + * @param item to check + */ + public static int indexOf(Graph graph, Node list, Node item) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + Objects.requireNonNull(item); + Node cell = list; + int index = 0; + while( !listEnd(graph, cell) ) { + Node elt = G.getOneSP(graph, cell, CAR); + Node next = G.getOneSP(graph, cell, CDR); + cell = next; + if ( item.sameTermAs(elt) ) + return index; + index++; + } + return -1; + } + + /** + * Return whether an item is in the list. + * Return for not in list. + * @param graph + * @param list + * @param item to check for + */ + public static boolean contains(Graph graph, Node list, Node item) { + return indexOf(graph, list, item) >= 0; + } + + /** + * Return the list element at an index (indexes start at 0). + * <p> + * Do not use this to iterate over a list - consider using {@link #members} to + * collect the list in a single pass. + */ + public static Node get(Graph graph, Node list, int index) { + Objects.requireNonNull(graph); + Objects.requireNonNull(list); + if ( index < 0 ) + return null; + Node cell = list; + Node elt = null ; + for ( int i = 0 ; i <= index ; i++ ) { + if ( listEnd(graph, cell) ) + return null; + elt = G.getOneSP(graph, cell, CAR); + Node next = G.getOneSP(graph, cell, CDR); + cell = next; + } + return elt; + } + + /** + * Run over a list, checking for cycles and well-formed list cons cells. + * Call an action on each element if {@ocde elementAction} is not null. + * For a bad list, throw an exception or return -1 if {@code exceptionMaker} is null. + */ + private static long forEachMember(Graph graph, Node node, + boolean closedCells, + Consumer<Node> elementAction, + Function<String, RuntimeException> exceptionMaker) { + Node cell = node; + Set<Node> visited = new HashSet<>(); + long numListElements = 0 ; + while (!listEnd(graph, cell)) { + if ( visited.contains(cell) ) { + badListEx("Cyclic list", cell, exceptionMaker); + return -1; + } + visited.add(cell); + // rdf:first elt; rdf:rest next; + Node elt = null; + Node next = null; + ExtendedIterator<Triple> iterSP = G.find(graph, cell, null, null); + try { + while(iterSP.hasNext()) { + Triple t = iterSP.next(); + Node predicate = t.getPredicate(); + + if ( CAR.equals(predicate) ) { + if ( elt != null ) { + badListEx("List contains an element with two rdf:first", cell, exceptionMaker); + return -1; + } + elt = t.getObject(); + continue; + } + if ( CDR.equals(predicate) ) { + if ( next != null ) { + badListEx("List contains an element with two rdf:next", cell, exceptionMaker); + return -1; + } + next = t.getObject(); + continue; + } + if ( RDF_TYPE.equals(predicate) ) { + // Allow rdf:type on a list cons cell. + continue; + } + + if ( closedCells ) { + badListEx("List contains non-list triples", cell, exceptionMaker); + return -1; + } + } + + if ( elt == null ) { + badListEx("List contains an element with no rdf:first", node, exceptionMaker); + return -1; + } + if ( next == null ) { + badListEx("List contains an element with no rdf:next", node, exceptionMaker); + return -1; + } + // Valid list element + numListElements++; + if ( elementAction != null ) + elementAction.accept(elt); + cell = next; + } finally { iterSP.close(); } + } + return numListElements; + } + + private static class RDFListIterator implements Iterator<Node> { + + private Function<String, RuntimeException> exceptionMaker = RDFDataException::new; + private final Graph graph; + private Node current = null; + + RDFListIterator(Graph graph, Node node) { + this.graph = graph; + this.current = node; + //isWellformedListEx(graph, node); + } + + @Override + public boolean hasNext() { + return ! listEnd(graph, current); + } + + @Override + public Node next() { + // Assume well-formed. + Node elt = G.getOneSP(graph, current, CAR); + Node next = G.getOneSP(graph, current, CDR); + current = next; + return elt; + } + + // Very complicated for the sake of walking the list once. + // Instead, expect the caller to have validated the list once before list operations. +// @Override +// public Node next() { +// if ( listEnd(graph, current) ) +// throw new NoSuchElementException(); +// if ( true ) { +// // Checking. +// ExtendedIterator<Triple> iterSP = G.find(graph, current, null, null); +// try { +// Node elt = null; +// Node next = null; +// boolean closedCells = true; +// +// while(iterSP.hasNext()) { +// Triple t = iterSP.next(); +// Node predicate = t.getPredicate(); +// +// if ( CAR.equals(predicate) ) { +// if ( elt != null ) { +// badListEx("List contains an element with two rdf:first", null, exceptionMaker); +// throw new NoSuchElementException(); +// } +// elt = t.getObject(); +// continue; +// } +// if ( CDR.equals(predicate) ) { +// if ( next != null ) { +// badListEx("List contains an element with two rdf:next", null, exceptionMaker); +// throw new NoSuchElementException(); +// } +// next = t.getObject(); +// continue; +// } +// if ( RDF_TYPE.equals(predicate) ) { +// // Allow rdf:type on a list cons cell. +// continue; +// } +// +// if ( closedCells ) { +// badListEx("List contains non-list triples", null, exceptionMaker); +// throw new NoSuchElementException(); +// } +// } +// current = next; +// return elt; +// } finally { iterSP.close(); } +// +// } else { +// // Assume well-formed. +// Node elt = G.getOneSP(graph, current, CAR); +// Node next = G.getOneSP(graph, current, CDR); +// current = next; +// return elt; +// } +// } + } + + + + private static boolean isListNode(Graph graph, Node node) { + if ( node.equals(NIL) ) + return true; + // Well-formedness check. + return isCons(graph, node); + } + + private static boolean isCons (Graph graph, Node node) { + return G.hasOneSP(graph, node, CDR) && G.hasOneSP(graph, node, CAR); + } + + private static boolean listEnd(Graph graph, Node node) { + return node == null || node.equals(NIL); + } + + /** Check for a valid list; return true if the list is well-formed else return false + * @param graph + * @param node + * @param closedCells - whether to check that only rdf:first and rdf:next are used on a list cell + */ + private static void badListEx(String msg, Node node, Function<String, RuntimeException> exceptionMaker ) { + if ( exceptionMaker != null ) + throw exceptionMaker.apply(msg); + } +} diff --git a/jena-arq/src/test/java/org/apache/jena/system/TestGList.java b/jena-arq/src/test/java/org/apache/jena/system/TestGList.java new file mode 100644 index 0000000000..6c62691132 --- /dev/null +++ b/jena-arq/src/test/java/org/apache/jena/system/TestGList.java @@ -0,0 +1,469 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.system; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Iterator; +import java.util.ArrayList; +import java.util.function.BiConsumer; + +import org.junit.jupiter.api.Test; + +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFParser; +import org.apache.jena.sparql.graph.GraphZero; +import org.apache.jena.sparql.sse.SSE; +import org.apache.jena.sys.JenaSystem; +import org.apache.jena.vocabulary.RDF; + +public class TestGList { + + static { JenaSystem.init(); } + + private static Node x = SSE.parseNode(":x"); + private static Node p = SSE.parseNode(":p"); + private static Node elt1 = SSE.parseNode("'A'"); + private static Node elt2 = SSE.parseNode("'B'"); + + @Test public void glist_members_01() { + Graph graph = GraphZero.instance(); + List<Node> members = members(graph, RDF.Nodes.nil); + assertTrue(members.isEmpty()); + } + + @Test public void glist_members_02() { + test("()", (graph, list)->{ + List<Node> members = members(graph, list); + assertTrue(members.isEmpty()); + }); + } + + @Test public void glist_members_03() { + test("('A')", (graph, list)->{ + List<Node> members = members(graph, list); + assertEquals(1, members.size()); + assertEquals(elt1, members.getFirst()); + }); + } + + @Test public void glist_members_04() { + test("('A' 'B')", (graph, list)->{ + List<Node> members = members(graph, list); + assertEquals(2, members.size()); + assertEquals(elt1, members.getFirst()); + assertEquals(elt2, members.getLast()); + }); + } + + @Test public void glist_members_05() { + test("((:a :b :c))", (graph, list)->{ + List<Node> members = members(graph, list); + assertEquals(1, members.size()); + assertTrue(members.getFirst().isBlank()); + + Node listInner = members.getFirst(); + List<Node> membersInner = members(graph, listInner); + assertEquals(3, membersInner.size()); + }); + } + + @Test public void glist_members_bad_01() { + testEx("[ rdf:first 1 ]", (graph, list)->members(graph, list)); + } + + @Test public void glist_members_bad_02() { + testEx("[ rdf:rest rdf:nil ]", (graph, list)->members(graph, list)); + } + + @Test public void glist_members_bad_03() { + testWellformed("[ rdf:first 1 ; rdf:rest () ]"); + testEx("[ rdf:first 1 ; rdf:first 2 ; rdf:rest () ]", (graph, list)->members(graph, list)); + } + + @Test public void glist_members_bad_04() { + testWellformed("[ rdf:first 1 ; rdf:rest ('A') ]"); + testEx("[ rdf:first 1 ; rdf:rest ('A') ; rdf:rest () ]", (graph, list)->members(graph, list)); + } + + @Test public void glist_wellformed_01() { + testWellformed("()"); + } + + @Test public void glist_wellformed_02() { + testWellformed("('A')"); + } + + // Allow rdf:type + @Test public void glist_wellformed_03() { + testWellformed("[ rdf:first 1 ; rdf:rest rdf:nil; rdf:type :Cell]"); + } + + // Allow rdf:type + @Test public void glist_wellformed_04() { + testWellformed(""" + [ rdf:first 1 ; + rdf:rest [ + rdf:first 1 ; + rdf:rest rdf:nil ; + rdf:type :Cell + ] + ] + """); + } + + @Test public void glist_wellformed_bad_01() { + testNotWellformed("[ rdf:first 1 ]"); + } + + @Test public void glist_wellformed_bad_02() { + testNotWellformed("[ rdf:rest rdf:nil ]"); + } + + @Test public void glist_wellformed_bad_03() { + testWellformed("[ rdf:first 1 ; rdf:rest () ]"); + testNotWellformed("[ rdf:first 1 ; rdf:first 2 ; rdf:rest () ]"); + } + + @Test public void glist_wellformed_bad_04() { + testWellformed("[ rdf:first 1 ; rdf:rest ('A') ]"); + testNotWellformed("[ rdf:first 1 ; rdf:rest ('A') ; rdf:rest () ]"); + } + + @Test public void glist_wellformed_bad_10() { + testWellformed("[ rdf:first 1 ; rdf:rest [ rdf:first 'A' ; rdf:rest rdf:nil ] ]"); + testNotWellformed(""" + [ rdf:first 1 ; + rdf:rest [ + rdf:first 'A' ; + rdf:first 'B' ; + rdf:rest rdf:nil + ] + ] + """); + } + + @Test public void glist_wellformed_bad_11() { + testWellformed(""" + [ rdf:first 1 ; + rdf:rest [ + rdf:first 'A' ; + rdf:rest rdf:nil + ] + ] + """); + testNotWellformed(""" + [ rdf:first 1 ; + rdf:rest [ + rdf:first 'A' ; + rdf:rest 'B' ; + rdf:rest rdf:nil ] + ] + """); + } + + @Test public void glist_wellformed_bad_12() { + testWellformed("[ rdf:first 1 ; rdf:rest [ rdf:first 'A' ; rdf:rest rdf:nil ] ]"); + testNotWellformed("[ rdf:first 1 ; rdf:rest [ rdf:first 'A' ; ] ]"); + } + + @Test public void glist_wellformed_bad_13() { + testWellformed("[ rdf:first 1 ; rdf:rest [ rdf:first 'A' ; rdf:rest rdf:nil ] ]"); + testNotWellformed("[ rdf:first 1 ; rdf:rest [ rdf:rest rdf:nil ] ]"); + } + + @Test public void glist_wellformed_bad_14() { + testWellformed("[ rdf:first 1 ; rdf:rest [ rdf:first 'A' ; rdf:rest rdf:nil ] ]"); + testNotWellformed("[ rdf:first 1 ; rdf:rest [ ] ]"); + } + + @Test public void glist_wellformed_cyclic_01() { + String graphStr = """ + :head rdf:first 0 ; rdf:rest :x . + :x rdf:first 1 ; rdf:rest :y . + :y rdf:first 1 ; rdf:rest :x . + """; + Graph graph = graph(graphStr); + Node list = x; + boolean b = GList.isWellformedList(graph, list); + assertFalse(b); + assertThrows(RDFDataException.class, ()->GList.isWellformedListEx(graph, list)); + } + + @Test public void glist_isWellformedEx_message_no_rest() { + // A cell with rdf:first but no rdf:rest should cause the "no rdf:next" error message + String s = "[ rdf:first 1 ]"; + String g = """ + PREFIX : <http://example/> + PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> + :x :p """ + s; + Graph graph = RDFParser.fromString(g, Lang.TURTLE).toGraph(); + Node list = G.getOneSP(graph, SSE.parseNode(":x"), SSE.parseNode(":p")); + RDFDataException ex = assertThrows(RDFDataException.class, ()->GList.isWellformedListEx(graph, list)); + String msg = ex.getMessage(); + assertTrue(msg != null && msg.contains("no rdf:next"), "Expected exception message to mention 'no rdf:next', got: " + msg); + } + + @Test public void glist_length_01() { + testlength("()", 0); + } + + @Test public void glist_length_02() { + testlength("(1 2 3)", 3); + } + + @Test public void glist_length_03() { + testlength("( () )", 1); + } + + @Test public void glist_length_04() { + testlength("( (:a :b) )", 1); + } + + @Test public void glist_length_05() { + testlength("( :A (:a :b) )", 2); + } + + @Test public void glist_length_06() { + testlength("( (:a :b) :Z)", 2); + } + + @Test public void glist_length_bad_01() { + assertThrows(RDFDataException.class, ()->testlength("[ rdf:first 1 ; rdf:rest [ rdf:first 'A' ; ] ]", -1)); + } + + @Test public void glist_indexOf_01() { + test("('A')", (graph, list)->{ + Node elt = NodeFactory.createLiteralString("A"); + int idx = GList.indexOf(graph, list, elt); + assertEquals(0, idx); + }); + } + + @Test public void glist_indexOf_02() { + test("('A')", (graph, list)->{ + Node elt = NodeFactory.createLiteralString("B"); + int idx = GList.indexOf(graph, list, elt); + assertEquals(-1, idx); + }); + } + + @Test public void glist_indexOf_03() { + test("('A' 'A' 'A')", (graph, list)->{ + Node elt = NodeFactory.createLiteralString("A"); + int idx = GList.indexOf(graph, list, elt); + assertEquals(0, idx); + }); + } + + + @Test public void glist_indexOf_04() { + // Does not recurse in lists in lists. + test("(('A'))", (graph, list)->{ + Node elt = NodeFactory.createLiteralString("A"); + int idx = GList.indexOf(graph, list, elt); + assertEquals(-1, idx); + }); + } + + @Test public void glist_indexOf_05() { + test("()", (graph, list)->{ + Node elt = NodeFactory.createLiteralString("A"); + int idx = GList.indexOf(graph, list, elt); + assertEquals(-1, idx); + }); + } + + @Test public void glist_get_01() { + test("('A')", (graph, list)->{ + Node x = GList.get(graph, list, 0); + assertEquals("A", x.getLiteral().getLexicalForm()); + }); + } + + @Test public void glist_get_02() { + test("('A')", (graph, list)->{ + Node x = GList.get(graph, list, 1); + assertNull(x); + }); + } + + @Test public void glist_get_03() { + test("()", (graph, list)->{ + Node x = GList.get(graph, list, 0); + assertNull(x); + }); + } + + @Test public void glist_get_04() { + test("('A' 'B' 'C')", (graph, list)->{ + Node x = GList.get(graph, list, 1); + assertEquals("B", x.getLiteral().getLexicalForm()); + }); + } + + @Test public void glist_get_negative_index() { + test("('A')", (graph, list)->{ + assertNull(GList.get(graph, list, -1)); + }); + } + + @Test public void glist_forEach_01() { + test("()", (graph, list)->{ + StringBuilder sb = new StringBuilder(); + GList.forEach(graph, list, n -> { + // numeric literals; use lexical form + sb.append(n.getLiteral().getLexicalForm()).append(","); + }); + assertEquals("", sb.toString()); + }); + } + + @Test public void glist_forEach_02() { + test("(1 2 3)", (graph, list)->{ + StringBuilder sb = new StringBuilder(); + GList.forEach(graph, list, n -> { + // numeric literals; use lexical form + sb.append(n.getLiteral().getLexicalForm()).append(","); + }); + assertEquals("1,2,3,", sb.toString()); + }); + } + + @Test public void glist_iterator_traverse() { + test("('A' 'B' 'C')", (graph, list)->{ + List<Node> seen = new ArrayList<>(); + Iterator<Node> it = GList.iterator(graph, list); + while (it.hasNext()) { + seen.add(it.next()); + } + List<Node> expected = GList.elements(graph, list); + assertEquals(expected, seen); + }); + } + + @Test public void glist_iterator_empty() { + Graph graph = graph(":s :p :o"); + Iterator<Node> it = GList.iterator(graph, RDF.Nodes.nil); + assertFalse(it.hasNext()); + } + + @Test public void glist_closedCells_reject_extra_predicate() { + // Create a list cell that has an extra non-list triple :p :o + // Use closedCells = true to exercise that branch. + test("[ rdf:first 1 ; rdf:rest rdf:nil ; :p :o ]", (graph, list)->{ + assertFalse(GList.isWellformedList(graph, list, true)); + assertThrows(RDFDataException.class, ()->GList.isWellformedListEx(graph, list, true)); + }); + } + + @Test public void glist_indexOf_blanknode_identity() { + // Outer list contains one inner list (a blank node). indexOf should handle blank-node identity. + test("((:a :b :c))", (graph, list)->{ + // get the actual inner blank node from the members method + List<Node> members = members(graph, list); + assertEquals(1, members.size()); + Node inner = members.get(0); + + // Searching for the same blank node returns 0 + assertEquals(0, GList.indexOf(graph, list, inner)); + + // Searching for a different (not-equal) blank node returns -1 + Node otherBNode = NodeFactory.createBlankNode(); + assertEquals(-1, GList.indexOf(graph, list, otherBNode)); + }); + } + + @Test public void glist_contains_01() { + test("()", (graph, list)->{ + Node c = NodeFactory.createLiteralString("C"); + assertFalse(GList.contains(graph, list, c)); + }); + } + + @Test public void glist_contains_02() { + test("('A' 'B')", (graph, list)->{ + Node a = NodeFactory.createLiteralString("A"); + Node c = NodeFactory.createLiteralString("C"); + assertTrue(GList.contains(graph, list, a)); + assertFalse(GList.contains(graph, list, c)); + }); + } + + // ---- + + private List<Node> members(Graph graph, Node list) { + // Do the checking form first in case it throws RDFDataException. + List<Node> x1 = GList.members(graph, list); + List<Node> x2 = GList.elements(graph, list); + assertEquals(x1, x2); + return x1; + } + + private static void testWellformed(String string) { + test(string, (graph, list)->{ + assertTrue(GList.isWellformedList(graph, list)); + }); + } + + private static void testNotWellformed(String string) { + test(string, (graph, list)->{ + assertFalse(GList.isWellformedList(graph, list)); + assertThrows(RDFDataException.class, ()->GList.isWellformedListEx(graph, list)); + }); + } + + private static void testlength(String string, int expected) { + test(string, (graph, list)->{ + long actual = GList.listLength(graph, list); + assertEquals(expected, actual); + }); + } + + private static void test(String string, BiConsumer<Graph, Node> action) { + String graphStr = ":x :p "+string; + Graph graph = graph(graphStr); + Node list = G.getOneSP(graph, x, p); + action.accept(graph, list); + } + + private static void testEx(String string, BiConsumer<Graph, Node> action) { + String graphStr = ":x :p "+string; + Graph graph = graph(graphStr); + Node list = G.getOneSP(graph, x, p); + assertThrows(RDFDataException.class, ()->action.accept(graph, list)); + } + + + private static Graph graph(String str) { + String setup = """ + PREFIX : <http://example/> + PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> + """; + return RDFParser.fromString(setup+str, Lang.TURTLE).toGraph(); + } + +}
