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 da35f7fae777f0ab7e29e85884bd3acafae93909
Author: Andy Seaborne <[email protected]>
AuthorDate: Sun Jul 5 16:19:26 2026 +0100

    GH-4039: Add HTTP QUERY and PATCH to query/update options
---
 .../src/main/java/org/apache/jena/http/HttpOp.java | 31 ++++++++++++--
 .../org/apache/jena/fuseki/servlets/ActionLib.java |  1 +
 .../jena/fuseki/servlets/SPARQLQueryProcessor.java | 19 ++++++---
 .../apache/jena/fuseki/servlets/SPARQL_Update.java |  2 +-
 .../apache/jena/fuseki/main/TestHttpOptions.java   |  8 ++--
 .../jena/fuseki/main/TestSPARQLProtocol.java       | 47 +++++++++++++++++++++-
 .../org/apache/jena/fuseki/test/FusekiTest.java    |  6 +--
 7 files changed, 95 insertions(+), 19 deletions(-)

diff --git a/jena-arq/src/main/java/org/apache/jena/http/HttpOp.java 
b/jena-arq/src/main/java/org/apache/jena/http/HttpOp.java
index 078844ce29..d948621715 100644
--- a/jena-arq/src/main/java/org/apache/jena/http/HttpOp.java
+++ b/jena-arq/src/main/java/org/apache/jena/http/HttpOp.java
@@ -266,6 +266,7 @@ public class HttpOp {
     }
 
     // ---- POST stream response.
