Repository: apache/jena (java)

Issue:
SPARQL query round-trip serialization error
### Version

5.0.0

### What happened?

We're building a SPARQL query programatically and then serializing the query to a string to be able to run it against a Fuseki SPARQL endpoint. In one case, we build a query with a valid URI node that is then serialized as an invalid prefixed name, because the local part starts with an invalid character according to https://www.w3.org/TR/sparql11-query/#rPN_LOCAL

This query exhibits the issue:

```sparql
PREFIX eg: <http://example.com/with-trailing/>

ASK { <http://example.com/with-trailing/-with_leading-> a eg:BadPN_LOCAL . }
```

Using `qparse` to parse the above throws a `QueryParseException` as below when it checks the output query by re-parsing it.

### Relevant output and stacktrace

```shell
$ qparse 'PREFIX eg: <http://example.com/with-trailing/> ASK { <http://example.com/with-trailing/-with_leading-> a eg:BadPN_LOCAL . }'
PREFIX  eg:   <http://example.com/with-trailing/>

ASK
WHERE
  { eg:-with_leading-
              a  eg:BadPN_LOCAL
  }


**** Check failure: could not parse output query
org.apache.jena.query.QueryParseException: Encountered " "-" "- "" at line 5, column 8.
Was expecting one of:
    <IRIref> ...
    <PNAME_NS> ...
    <PNAME_LN> ...
    <VAR1> ...
    <VAR2> ...
    "a" ...
    "distinct" ...
    "multi" ...
    "shortest" ...
    "(" ...
    "!" ...
    "^" ...
    
        at org.apache.jena.sparql.lang.ParserARQ.perform(ParserARQ.java:99)
        at org.apache.jena.sparql.lang.ParserARQ.parse$(ParserARQ.java:52)
        at org.apache.jena.sparql.lang.SPARQLParser.parse(SPARQLParser.java:33)
        at org.apache.jena.query.QueryFactory.parse(QueryFactory.java:144)
        at org.apache.jena.query.QueryFactory.create(QueryFactory.java:83)
        at org.apache.jena.sparql.util.QueryUtils.checkParse(QueryUtils.java:96)
        at org.apache.jena.sparql.util.QueryUtils.checkQuery(QueryUtils.java:38)
        at arq.qparse.exec(qparse.java:158)
        at org.apache.jena.cmd.CmdMain.mainMethod(CmdMain.java:87)
        at org.apache.jena.cmd.CmdMain.mainRun(CmdMain.java:56)
        at org.apache.jena.cmd.CmdMain.mainRun(CmdMain.java:43)
        at arq.qparse.main(qparse.java:66)
PREFIX  eg:   <http://example.com/with-trailing/>

ASK
WHERE
  { eg:-with_leading-
              a  eg:BadPN_LOCAL
  }
```


### Are you interested in making a pull request?

Yes

Pull request:
Fix query serialization round-trip issue
GitHub issue resolved #2585

Pull request Description:

Re-do char-by-char local PName validation following grammar rules at https://www.w3.org/TR/sparql11-query/#rPN_LOCAL

----

 - [X] Tests are included.
 - [ ] Documentation change and updates are provided for the [Apache Jena website](https://github.com/apache/jena-site/)
 - [ ] Commits have been squashed to remove intermediate development commit messages.
 - [ ] 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).

----

