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