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();
+    }
+
+}


Reply via email to