See the [Apache Jena "Contributing" guide](https://github.com/apache/jena/blob/main/CONTRIBUTING.md).


Patch:
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/util/FmtUtils.java b/jena-arq/src/main/java/org/apache/jena/sparql/util/FmtUtils.java
index 6c171820ab..cd40a017ab 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/util/FmtUtils.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/util/FmtUtils.java
@@ -33,6 +33,7 @@ import org.apache.jena.rdf.model.Model ;
 import org.apache.jena.rdf.model.RDFNode ;
 import org.apache.jena.rdf.model.Resource ;
 import org.apache.jena.riot.out.NodeFmtLib;
+import org.apache.jena.riot.system.RiotChars;
 import org.apache.jena.shared.PrefixMapping ;
 import org.apache.jena.sparql.ARQConstants ;
 import org.apache.jena.sparql.ARQInternalErrorException ;
@@ -476,28 +477,34 @@ public class FmtUtils {
             return true;
 
         for ( int idx = 0 ; idx < localname.length() ; idx++ ) {
-            char ch = localname.charAt(idx);
-            if ( !validPNameChar(ch) )
-                return false;
+            int ch = localname.codePointAt(idx);
+            if ( idx == 0 ) {
+                if ( !validFirstPNameChar(ch) )
+                    return false;
+            } else if ( idx == localname.length() - 1 ) {
+                if ( !validEndPNameChar(ch) )
+                    return false;
+            } else {
+                if ( !validMidPNameChar(ch) )
+                    return false;
+            }
         }
+        return true;
+    }
 
-        // Test start and end - at least one character in the name.
-
-        if ( localname.endsWith(".") )
-            return false;
-        if ( localname.startsWith(".") )
-            return false;
+    // first char as per options in first bracketed production in 169: https://www.w3.org/TR/sparql11-query/#rPN_LOCAL
+    private static boolean validFirstPNameChar(int ch) {
+        return RiotChars.isPNChars_U_N(ch) || ( ch == '%' ) ;
+    }
 
-        return true;
+    // neither first char nor last in middle repeated production in 169: https://www.w3.org/TR/sparql11-query/#rPN_LOCAL
+    private static boolean validMidPNameChar(int ch) {
+        return RiotChars.isPNChars(ch) || ( ch == '.' ) || ( ch == ':' ) || ( ch == '%' ) ;
     }
 
-    private static boolean validPNameChar(char ch) {
-        if ( Character.isLetterOrDigit(ch) ) return true ;
-        if ( ch == '.' )    return true ;
-        if ( ch == ':' )    return true ;
-        if ( ch == '-' )    return true ;
-        if ( ch == '_' )    return true ;
-        return false ;
+    // last char, but not first, in last production in 169: https://www.w3.org/TR/sparql11-query/#rPN_LOCAL
+    private static boolean validEndPNameChar(int ch) {
+        return RiotChars.isPNChars(ch) || ( ch == ':' ) || ( ch == '%' ) ;
     }
 
     static boolean applyUnicodeEscapes = false ;
diff --git a/pom.xml b/pom.xml
index 472140d888..d9e412108f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,7 +77,7 @@
 
     <ver.caffeine>3.1.8</ver.caffeine>
     <!-- Most of Jena uses other libraries for equivalent functionality. -->
-    <ver.guava>33.2.1-jre</ver.guava>
+    <ver.guava>33.3.0-jre</ver.guava>
 
     <ver.gson>2.11.0</ver.gson>
     <ver.lucene>9.11.1</ver.lucene>
@@ -93,12 +93,12 @@
     <ver.commons-fileupload>2.0.0-M2</ver.commons-fileupload>
 
     <ver.dexxcollection>0.7</ver.dexxcollection>
-    <ver.micrometer>1.13.2</ver.micrometer>
+    <ver.micrometer>1.13.3</ver.micrometer>
     <ver.roaringbitmap>1.2.1</ver.roaringbitmap>
 
     <!-- Testing -->
     <ver.junit4>4.13.2</ver.junit4>
-    <ver.junit5>5.10.3</ver.junit5>
+    <ver.junit5>5.11.0</ver.junit5>
     <ver.junit5-platform>1.10.2</ver.junit5-platform>
  
     <ver.mockito>5.12.0</ver.mockito>
@@ -120,7 +120,7 @@
     <ver.plugin.rat>0.16.1</ver.plugin.rat>
     <ver.plugin.assembly>3.7.1</ver.plugin.assembly>
     <ver.plugin.compiler>3.13.0</ver.plugin.compiler>
-    <ver.plugin.surefire>3.3.1</ver.plugin.surefire>
+    <ver.plugin.surefire>3.4.0</ver.plugin.surefire>
     <ver.plugin.javadoc>3.8.0</ver.plugin.javadoc>
     <ver.plugin.war>3.4.0</ver.plugin.war>
     <ver.plugin.shade>3.6.0</ver.plugin.shade>


Test patch:
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/syntax/TestSerialization.java b/jena-arq/src/test/java/org/apache/jena/sparql/syntax/TestSerialization.java
index 08c3fd708f..6efb1eaef4 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/syntax/TestSerialization.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/syntax/TestSerialization.java
@@ -118,6 +118,9 @@ public class TestSerialization
     @Test public void test_PName_Bad_7()
     { fmtURI_Prefix("http://example/x.", "<http://example/x.>", pmap1) ; }
 
+    @Test public void test_PName_Bad_8()
+    { fmtURI_Prefix("http://default/-x", "<http://default/-x>", pmap1) ; }
+
     // Dots
     @Test public void test_Dots_1() // Internal DOT
     { fmtURI_Prefix("http://example/x#a.b", "ex:a.b", pmap1) ; }