+    // The URL may have a query string.
 
     /** POST - the application MUST close the InputStream.*/
     public static TypedInputStream httpPostStream(String url) {
@@ -287,7 +288,7 @@ public class HttpOp {
         return execPostStream(httpClient, url, acceptHeader);
     }
 
-    /** POST(URL) -> InputStream+Content-Type. The application MUST close the 
InputStream. */
+    /** POST(URL), no body -> InputStream+Content-Type. The application MUST 
close the InputStream. */
     private static TypedInputStream execPostStream(HttpClient httpClient, 
String url, String acceptHeader) {
         return execPostStream(httpClient, url, null, null, acceptHeader);
     }
@@ -319,15 +320,19 @@ public class HttpOp {
         return execPostStream(httpClient, url, contentType, bodyContent, 
acceptHeader);
     }
 
-    /** POST(URL), with a body -> InputStream+Content-Type. The application 
MUST close the InputStream. */
     private static TypedInputStream execPostStream(HttpClient httpClient, 
String url, String contentType, BodyPublisher bodyPublisher, String 
acceptHeader) {
+        return execStream(httpClient, url, HttpMethod.POST, contentType, 
bodyPublisher, acceptHeader);
+    }
+
+    /** QUERY/POST/PATCH(URL), with a body -> InputStream+Content-Type. The 
application MUST close the InputStream. */
+    private static TypedInputStream execStream(HttpClient httpClient, String 
url, HttpMethod method, String contentType, BodyPublisher bodyPublisher, String 
acceptHeader) {
         acceptHeader = HttpLib.dft(acceptHeader, "*/");
         if ( bodyPublisher == null )
             bodyPublisher = BodyPublishers.noBody();
         HttpRequest.Builder builder = 
HttpLib.requestBuilderFor(url).uri(toRequestURI(url));
         HttpLib.contentTypeHeader(builder, contentType);
         HttpLib.acceptHeader(builder, acceptHeader);
-        HttpRequest request = builder.POST(bodyPublisher).build();
+        HttpRequest request = builder.method(method.method(), 
bodyPublisher).build();
         HttpResponse<InputStream> response = HttpLib.execute(httpClient, 
request);
         return HttpLib.handleResponseTypedInputStream(response);
     }
@@ -350,6 +355,26 @@ public class HttpOp {
         execPushData(httpClient, PUT, url, contentType, body);
     }
 
+    // ---- QUERY
+
+    /** QUERY
+     *
+     */
+    /** Perform an HTTP QUERY to a URL. No body. The application MUST close 
the InputStream. */
+    public static TypedInputStream httpQueryStream(String url, String 
acceptHeader) {
+        return httpQueryStream(HttpEnv.getHttpClient(url), url, null, null, 
acceptHeader);
+    }
+
+    /** QUERY (HTTP method), with a HTTP body - the application MUST close the 
InputStream.*/
+    public static TypedInputStream httpQueryStream(String url, String 
contentType, BodyPublisher bodyPublisher, String acceptHeader) {
+        return httpQueryStream(HttpEnv.getHttpClient(url), url, contentType, 
bodyPublisher, acceptHeader);
+    }
+
+    /** QUERY (HTTP method), with a HTTP body - the application MUST close the 
InputStream.*/
+    public static TypedInputStream httpQueryStream(HttpClient httpClient, 
String url, String contentType, BodyPublisher bodyPublisher, String 
acceptHeader) {
+        return execStream(httpClient, url, HttpMethod.QUERY, contentType, 
bodyPublisher, acceptHeader);
+    }
+
     // ---- PATCH
 
     /** PATCH
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java
index 1ac65144fa..2b16b79652 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java
@@ -466,6 +466,7 @@ public class ActionLib {
     }
 
     // Packing of OPTIONS.
+    // Query (SPARQLQueryProcessor) and update (SPARQL_Update) do special 
handling to allow add QUERY and PATCH.
 
     public static void doOptionsGet(HttpAction action) {
         setCommonHeadersForOptions(action);
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SPARQLQueryProcessor.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SPARQLQueryProcessor.java
index 6192fdbf79..1cf1606a03 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SPARQLQueryProcessor.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SPARQLQueryProcessor.java
@@ -73,7 +73,8 @@ public abstract class SPARQLQueryProcessor extends 
ActionService
 
     @Override
     public void execOptions(HttpAction action) {
-        ActionLib.doOptionsGetPost(action);
+        ActionLib.setCommonHeadersForOptions(action);
+        action.setResponseHeader(HttpNames.hAllow, 
"HEAD,GET,QUERY,POST,OPTIONS");
         ServletOps.success(action);
     }
 
@@ -88,6 +89,10 @@ public abstract class SPARQLQueryProcessor extends 
ActionService
         executeLifecycle(action);
     }
 
+    @Override public void execQuery(HttpAction action) {
+        executeLifecycle(action);
+    }
+
     /**
      * All the query parameters that are acceptable in a given request.
      * This is comprised of, by default,
@@ -120,8 +125,12 @@ public abstract class SPARQLQueryProcessor extends 
ActionService
         if ( HttpMethod.METHOD_OPTIONS.equals(method) )
             return;
 
-        if ( !HttpMethod.METHOD_POST.equals(method) && 
!HttpMethod.METHOD_GET.equals(method) )
-            ServletOps.errorMethodNotAllowed("Not a GET or POST request");
+        switch(method) {
+            case HttpMethod.METHOD_POST, 
HttpMethod.METHOD_GET,HttpMethod.METHOD_QUERY -> {}
+            default -> {
+                ServletOps.errorMethodNotAllowed("Not a GET, POST or QUERY 
request");
+            }
+        }
 
         if ( HttpMethod.METHOD_GET.equals(method) && 
action.getRequestQueryString() == null ) {
             ServletOps.warning(action, "Service Description / SPARQL Query / " 
+ action.getRequestRequestURI());
@@ -155,8 +164,6 @@ public abstract class SPARQLQueryProcessor extends 
ActionService
         ContentType ct = FusekiNetLib.getContentType(request);
         boolean mustHaveQueryParam = true;
         if ( ct != null ) {
-            String incoming = ct.getContentTypeStr();
-
             if ( matchContentType(ctSPARQLQuery, ct) ) {
                 mustHaveQueryParam = false;
                 // Drop through.
@@ -164,7 +171,7 @@ public abstract class SPARQLQueryProcessor extends 
ActionService
                 // Nothing specific to do
             }
             else
-                ServletOps.error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, 
"Unsupported: " + incoming);
+                ServletOps.error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, 
"Unsupported: " + ct);
         }
 
         // GET/POST of a form at this point.
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Update.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Update.java
index a7306e3860..1f316c0eaf 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Update.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Update.java
@@ -80,7 +80,7 @@ public class SPARQL_Update extends ActionService
     @Override
     public void execOptions(HttpAction action) {
         ActionLib.setCommonHeadersForOptions(action);
-        action.setResponseHeader(HttpNames.hAllow, "POST,PATCH,OPTIONS");
+        action.setResponseHeader(HttpNames.hAllow, "HEAD,POST,PATCH,OPTIONS");
         ServletOps.success(action);
     }
 
diff --git 
a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestHttpOptions.java
 
b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestHttpOptions.java
index 30838fe79c..dd2d118957 100644
--- 
a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestHttpOptions.java
+++ 
b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestHttpOptions.java
@@ -30,26 +30,26 @@ public class TestHttpOptions extends AbstractFusekiTest
     @Test
     public void options_query() {
         String v = HttpOp.httpOptions(serviceQuery());
-        FusekiTest.assertStringList(v, "GET", "OPTIONS", "POST");
+        FusekiTest.assertStringList(v, "GET", "QUERY", "OPTIONS", "POST", 
"HEAD");
     }
 
     @Test
     public void options_update() {
         String v = HttpOp.httpOptions(serviceUpdate());
-        FusekiTest.assertStringList(v, "OPTIONS", "POST", "PATCH");
+        FusekiTest.assertStringList(v, "OPTIONS", "POST", "PATCH", "HEAD");
     }
 
     @Test
     public void options_dataset_01() {
         String v = HttpOp.httpOptions(databaseURL());
         // Not DELETE
-        FusekiTest.assertStringList(v, "HEAD", "GET", "OPTIONS", "POST", 
"PUT");
+        FusekiTest.assertStringList(v, "GET", "OPTIONS", "POST", "PUT", 
"HEAD");
     }
 
     @Test
     public void options_dataset_02() {
         String v = HttpOp.httpOptions(serviceGSP());
-        FusekiTest.assertStringList(v, "GET", "OPTIONS", "HEAD", "POST", 
"PUT");
+        FusekiTest.assertStringList(v, "GET", "OPTIONS", "POST", "PUT", 
"HEAD");
     }
 
     @Test
diff --git 
a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestSPARQLProtocol.java
 
b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestSPARQLProtocol.java
index d06b6b7ebd..02fb2bf46a 100644
--- 
a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestSPARQLProtocol.java
+++ 
b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestSPARQLProtocol.java
@@ -21,17 +21,25 @@
 
 package org.apache.jena.fuseki.main;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import org.apache.jena.atlas.web.TypedInputStream;
 import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.http.HttpOp;
 import org.apache.jena.query.Query;
 import org.apache.jena.query.QueryFactory;
 import org.apache.jena.riot.WebContent;
+import org.apache.jena.riot.resultset.ResultSetLang;
+import org.apache.jena.riot.rowset.RowSetReader;
 import org.apache.jena.sparql.exec.QueryExec;
 import org.apache.jena.sparql.exec.RowSet;
 import org.apache.jena.sparql.exec.RowSetOps;
@@ -60,8 +68,8 @@ public class TestSPARQLProtocol extends AbstractFusekiTest
         GSP.service(serviceGSP()).graphName(gn1).PUT(graph2);
     }
 
-    static String query(String base, String queryString) {
-        return base + "?query=" + Convert.encWWWForm(queryString);
+    static String httpQueryString(String queryString) {
+        return "?query=" + Convert.encWWWForm(queryString);
     }
 
     @Test
@@ -98,4 +106,39 @@ public class TestSPARQLProtocol extends AbstractFusekiTest
         UpdateExecution proc = UpdateExecutionFactory.createRemoteForm(update, 
serviceUpdate());
         proc.execute();
     }
+
+    // Using QUERY and query string in URL
+    @Test
+    public void query_method_qs() {
+        String qs = httpQueryString("SELECT * { ?s ?p ?o }");
+        String URL = serviceQuery()+qs;
+
+        TypedInputStream results = HttpOp.httpQueryStream(URL, 
WebContent.contentTypeResultsJSON);
+
+        assertEquals(results.getContentType(), 
WebContent.contentTypeResultsJSON);
+        RowSet rs = 
RowSetReader.createReader(ResultSetLang.RS_JSON).read(results.getInputStream(), 
null);
+    }
+
+    // Using QUERY + body
+    @Test
+    public void query_method_body() {
+        String qs = "SELECT * { ?s ?p ?o }";
+        String URL = serviceQuery();
+        BodyPublisher bodyPublisher = BodyPublishers.ofString(qs);
+
+        try ( TypedInputStream results =
+                HttpOp.httpQueryStream(URL, WebContent.contentTypeSPARQLQuery, 
bodyPublisher, WebContent.contentTypeResultsJSON) ) {
+            assertEquals(results.getContentType(), 
WebContent.contentTypeResultsJSON);
+            RowSet rs = 
RowSetReader.createReader(ResultSetLang.RS_JSON).read(results.getInputStream(), 
null);
+        }
+    }
+
+    // Using PATCH + body
+    @Test
+    public void patch_method_body() {
+        String update = "INSERT DATA {}";
+        String URL = serviceUpdate();
+        BodyPublisher bodyPublisher = BodyPublishers.ofString(update);
+        HttpOp.httpPatch(URL, WebContent.contentTypeSPARQLUpdate, 
bodyPublisher);
+    }
 }
diff --git 
a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/test/FusekiTest.java
 
b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/test/FusekiTest.java
index 317dfe64d6..d5b8d34887 100644
--- 
a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/test/FusekiTest.java
+++ 
b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/test/FusekiTest.java
@@ -28,14 +28,14 @@ import static org.junit.jupiter.api.Assertions.*;
 public class FusekiTest {
 
     /** Check whether str is a comma separated list of expected (unordered) */
-    public static void assertStringList(String str, String... expected) {
-        str = str.replace(" ", "");
+    public static void assertStringList(String actual, String... expected) {
+        String str = actual.replace(" ", "");
         String[] x = str.split(",");
         for ( String ex : expected ) {
             assertTrue(containsStr(ex, x), "Got: "+str+" - Does not contain 
"+ex);
         }
         for ( String s : x ) {
-            assertTrue(containsStr(s, expected), "Got: "+str+" - Not expected 
"+s);
+            assertTrue(containsStr(s, expected), "Got: "+str+" - Did not 
expect "+s);
         }
     }
 

Reply via email to