Repository: apache/jena (java)

Issue:
"Safe" flag for riot RDFS inference
### Version

4.7.0

### Feature

Currently, riot RDFS inference outputs “generalized” RDF which may not be parsable by other tools (including various Jena CLI tools). For example:

`data.ttl`

```ttl
PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>

ex:louie ex:birthdate "2022-08-11”^^xsd:date .
```

`vocab.ttl`

```ttl
PREFIX ex: <http://example.org/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> 

ex:birthdate rdfs:range xsd:date .
```

Output of `riot --rdfs=vocab.ttl data.ttl`

```ttl
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> 

"2022-08-11”^^xsd:date rdf:type xsd:date .
```

… which is generalized RDF due to the literal in the subject position.

It would be nice if there were a flag to filter out such triples (using [SafeGraph](https://github.com/apache/jena/blob/main/jena-core/src/main/java/org/apache/jena/reasoner/rulesys/impl/SafeGraph.java)), so as to avoid errors when subsequently processing the output with tools that don't accept generalized RDF.

I’m interested in contributing a solution, but would need some pointers as to where in the codebase this should go.

### Are you interested in contributing a solution yourself?

Perhaps?

Pull request:
GH-1720: Filter out literals-as-subjects from --rdfs
GitHub issue resolved #1720


----

 - [x] Key commit messages start with the issue number (GH-xxxx)

By submitting this pull request, I acknowledge that I am making a contribution to the Apache Software Foundation under the terms and conditions of the [Contributor's Agreement](https://www.apache.org/licenses/contributor-agreements.html).


Patch:
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/RDFSFactory.java b/jena-arq/src/main/java/org/apache/jena/rdfs/RDFSFactory.java
index aaf101e70f..37e42fe723 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/RDFSFactory.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/RDFSFactory.java
@@ -22,6 +22,7 @@ import org.apache.jena.query.Dataset;
 import org.apache.jena.query.DatasetFactory;
 import org.apache.jena.riot.system.StreamRDF;
 import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.util.NodeUtils;
 
 /** Factory for data+RDFS inference. */
 public class RDFSFactory {
@@ -79,4 +80,9 @@ public class RDFSFactory {
     public static StreamRDF streamRDFS(StreamRDF data, SetupRDFS setup) {
         return new InfStreamRDFS(data, setup);
     }
+
+    /** RDFS can generate "symmetric RDF", "generalized RDF" - e.g. triples with literals as subjects. */
+    public static StreamRDF removeGeneralizedRDF(StreamRDF data) {
+        return NodeUtils.removeGeneralizedRDF(data);
+    }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/SetupRDFS.java b/jena-arq/src/main/java/org/apache/jena/rdfs/SetupRDFS.java
index 84cc71cc7a..9f9dc90d60 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/SetupRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/SetupRDFS.java
@@ -24,14 +24,6 @@ import org.apache.jena.rdfs.setup.BaseSetupRDFS;
 /** RDFS setup in Node space */
 public class SetupRDFS extends BaseSetupRDFS<Node> {
 
-    /**
-     * {@code incDerivedDataRDFS} causes the engine to look for RDFS relationships in the data
-     * as if TBox (rules) and ABox (ground data) are one unit.
-     * <p>
-     * Set true if abox == tbox.
-     * <p>
-     * Can choose false or true if abox != tbox.
-     */
     public SetupRDFS(Graph vocab) {
         super(vocab);
     }
diff --git a/jena-arq/src/main/java/org/apache/jena/riot/system/StreamRDF2.java b/jena-arq/src/main/java/org/apache/jena/riot/system/StreamRDF2.java
index e066ccc613..7b8c4d2a51 100644
--- a/jena-arq/src/main/java/org/apache/jena/riot/system/StreamRDF2.java
+++ b/jena-arq/src/main/java/org/apache/jena/riot/system/StreamRDF2.java
@@ -21,8 +21,8 @@ package org.apache.jena.riot.system;
 import org.apache.jena.graph.Triple ;
 import org.apache.jena.sparql.core.Quad ;
 
-/** Send to two stream */
-public class StreamRDF2 implements StreamRDF 
+/** Send to two streams */
+public class StreamRDF2 implements StreamRDF
 {
     protected final StreamRDF sink1 ;
     protected final StreamRDF sink2 ;
@@ -31,7 +31,7 @@ public class StreamRDF2 implements StreamRDF
         this.sink1 = sink1 ;
         this.sink2 = sink2 ;
     }
-    
+
     @Override
     public void start() {
         sink1.start() ;
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDatasetBuilder.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDatasetBuilder.java
index 42e2dcf7f5..d2d7aa6989 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDatasetBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDatasetBuilder.java
@@ -164,7 +164,7 @@ public class QueryExecDatasetBuilder implements QueryExecMod, QueryExecBuilder {
 
     @Override
     public QueryExecDatasetBuilder timeout(long timeout) {
-        return timeout(timeout, TimeUnit.MICROSECONDS);
+        return timeout(timeout, TimeUnit.MILLISECONDS);
     }
 
     @Override
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/modify/TemplateLib.java b/jena-arq/src/main/java/org/apache/jena/sparql/modify/TemplateLib.java
index a9a9b880f1..bf2026174f 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/modify/TemplateLib.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/modify/TemplateLib.java
@@ -30,7 +30,7 @@ import org.apache.jena.sparql.core.Quad;
 import org.apache.jena.sparql.core.Substitute;
 import org.apache.jena.sparql.core.Var;
 import org.apache.jena.sparql.engine.binding.Binding;
-import org.apache.jena.sparql.util.ModelUtils;
+import org.apache.jena.sparql.util.NodeUtils;
 
 public class TemplateLib {
     // See also Substitute -- combine?
@@ -79,7 +79,7 @@ public class TemplateLib {
                 List<Triple> tripleList = new ArrayList<>(triples.size());
                 for ( Triple triple : triples ) {
                     Triple q = subst(triple, b, bNodeMap);
-                    if ( !q.isConcrete() || !ModelUtils.isValidAsStatement(q.getSubject(), q.getPredicate(), q.getObject()) ) {
+                    if ( !q.isConcrete() || ! NodeUtils.isValidAsRDF(q.getSubject(), q.getPredicate(), q.getObject()) ) {
                         // Log.warn(TemplateLib.class, "Unbound quad:
                         // "+FmtUtils.stringForQuad(quad)) ;
                         continue;
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/util/ModelUtils.java b/jena-arq/src/main/java/org/apache/jena/sparql/util/ModelUtils.java
index 48c698e3a6..adee7d810e 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/util/ModelUtils.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/util/ModelUtils.java
@@ -18,22 +18,22 @@
 
 package org.apache.jena.sparql.util;
 
-import java.util.Iterator ;
-
-import org.apache.jena.atlas.iterator.Iter ;
-import org.apache.jena.graph.Node ;
-import org.apache.jena.graph.Triple ;
-import org.apache.jena.query.QueryException ;
-import org.apache.jena.rdf.model.Model ;
-import org.apache.jena.rdf.model.RDFNode ;
-import org.apache.jena.rdf.model.Statement ;
-import org.apache.jena.rdf.model.StmtIterator ;
-import org.apache.jena.rdf.model.impl.LiteralImpl ;
-import org.apache.jena.rdf.model.impl.ResourceImpl ;
-import org.apache.jena.rdf.model.impl.StmtIteratorImpl ;
-import org.apache.jena.sparql.ARQInternalErrorException ;
+import java.util.Iterator;
+
+import org.apache.jena.atlas.iterator.Iter;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.query.QueryException;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.rdf.model.RDFNode;
+import org.apache.jena.rdf.model.Statement;
+import org.apache.jena.rdf.model.StmtIterator;
+import org.apache.jena.rdf.model.impl.LiteralImpl;
+import org.apache.jena.rdf.model.impl.ResourceImpl;
+import org.apache.jena.rdf.model.impl.StmtIteratorImpl;
+import org.apache.jena.sparql.ARQInternalErrorException;
 import org.apache.jena.util.ModelCollector;
-import org.apache.jena.util.iterator.ClosableIterator ;
+import org.apache.jena.util.iterator.ClosableIterator;
 
 public class ModelUtils
 {
@@ -46,22 +46,22 @@ public class ModelUtils
 
     public static RDFNode convertGraphNodeToRDFNode(Node node, Model model) {
         if ( node.isVariable() )
-            throw new QueryException("Variable: "+node) ;
+            throw new QueryException("Variable: "+node);
 
         // Best way.
         if ( model != null )
-             return model.asRDFNode(node) ;
+             return model.asRDFNode(node);
 
         if ( node.isLiteral() )
-            return new LiteralImpl(node, null) ;
+            return new LiteralImpl(node, null);
 
         if ( node.isURI() || node.isBlank() )
-            return new ResourceImpl(node, null) ;
+            return new ResourceImpl(node, null);
 
         if ( node.isNodeTriple() )
-            return new ResourceImpl(node, null) ;
+            return new ResourceImpl(node, null);
 
-        throw new ARQInternalErrorException("Unknown node type for node: "+node) ;
+        throw new ARQInternalErrorException("Unknown node type for node: "+node);
     }
 
     /** Convert a {@link Node} (graph SPI) to an RDFNode (model API)
@@ -73,18 +73,17 @@ public class ModelUtils
         return convertGraphNodeToRDFNode(node, null);
     }
 
-    public static Statement tripleToStatement(Model model, Triple t)
-    {
+    public static Statement tripleToStatement(Model model, Triple t) {
         if ( model == null )
-            throw new ARQInternalErrorException("Attempt to create statement with null model") ;
+            throw new ARQInternalErrorException("Attempt to create statement with null model");
 
-        Node sNode = t.getSubject() ;
-        Node pNode = t.getPredicate() ;
-        Node oNode = t.getObject() ;
+        Node sNode = t.getSubject();
+        Node pNode = t.getPredicate();
+        Node oNode = t.getObject();
 
         if (!isValidAsStatement(sNode, pNode, oNode)) return null;
 
-        return model.asStatement(t) ;
+        return model.asStatement(t);
     }
 
     /**
@@ -97,19 +96,12 @@ public class ModelUtils
      * @param p Predicate
      * @param o Object
      * @return True if a valid Statement can be formed
+     *
+     * @deprecated Use {@link NodeUtils#isValidAsRDF(Node, Node, Node)}.
      */
-    public static boolean isValidAsStatement(Node s, Node p, Node o)
-    {
-        if ( s.isLiteral() || s.isVariable() )
-            return false ;
-
-        if ( ! p.isURI() )  // Not variable, literal or blank.
-            return false ;
-
-        if ( o.isVariable() )
-            return false ;
-
-        return true;
+    @Deprecated
+    public static boolean isValidAsStatement(Node s, Node p, Node o) {
+        return NodeUtils.isValidAsRDF(s, p, o);
     }
 
     public static StmtIterator triplesToStatements(final Iterator<Triple> it, final Model refModel) {
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/util/NodeUtils.java b/jena-arq/src/main/java/org/apache/jena/sparql/util/NodeUtils.java
index d1d50056c2..d8c791e174 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/util/NodeUtils.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/util/NodeUtils.java
@@ -25,7 +25,11 @@ import org.apache.jena.datatypes.RDFDatatype ;
 import org.apache.jena.datatypes.xsd.XSDDatatype ;
 import org.apache.jena.graph.Node ;
 import org.apache.jena.graph.NodeFactory ;
+import org.apache.jena.graph.Triple;
 import org.apache.jena.rdf.model.impl.Util;
+import org.apache.jena.riot.system.StreamRDF;
+import org.apache.jena.riot.system.StreamRDFWrapper;
+import org.apache.jena.sparql.core.Quad;
 import org.apache.jena.sparql.expr.ExprEvalException ;
 import org.apache.jena.sparql.expr.NodeValue ;
 import org.apache.jena.sparql.expr.nodevalue.NodeFunctions ;
@@ -184,4 +188,66 @@ public class NodeUtils
      */
     public static boolean isLangString(Node n) { return Util.isLangString(n) ; }
 
+    /**
+     * Determines whether a triple (as s/p/o) is valid as a RDF statement.
+     * <p>
+     * This function reflects the fact that the {@link Triple} API is flexible in
+     * allowing any Node type in any position (including non-RDF node types like
+     * Variable) and as such not all Triples can be safely converted into Statements
+     * </p>
+     * @param s Subject
+     * @param p Predicate
+     * @param o Object
+     * @return True if a valid as a statement
+     */
+    public static boolean isValidAsRDF(Node s, Node p, Node o) {
+        if ( s == null || ( ! s.isBlank() && ! s.isURI() ) )
+            return false;
+        if ( p == null || ( ! p.isURI() ) )
+            return false;
+        if ( o == null || ( ! o.isBlank() && ! o.isURI() && ! o.isLiteral() && !o.isNodeTriple() ) )
+            return false;
+        return true;
+    }
+
+    /**
+     * Determines whether a quad (as g/s/p/o) is valid as a RDF statement.
+     * <p>
+     * This function reflects the fact that the {@link Triple} API is flexible in
+     * allowing any Node type in any position (including non-RDF node types like
+     * Variable) and as such not all Triples can be safely converted into Statements
+     * </p>
+     * @param s Subject
+     * @param p Predicate
+     * @param o Object
+     * @return True if a valid as a statement
+     */
+    public static boolean isValidAsRDF(Node g, Node s, Node p, Node o) {
+        if ( g == null || ( ! g.isURI() && ! g.isBlank() ) )
+            return false;
+        return isValidAsRDF(s, p, o);
+    }
+
+    /** Filter out triples and quads that are "generalized" RDF */
+    public static StreamRDF removeGeneralizedRDF(StreamRDF data) {
+        return new StreamRDFWrapper(data) {
+            @Override
+            public void quad(Quad quad) {
+                if ( ! isValidAsRDF(quad.getGraph(), quad.getSubject(), quad.getPredicate(), quad.getObject()) ) {
+                    // Reject
+                    return;
+                }
+                super.quad(quad);
+            }
+
+            @Override
+            public void triple(Triple triple) {
+                if ( ! isValidAsRDF(triple.getSubject(), triple.getPredicate(), triple.getObject()) ) {
+                    // Reject
+                    return;
+                }
+                super.triple(triple);
+            }
+        };
+    }
 }
diff --git a/jena-cmds/src/main/java/riotcmd/CmdLangParse.java b/jena-cmds/src/main/java/riotcmd/CmdLangParse.java
index 429e69c566..2d26ac9ad1 100644
--- a/jena-cmds/src/main/java/riotcmd/CmdLangParse.java
+++ b/jena-cmds/src/main/java/riotcmd/CmdLangParse.java
@@ -61,7 +61,7 @@ public abstract class CmdLangParse extends CmdGeneral {
     protected ModTime modTime = new ModTime();
     protected ModLangParse modLangParse = new ModLangParse();
     protected ModLangOutput modLangOutput = new ModLangOutput();
-    protected SetupRDFS setup = null;
+    protected SetupRDFS setupRDFS = null;
     protected ModContext modContext = new ModContext();
     protected ArgDecl argStrict = new ArgDecl(ArgDecl.NoValue, "strict");
 
@@ -181,7 +181,7 @@ public abstract class CmdLangParse extends CmdGeneral {
     protected void exec$() {
 
         if ( modLangParse.getRDFSVocab() != null )
-            setup = RDFSFactory.setupRDFS(modLangParse.getRDFSVocab().getGraph());
+            setupRDFS = RDFSFactory.setupRDFS(modLangParse.getRDFSVocab().getGraph());
 
         if ( modLangOutput.compressedOutput() ) {
             try {
@@ -374,8 +374,13 @@ public abstract class CmdLangParse extends CmdGeneral {
 
         // Build parser output additions.
         StreamRDF s = parserOutputStream;
-        if ( setup != null )
-            s = RDFSFactory.streamRDFS(s, setup);
+        if ( setupRDFS != null ) {
+            // Remove literals as subjects
+            s = RDFSFactory.removeGeneralizedRDF(s);
+            // Generate RDFS (this feeds into the stream created above).
+            s = RDFSFactory.streamRDFS(s, setupRDFS);
+            // Parser sends data to RDFS, which goes to the filter, then to parserOutputStream
+        }
         // If added here, count is quads and triples seen in the input.
         if ( modLangParse.mergeQuads() )
             s = new QuadsToTriples(s);
diff --git a/pom.xml b/pom.xml
index dad19d0e5b..2f335c893a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,7 +75,7 @@
     <ver.protobuf>4.29.3</ver.protobuf>
     <ver.libthrift>0.21.0</ver.libthrift>
 
-    <ver.caffeine>3.1.8</ver.caffeine>
+    <ver.caffeine>3.2.0</ver.caffeine>
     <!-- Most of Jena uses other libraries for equivalent functionality. -->
     <ver.guava>33.4.0-jre</ver.guava>
 


Test patch:
diff --git a/jena-arq/src/test/java/org/apache/jena/arq/ARQTestSuite.java b/jena-arq/src/test/java/org/apache/jena/arq/ARQTestSuite.java
index 12534c53d1..ef199eed01 100644
--- a/jena-arq/src/test/java/org/apache/jena/arq/ARQTestSuite.java
+++ b/jena-arq/src/test/java/org/apache/jena/arq/ARQTestSuite.java
@@ -18,6 +18,11 @@
 
 package org.apache.jena.arq;
 
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
 import org.apache.jena.atlas.TC_Atlas_ARQ;
 import org.apache.jena.atlas.legacy.BaseTest2;
 import org.apache.jena.external.Scripts_LangSuiteCG;
@@ -34,10 +39,7 @@ import org.apache.jena.sparql.expr.NodeValue;
 import org.apache.jena.sys.JenaSystem;
 import org.apache.jena.system.TS_System;
 import org.apache.jena.system.buffering.TS_Buffering;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
+import org.apache.jena.util.TS_UtilsARQ;
 
 /**
  * All the ARQ tests
@@ -47,6 +49,8 @@ import org.junit.runners.Suite;
 @Suite.SuiteClasses( {
     TC_Atlas_ARQ.class,
 
+    TS_UtilsARQ.class,
+
     TC_Riot.class,
 
     TS_System.class,
diff --git a/jena-arq/src/test/java/org/apache/jena/util/TS_UtilsARQ.java b/jena-arq/src/test/java/org/apache/jena/util/TS_UtilsARQ.java
new file mode 100644
index 0000000000..a813e7919c
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/util/TS_UtilsARQ.java
@@ -0,0 +1,29 @@
+/*
+ * 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.jena.util;
+
+import org.junit.runner.RunWith ;
+import org.junit.runners.Suite ;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses( {
+    TestNodeUtils.class
+})
+
+public class TS_UtilsARQ {}
diff --git a/jena-arq/src/test/java/org/apache/jena/util/TestNodeUtils.java b/jena-arq/src/test/java/org/apache/jena/util/TestNodeUtils.java
new file mode 100644
index 0000000000..828637949d
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/util/TestNodeUtils.java
@@ -0,0 +1,106 @@
+/*
+ * 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.jena.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.sparql.core.Quad;
+import org.apache.jena.sparql.sse.SSE;
+import org.apache.jena.sparql.util.NodeUtils;
+
+public class TestNodeUtils {
+
+    private static final Node g = SSE.parseNode(":g");
+    private static final Node s = SSE.parseNode(":s");
+    private static final Node p = SSE.parseNode(":p");
+    private static final Node o = SSE.parseNode(":o");
+
+    @Test public void valid_triple_01() { testValidityTriple(true, "(:s :p :o)"); }
+    @Test public void valid_triple_02() { testValidityTriple(true, Triple.create(s, p, o)); }
+    @Test public void valid_triple_03() { testValidityTriple(true, "(:s :p 123)"); }
+    @Test public void valid_triple_04() { testValidityTriple(true, "(:s :p _:b)"); }
+    @Test public void valid_triple_05() { testValidityTriple(true, "(_:b :p :o)"); }
+
+    // RDF 1.2
+    //@Test public void valid_triple_06() { testValidityTriple(true, "(:s :p <<( :x :y :x)>>)"); }
+
+
+    @Test public void invalid_triple_01() { testValidityTriple(false, "('literal' :p :o)"); }
+    @Test public void invalid_triple_02() { testValidityTriple(false, "(:s 'literal' :o)"); }
+    @Test public void invalid_triple_03() { testValidityTriple(false, "(:s :p ?var)"); }
+    @Test public void invalid_triple_04() { testValidityTriple(false, "(:s ?var :o)"); }
+    @Test public void invalid_triple_05() { testValidityTriple(false, "(?var :p :o)"); }
+
+    // _ is Node.ANY for triples.
+
+    @Test public void invalid_triple_10() { testValidityTriple(false, "(_ :p :o)"); }
+    @Test public void invalid_triple_11() { testValidityTriple(false, "(:s _ :o)"); }
+    @Test public void invalid_triple_12() { testValidityTriple(false, "(:s :p _)"); }
+
+    private void testValidityTriple(boolean expected, String triple) {
+        testValidityTriple(expected, SSE.parseTriple(triple));
+    }
+
+    private void testValidityTriple(boolean expected, Triple triple) {
+        Node s = triple.getSubject(), p = triple.getPredicate(), o = triple.getObject();
+        assertEquals(expected, NodeUtils.isValidAsRDF(s, p, o));
+    }
+
+    @Test public void valid_quad_01() { testValidityQuad(true, "(:g :s :p :o)"); }
+    @Test public void valid_quad_02() { testValidityQuad(true, Quad.create(g, s, p, o)); }
+
+    @Test public void valid_quad_03() { testValidityQuad(true, "(:g :s :p _:b)"); }
+    @Test public void valid_quad_04() { testValidityQuad(true, "(:g _:b :p :o)"); }
+    @Test public void valid_quad_05() { testValidityQuad(true, "(_:b :s :p :o)"); }
+    @Test public void valid_quad_06() { testValidityQuad(true, "(:g :s :p 'literal')"); }
+
+    // _ in the graph position is null/default graph
+    @Test public void valid_quad_09() { testValidityQuad(true, "(_ :s :p :o)"); }
+
+    // RDF 1.2
+    //@Test public void valid_quad_06() { testValidityQuad(true, "(:s :p <<( :x :y :x)>>)"); }
+
+    @Test public void invalid_quad_01() { testValidityQuad(false, "(:g :s 'literal' :o)"); }
+    @Test public void invalid_quad_02() { testValidityQuad(false, "(:g 'literal' :p :o)"); }
+    @Test public void invalid_quad_03() { testValidityQuad(false, "('literal' :s :p :o)"); }
+
+    @Test public void invalid_quad_04() { testValidityQuad(false, "(:g :s :p ?var)"); }
+    @Test public void invalid_quad_05() { testValidityQuad(false, "(:g :s ?var :o)"); }
+    @Test public void invalid_quad_06() { testValidityQuad(false, "(:g ?var :p :o)"); }
+    @Test public void invalid_quad_07() { testValidityQuad(false, "(?var :s :p :o)"); }
+
+    @Test public void invalid_quad_10() { testValidityQuad(false, "(:g _ :p :o)"); }
+    @Test public void invalid_quad_11() { testValidityQuad(false, "(:g :s _ :o)"); }
+    @Test public void invalid_quad_12() { testValidityQuad(false, "(:g :s :p _)"); }
+
+    private void testValidityQuad(boolean expected, String quad) {
+        testValidityQuad(expected, SSE.parseQuad(quad));
+    }
+
+    private void testValidityQuad(boolean expected, Quad quad) {
+        Node g = quad.getGraph(), s = quad.getSubject(), p = quad.getPredicate(), o = quad.getObject();
+        assertEquals(expected, NodeUtils.isValidAsRDF(g, s, p, o));
+    }
+
+
+}