You are viewing a plain text version of this content. The canonical link for it is here.
Posted to pr@jena.apache.org by GitBox <gi...@apache.org> on 2021/09/24 06:39:53 UTC

[GitHub] [jena] kinow commented on a change in pull request #1030: SPARQL operations

kinow commented on a change in pull request #1030:
URL: https://github.com/apache/jena/pull/1030#discussion_r715193834



##########
File path: jena-arq/src-examples/arq/examples/ExampleDBpedia2.java
##########
@@ -18,22 +18,28 @@
 
 package arq.examples;
 
-import org.apache.jena.query.* ;
-import org.apache.jena.rdf.model.ModelFactory ;
+import org.apache.jena.query.*;
+import org.apache.jena.sparql.exec.http.QueryExecutionHTTP;
+import org.apache.jena.sparql.util.Context;
 
 public class ExampleDBpedia2
 {
-    static public void main(String... argv) {
-        String queryString = 
-            "SELECT * WHERE { " +
-            "    SERVICE <http://dbpedia-live.openlinksw.com/sparql?timeout=2000> { " +
-            "        SELECT DISTINCT ?company where {?company a <http://dbpedia.org/ontology/Company>} LIMIT 20" +
-            "    }" +
-            "}" ;
-        Query query = QueryFactory.create(queryString) ;
-        try (QueryExecution qexec = QueryExecutionFactory.create(query, ModelFactory.createDefaultModel())) {
-            ResultSet rs = qexec.execSelect() ;
-            ResultSetFormatter.out(System.out, rs, query) ;
+    static public void main(String...argv)

Review comment:
       Missing space between `String...` and `argv`.

##########
File path: jena-arq/src/main/java/org/apache/jena/atlas/web/HttpException.java
##########
@@ -21,21 +21,32 @@
 import org.apache.jena.web.HttpSC;
 
 /**
- * Class of HTTP Exceptions from Atlas code
- *
+ * Class of HTTP Exceptions
  */
 public class HttpException extends RuntimeException {
-    private int statusCode = -1;
-    private String statusLine = null ;
-	private String response;
+    private final int statusCode;
+    private final String statusLine;
+	private final String response;

Review comment:
       Mixed indentation? :point_up: 

##########
File path: jena-arq/src/main/java/org/apache/jena/http/auth/AuthDomain.java
##########
@@ -0,0 +1,63 @@
+/*
+ * 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.http.auth;
+
+import java.net.URI;
+import java.util.Objects;
+
+/** URI and optional realm, as a value-equality pair. */
+public class AuthDomain {
+        URI uri;
+        // May be null;
+        private String realm;
+
+        public AuthDomain(URI uri) {
+            this(uri, null);
+        }
+
+        public AuthDomain(URI uri, String realm) {
+            this.uri = uri;
+            this.realm = realm;
+        }
+
+        public URI getURI() {
+            return uri;
+        }
+
+        public String getRealm() {
+            return realm;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(realm, uri);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if ( this == obj )
+                return true;
+            if ( obj == null )
+                return false;
+            if ( getClass() != obj.getClass() )
+                return false;
+            AuthDomain other = (AuthDomain)obj;
+            return Objects.equals(realm, other.realm) && Objects.equals(uri, other.uri);
+        }
+    }

Review comment:
       Missing newline.

##########
File path: jena-arq/src/main/java/org/apache/jena/http/HttpLib.java
##########
@@ -0,0 +1,711 @@
+/*
+ * 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.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpRequest.Builder;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.net.http.HttpResponse.BodySubscribers;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.InflaterInputStream;
+
+import org.apache.jena.atlas.RuntimeIOException;
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.lib.IRILib;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.atlas.web.TypedInputStream;
+import org.apache.jena.http.auth.AuthEnv;
+import org.apache.jena.http.auth.AuthLib;
+import org.apache.jena.http.sys.HttpRequestModifier;
+import org.apache.jena.http.sys.RegistryRequestModifier;
+import org.apache.jena.query.ARQ;
+import org.apache.jena.riot.web.HttpNames;
+import org.apache.jena.sparql.exec.http.Params;
+import org.apache.jena.sparql.util.Context;
+import org.apache.jena.web.HttpSC;
+
+/**
+ * Operations related to SPARQL HTTP requests - Query, Update and Graph Store protocols.
+ * This class is not considered "API".
+ */
+public class HttpLib {
+
+    private HttpLib() {}
+
+    public static BodyHandler<Void> noBody() { return BodyHandlers.discarding(); }
+
+    public static BodyPublisher stringBody(String str) { return BodyPublishers.ofString(str); }
+
+    private static BodyHandler<InputStream> bodyHandlerInputStream = buildDftBodyHandlerInputStream();
+
+    private static BodyHandler<InputStream> buildDftBodyHandlerInputStream() {
+        return responseInfo -> {
+            return BodySubscribers.ofInputStream();
+        };
+    }
+
+    /** Read the body of a response as a string in UTF-8. */
+    private static Function<HttpResponse<InputStream>, String> bodyInputStreamToString = r-> {
+        try {
+            InputStream in = r.body();
+            String msg = IO.readWholeFileAsUTF8(in);
+            return msg;
+        } catch (Throwable ex) { throw new HttpException(ex); }
+    };
+
+    /**
+     * Calculate basic auth header value. Use with header "Authorization" (constant
+     * {@link HttpNames#hAuthorization}). Best used over https.
+     */
+    public static String basicAuth(String username, String password) {
+        return "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
+    }
+
+    /**
+     * Get the InputStream from an HttpResponse, handling possible compression settings.
+     * The application must consume or close the {@code InputStream} (see {@link #finish(InputStream)}).
+     * Closing the InputStream may close the HTTP connection.
+     * Assumes the status code has been handled e.g. {@link #handleHttpStatusCode} has been called.
+     */
+    public static InputStream getInputStream(HttpResponse<InputStream> httpResponse) {
+        String encoding = httpResponse.headers().firstValue(HttpNames.hContentEncoding).orElse("");
+        InputStream responseInput = httpResponse.body();
+        // Only support "Content-Encoding: <compression>" and not
+        // "Content-Encoding: chunked, <compression>"
+        try {
+            switch (encoding) {
+                case "" :
+                case "identity" : // Proper name for no compression.
+                    return responseInput;
+                case "gzip" :
+                    return new GZIPInputStream(responseInput, 2*1024);
+                case "inflate" :
+                    return new InflaterInputStream(responseInput);
+                case "br" : // RFC7932
+                default :
+                    throw new UnsupportedOperationException("Not supported: Content-Encoding: " + encoding);
+            }
+        } catch (IOException ex) {
+            throw new UncheckedIOException(ex);
+        }
+    }
+
+    /**
+     * Deal with status code and any error message sent as a body in the response.
+     * <p>
+     * It is this handling 4xx/5xx error messages in the body that forces the use of
+     * {@code InputStream}, not generic {@code T}. We don't know until we see the
+     * status code how we are going to process the response body.
+     * <p>
+     * Exits normally without processing the body if the response is 200.
+     * <p>
+     * Throws {@link HttpException} for 3xx (redirection should have happened by
+     * now), 4xx and 5xx, having consumed the body input stream.
+     */
+    public static void handleHttpStatusCode(HttpResponse<InputStream> response) {
+        int httpStatusCode = response.statusCode();
+        // There is no status message in HTTP/2.
+        if ( ! inRange(httpStatusCode, 100, 599) )
+            throw new HttpException("Status code out of range: "+httpStatusCode);
+        else if ( inRange(httpStatusCode, 100, 199) ) {
+            // Informational
+        }
+        else if ( inRange(httpStatusCode, 200, 299) ) {
+            // Success. Continue processing.
+        }
+        else if ( inRange(httpStatusCode, 300, 399) ) {
+            // We had follow redirects on (default client) so it's http->https,
+            // or the application passed on a HttpClient with redirects off.
+            // Either way, we should not continue processing.
+            try {
+                finish(response);
+            } catch (Exception ex) {
+                throw new HttpException("Error discarding body of "+httpStatusCode , ex);
+            }
+            throw new HttpException(httpStatusCode, HttpSC.getMessage(httpStatusCode));
+        }
+        else if ( inRange(httpStatusCode, 400, 499) ) {
+            throw exception(response, httpStatusCode);
+        }
+        else if ( inRange(httpStatusCode, 500, 599) ) {
+            throw exception(response, httpStatusCode);
+        }
+    }
+
+    /**
+     * Handle the HTTP response (see {@link #handleHttpStatusCode(HttpResponse)}) and
+     * return the InputStream if a 200.
+     *
+     * @param httpResponse
+     * @return InputStream
+     */
+    public static InputStream handleResponseInputStream(HttpResponse<InputStream> httpResponse) {
+        handleHttpStatusCode(httpResponse);
+        return getInputStream(httpResponse);
+    }
+
+    /**
+     * Handle the HTTP response (see {@link #handleHttpStatusCode(HttpResponse)}) and
+     * return the TypedInputStream that includes the {@code Content-Type} if a 200.
+     *
+     * @param httpResponse
+     * @return TypedInputStream
+     */
+    public static TypedInputStream handleResponseTypedInputStream(HttpResponse<InputStream> httpResponse) {
+        InputStream input = handleResponseInputStream(httpResponse);
+        String ct = HttpLib.responseHeader(httpResponse, HttpNames.hContentType);
+        return new TypedInputStream(input, ct);
+    }
+
+    /**
+     * Handle the HTTP response and consume the body if a 200.
+     * Otherwise, throw an {@link HttpException}.
+     * @param response
+     */
+    public static void handleResponseNoBody(HttpResponse<InputStream> response) {
+        handleHttpStatusCode(response);
+        finish(response);
+    }
+
+    /**
+     * Handle the HTTP response and read the body to produce a string if a 200.
+     * Otherwise, throw an {@link HttpException}.
+     * @param response
+     * @return String
+     */
+    public static String handleResponseRtnString(HttpResponse<InputStream> response) {
+        InputStream input = handleResponseInputStream(response);
+        try {
+            return IO.readWholeFileAsUTF8(input);
+        } catch (RuntimeIOException e) { throw new HttpException(e); }
+    }
+
+    static HttpException exception(HttpResponse<InputStream> response, int httpStatusCode) {
+
+        URI uri = response.request().uri();
+
+        //long length = HttpLib.getContentLength(response);
+        // Not critical path code. Read body regardless.
+        InputStream in = response.body();
+        String msg;
+        try {
+            msg = IO.readWholeFileAsUTF8(in);
+            if ( msg.isBlank())
+                msg = null;
+        } catch (RuntimeIOException e) {
+            msg = null;
+        }
+        return new HttpException(httpStatusCode, HttpSC.getMessage(httpStatusCode), msg);
+    }
+
+    private static long getContentLength(HttpResponse<InputStream> response) {
+        Optional<String> x = response.headers().firstValue(HttpNames.hContentLength);
+        if ( x.isEmpty() )
+            return -1;
+        try {
+            return Long.parseLong(x.get());
+        } catch (NumberFormatException ex) { return -1; }
+    }
+
+    /** Test x:int in [min, max] */
+    private static boolean inRange(int x, int min, int max) { return min <= x && x <= max; }
+
+    /** Finish with {@code HttpResponse<InputStream>}.
+     * This read and drops any remaining bytes in the response body.
+     * {@code close} may close the underlying HTTP connection.
+     *  See {@link BodySubscribers#ofInputStream()}.
+     */
+    private static void finish(HttpResponse<InputStream> response) {
+        finish(response.body());
+    }
+
+    /** Read to end of {@link InputStream}.
+     *  {@code close} may close the underlying HTTP connection.
+     *  See {@link BodySubscribers#ofInputStream()}.
+     */
+    public static void finish(InputStream input) {
+        consume(input);
+    }
+
+    // This is extracted from commons-io, IOUtils.skip.
+    // Changes:
+    // * No exception.
+    // * Always consumes to the end of stream (or stream throws IOException)
+    // * Larger buffer
+    private static int SKIP_BUFFER_SIZE = 8*1024;
+    private static byte[] SKIP_BYTE_BUFFER = null;
+
+    private static void consume(final InputStream input) {
+        /*
+         * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data
+         * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer
+         * size were variable, we would need to synch. to ensure some other thread did not create a smaller one)
+         */
+        if (SKIP_BYTE_BUFFER == null) {
+            SKIP_BYTE_BUFFER = new byte[SKIP_BUFFER_SIZE];
+        }
+        int bytesRead = 0; // Informational
+        try {
+            for(;;) {
+                // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
+                final long n = input.read(SKIP_BYTE_BUFFER, 0, SKIP_BUFFER_SIZE);
+                if (n < 0) { // EOF
+                    break;
+                }
+                bytesRead += n;
+            }
+        } catch (IOException ex) { /*ignore*/ }
+    }
+
+    /** String to {@link URI}. Throws {@link HttpException} on bad syntax or if the URI isn't absolute. */
+    public static URI toRequestURI(String uriStr) {
+        try {
+            URI uri = new URI(uriStr);
+            if ( ! uri.isAbsolute() )
+                throw new HttpException("Not an absolute URL: <"+uriStr+">");
+            return uri;
+        } catch (URISyntaxException ex) {
+            int idx = ex.getIndex();
+            String msg = (idx<0)
+                ? String.format("Bad URL: %s", uriStr)
+                : String.format("Bad URL: %s starting at character %d", uriStr, idx);
+            throw new HttpException(msg, ex);
+        }
+    }
+
+    // Terminology:
+    // RFC 2616:   Request-Line   = Method SP Request-URI SP HTTP-Version CRLF
+
+    // RFC 7320:   request-line   = method SP request-target SP HTTP-version CRLF
+    // https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1
+
+    // request-target:
+    // https://datatracker.ietf.org/doc/html/rfc7230#section-5.3
+    // When it is for the origin server ==> absolute-path [ "?" query ]
+
+    // EndpointURI: URL for a service, no query string.
+
+    /** Test whether a URI is a service endpoint. It must be absolute, with host and path, and without query string or fragment. */
+    public static boolean isEndpoint(URI uri) {
+        return uri.isAbsolute() &&
+                uri.getHost() != null &&
+                uri.getRawPath() != null &&
+                uri.getRawQuery() == null &&
+                uri.getRawFragment() == null;
+    }
+
+    /**
+     * Return a string (assumed ot be a URI) without query string or fragment.
+     */
+    public static String endpoint(String uriStr) {
+        int idx1 = uriStr.indexOf('?');
+        int idx2 = uriStr.indexOf('#');
+
+        if ( idx1 < 0 && idx2 < 0 )
+            return uriStr;
+
+        int idx = -1;
+        if ( idx1 < 0 && idx2 > 0 )
+            idx = idx2;
+        else if ( idx1 > 0 && idx2 < 0 )
+            idx = idx1;
+        else
+            idx = Math.min(idx1,  idx2);
+        return uriStr.substring(0, idx);
+    }
+
+    /** RFC7320 "request-target", used in digest authentication. */
+    public static String requestTarget(URI uri) {
+        String path = uri.getRawPath();
+        if ( path == null || path.isEmpty() )
+            path = "/";
+        String qs = uri.getQuery();
+        if ( qs == null || qs.isEmpty() )
+            return path;
+        return path+"?"+qs;
+    }
+
+    /** URI, without query string and fragment. */
+    public static URI endpointURI(URI uri) {
+        if ( uri.getRawQuery() == null && uri.getRawFragment() == null )
+            return uri;
+        try {
+            // Same URI except without query strinf an fragment.
+            return new URI(uri.getScheme(), uri.getRawAuthority(), uri.getRawPath(), null, null);
+        } catch (URISyntaxException x) {
+            throw new IllegalArgumentException(x.getMessage(), x);
+        }
+    }
+
+    /** Return a HttpRequest */
+    public static HttpRequest newGetRequest(String url, Consumer<HttpRequest.Builder> modifier) {
+        HttpRequest.Builder builder = HttpLib.requestBuilderFor(url).uri(toRequestURI(url)).GET();
+        if ( modifier != null )
+            modifier.accept(builder);
+        return builder.build();
+    }
+
+    public static <X> X dft(X value, X dftValue) {
+        return (value != null) ? value : dftValue;
+    }
+
+    public static <X> List<X> copyArray(List<X> array) {
+        if ( array == null )
+            return null;
+        return new ArrayList<>(array);
+    }
+
+    /** Encode a string suitable for use in an URL query string */
+    public static String urlEncodeQueryString(String str) {
+        // java.net.URLEncoder is excessive - it encodes / and : which
+        // is not necessary in a query string or fragment.
+        return IRILib.encodeUriQueryFrag(str);
+    }
+
+    /** Query string is assumed to already be encoded. */
+    public static String requestURL(String url, String queryString) {
+        if ( queryString == null || queryString.isEmpty() )
+            // Empty string. Don't add "?"
+            return url;
+        String sep =  url.contains("?") ? "&" : "?";
+        String requestURL = url+sep+queryString;
+        return requestURL;
+    }
+
+    public static HttpRequest.Builder requestBuilderFor(String serviceEndpoint) {
+        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
+        return AuthEnv.get().addAuth(requestBuilder, serviceEndpoint);
+    }
+
+    public static Builder requestBuilder(String url, Map<String, String> httpHeaders, long readTimeout, TimeUnit readTimeoutUnit) {
+        HttpRequest.Builder builder = HttpLib.requestBuilderFor(url);
+        headers(builder, httpHeaders);
+        builder.uri(toRequestURI(url));
+        if ( readTimeout >= 0 )
+            builder.timeout(Duration.ofMillis(readTimeoutUnit.toMillis(readTimeout)));
+        return builder;
+    }
+
+    /** Create a {@code HttpRequest.Builder} from an {@code HttpRequest}. */
+    public static HttpRequest.Builder createBuilder(HttpRequest request) {
+        HttpRequest.Builder builder = HttpRequest.newBuilder()
+                .expectContinue(request.expectContinue())
+                .uri(request.uri());
+        builder.method(request.method(), request.bodyPublisher().orElse(BodyPublishers.noBody()));
+        request.timeout().ifPresent(builder::timeout);
+        request.version().ifPresent(builder::version);
+        request.headers().map().forEach((name, values)->values.forEach(value->builder.header(name, value)));
+        return builder;
+    }
+
+    /** Set the headers from the Map if the map is not null. Returns the Builder. */
+    static Builder headers(Builder builder, Map<String, String> httpHeaders) {
+        if ( httpHeaders != null )
+            httpHeaders.forEach(builder::header);
+        return builder;
+    }
+
+    /** Set the "Accept" header if value is not null. Returns the builder. */
+    public static Builder acceptHeader(Builder builder, String acceptHeader) {
+        if ( acceptHeader != null )
+            builder.header(HttpNames.hAccept, acceptHeader);
+        return builder;
+    }
+
+    /** Set the "Content-Type" header if value is not null. Returns the builder. */
+    public static Builder contentTypeHeader(Builder builder, String contentType) {
+        if ( contentType != null )
+            builder.header(HttpNames.hContentType, contentType);
+        return builder;
+    }
+
+    // Disabled. Don't encourage using compression ("Content-Encoding: gzip") because it interacts with streaming.
+    // Specifically, streaming (unknown Content-Length) needs chunking. Both chunking and compression
+    // encodings use the same HTTP header. Yet they are handled by different layers.
+    // The basic http code handles chunking.
+    // The complete encoding header can get removed resulting in a compressed stream
+    // without any indication being passed to the application.
+//    /**
+//     * Set the "Accept-Encoding" header. Returns the builder.
+//     * See {@link #getInputStream(HttpResponse)}.
+//     */
+//    public
+//    /*package*/ static Builder acceptEncodingCompressed(Builder builder) {
+//        builder.header(HttpNames.hAcceptEncoding, WebContent.acceptEncodingCompressed);
+//        return builder;
+//    }
+
+    /**
+     * Execute a request, return a {@code HttpResponse<InputStream>} which
+     * can be passed to {@link #handleResponseInputStream(HttpResponse)} which will
+     * convert non-2xx status code to {@link HttpException HttpExceptions}.
+     * <p>
+     * This function applies the HTTP authentication challenge support
+     * and will repeat the request if necessary with added authentication.
+     * <p>
+     * See {@link AuthEnv} for authentication registration.
+     * <br/>
+     * See {@link #executeJDK} to execute exactly once without challenge response handling.
+     *
+     * @see AuthEnv AuthEnv for authentic registration
+     * @see #executeJDK executeJDK to execute exacly once.
+     *
+     * @param httpClient
+     * @param httpRequest
+     * @return HttpResponse
+     */
+    public static HttpResponse<InputStream> execute(HttpClient httpClient, HttpRequest httpRequest) {
+        return execute(httpClient, httpRequest, BodyHandlers.ofInputStream());
+    }
+
+    /**
+     * Execute a request, return a {@code HttpResponse<X>} which
+     * can be passed to {@link #handleHttpStatusCode(HttpResponse)} which will
+     * convert non-2xx status code to {@link HttpException HttpExceptions}.
+     * <p>
+     * This function applies the HTTP authentication challenge support
+     * and will repeat the request if necessary with added authentication.
+     * <p>
+     * See {@link AuthEnv} for authentication registration.
+     * <br/>
+     * See {@link #executeJDK} to execute exactly once without challenge response handling.
+     *
+     * @see AuthEnv AuthEnv for authentic registration
+     * @see #executeJDK executeJDK to execute exacly once.
+     *
+     * @param httpClient
+     * @param httpRequest
+     * @param bodyHandler
+     * @return HttpResponse
+     */
+    public
+    /*package*/ static <X> HttpResponse<X> execute(HttpClient httpClient, HttpRequest httpRequest, BodyHandler<X> bodyHandler) {
+        // To run with no jena-supplied authentication handling.
+        if ( false )
+            return executeJDK(httpClient, httpRequest, bodyHandler);
+        URI uri = httpRequest.uri();
+        URI key = null;
+
+        AuthEnv authEnv = AuthEnv.get();
+
+        if ( uri.getUserInfo() != null ) {
+            String[] up = uri.getUserInfo().split(":");
+            if ( up.length == 2 ) {
+                // Only if "user:password@host", not "user@host"
+                key = HttpLib.endpointURI(uri);
+                // The auth key will be with u:p making it specific.
+                authEnv.registerUsernamePassword(key, up[0], up[1]);
+            }
+        }
+        try {
+            return AuthLib.authExecute(httpClient, httpRequest, bodyHandler);
+        } finally {
+            if ( key != null )
+                authEnv.unregisterUsernamePassword(key);
+        }
+    }
+
+    /**
+     * Execute request and return a response without authentication challenge handling.
+     * @param httpClient
+     * @param httpRequest
+     * @param bodyHandler
+     * @return HttpResponse
+     */
+    public static <T> HttpResponse<T> executeJDK(HttpClient httpClient, HttpRequest httpRequest, BodyHandler<T> bodyHandler) {
+        try {
+            // This is the one place all HTTP requests go through.
+            logRequest(httpRequest);
+            HttpResponse<T> httpResponse = httpClient.send(httpRequest, bodyHandler);
+            logResponse(httpResponse);
+            return httpResponse;
+        //} catch (HttpTimeoutException ex) {
+        } catch (IOException | InterruptedException ex) {
+            if ( ex.getMessage() != null ) {
+                // This is silly.
+                // Rather than an HTTP exception, bad authentication becomes IOException("too many authentication attempts");
+                // or IOException("No credentials provided") if the authenticator decides to return null.
+                if ( ex.getMessage().contains("too many authentication attempts") ||
+                     ex.getMessage().contains("No credentials provided") ) {

Review comment:
       Ah, I'm thinking these exceptions are coming from the HTTP client lib we are using?

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLink.java
##########
@@ -0,0 +1,491 @@
+/*
+ * 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.rdflink;
+
+import java.util.function.Consumer;
+
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.query.QueryFactory;
+import org.apache.jena.rdfconnection.JenaConnectionException;
+import org.apache.jena.rdfconnection.RDFConnection;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.Quad;
+import org.apache.jena.sparql.core.Transactional;
+import org.apache.jena.sparql.engine.binding.Binding;
+import org.apache.jena.sparql.exec.QueryExec;
+import org.apache.jena.sparql.exec.QueryExecBuilder;
+import org.apache.jena.sparql.exec.RowSet;
+import org.apache.jena.system.Txn;
+import org.apache.jena.update.Update;
+import org.apache.jena.update.UpdateFactory;
+import org.apache.jena.update.UpdateRequest;
+
+/**
+ * Interface for SPARQL operations on a datasets, whether local or remote.
+ * Operations can performed via this interface or via the various

Review comment:
       can be performed

##########
File path: jena-arq/src/main/java/org/apache/jena/http/HttpOp.java
##########
@@ -0,0 +1,436 @@
+/*
+ * 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.http;
+
+import static org.apache.jena.http.HttpLib.*;
+import static org.apache.jena.http.Push.PATCH;
+import static org.apache.jena.http.Push.POST;
+import static org.apache.jena.http.Push.PUT;
+
+import java.io.InputStream;
+import java.net.Authenticator;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.util.Objects;
+
+import org.apache.jena.atlas.json.JSON;
+import org.apache.jena.atlas.json.JsonValue;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.atlas.web.TypedInputStream;
+import org.apache.jena.http.auth.AuthEnv;
+import org.apache.jena.riot.WebContent;
+import org.apache.jena.riot.web.HttpNames;
+import org.apache.jena.sparql.exec.http.GSP;
+import org.apache.jena.sparql.exec.http.Params;
+import org.apache.jena.web.HttpSC;
+
+/**
+ * This is a collection of convenience operations for HTTP requests, mostly in
+ * support of RDF handling and common, basic use cases for HTTP. It is not
+ * comprehensive. For more complicated requirements of HTTP, then the
+ * application can use {@link java.net.http.HttpClient} directly.
+ * <p>
+ * Authentication can be handled by supplying a {@link java.net.http.HttpClient} which
+ * has been built with an {@link Authenticator} or for challenge response (basic and digest)
+ * see {@link AuthEnv}.
+ * <p>
+ * Operations throw {@link HttpException} when the response is not 2xx, except for
+ * "httpGetString" and "httpPostRtnString" which return null for a 404 response.
+ * </p>
+ * <p>
+ * Also supported:
+ * <ul>
+ * <li>GET and return a string</li>
+ * <li>GET and return a JSON structure</li>
+ * </ul>
+ * </p>
+ * @see HttpRDF
+ * @see GSP
+ */
+public class HttpOp {
+
+    private HttpOp() {}
+
+    // -- GET, POST returning a string (shorthand helpers, esp for tests).
+
+    /** Perform an HTTP and return the body as a string, Return null for a "404 Not Found". */
+    public static String httpGetString(String url) {
+        return httpGetString(HttpEnv.getDftHttpClient(), url, null);
+    }
+
+    /** Perform an HTTP and return the body as a string, Return null for a "404 Not Found". */
+    public static String httpGetString(String url, String acceptHeader) {
+        return httpGetString(HttpEnv.getDftHttpClient(), url, acceptHeader);
+    }
+
+    /** Perform an HTTP and return the body as a string. Return null for a "404 Not Found". */
+    public static String httpGetString(HttpClient httpClient, String url) {
+        return httpGetString(httpClient, url, null);
+    }
+
+    /** Perform an HTTP and return the body as a string. Return null for a "404 Not Found". */
+    public static String httpGetString(HttpClient httpClient, String url, String acceptHeader) {
+        HttpRequest request = newGetRequest(url, setAcceptHeader(acceptHeader));
+        HttpResponse<InputStream> response = execute(httpClient, request);
+        try {
+            return handleResponseRtnString(response);
+        } catch (HttpException ex) {
+            if ( ex.getStatusCode() == HttpSC.NOT_FOUND_404 )
+                return null;
+            throw ex;
+        }
+    }
+
+    /**
+     * POST (without a body) - like httpGetString but uses POST - expects a response.
+     * Return null for a "404 Not Found".
+     */
+    public static String httpPostRtnString(String url) {
+        return httpPostRtnString(HttpEnv.getDftHttpClient(), url);
+    }
+
+    /**
+     * POST (without a body) - like httpGetString but uses POST - expects a response.
+     * Return null for a "404 Not Found".
+     */
+    public static String httpPostRtnString(HttpClient httpClient, String url) {
+        HttpRequest requestData = HttpLib.requestBuilderFor(url)
+            .POST(BodyPublishers.noBody())
+            .uri(toRequestURI(url))
+            .build();
+        HttpResponse<InputStream> response = execute(httpClient, requestData);
+        try {
+            return handleResponseRtnString(response);
+        } catch (HttpException ex) {
+            if ( ex.getStatusCode() == HttpSC.NOT_FOUND_404 )
+                return null;
+            throw ex;
+        }
+    }
+
+    // ---- POST HTML form
+
+    /** POST params as a HTML form. */
+    public static void httpPostForm(String url, Params params) {
+        try ( TypedInputStream in = execPostForm(HttpEnv.getDftHttpClient(), url, params, null) ) {}
+    }
+
+    /** POST params as a HTML form. */
+    public static TypedInputStream httpPostForm(String url, Params params, String acceptString) {
+        return execPostForm(HttpEnv.getDftHttpClient(), url, params, acceptString);
+    }
+
+    private static TypedInputStream execPostForm(HttpClient httpClient, String url, Params params, String acceptString) {
+        Objects.requireNonNull(url);
+        acceptString = HttpLib.dft(acceptString, "*/*");
+        URI uri = toRequestURI(url);
+        String formData = params.httpString();
+        HttpRequest request = HttpLib.requestBuilderFor(url)
+            .uri(uri)
+            .POST(BodyPublishers.ofString(formData))
+            .header(HttpNames.hContentType, WebContent.contentTypeHTMLForm)
+            .header(HttpNames.hAccept, acceptString)
+            .build();
+        HttpResponse<InputStream> response = execute(httpClient, request);
+        return handleResponseTypedInputStream(response);
+    }
+
+    // ---- JSON
+
+    public static JsonValue httpPostRtnJSON(String url) {
+        try ( TypedInputStream in = httpPostStream(url, WebContent.contentTypeJSON) ) {
+            return JSON.parseAny(in.getInputStream());
+        }
+    }
+
+    public static JsonValue httpGetJson(String url) {

Review comment:
       The `Rtn` in other methods stands for `Returns`? Maybe name other methods just `Json`, or just `JSON`? This one doesn't follow the pattern used in others...

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetFactory.java
##########
@@ -0,0 +1,23 @@
+/*
+ * 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.sparql.exec;
+
+public class RowSetFactory {
+

Review comment:
       There's no methods for the factory for a `RowSet`?

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExec.java
##########
@@ -0,0 +1,254 @@
+/*
+ * 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.sparql.exec;
+
+import java.util.Iterator;
+
+import org.apache.jena.atlas.json.JsonArray;
+import org.apache.jena.atlas.json.JsonObject;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.core.Quad;
+import org.apache.jena.sparql.exec.http.QueryExecHTTPBuilder;
+import org.apache.jena.sparql.graph.GraphFactory;
+import org.apache.jena.sparql.util.Context;
+
+/**
+ * Query execution interface working at the Graph-Node-Triple level.
+ *
+ * @see QueryExecution
+ */
+public interface QueryExec extends AutoCloseable {
+
+    /**
+     * Create a {@link QueryExecBuilder} for a dataset.
+     * For local dataset specific configuration, use {@link #newBuilder}().dataset(dataset)
+     * to get a {@link QueryExecDatasetBuilder}.
+     */
+    public static QueryExecBuilder dataset(DatasetGraph dataset) {
+        return QueryExecDatasetBuilder.create().dataset(dataset);
+    }
+
+    /** Create a {@link QueryExecBuilder} for a graph. */
+    public static QueryExecBuilder dataset(Graph graph) {
+        return QueryExecDatasetBuilder.create().graph(graph);
+    }
+
+    /** Create a {@link QueryExecBuilder} for a remote endpoint. */
+    public static QueryExecBuilder endpoint(String serviceURL) {
+        return QueryExecHTTPBuilder.create().endpoint(serviceURL);
+    }
+
+    /** Create an uninitialized {@link QueryExecDatasetBuilder}. */
+    public static QueryExecDatasetBuilder newBuilder() {
+        return QueryExecDatasetBuilder.create();
+    }
+
+    /**
+     * The dataset against which the query will execute. May be null - the dataset
+     * may be remote or the query itself has a dataset description.s

Review comment:
       Dangling `s` after period.

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkFactory.java
##########
@@ -0,0 +1,176 @@
+/*
+ * 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.rdflink;
+
+import org.apache.jena.rdfconnection.Isolation;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sys.JenaSystem;
+
+public class RDFLinkFactory {
+    static { JenaSystem.init(); }
+
+    /**
+     * Create a connection to a remote location by URL. This is the URL for the
+     * dataset. This call assumes the SPARQL Query endpoint, SPARQL Update endpoint
+     * and SPARQL Graph Store Protocol endpoinst are the same URL.
+     * Thisis suported by <a href="http://jena.apache.org/documentation/fuseki2">Apache Jena Fuseki</a>.

Review comment:
       Thisis suported-> This is supported

##########
File path: jena-arq/src-examples/arq/examples/ExampleDBpedia2.java
##########
@@ -18,22 +18,28 @@
 
 package arq.examples;
 
-import org.apache.jena.query.* ;
-import org.apache.jena.rdf.model.ModelFactory ;
+import org.apache.jena.query.*;
+import org.apache.jena.sparql.exec.http.QueryExecutionHTTP;
+import org.apache.jena.sparql.util.Context;
 
 public class ExampleDBpedia2
 {
-    static public void main(String... argv) {
-        String queryString = 
-            "SELECT * WHERE { " +
-            "    SERVICE <http://dbpedia-live.openlinksw.com/sparql?timeout=2000> { " +
-            "        SELECT DISTINCT ?company where {?company a <http://dbpedia.org/ontology/Company>} LIMIT 20" +
-            "    }" +
-            "}" ;
-        Query query = QueryFactory.create(queryString) ;
-        try (QueryExecution qexec = QueryExecutionFactory.create(query, ModelFactory.createDefaultModel())) {
-            ResultSet rs = qexec.execSelect() ;
-            ResultSetFormatter.out(System.out, rs, query) ;
+    static public void main(String...argv)
+    {
+        String queryStr = "select distinct ?Concept where {[] a ?Concept} LIMIT 10";

Review comment:
       Nit-picking, but maybe for consistency:
   
   ```suggestion
          String queryStr = "SELECT DISTINCT ?Concept WHERE {[] a ?Concept} LIMIT 10";
   ```

##########
File path: jena-tdb/src/test/java/org/apache/jena/tdb/store/TestQuadFilter.java
##########
@@ -102,6 +102,7 @@ private static void test(Dataset dataset, String qs, int withFilter, int without
             // No filter.
             try(QueryExecution qExec = QueryExecutionFactory.create(query, dataset)) {
                 qExec.getContext().setTrue(TDB.symUnionDefaultGraph);
+                Dataset dsx = qExec.getDataset();

Review comment:
       Should we use the `dsx` in this test for something, `assertNotNull`? Or are we retrieving it just to test that it worked? 

##########
File path: jena-arq/src/main/java/org/apache/jena/http/HttpEnv.java
##########
@@ -0,0 +1,74 @@
+/*
+ * 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.http;
+
+import java.net.http.HttpClient;
+import java.net.http.HttpClient.Redirect;
+import java.time.Duration;
+
+import org.apache.jena.riot.RDFFormat;
+
+/**
+ * JVM wide settings.
+ */
+public class HttpEnv {
+
+    // These preserve prefixes.
+    public static final RDFFormat dftTriplesFormat = RDFFormat.TURTLE_BLOCKS;

Review comment:
       `dft` == default? depth-first ... traversal? 

##########
File path: jena-arq/src/main/java/org/apache/jena/http/AsyncHttpRDF.java
##########
@@ -0,0 +1,190 @@
+/*
+ * 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.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.function.Consumer;
+
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.riot.WebContent;
+import org.apache.jena.riot.system.StreamRDF;
+import org.apache.jena.riot.system.StreamRDFLib;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.core.Transactional;
+import org.apache.jena.sparql.core.TransactionalNull;
+import org.apache.jena.sparql.graph.GraphFactory;
+import org.apache.jena.web.HttpSC;
+
+/**
+ * A collection of convenience operations for HTTP level operations
+ * for RDF related tasks. performed asynchronously.
+ *
+ * See also {@link HttpRDF}.
+ */
+public class AsyncHttpRDF {
+    // Add POST and PUT
+
+    private static Consumer<HttpRequest.Builder> acceptHeaderGraph = HttpLib.setAcceptHeader(WebContent.defaultGraphAcceptHeader);
+    private static Consumer<HttpRequest.Builder> acceptHeaderDatasetGraph = HttpLib.setAcceptHeader(WebContent.defaultDatasetAcceptHeader);
+
+    /** Get a graph, asynchronously */
+    public static CompletableFuture<Graph> asyncGetGraph(String url) {
+        return asyncGetGraph(HttpEnv.getDftHttpClient(), url);
+    }
+
+    /** Get a graph, asynchronously */
+    public static CompletableFuture<Graph> asyncGetGraph(HttpClient httpClient, String url) {
+        Objects.requireNonNull(httpClient, "HttpClient");
+        Objects.requireNonNull(url, "URL");
+        Graph graph = GraphFactory.createDefaultGraph();
+        StreamRDF dest = StreamRDFLib.graph(graph);
+        CompletableFuture<Void> cf = asyncGetToStream(httpClient, url, acceptHeaderGraph, dest, null);
+        return cf.thenApply(x->graph);
+    }
+
+    /** Get a dataset, asynchronously */
+    public static CompletableFuture<DatasetGraph> asyncGetDatasetGraph(String url) {
+        return asyncGetDatasetGraph(HttpEnv.getDftHttpClient(), url);
+    }
+
+    /** Get a dataset, asynchronously */
+    public static CompletableFuture<DatasetGraph> asyncGetDatasetGraph(HttpClient httpClient, String url) {
+        Objects.requireNonNull(httpClient, "HttpClient");
+        Objects.requireNonNull(url, "URL");
+        DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+        StreamRDF dest = StreamRDFLib.dataset(dsg);
+        CompletableFuture<Void> cf = asyncGetToStream(httpClient, url, acceptHeaderDatasetGraph, dest, dsg);
+        return cf.thenApply(x->dsg);
+    }
+
+    /**
+     * Load a DatasetGraph asynchronously.
+     * The dataset is updated inside a transaction.
+     */
+    public static CompletableFuture<Void> asyncLoadDatasetGraph(String url, DatasetGraph dsg) {
+        return asyncLoadDatasetGraph(HttpEnv.getDftHttpClient(), url, dsg);
+    }
+
+    /**
+     * Load a DatasetGraph asynchronously.
+     * The dataset is updated inside a transaction.
+     */
+    public static CompletableFuture<Void> asyncLoadDatasetGraph(HttpClient httpClient, String url, DatasetGraph dsg) {
+        Objects.requireNonNull(httpClient, "HttpClient");
+        Objects.requireNonNull(url, "URL");
+        Objects.requireNonNull(dsg, "dataset");
+        StreamRDF dest = StreamRDFLib.dataset(dsg);
+        return asyncGetToStream(httpClient, url, acceptHeaderDatasetGraph, dest, dsg);
+    }
+
+    /**
+     * Load a DatasetGraph asynchronously.
+     * The dataset is updated inside a transaction.
+     */
+    public static CompletableFuture<Void> asyncLoadDatasetGraph(HttpClient httpClient, String url, Map<String, String> headers, DatasetGraph dsg) {
+        Objects.requireNonNull(httpClient, "HttpClient");
+        Objects.requireNonNull(url, "URL");
+        Objects.requireNonNull(dsg, "dataset");
+        StreamRDF dest = StreamRDFLib.dataset(dsg);
+        return asyncGetToStream(httpClient, url, HttpLib.setHeaders(headers), dest, dsg);
+    }
+
+    /**
+     * Execute an asynchronous GET and parse the result to a StreamRDF.
+     * If "transactional" is not null, the object is used a write transaction around the parsing step.
+     * <p>
+     * Call {@link CompletableFuture#join} to await completion.<br/>
+     * Call {@link #syncOrElseThrow(CompletableFuture)} to await completion,
+     * with exceptions translated to the underlying {@code RuntimeException}.
+     */
+    public static CompletableFuture<Void> asyncGetToStream(HttpClient httpClient, String url, String acceptHeader, StreamRDF dest, Transactional transactional) {
+        Objects.requireNonNull(httpClient, "HttpClient");
+        Objects.requireNonNull(url, "URL");
+        Objects.requireNonNull(dest, "StreamRDF");
+        Consumer<HttpRequest.Builder> setAcceptHeader = HttpLib.setAcceptHeader(acceptHeader);
+        return asyncGetToStream(httpClient, url, setAcceptHeader, dest, transactional);
+    }
+
+    private static CompletableFuture<Void> asyncGetToStream(HttpClient httpClient, String url, Consumer<HttpRequest.Builder> modifier, StreamRDF dest, Transactional _transactional) {
+        CompletableFuture<HttpResponse<InputStream>> cf = asyncGetToInput(httpClient, url, modifier);
+        Transactional transact = ( _transactional == null ) ? TransactionalNull.create() : _transactional;
+        return cf.thenApply(httpResponse->{
+            transact.executeWrite(()->HttpRDF.httpResponseToStreamRDF(url, httpResponse, dest));
+            return null;
+        });
+    }
+
+    /**
+     * Wait for the {@code CompletableFuture} or throw a runtime exception.
+     * This operation extracts RuntimeException from the {@code CompletableFuture}.
+     */
+    public static void syncOrElseThrow(CompletableFuture<Void> cf) {
+        getOrElseThrow(cf);
+    }
+
+    /**
+     * Wait for the {@code CompletableFuture} then return the result or throw a runtime exception.
+     * This operation extracts RuntimeException from the {@code CompletableFuture}.
+     */
+    public static <T> T getOrElseThrow(CompletableFuture<T> cf) {
+        Objects.requireNonNull(cf);
+        try {
+            return cf.join();
+        //} catch (CancellationException ex1) { // Let this pass out.
+        } catch (CompletionException ex) {
+            if ( ex.getCause() != null ) {
+                Throwable cause = ex.getCause();
+                if ( cause instanceof RuntimeException )
+                    throw (RuntimeException)cause;
+                if ( cause instanceof IOException ) {
+                    IOException iox = (IOException)cause;
+                    // Rather than an HTTP exception, bad authentication becomes IOException("too many authentication attempts");
+                    if ( iox.getMessage().contains("too many authentication attempts") ||
+                            iox.getMessage().contains("No credentials provided") ) {

Review comment:
       If the IOE is thrown by Jena, it would probably be better to throw a more specialized Exception instead of relying on the string values?

##########
File path: jena-arq/src/main/java/org/apache/jena/http/AsyncHttpRDF.java
##########
@@ -0,0 +1,190 @@
+/*
+ * 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.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.function.Consumer;
+
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.riot.WebContent;
+import org.apache.jena.riot.system.StreamRDF;
+import org.apache.jena.riot.system.StreamRDFLib;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.core.Transactional;
+import org.apache.jena.sparql.core.TransactionalNull;
+import org.apache.jena.sparql.graph.GraphFactory;
+import org.apache.jena.web.HttpSC;
+
+/**
+ * A collection of convenience operations for HTTP level operations
+ * for RDF related tasks. performed asynchronously.

Review comment:
       ```suggestion
     * for RDF related tasks. Performed asynchronously.
   ```

##########
File path: jena-rdfconnection/src/test/java/org/apache/jena/rdflink/AbstractTestRDFLink.java
##########
@@ -0,0 +1,518 @@
+/*
+ * 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.rdflink;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.jena.atlas.iterator.Iter;
+import org.apache.jena.atlas.lib.StrUtils;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.graph.compose.Union;
+import org.apache.jena.query.ReadWrite;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.sparql.JenaTransactionException;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.Var;
+import org.apache.jena.sparql.engine.binding.Binding;
+import org.apache.jena.sparql.exec.QueryExec;
+import org.apache.jena.sparql.exec.RowSet;
+import org.apache.jena.sparql.sse.SSE;
+import org.apache.jena.sparql.util.IsoMatcher;
+import org.apache.jena.system.Txn;
+import org.apache.jena.update.UpdateRequest;
+import org.apache.jena.web.HttpSC;
+import org.junit.Assume;
+import org.junit.Test;
+
+public abstract class AbstractTestRDFLink {
+    // Testing data.
+    static String DIR = "testing/RDFLink/";
+
+    protected abstract RDFLink link();
+    // Not all link types support abort.
+    protected abstract boolean supportsAbort();
+
+    // ---- Data
+    static String dsgdata = StrUtils.strjoinNL
+        ("(dataset"
+        ,"  (graph (:s :p :o) (:s0 :p0 _:a))"
+        ,"  (graph :g1 (:s :p :o) (:s1 :p1 :o1))"
+        ,"  (graph :g2 (:s :p :o) (:s2 :p2 :o))"
+        ,")"
+        );
+
+    static String dsgdata2 = StrUtils.strjoinNL
+        ("(dataset"
+        ,"  (graph (:x :y :z))"
+        ,"  (graph :g9 (:s :p :o))"
+        ,")"
+        );
+
+
+    static String graphData1 = StrUtils.strjoinNL
+        ("(graph (:s :p :o) (:s1 :p1 :o))"
+        );
+
+    static String graphData2 = StrUtils.strjoinNL
+        ("(graph (:s :p :o) (:s2 :p2 :o))"
+        );
+
+    static DatasetGraph dsg      = SSE.parseDatasetGraph(dsgdata);
+    static DatasetGraph dsg2     = SSE.parseDatasetGraph(dsgdata2);
+
+    static Node       graphName  = NodeFactory.createURI("http://test/graph");
+    static Node       graphName2 = NodeFactory.createURI("http://test/graph2");
+    static Graph      graph1     = SSE.parseGraph(graphData1);
+    static Graph      graph2     = SSE.parseGraph(graphData2);
+    // ---- Data
+
+    @Test public void connect_01() {
+        @SuppressWarnings("resource")
+        RDFLink link = link();
+        assertFalse(link.isClosed());
+        link.close();
+        assertTrue(link.isClosed());
+        // Allow multiple close()
+        link.close();
+    }
+
+    @Test public void dataset_load_1() {
+        String testDataFile = DIR+"data.trig";
+        try ( RDFLink link = link() ) {
+            link.loadDataset(testDataFile);
+            DatasetGraph ds0 = RDFDataMgr.loadDatasetGraph(testDataFile);
+            DatasetGraph ds = link.getDataset();
+            assertTrue("Datasets not isomorphic", isomorphic(ds0, ds));
+        }
+    }
+
+    @Test public void dataset_put_1() {
+        try ( RDFLink link = link() ) {
+            link.putDataset(dsg);
+            DatasetGraph dsg1 = link.getDataset();
+            assertTrue("Datasets not isomorphic", isomorphic(dsg, dsg1));
+        }
+    }
+
+    @Test public void dataset_put_2() {
+        try ( RDFLink link = link() ) {
+            link.putDataset(dsg);
+            link.putDataset(dsg2);
+            DatasetGraph dsg1 = link.getDataset();
+            assertTrue("Datasets not isomorphic", isomorphic(dsg2, dsg1));
+        }
+    }
+
+    @Test public void dataset_post_1() {
+        try ( RDFLink link = link() ) {
+            link.loadDataset(dsg);
+            DatasetGraph dsg1 = link.getDataset();
+            assertTrue("Datasets not isomorphic", isomorphic(dsg, dsg1));
+        }
+    }
+
+    @Test public void dataset_post_2() {
+        try ( RDFLink link = link() ) {
+            link.loadDataset(dsg);
+            link.loadDataset(dsg2);
+            DatasetGraph dsg1 = link.getDataset();
+            long x = Iter.count(dsg1.listGraphNodes());
+            assertEquals("NG count", 3, x);
+            assertFalse("Datasets are isomorphic", isomorphic(dsg, dsg1));
+            assertFalse("Datasets are isomorphic", isomorphic(dsg2, dsg1));
+        }
+    }
+
+    @Test public void dataset_clear_1() {
+        try ( RDFLink link = link() ) {
+            link.loadDataset(dsg);
+            {
+                DatasetGraph dsg1 = link.getDataset();
+                assertFalse(dsg1.isEmpty());
+            }
+            link.clearDataset();
+            {
+                DatasetGraph dsg2 = link.getDataset();
+                assertTrue(dsg2.isEmpty());
+            }
+        }
+    }
+
+
+    // Default graph
+
+    @Test public void graph_load_1() {
+        String testDataFile = DIR+"data.ttl";
+        Graph g0 = RDFDataMgr.loadGraph(testDataFile);
+        try ( RDFLink link = link() ) {
+            link.load(testDataFile);
+            Graph g1 = link.get();
+            assertTrue("Graphs not isomorphic", isomorphic(g0, g1));
+        }
+    }
+
+    @Test public void graph_put_1() {
+        try ( RDFLink link = link() ) {
+            link.put(graph1);
+            DatasetGraph dsg1 = link.getDataset();
+            Graph g0 = link.get();
+            assertTrue("Graphs not isomorphic", isomorphic(graph1, dsg1.getDefaultGraph()));
+            Graph g1 = link.get();
+            assertTrue("Graphs not isomorphic", isomorphic(graph1, g1));
+        }
+    }
+
+    @Test public void graph_put_2() {
+        try ( RDFLink link = link() ) {
+            link.put(graph1);
+            link.put(graph2);
+            Graph g = link.get();
+            assertTrue("Graphs not isomorphic", isomorphic(g, graph2));
+            assertFalse("Graphs not isomorphic", isomorphic(g, graph1));
+        }
+    }
+
+    @Test public void graph_post_1() {
+        try ( RDFLink link = link() ) {
+            link.load(graph1);
+            Graph g = link.get();
+            assertTrue("Graphs not isomorphic", isomorphic(g, graph1));
+        }
+    }
+
+    @Test public void graph_post_2() {
+        try ( RDFLink link = link() ) {
+            link.load(graph1);
+            link.load(graph2);
+            Graph g = link.get();
+            Graph g0 = new Union( graph2, graph1);
+            assertTrue("Graphs are not isomorphic", isomorphic(g0, g));
+        }
+    }
+
+    @Test public void graph_delete_1() {
+        String testDataFile = DIR+"data.ttl";
+        Graph g0 = RDFDataMgr.loadGraph(testDataFile);
+        try ( RDFLink link = link() ) {
+            link.load(testDataFile);
+            Graph g1 = link.get();
+            assertFalse(g1.isEmpty());
+            link.delete();
+            Graph g2 = link.get();
+            assertTrue(g2.isEmpty());
+        }
+    }
+
+    // Named graphs
+
+    @Test public void named_graph_load_1() {
+        String testDataFile = DIR+"data.ttl";
+        Graph g0 = RDFDataMgr.loadGraph(testDataFile);
+        try ( RDFLink link = link() ) {
+            link.load(graphName, testDataFile);
+            Graph g = link.get(graphName);
+            assertTrue("Graphs not isomorphic", isomorphic(g0, g));
+            Graph gDft = link.get();
+            assertTrue(gDft.isEmpty());
+        }
+    }
+
+    @Test public void named_graph_put_1() {
+        try ( RDFLink link = link() ) {
+            link.put(graphName, graph1);
+            DatasetGraph dsg1 = link.getDataset();
+            Graph g0 = link.get(graphName);
+            assertTrue("Graphs not isomorphic", isomorphic(graph1, dsg1.getGraph(graphName)));
+            Graph g = link.get(graphName);
+            assertTrue("Graphs not isomorphic", isomorphic(graph1, g));
+        }
+    }
+
+    @Test public void named_graph_put_2() {
+        try ( RDFLink link = link() ) {
+            link.put(graphName, graph1);
+            link.put(graphName, graph2);
+            Graph g = link.get(graphName);
+            assertTrue("Graphs not isomorphic", isomorphic(g, graph2));
+            assertFalse("Graphs not isomorphic", isomorphic(g, graph1));
+        }
+    }
+
+    @Test public void named_graph_put_2_different() {
+        try ( RDFLink link = link() ) {
+            link.put(graphName, graph1);
+            link.put(graphName2, graph2);
+            Graph g1 = link.get(graphName);
+            Graph g2 = link.get(graphName2);
+            assertTrue("Graphs not isomorphic", isomorphic(g1, graph1));
+            assertTrue("Graphs not isomorphic", isomorphic(g2, graph2));
+        }
+    }
+
+    @Test public void named_graph_post_1() {
+        try ( RDFLink link = link() ) {
+            link.load(graphName, graph1);
+            Graph g = link.get(graphName);
+            assertTrue("Graphs not isomorphic", isomorphic(g, graph1));
+        }
+    }
+
+    @Test public void named_graph_post_2() {
+        try ( RDFLink link = link() ) {
+            link.load(graphName, graph1);
+            link.load(graphName, graph2);
+            Graph g = link.get(graphName);
+            Graph g0 = new Union(graph2, graph1);
+            assertTrue("Graphs are not isomorphic", isomorphic(g0, g));
+        }
+    }
+
+    // DELETE
+
+    // Remote connections don't support transactions fully.
+    //@Test public void transaction_01()
+
+    @Test public void named_graph_delete_1() {
+        String testDataFile = DIR+"data.ttl";
+        Graph g0 = RDFDataMgr.loadGraph(testDataFile);
+        try ( RDFLink link = link() ) {
+            link.load(graphName, testDataFile);
+            Graph g1 = link.get(graphName);
+            assertFalse(g1.isEmpty());
+            link.delete(graphName);
+            // either isEmpty (local-all graph "exist"), null (general dataset,
+            // or fixed set of graphs) or 404 (remote);
+            try {
+                Graph g2 = link.get(graphName);
+                if ( g2 != null )
+                    assertTrue(g2.isEmpty());
+            } catch (HttpException ex) {
+                assertEquals(HttpSC.NOT_FOUND_404, ex.getStatusCode());
+            }
+        }
+    }
+    private static boolean isomorphic(DatasetGraph ds1, DatasetGraph ds2) {
+        return IsoMatcher.isomorphic(ds1, ds2);
+    }
+
+    private static boolean isomorphic(Graph graph1, Graph graph2) {
+        return graph1.isIsomorphicWith(graph2);
+    }
+
+    @Test public void query_01() {
+        try ( RDFLink link = link() ) {
+            Txn.executeRead(link, ()->{
+                try ( QueryExec qExec = link.query("SELECT ?x {}") ) {
+                    RowSet rs = qExec.select();
+                    assertNotNull(rs);
+                }
+            });
+        }
+    }
+
+    @Test public void query_02() {
+        try ( RDFLink link = link() ) {
+            Txn.executeRead(link, ()->{
+                try ( QueryExec qExec = link.query("ASK{}") ) {
+                    boolean b = qExec.ask();
+                    assertTrue(b);
+                }
+            });
+        }
+    }
+
+    @Test public void query_03() {
+        try ( RDFLink link = link() ) {
+            Txn.executeRead(link, ()->{
+                try ( QueryExec qExec = link.query("CONSTRUCT WHERE{}") ) {
+                    Graph g = qExec.construct();
+                    assertNotNull(g);
+                }
+            });
+        }
+    }
+
+    @Test public void query_ask_01() {
+        try ( RDFLink link = link() ) {
+            Txn.executeRead(link, ()->{
+                boolean b = link.queryAsk("ASK{}");
+                assertTrue(b);
+            });
+        }
+    }
+
+    @Test public void query_ask_02() {
+        try ( RDFLink link = link() ) {
+            boolean b = link.queryAsk("ASK{}");
+            assertTrue(b);
+        }
+    }
+
+    @Test public void query_select_01() {
+        AtomicInteger counter = new AtomicInteger(0);
+        try ( RDFLink link = link() ) {
+            Txn.executeWrite(link, ()->link.loadDataset(DIR+"data.trig"));
+            Txn.executeRead(link, ()->
+                link.querySelect("SELECT * { ?s ?p ?o }" , (r)->counter.incrementAndGet()));
+            assertEquals(2, counter.get());
+        }
+    }
+
+    @Test public void query_select_02() {
+        AtomicInteger counter = new AtomicInteger(0);
+        try ( RDFLink link = link() ) {
+            link.loadDataset(DIR+"data.trig");
+            link.querySelect("SELECT * { ?s ?p ?o}" , (r)->counter.incrementAndGet());
+            assertEquals(2, counter.get());
+        }
+    }
+
+    @Test public void query_construct_01() {
+        try ( RDFLink link = link() ) {
+            Txn.executeWrite(link, ()->link.loadDataset(DIR+"data.trig"));
+            Txn.executeRead(link, ()-> {
+                Graph g = link.queryConstruct("CONSTRUCT WHERE { ?s ?p ?o }");
+                assertEquals(2, g.size());
+            });
+        }
+    }
+
+    @Test public void query_construct_02() {
+        try ( RDFLink link = link() ) {
+            link.loadDataset(DIR+"data.trig");
+            Graph g = link.queryConstruct("CONSTRUCT WHERE { ?s ?p ?o }");
+            assertEquals(2, g.size());
+        }
+    }
+
+    @Test public void query_build_01() {
+        try ( RDFLink link = link() ) {
+            Txn.executeRead(link, ()->{
+                RowSet rs = link.newQuery().query("SELECT * { ?s ?p ?o}").select();
+                assertNotNull(rs);
+            });
+        }
+    }
+
+    @Test public void query_build_02() {
+        try ( RDFLink link = link() ) {
+            Txn.executeRead(link, ()->{
+                Binding binding = SSE.parseBinding("(binding (?X 123))");
+                QueryExec qExec = link.newQuery().query("SELECT ?X { }")
+                        .substitution(binding)
+                        .build();
+//                String s = qExec.getQueryString();
+//                assertTrue(s.contains("123"));

Review comment:
       Debug left-over?

##########
File path: jena-arq/src-examples/arq/examples/ExampleDBpedia2.java
##########
@@ -18,22 +18,28 @@
 
 package arq.examples;
 
-import org.apache.jena.query.* ;
-import org.apache.jena.rdf.model.ModelFactory ;
+import org.apache.jena.query.*;
+import org.apache.jena.sparql.exec.http.QueryExecutionHTTP;
+import org.apache.jena.sparql.util.Context;
 
 public class ExampleDBpedia2
 {
-    static public void main(String... argv) {
-        String queryString = 
-            "SELECT * WHERE { " +
-            "    SERVICE <http://dbpedia-live.openlinksw.com/sparql?timeout=2000> { " +
-            "        SELECT DISTINCT ?company where {?company a <http://dbpedia.org/ontology/Company>} LIMIT 20" +
-            "    }" +
-            "}" ;
-        Query query = QueryFactory.create(queryString) ;
-        try (QueryExecution qexec = QueryExecutionFactory.create(query, ModelFactory.createDefaultModel())) {
-            ResultSet rs = qexec.execSelect() ;
-            ResultSetFormatter.out(System.out, rs, query) ;
+    static public void main(String...argv)
+    {
+        String queryStr = "select distinct ?Concept where {[] a ?Concept} LIMIT 10";
+        Query query = QueryFactory.create(queryStr);
+
+        Context cxt = ARQ.getContext().copy();
+
+        // Build-execute
+        try ( QueryExecution qExec = QueryExecutionHTTP.create()
+                    .endpoint("http://dbpedia.org/sparql")
+                    .query(query)
+                    .param("timeout", "10000")
+                    .build() ) {
+          // Execute.
+          ResultSet rs = qExec.execSelect();
+          ResultSetFormatter.out(System.out, rs, query);

Review comment:
       More nit-picking, 2 spaces versus 4 elsewhere.

##########
File path: jena-arq/src/main/java/org/apache/jena/http/AsyncHttpRDF.java
##########
@@ -0,0 +1,190 @@
+/*
+ * 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.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.function.Consumer;
+
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.riot.WebContent;
+import org.apache.jena.riot.system.StreamRDF;
+import org.apache.jena.riot.system.StreamRDFLib;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.core.Transactional;
+import org.apache.jena.sparql.core.TransactionalNull;
+import org.apache.jena.sparql.graph.GraphFactory;
+import org.apache.jena.web.HttpSC;
+
+/**
+ * A collection of convenience operations for HTTP level operations
+ * for RDF related tasks. performed asynchronously.
+ *
+ * See also {@link HttpRDF}.
+ */
+public class AsyncHttpRDF {
+    // Add POST and PUT
+
+    private static Consumer<HttpRequest.Builder> acceptHeaderGraph = HttpLib.setAcceptHeader(WebContent.defaultGraphAcceptHeader);
+    private static Consumer<HttpRequest.Builder> acceptHeaderDatasetGraph = HttpLib.setAcceptHeader(WebContent.defaultDatasetAcceptHeader);
+
+    /** Get a graph, asynchronously */
+    public static CompletableFuture<Graph> asyncGetGraph(String url) {
+        return asyncGetGraph(HttpEnv.getDftHttpClient(), url);
+    }
+
+    /** Get a graph, asynchronously */
+    public static CompletableFuture<Graph> asyncGetGraph(HttpClient httpClient, String url) {
+        Objects.requireNonNull(httpClient, "HttpClient");
+        Objects.requireNonNull(url, "URL");
+        Graph graph = GraphFactory.createDefaultGraph();
+        StreamRDF dest = StreamRDFLib.graph(graph);
+        CompletableFuture<Void> cf = asyncGetToStream(httpClient, url, acceptHeaderGraph, dest, null);
+        return cf.thenApply(x->graph);
+    }
+
+    /** Get a dataset, asynchronously */
+    public static CompletableFuture<DatasetGraph> asyncGetDatasetGraph(String url) {
+        return asyncGetDatasetGraph(HttpEnv.getDftHttpClient(), url);
+    }
+
+    /** Get a dataset, asynchronously */
+    public static CompletableFuture<DatasetGraph> asyncGetDatasetGraph(HttpClient httpClient, String url) {
+        Objects.requireNonNull(httpClient, "HttpClient");
+        Objects.requireNonNull(url, "URL");
+        DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+        StreamRDF dest = StreamRDFLib.dataset(dsg);
+        CompletableFuture<Void> cf = asyncGetToStream(httpClient, url, acceptHeaderDatasetGraph, dest, dsg);
+        return cf.thenApply(x->dsg);
+    }
+
+    /**
+     * Load a DatasetGraph asynchronously.
+     * The dataset is updated inside a transaction.
+     */
+    public static CompletableFuture<Void> asyncLoadDatasetGraph(String url, DatasetGraph dsg) {
+        return asyncLoadDatasetGraph(HttpEnv.getDftHttpClient(), url, dsg);
+    }
+
+    /**
+     * Load a DatasetGraph asynchronously.
+     * The dataset is updated inside a transaction.
+     */
+    public static CompletableFuture<Void> asyncLoadDatasetGraph(HttpClient httpClient, String url, DatasetGraph dsg) {
+        Objects.requireNonNull(httpClient, "HttpClient");
+        Objects.requireNonNull(url, "URL");
+        Objects.requireNonNull(dsg, "dataset");
+        StreamRDF dest = StreamRDFLib.dataset(dsg);
+        return asyncGetToStream(httpClient, url, acceptHeaderDatasetGraph, dest, dsg);
+    }
+
+    /**
+     * Load a DatasetGraph asynchronously.
+     * The dataset is updated inside a transaction.
+     */
+    public static CompletableFuture<Void> asyncLoadDatasetGraph(HttpClient httpClient, String url, Map<String, String> headers, DatasetGraph dsg) {
+        Objects.requireNonNull(httpClient, "HttpClient");
+        Objects.requireNonNull(url, "URL");
+        Objects.requireNonNull(dsg, "dataset");
+        StreamRDF dest = StreamRDFLib.dataset(dsg);
+        return asyncGetToStream(httpClient, url, HttpLib.setHeaders(headers), dest, dsg);
+    }
+
+    /**
+     * Execute an asynchronous GET and parse the result to a StreamRDF.
+     * If "transactional" is not null, the object is used a write transaction around the parsing step.
+     * <p>
+     * Call {@link CompletableFuture#join} to await completion.<br/>
+     * Call {@link #syncOrElseThrow(CompletableFuture)} to await completion,
+     * with exceptions translated to the underlying {@code RuntimeException}.
+     */
+    public static CompletableFuture<Void> asyncGetToStream(HttpClient httpClient, String url, String acceptHeader, StreamRDF dest, Transactional transactional) {
+        Objects.requireNonNull(httpClient, "HttpClient");
+        Objects.requireNonNull(url, "URL");
+        Objects.requireNonNull(dest, "StreamRDF");
+        Consumer<HttpRequest.Builder> setAcceptHeader = HttpLib.setAcceptHeader(acceptHeader);
+        return asyncGetToStream(httpClient, url, setAcceptHeader, dest, transactional);
+    }
+
+    private static CompletableFuture<Void> asyncGetToStream(HttpClient httpClient, String url, Consumer<HttpRequest.Builder> modifier, StreamRDF dest, Transactional _transactional) {
+        CompletableFuture<HttpResponse<InputStream>> cf = asyncGetToInput(httpClient, url, modifier);
+        Transactional transact = ( _transactional == null ) ? TransactionalNull.create() : _transactional;
+        return cf.thenApply(httpResponse->{
+            transact.executeWrite(()->HttpRDF.httpResponseToStreamRDF(url, httpResponse, dest));
+            return null;
+        });
+    }
+
+    /**
+     * Wait for the {@code CompletableFuture} or throw a runtime exception.
+     * This operation extracts RuntimeException from the {@code CompletableFuture}.
+     */
+    public static void syncOrElseThrow(CompletableFuture<Void> cf) {
+        getOrElseThrow(cf);
+    }
+
+    /**
+     * Wait for the {@code CompletableFuture} then return the result or throw a runtime exception.
+     * This operation extracts RuntimeException from the {@code CompletableFuture}.
+     */
+    public static <T> T getOrElseThrow(CompletableFuture<T> cf) {
+        Objects.requireNonNull(cf);
+        try {
+            return cf.join();
+        //} catch (CancellationException ex1) { // Let this pass out.
+        } catch (CompletionException ex) {
+            if ( ex.getCause() != null ) {
+                Throwable cause = ex.getCause();
+                if ( cause instanceof RuntimeException )
+                    throw (RuntimeException)cause;
+                if ( cause instanceof IOException ) {
+                    IOException iox = (IOException)cause;
+                    // Rather than an HTTP exception, bad authentication becomes IOException("too many authentication attempts");
+                    if ( iox.getMessage().contains("too many authentication attempts") ||
+                            iox.getMessage().contains("No credentials provided") ) {

Review comment:
       Ditto comment above on using specific exceptions if possible?

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilder.java
##########
@@ -0,0 +1,101 @@
+/*
+ * 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.sparql.exec;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.query.ARQ;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.Syntax;
+import org.apache.jena.sparql.core.Var;
+import org.apache.jena.sparql.engine.binding.Binding;
+import org.apache.jena.sparql.util.Context;
+import org.apache.jena.sparql.util.Symbol;
+
+/** The common elements of a {@link QueryExec} builder. */
+public interface QueryExecBuilder extends QueryExecMod {
+
+    /** Set the query. */
+    public QueryExecBuilder query(Query query);
+
+    /** Set the query. */
+    public QueryExecBuilder query(String queryString);
+
+    /** Set the query. */
+    public QueryExecBuilder query(String queryString, Syntax syntax);
+
+    /** Set a context entry. */
+    public QueryExecBuilder set(Symbol symbol, Object value);
+
+    /** Set a context entry. */
+    public QueryExecBuilder set(Symbol symbol, boolean value);
+
+    /**
+     * Set the context. if not set, defaults to the system context
+     * ({@link ARQ#getContext}).
+     */
+    public QueryExecBuilder context(Context context);
+
+    /** Provide a set of (Var, Node) for substitution in the query when QueryExec is built. */
+    public QueryExecBuilder substitution(Binding binding);
+
+    /** Provide a (Var, Node) for substitution in the query when QueryExec is built. */
+    public QueryExecBuilder substitution(Var var, Node value);
+
+    /** Set the overall query execution timeout. */
+    @Override
+    public QueryExecBuilder timeout(long value, TimeUnit timeUnit);
+
+    /**
+     * Build the {@link QueryExec}. Further changes to he builder do not affect this
+     * {@link QueryExec}.
+     */
+    @Override
+    public QueryExec build();
+
+    // build-and-use short cuts
+
+    /** Build and execute as a SELECT query. */
+    public default RowSet select() {
+        return build().select();
+    }
+
+    /** Build and execute as a CONSTRUCT query. */
+    public default Graph construct() {
+        try ( QueryExec qExec = build() ) {
+            return qExec.construct();
+        }
+    }
+
+    /** Build and execute as a CONSTRUCT query. */
+    public default Graph describe() {
+        try ( QueryExec qExec = build() ) {
+            return qExec.describe();
+        }
+    }
+
+    /** Build and execute as an ASK query. */
+    public default boolean ask() {
+        try ( QueryExec qExec = build() ) {
+            return qExec.ask();
+        }
+    }
+}

Review comment:
       Missing newline

##########
File path: jena-arq/src/main/java/org/apache/jena/http/auth/AuthStringException.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.http.auth;
+
+public class AuthStringException extends RuntimeException {
+    public AuthStringException() {
+        super();
+    }
+
+    public AuthStringException(String msg) {
+        super(msg);
+    }
+}

Review comment:
       Missing newline.

##########
File path: jena-arq/src/main/java/org/apache/jena/http/auth/AuthRequestModifier.java
##########
@@ -0,0 +1,24 @@
+/*
+ * 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.http.auth;
+
+import java.net.http.HttpRequest;
+
+@FunctionalInterface
+public interface AuthRequestModifier { HttpRequest.Builder addAuth(HttpRequest.Builder builder); }

Review comment:
       Missing newline.

##########
File path: jena-arq/src/main/java/org/apache/jena/http/auth/AuthCredentials.java
##########
@@ -0,0 +1,81 @@
+/*
+ * 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.http.auth;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.jena.atlas.lib.Trie;
+import org.apache.jena.atlas.web.HttpException;
+
+/**
+ * Registry of (username, password) for a remote location (endpoint URI and optional realm.
+ */
+public class AuthCredentials {
+    private Map<AuthDomain, PasswordRecord> authRegistry = new ConcurrentHashMap<>();
+    private Trie<AuthDomain> prefixes = new Trie<>();
+    public AuthCredentials() {}
+
+    public void put(AuthDomain location, PasswordRecord pwRecord) {
+        // Checks.
+        URI uri = location.uri;
+        if ( uri.getRawQuery() != null || uri.getRawFragment() != null )
+            throw new HttpException("Endpoint URI must not have query string or fragment: "+uri);
+        authRegistry.put(location, pwRecord);
+        prefixes.add(uri.toString(), location);
+    }
+
+    public boolean contains(AuthDomain location) {
+        return prefixes.contains(location.uri.toString());
+    }
+
+    public List<AuthDomain> registered() {
+        return new ArrayList<>(authRegistry.keySet());
+    }
+
+    public PasswordRecord get(AuthDomain location) {
+        PasswordRecord pwRecord = authRegistry.get(location);
+        if ( pwRecord != null )
+            return pwRecord;
+
+        prefixes.partialSearch(location.uri.toString());
+
+        AuthDomain match = prefixes.longestMatch(location.uri.toString());
+        if ( match == null )
+            return null;
+        if ( match.getRealm() != null ) {
+            if ( location.getRealm() != null && ! Objects.equals(location.getRealm(), match.getRealm()) )
+            return null;
+        }
+        return authRegistry.get(match);
+    }
+
+    public void remove(AuthDomain location) {
+        prefixes.remove(location.uri.toString());
+        authRegistry.remove(location);
+    }
+
+    public void clearAll() {
+        authRegistry.clear();
+    }
+}

Review comment:
       Missing newline.

##########
File path: jena-arq/src/main/java/org/apache/jena/http/sys/AbstractRegistryWithPrefix.java
##########
@@ -0,0 +1,90 @@
+/*
+ * 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.http.sys;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import org.apache.jena.atlas.lib.Trie;
+
+/**
+ * Abstract base class for registries with exact and prefix lookup..

Review comment:
       s/../.

##########
File path: jena-arq/src/main/java/org/apache/jena/http/HttpLib.java
##########
@@ -0,0 +1,711 @@
+/*
+ * 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.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpRequest.Builder;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.net.http.HttpResponse.BodySubscribers;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.InflaterInputStream;
+
+import org.apache.jena.atlas.RuntimeIOException;
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.lib.IRILib;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.atlas.web.TypedInputStream;
+import org.apache.jena.http.auth.AuthEnv;
+import org.apache.jena.http.auth.AuthLib;
+import org.apache.jena.http.sys.HttpRequestModifier;
+import org.apache.jena.http.sys.RegistryRequestModifier;
+import org.apache.jena.query.ARQ;
+import org.apache.jena.riot.web.HttpNames;
+import org.apache.jena.sparql.exec.http.Params;
+import org.apache.jena.sparql.util.Context;
+import org.apache.jena.web.HttpSC;
+
+/**
+ * Operations related to SPARQL HTTP requests - Query, Update and Graph Store protocols.
+ * This class is not considered "API".
+ */
+public class HttpLib {
+
+    private HttpLib() {}
+
+    public static BodyHandler<Void> noBody() { return BodyHandlers.discarding(); }
+
+    public static BodyPublisher stringBody(String str) { return BodyPublishers.ofString(str); }
+
+    private static BodyHandler<InputStream> bodyHandlerInputStream = buildDftBodyHandlerInputStream();
+
+    private static BodyHandler<InputStream> buildDftBodyHandlerInputStream() {
+        return responseInfo -> {
+            return BodySubscribers.ofInputStream();
+        };
+    }
+
+    /** Read the body of a response as a string in UTF-8. */
+    private static Function<HttpResponse<InputStream>, String> bodyInputStreamToString = r-> {
+        try {
+            InputStream in = r.body();
+            String msg = IO.readWholeFileAsUTF8(in);
+            return msg;
+        } catch (Throwable ex) { throw new HttpException(ex); }
+    };
+
+    /**
+     * Calculate basic auth header value. Use with header "Authorization" (constant
+     * {@link HttpNames#hAuthorization}). Best used over https.
+     */
+    public static String basicAuth(String username, String password) {
+        return "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
+    }
+
+    /**
+     * Get the InputStream from an HttpResponse, handling possible compression settings.
+     * The application must consume or close the {@code InputStream} (see {@link #finish(InputStream)}).
+     * Closing the InputStream may close the HTTP connection.
+     * Assumes the status code has been handled e.g. {@link #handleHttpStatusCode} has been called.
+     */
+    public static InputStream getInputStream(HttpResponse<InputStream> httpResponse) {
+        String encoding = httpResponse.headers().firstValue(HttpNames.hContentEncoding).orElse("");
+        InputStream responseInput = httpResponse.body();
+        // Only support "Content-Encoding: <compression>" and not
+        // "Content-Encoding: chunked, <compression>"
+        try {
+            switch (encoding) {
+                case "" :
+                case "identity" : // Proper name for no compression.
+                    return responseInput;
+                case "gzip" :
+                    return new GZIPInputStream(responseInput, 2*1024);
+                case "inflate" :
+                    return new InflaterInputStream(responseInput);
+                case "br" : // RFC7932
+                default :
+                    throw new UnsupportedOperationException("Not supported: Content-Encoding: " + encoding);
+            }
+        } catch (IOException ex) {
+            throw new UncheckedIOException(ex);
+        }
+    }
+
+    /**
+     * Deal with status code and any error message sent as a body in the response.
+     * <p>
+     * It is this handling 4xx/5xx error messages in the body that forces the use of
+     * {@code InputStream}, not generic {@code T}. We don't know until we see the
+     * status code how we are going to process the response body.
+     * <p>
+     * Exits normally without processing the body if the response is 200.
+     * <p>
+     * Throws {@link HttpException} for 3xx (redirection should have happened by
+     * now), 4xx and 5xx, having consumed the body input stream.
+     */
+    public static void handleHttpStatusCode(HttpResponse<InputStream> response) {
+        int httpStatusCode = response.statusCode();
+        // There is no status message in HTTP/2.
+        if ( ! inRange(httpStatusCode, 100, 599) )
+            throw new HttpException("Status code out of range: "+httpStatusCode);
+        else if ( inRange(httpStatusCode, 100, 199) ) {
+            // Informational
+        }
+        else if ( inRange(httpStatusCode, 200, 299) ) {
+            // Success. Continue processing.
+        }
+        else if ( inRange(httpStatusCode, 300, 399) ) {
+            // We had follow redirects on (default client) so it's http->https,
+            // or the application passed on a HttpClient with redirects off.
+            // Either way, we should not continue processing.
+            try {
+                finish(response);
+            } catch (Exception ex) {
+                throw new HttpException("Error discarding body of "+httpStatusCode , ex);
+            }
+            throw new HttpException(httpStatusCode, HttpSC.getMessage(httpStatusCode));
+        }
+        else if ( inRange(httpStatusCode, 400, 499) ) {
+            throw exception(response, httpStatusCode);
+        }
+        else if ( inRange(httpStatusCode, 500, 599) ) {
+            throw exception(response, httpStatusCode);
+        }
+    }
+
+    /**
+     * Handle the HTTP response (see {@link #handleHttpStatusCode(HttpResponse)}) and
+     * return the InputStream if a 200.
+     *
+     * @param httpResponse
+     * @return InputStream
+     */
+    public static InputStream handleResponseInputStream(HttpResponse<InputStream> httpResponse) {
+        handleHttpStatusCode(httpResponse);
+        return getInputStream(httpResponse);
+    }
+
+    /**
+     * Handle the HTTP response (see {@link #handleHttpStatusCode(HttpResponse)}) and
+     * return the TypedInputStream that includes the {@code Content-Type} if a 200.
+     *
+     * @param httpResponse
+     * @return TypedInputStream
+     */
+    public static TypedInputStream handleResponseTypedInputStream(HttpResponse<InputStream> httpResponse) {
+        InputStream input = handleResponseInputStream(httpResponse);
+        String ct = HttpLib.responseHeader(httpResponse, HttpNames.hContentType);
+        return new TypedInputStream(input, ct);
+    }
+
+    /**
+     * Handle the HTTP response and consume the body if a 200.
+     * Otherwise, throw an {@link HttpException}.
+     * @param response
+     */
+    public static void handleResponseNoBody(HttpResponse<InputStream> response) {
+        handleHttpStatusCode(response);
+        finish(response);
+    }
+
+    /**
+     * Handle the HTTP response and read the body to produce a string if a 200.
+     * Otherwise, throw an {@link HttpException}.
+     * @param response
+     * @return String
+     */
+    public static String handleResponseRtnString(HttpResponse<InputStream> response) {
+        InputStream input = handleResponseInputStream(response);
+        try {
+            return IO.readWholeFileAsUTF8(input);
+        } catch (RuntimeIOException e) { throw new HttpException(e); }
+    }
+
+    static HttpException exception(HttpResponse<InputStream> response, int httpStatusCode) {
+
+        URI uri = response.request().uri();
+
+        //long length = HttpLib.getContentLength(response);
+        // Not critical path code. Read body regardless.
+        InputStream in = response.body();
+        String msg;
+        try {
+            msg = IO.readWholeFileAsUTF8(in);
+            if ( msg.isBlank())
+                msg = null;
+        } catch (RuntimeIOException e) {
+            msg = null;
+        }
+        return new HttpException(httpStatusCode, HttpSC.getMessage(httpStatusCode), msg);
+    }
+
+    private static long getContentLength(HttpResponse<InputStream> response) {
+        Optional<String> x = response.headers().firstValue(HttpNames.hContentLength);
+        if ( x.isEmpty() )
+            return -1;
+        try {
+            return Long.parseLong(x.get());
+        } catch (NumberFormatException ex) { return -1; }
+    }
+
+    /** Test x:int in [min, max] */
+    private static boolean inRange(int x, int min, int max) { return min <= x && x <= max; }
+
+    /** Finish with {@code HttpResponse<InputStream>}.
+     * This read and drops any remaining bytes in the response body.
+     * {@code close} may close the underlying HTTP connection.
+     *  See {@link BodySubscribers#ofInputStream()}.
+     */
+    private static void finish(HttpResponse<InputStream> response) {
+        finish(response.body());
+    }
+
+    /** Read to end of {@link InputStream}.
+     *  {@code close} may close the underlying HTTP connection.
+     *  See {@link BodySubscribers#ofInputStream()}.
+     */
+    public static void finish(InputStream input) {
+        consume(input);
+    }
+
+    // This is extracted from commons-io, IOUtils.skip.
+    // Changes:
+    // * No exception.
+    // * Always consumes to the end of stream (or stream throws IOException)
+    // * Larger buffer
+    private static int SKIP_BUFFER_SIZE = 8*1024;
+    private static byte[] SKIP_BYTE_BUFFER = null;
+
+    private static void consume(final InputStream input) {
+        /*
+         * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data
+         * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer
+         * size were variable, we would need to synch. to ensure some other thread did not create a smaller one)
+         */
+        if (SKIP_BYTE_BUFFER == null) {
+            SKIP_BYTE_BUFFER = new byte[SKIP_BUFFER_SIZE];
+        }
+        int bytesRead = 0; // Informational
+        try {
+            for(;;) {
+                // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
+                final long n = input.read(SKIP_BYTE_BUFFER, 0, SKIP_BUFFER_SIZE);
+                if (n < 0) { // EOF
+                    break;
+                }
+                bytesRead += n;
+            }
+        } catch (IOException ex) { /*ignore*/ }
+    }
+
+    /** String to {@link URI}. Throws {@link HttpException} on bad syntax or if the URI isn't absolute. */
+    public static URI toRequestURI(String uriStr) {
+        try {
+            URI uri = new URI(uriStr);
+            if ( ! uri.isAbsolute() )
+                throw new HttpException("Not an absolute URL: <"+uriStr+">");
+            return uri;
+        } catch (URISyntaxException ex) {
+            int idx = ex.getIndex();
+            String msg = (idx<0)
+                ? String.format("Bad URL: %s", uriStr)
+                : String.format("Bad URL: %s starting at character %d", uriStr, idx);
+            throw new HttpException(msg, ex);
+        }
+    }
+
+    // Terminology:
+    // RFC 2616:   Request-Line   = Method SP Request-URI SP HTTP-Version CRLF
+
+    // RFC 7320:   request-line   = method SP request-target SP HTTP-version CRLF
+    // https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1
+
+    // request-target:
+    // https://datatracker.ietf.org/doc/html/rfc7230#section-5.3
+    // When it is for the origin server ==> absolute-path [ "?" query ]
+
+    // EndpointURI: URL for a service, no query string.
+
+    /** Test whether a URI is a service endpoint. It must be absolute, with host and path, and without query string or fragment. */
+    public static boolean isEndpoint(URI uri) {
+        return uri.isAbsolute() &&
+                uri.getHost() != null &&
+                uri.getRawPath() != null &&
+                uri.getRawQuery() == null &&
+                uri.getRawFragment() == null;
+    }
+
+    /**
+     * Return a string (assumed ot be a URI) without query string or fragment.
+     */
+    public static String endpoint(String uriStr) {
+        int idx1 = uriStr.indexOf('?');
+        int idx2 = uriStr.indexOf('#');
+
+        if ( idx1 < 0 && idx2 < 0 )
+            return uriStr;
+
+        int idx = -1;
+        if ( idx1 < 0 && idx2 > 0 )
+            idx = idx2;
+        else if ( idx1 > 0 && idx2 < 0 )
+            idx = idx1;
+        else
+            idx = Math.min(idx1,  idx2);
+        return uriStr.substring(0, idx);
+    }
+
+    /** RFC7320 "request-target", used in digest authentication. */
+    public static String requestTarget(URI uri) {
+        String path = uri.getRawPath();
+        if ( path == null || path.isEmpty() )
+            path = "/";
+        String qs = uri.getQuery();
+        if ( qs == null || qs.isEmpty() )
+            return path;
+        return path+"?"+qs;
+    }
+
+    /** URI, without query string and fragment. */
+    public static URI endpointURI(URI uri) {
+        if ( uri.getRawQuery() == null && uri.getRawFragment() == null )
+            return uri;
+        try {
+            // Same URI except without query strinf an fragment.
+            return new URI(uri.getScheme(), uri.getRawAuthority(), uri.getRawPath(), null, null);
+        } catch (URISyntaxException x) {
+            throw new IllegalArgumentException(x.getMessage(), x);
+        }
+    }
+
+    /** Return a HttpRequest */
+    public static HttpRequest newGetRequest(String url, Consumer<HttpRequest.Builder> modifier) {
+        HttpRequest.Builder builder = HttpLib.requestBuilderFor(url).uri(toRequestURI(url)).GET();
+        if ( modifier != null )
+            modifier.accept(builder);
+        return builder.build();
+    }
+
+    public static <X> X dft(X value, X dftValue) {
+        return (value != null) ? value : dftValue;
+    }
+
+    public static <X> List<X> copyArray(List<X> array) {
+        if ( array == null )
+            return null;
+        return new ArrayList<>(array);
+    }
+
+    /** Encode a string suitable for use in an URL query string */
+    public static String urlEncodeQueryString(String str) {
+        // java.net.URLEncoder is excessive - it encodes / and : which
+        // is not necessary in a query string or fragment.
+        return IRILib.encodeUriQueryFrag(str);
+    }
+
+    /** Query string is assumed to already be encoded. */
+    public static String requestURL(String url, String queryString) {
+        if ( queryString == null || queryString.isEmpty() )
+            // Empty string. Don't add "?"
+            return url;
+        String sep =  url.contains("?") ? "&" : "?";
+        String requestURL = url+sep+queryString;
+        return requestURL;
+    }
+
+    public static HttpRequest.Builder requestBuilderFor(String serviceEndpoint) {
+        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
+        return AuthEnv.get().addAuth(requestBuilder, serviceEndpoint);
+    }
+
+    public static Builder requestBuilder(String url, Map<String, String> httpHeaders, long readTimeout, TimeUnit readTimeoutUnit) {
+        HttpRequest.Builder builder = HttpLib.requestBuilderFor(url);
+        headers(builder, httpHeaders);
+        builder.uri(toRequestURI(url));
+        if ( readTimeout >= 0 )
+            builder.timeout(Duration.ofMillis(readTimeoutUnit.toMillis(readTimeout)));
+        return builder;
+    }
+
+    /** Create a {@code HttpRequest.Builder} from an {@code HttpRequest}. */
+    public static HttpRequest.Builder createBuilder(HttpRequest request) {
+        HttpRequest.Builder builder = HttpRequest.newBuilder()
+                .expectContinue(request.expectContinue())
+                .uri(request.uri());
+        builder.method(request.method(), request.bodyPublisher().orElse(BodyPublishers.noBody()));
+        request.timeout().ifPresent(builder::timeout);
+        request.version().ifPresent(builder::version);
+        request.headers().map().forEach((name, values)->values.forEach(value->builder.header(name, value)));
+        return builder;
+    }
+
+    /** Set the headers from the Map if the map is not null. Returns the Builder. */
+    static Builder headers(Builder builder, Map<String, String> httpHeaders) {
+        if ( httpHeaders != null )
+            httpHeaders.forEach(builder::header);
+        return builder;
+    }
+
+    /** Set the "Accept" header if value is not null. Returns the builder. */
+    public static Builder acceptHeader(Builder builder, String acceptHeader) {
+        if ( acceptHeader != null )
+            builder.header(HttpNames.hAccept, acceptHeader);
+        return builder;
+    }
+
+    /** Set the "Content-Type" header if value is not null. Returns the builder. */
+    public static Builder contentTypeHeader(Builder builder, String contentType) {
+        if ( contentType != null )
+            builder.header(HttpNames.hContentType, contentType);
+        return builder;
+    }
+
+    // Disabled. Don't encourage using compression ("Content-Encoding: gzip") because it interacts with streaming.
+    // Specifically, streaming (unknown Content-Length) needs chunking. Both chunking and compression
+    // encodings use the same HTTP header. Yet they are handled by different layers.
+    // The basic http code handles chunking.
+    // The complete encoding header can get removed resulting in a compressed stream
+    // without any indication being passed to the application.
+//    /**
+//     * Set the "Accept-Encoding" header. Returns the builder.
+//     * See {@link #getInputStream(HttpResponse)}.
+//     */
+//    public
+//    /*package*/ static Builder acceptEncodingCompressed(Builder builder) {
+//        builder.header(HttpNames.hAcceptEncoding, WebContent.acceptEncodingCompressed);
+//        return builder;
+//    }
+
+    /**
+     * Execute a request, return a {@code HttpResponse<InputStream>} which
+     * can be passed to {@link #handleResponseInputStream(HttpResponse)} which will
+     * convert non-2xx status code to {@link HttpException HttpExceptions}.
+     * <p>
+     * This function applies the HTTP authentication challenge support
+     * and will repeat the request if necessary with added authentication.
+     * <p>
+     * See {@link AuthEnv} for authentication registration.
+     * <br/>
+     * See {@link #executeJDK} to execute exactly once without challenge response handling.
+     *
+     * @see AuthEnv AuthEnv for authentic registration
+     * @see #executeJDK executeJDK to execute exacly once.
+     *
+     * @param httpClient
+     * @param httpRequest
+     * @return HttpResponse
+     */
+    public static HttpResponse<InputStream> execute(HttpClient httpClient, HttpRequest httpRequest) {
+        return execute(httpClient, httpRequest, BodyHandlers.ofInputStream());
+    }
+
+    /**
+     * Execute a request, return a {@code HttpResponse<X>} which
+     * can be passed to {@link #handleHttpStatusCode(HttpResponse)} which will
+     * convert non-2xx status code to {@link HttpException HttpExceptions}.
+     * <p>
+     * This function applies the HTTP authentication challenge support
+     * and will repeat the request if necessary with added authentication.
+     * <p>
+     * See {@link AuthEnv} for authentication registration.
+     * <br/>
+     * See {@link #executeJDK} to execute exactly once without challenge response handling.
+     *
+     * @see AuthEnv AuthEnv for authentic registration
+     * @see #executeJDK executeJDK to execute exacly once.
+     *
+     * @param httpClient
+     * @param httpRequest
+     * @param bodyHandler
+     * @return HttpResponse
+     */
+    public
+    /*package*/ static <X> HttpResponse<X> execute(HttpClient httpClient, HttpRequest httpRequest, BodyHandler<X> bodyHandler) {
+        // To run with no jena-supplied authentication handling.
+        if ( false )
+            return executeJDK(httpClient, httpRequest, bodyHandler);
+        URI uri = httpRequest.uri();
+        URI key = null;
+
+        AuthEnv authEnv = AuthEnv.get();
+
+        if ( uri.getUserInfo() != null ) {
+            String[] up = uri.getUserInfo().split(":");
+            if ( up.length == 2 ) {
+                // Only if "user:password@host", not "user@host"
+                key = HttpLib.endpointURI(uri);
+                // The auth key will be with u:p making it specific.
+                authEnv.registerUsernamePassword(key, up[0], up[1]);
+            }
+        }
+        try {
+            return AuthLib.authExecute(httpClient, httpRequest, bodyHandler);
+        } finally {
+            if ( key != null )
+                authEnv.unregisterUsernamePassword(key);
+        }
+    }
+
+    /**
+     * Execute request and return a response without authentication challenge handling.
+     * @param httpClient
+     * @param httpRequest
+     * @param bodyHandler
+     * @return HttpResponse
+     */
+    public static <T> HttpResponse<T> executeJDK(HttpClient httpClient, HttpRequest httpRequest, BodyHandler<T> bodyHandler) {
+        try {
+            // This is the one place all HTTP requests go through.
+            logRequest(httpRequest);
+            HttpResponse<T> httpResponse = httpClient.send(httpRequest, bodyHandler);
+            logResponse(httpResponse);
+            return httpResponse;
+        //} catch (HttpTimeoutException ex) {
+        } catch (IOException | InterruptedException ex) {
+            if ( ex.getMessage() != null ) {
+                // This is silly.
+                // Rather than an HTTP exception, bad authentication becomes IOException("too many authentication attempts");
+                // or IOException("No credentials provided") if the authenticator decides to return null.
+                if ( ex.getMessage().contains("too many authentication attempts") ||
+                     ex.getMessage().contains("No credentials provided") ) {
+                    throw new HttpException(401, HttpSC.getMessage(401));
+                }
+            }
+            throw new HttpException(httpRequest.method()+" "+httpRequest.uri().toString(), ex);
+        }
+    }
+
+    /*package*/ static CompletableFuture<HttpResponse<InputStream>> asyncExecute(HttpClient httpClient, HttpRequest httpRequest) {
+        logAsyncRequest(httpRequest);
+        return httpClient.sendAsync(httpRequest, BodyHandlers.ofInputStream());
+    }
+
+    /** Push data. POST, PUT, PATCH request with no response body data. */
+    public static void httpPushData(HttpClient httpClient, Push style, String url, Consumer<HttpRequest.Builder> modifier, BodyPublisher body) {
+        HttpResponse<InputStream> response = httpPushWithResponse(httpClient, style, url, modifier, body);
+        handleResponseNoBody(response);
+    }
+
+    // Worker
+    /*package*/ static HttpResponse<InputStream> httpPushWithResponse(HttpClient httpClient, Push style, String url,
+                                                                      Consumer<HttpRequest.Builder> modifier, BodyPublisher body) {
+        URI uri = toRequestURI(url);
+        HttpRequest.Builder builder = requestBuilderFor(url);
+        builder.uri(uri);
+        builder.method(style.method(), body);
+        if ( modifier != null )
+            modifier.accept(builder);
+        HttpResponse<InputStream> response = execute(httpClient, builder.build());
+        return response;
+    }
+
+
+    /** Request */
+    private static void logRequest(HttpRequest httpRequest) {
+        // Uses the SystemLogger which defaults to JUL.
+        // Add org.apache.jena.logging:log4j-jpl
+        // (java11 : 11.0.9, if using log4j-jpl, logging prints the request as {0} but response OK)
+//        httpRequest.uri();
+//        httpRequest.method();
+//        httpRequest.headers();

Review comment:
       Huh, looks like this method is used, but it was removed? Are the notes above for later?

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QueryExecutionHTTP.java
##########
@@ -0,0 +1,43 @@
+/*
+ * 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.sparql.exec.http;
+
+import org.apache.jena.sparql.exec.QueryExecutionAdapter;
+
+/**
+ * A query execution implementation where queries are executed
+ * against a remote service over HTTP.
+ */
+public class QueryExecutionHTTP extends QueryExecutionAdapter {
+
+    public static QueryExecutionHTTPBuilder create() { return QueryExecutionHTTPBuilder.create(); }
+    public static QueryExecutionHTTPBuilder newBuilder() { return QueryExecutionHTTPBuilder.create(); }
+
+    /** Create a new builder for the remote endpoint */
+    public static QueryExecutionHTTPBuilder service(String endpointURL) { return QueryExecutionHTTPBuilder.create().endpoint(endpointURL); }
+
+    public QueryExecutionHTTP(QueryExecHTTP qExecHTTP) {
+        super(qExecHTTP);
+    }
+
+    /** Get the content-type of the response. Only valid after successful execution of the query. */
+    public String getHttpResponseContentType() {
+        return ((QueryExecHTTP)get()).getHttpResponseContentType();
+    }
+}

Review comment:
       Missing newline

##########
File path: jena-arq/src/main/java/org/apache/jena/http/sys/HttpRequestModifier.java
##########
@@ -0,0 +1,30 @@
+/*
+ * 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.http.sys;
+
+import java.util.Map;
+
+import org.apache.jena.sparql.exec.http.Params;
+
+/**
+ * A {@code HttpRequestModifer} allows the application to HTTP query parameters and HTTP headers
+ * that will be used to create an {@link java.net.http.HttpRequest}.
+ */
+@FunctionalInterface
+public interface HttpRequestModifier { void modify(Params params, Map<String, String> httpHeaders) ; }

Review comment:
       Missing newline.

##########
File path: jena-arq/src/main/java/org/apache/jena/http/auth/AuthLib.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.http.auth;
+
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.util.List;
+
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.http.HttpLib;
+import org.apache.jena.riot.web.HttpNames;
+import org.apache.jena.web.HttpSC;
+
+public class AuthLib {

Review comment:
       Aren't we able to use Shiro for HTTP Basic, digest, etc, auth? I thought they would at least provide handling of initial challenges, headers, pojo's for password/user, etc?

##########
File path: jena-arq/src/main/java/org/apache/jena/http/HttpRDF.java
##########
@@ -0,0 +1,308 @@
+/*
+ * 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.http;
+
+import static org.apache.jena.http.HttpLib.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.riot.*;
+import org.apache.jena.riot.system.StreamRDF;
+import org.apache.jena.riot.system.StreamRDFLib;
+import org.apache.jena.riot.web.HttpNames;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.exec.http.GSP;
+import org.apache.jena.sparql.graph.GraphFactory;
+
+/**
+ * A collection of convenience operations for  HTTP level operations
+ * for RDF related tasks. This does not include GSP naming
+ * which is in {@link GSP}.
+ *
+ * See also {@link AsyncHttpRDF}.
+ */
+public class HttpRDF {
+
+    // ---- GET
+    /**
+     * GET a graph from a URL
+     *
+     * @throws HttpException
+     */
+    public static Graph httpGetGraph(String url) {
+        return httpGetGraph(HttpEnv.getDftHttpClient(), url);
+    }
+
+    /**
+     * GET a graph from a URL using the provided "Accept" header.
+     *
+     * @throws HttpException
+     */
+    public static Graph httpGetGraph(String url, String acceptHeader) {
+        return httpGetGraph(HttpEnv.getDftHttpClient(), url, acceptHeader);
+    }
+
+    /**
+     * GET a graph from a URL using the {@link HttpClient} provided.
+     *
+     * @throws HttpException
+     */
+    public static Graph httpGetGraph(HttpClient httpClient, String url) {
+        Graph graph = GraphFactory.createDefaultGraph();
+        httpGetToStream(httpClient, url,  WebContent.defaultGraphAcceptHeader, StreamRDFLib.graph(graph));
+        return graph;
+    }
+
+    /**
+     * GET a graph from a URL using the {@link HttpClient} provided
+     * and the "Accept" header.
+     *
+     * @throws HttpException
+     */
+    public static Graph httpGetGraph(HttpClient httpClient, String url, String acceptHeader) {
+        Graph graph = GraphFactory.createDefaultGraph();
+        httpGetToStream(httpClient, url, acceptHeader, StreamRDFLib.graph(graph));
+        return graph;
+    }
+
+    /**
+     * Send the RDF data from the resource at the URL to the StreamRDF.
+     * Beware of parse errors!
+     * @throws HttpException
+     */
+    public static void httpGetToStream(String url, String acceptHeader, StreamRDF dest) {
+        httpGetToStream(HttpEnv.getDftHttpClient(), url, acceptHeader, dest);
+    }
+
+    /**
+     * Read the RDF data from the resource at the URL and send to the StreamRDF.
+     * <p>
+     * Beware of parse errors!
+     * @throws HttpException
+     * @throws RiotException
+     */
+    public static void httpGetToStream(HttpClient client, String url, String acceptHeader, StreamRDF dest) {
+        if ( acceptHeader == null )
+            acceptHeader = "*/*";
+        httpGetToStream(client, url, HttpLib.setAcceptHeader(acceptHeader), dest);
+    }
+
+    /**
+     * Read the RDF data from the resource at the URL and send to the StreamRDF.
+     * <p>
+     * Beware of parse errors!
+     * @throws HttpException
+     * @throws RiotException
+     */
+    public static void httpGetToStream(HttpClient client, String url, Map<String, String> headers, StreamRDF dest) {
+        httpGetToStream(client, url, HttpLib.setHeaders(headers), dest);
+    }
+
+    // Worker
+    private static void httpGetToStream(HttpClient client, String url, Consumer<HttpRequest.Builder> modifier, StreamRDF dest) {
+        HttpResponse<InputStream> response = execGetToInput(client, url, modifier);
+        httpResponseToStreamRDF(url, response, dest);
+    }
+
+    /*package*/ static void httpResponseToStreamRDF(String url, HttpResponse<InputStream> response, StreamRDF dest) {
+        InputStream in = handleResponseInputStream(response);
+        String base = determineBaseURI(url, response);
+        Lang lang = determineSyntax(response, Lang.RDFXML);
+        try {
+            RDFParser.create()
+                .base(base)
+                .source(in)
+                .lang(lang)
+                .parse(dest);
+        } catch (RiotParseException ex) {
+            // We only read part of the input stream.
+            throw ex;
+        } finally {
+            // Even if parsing finished, it is possible we only read part of the input stream (e.g. RDF/XML).
+            finish(in);
+        }
+    }
+
+    /**
+     * MUST consume or close the input stream
+     * @see HttpLib#finish(HttpResponse)
+     */
+    private static HttpResponse<InputStream> execGetToInput(HttpClient client, String url, Consumer<HttpRequest.Builder> modifier) {
+        Objects.requireNonNull(client);
+        Objects.requireNonNull(url);
+        HttpRequest requestData = HttpLib.newGetRequest(url, modifier);
+        HttpResponse<InputStream> response = execute(client, requestData);
+        handleHttpStatusCode(response);
+        return response;
+    }
+
+    public static void httpPostGraph(String url, Graph graph) {
+        httpPostGraph(HttpEnv.getDftHttpClient(), url, graph, HttpEnv.dftTriplesFormat);
+    }
+
+    public static void httpPostGraph(HttpClient httpClient, String url, Graph graph, RDFFormat format) {
+        httpPostGraph(httpClient, url, graph, format, null);
+    }
+
+    public static void httpPostGraph(HttpClient httpClient, String url, Graph graph,
+                                     RDFFormat format, Map<String, String> httpHeaders) {
+        BodyPublisher bodyPublisher = graphToHttpBody(graph, format);
+        pushBody(httpClient, url, Push.POST, bodyPublisher, format, httpHeaders);
+    }
+
+    /** Post a graph and expect an RDF graph back as the result. */
+    public static Graph httpPostGraphRtn(String url, Graph graph) {
+        return httpPostGraphRtn(HttpEnv.getDftHttpClient(), url, graph,  HttpEnv.dftTriplesFormat, null);
+    }
+
+    /** Post a graph and expect an RDF graph back as the result. */
+    public static Graph httpPostGraphRtn(HttpClient httpClient, String url, Graph graph, RDFFormat format, Map<String, String> httpHeaders) {
+        BodyPublisher bodyPublisher = graphToHttpBody(graph, HttpEnv.dftTriplesFormat);
+        HttpResponse<InputStream> httpResponse = pushWithResponse(httpClient, url, Push.POST, bodyPublisher, format, httpHeaders);
+        Graph graphResponse = GraphFactory.createDefaultGraph();
+        StreamRDF dest = StreamRDFLib.graph(graphResponse);
+        httpResponseToStreamRDF(url, httpResponse, dest);
+        return graphResponse;
+    }
+
+    public static void httpPostDataset(HttpClient httpClient, String url, DatasetGraph dataset, RDFFormat format) {
+        httpPostDataset(httpClient, url, dataset, format, null);
+    }
+
+    public static void httpPostDataset(HttpClient httpClient, String url, DatasetGraph dataset,
+                                       RDFFormat format, Map<String, String> httpHeaders) {
+        BodyPublisher bodyPublisher = datasetToHttpBody(dataset, format);
+        pushBody(httpClient, url, Push.POST, bodyPublisher, format, httpHeaders);
+    }
+
+    public static void httpPutGraph(String url, Graph graph) {
+        httpPutGraph(HttpEnv.getDftHttpClient(), url, graph, HttpEnv.dftTriplesFormat);
+    }
+
+    public static void httpPutGraph(HttpClient httpClient, String url, Graph graph, RDFFormat fmt) {
+        httpPutGraph(httpClient, url, graph, fmt, null);
+    }
+
+    public static void httpPutGraph(HttpClient httpClient, String url, Graph graph,
+                                    RDFFormat format, Map<String, String> httpHeaders) {
+        BodyPublisher bodyPublisher = graphToHttpBody(graph, format);
+        pushBody(httpClient, url, Push.PUT, bodyPublisher, format, httpHeaders);
+    }
+
+    public static void httpPutDataset(HttpClient httpClient, String url, DatasetGraph dataset, RDFFormat format) {
+        httpPutDataset(httpClient, url, dataset, format, null);
+    }
+
+    public static void httpPutDataset(HttpClient httpClient, String url, DatasetGraph dataset,
+                                      RDFFormat format, Map<String, String> httpHeaders) {
+        BodyPublisher bodyPublisher = datasetToHttpBody(dataset, format);
+        pushBody(httpClient, url, Push.PUT, bodyPublisher, format, httpHeaders);
+    }
+
+    // Shared between push* and put*
+    private static void pushBody(HttpClient httpClient, String url, Push style, BodyPublisher bodyPublisher,
+                                 RDFFormat format, Map<String, String> httpHeaders) {
+        String contentType = format.getLang().getHeaderString();
+        if ( httpHeaders == null )
+            httpHeaders = Collections.singletonMap(HttpNames.hContentType, contentType);
+        else
+            httpHeaders.put(HttpNames.hContentType, contentType);
+        HttpLib.httpPushData(httpClient, style, url, HttpLib.setHeaders(httpHeaders), bodyPublisher);
+    }
+
+    private static HttpResponse<InputStream> pushWithResponse(HttpClient httpClient, String url, Push style, BodyPublisher bodyPublisher,
+                                                              RDFFormat format, Map<String, String> httpHeaders) {
+        String contentType = format.getLang().getHeaderString();
+        if ( httpHeaders == null )
+            httpHeaders = Collections.singletonMap(HttpNames.hContentType, contentType);
+        else
+            httpHeaders.put(HttpNames.hContentType, contentType);
+        return HttpLib.httpPushWithResponse(httpClient, style, url, HttpLib.setHeaders(httpHeaders), bodyPublisher);
+    }
+
+    public static void httpDeleteGraph(String url) {
+        httpDeleteGraph(HttpEnv.getDftHttpClient(), url);
+    }
+
+    public static void httpDeleteGraph(HttpClient httpClient, String url) {
+        URI uri = toRequestURI(url);
+        HttpRequest requestData = HttpLib.requestBuilderFor(url)
+            .DELETE()
+            .uri(uri)
+            .build();
+        HttpResponse<InputStream> response = execute(httpClient, requestData);
+        handleResponseNoBody(response);
+    }
+
+    /** RDF {@link Lang}. */
+    /*package*/ static <T> Lang determineSyntax(HttpResponse<T> response, Lang dftSyntax) {
+        String ctStr = responseHeader(response, HttpNames.hContentType);
+        if ( ctStr != null ) {
+            int i = ctStr.indexOf(';');
+            if ( i >= 0 )
+                ctStr = ctStr.substring(0, i);
+        }
+        Lang lang = RDFLanguages.contentTypeToLang(ctStr);
+        return dft(lang, dftSyntax);

Review comment:
       If the `HttpOp` `determineContentType` existed in a parent class or utility class, then `HttpOp` and `HttpRDF` could re-use it, and here it would just
   
   ```
   String ctStr = determineContentType(response);
   Lang lang = RDFLanguages.contentTypeToLang(ctStr);
   return dft(lang, dftSyntax);
   ```

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
##########
@@ -0,0 +1,151 @@
+/*
+ * 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.sparql.exec;
+
+import java.io.OutputStream ;
+import java.util.Iterator ;
+
+import org.apache.jena.query.QuerySolution;
+import org.apache.jena.query.ResultSet;
+import org.apache.jena.rdf.model.RDFNode ;
+import org.apache.jena.riot.ResultSetMgr ;
+import org.apache.jena.riot.resultset.ResultSetLang;
+import org.apache.jena.riot.resultset.rw.ResultsWriter;
+import org.apache.jena.riot.system.PrefixMap;
+import org.apache.jena.riot.system.Prefixes;
+import org.apache.jena.shared.PrefixMapping;
+import org.apache.jena.sparql.ARQConstants;
+import org.apache.jena.sparql.core.Prologue ;
+
+/** RowSetFormatter - Convenience ways to call the various output formatters.
+ *  in various formats.
+ *  @see ResultSetMgr
+ */
+
+public class RowSetOps {
+
+    private RowSetOps() {}
+
+    /**
+     * This operation faithfully walks the rowSet but does nothing with the rows.
+     */
+    public static void consume(RowSet rowSet)
+    { count(rowSet); }
+
+    /**
+     * Count the rows in the RowSet (from the current point of RowSet).
+     * This operation consumes the RowSet.
+     */
+    public static long count(RowSet rowSet)
+    { return rowSet.rewindable().size(); }
+
+    /**
+     * Output a result set in a text format.  The result set is consumed.
+     * Use @see{ResultSetFactory.makeRewindable(ResultSet)} for a rewindable one.
+     * <p>
+     *  This caches the entire results in memory in order to determine the appropriate
+     *  column widths and therefore may exhaust memory for large results
+     *  </p>
+     * @param rowSet   result set
+     */
+    public static void out(RowSet rowSet)
+    { out(System.out, rowSet) ; }
+
+    /**
+     * Output a result set in a text format.
+     * <p>
+     *  This caches the entire results in memory in order to determine the appropriate
+     *  column widths and therefore may exhaust memory for large results
+     *  </p>
+     * @param out        OutputStream
+     * @param rowSet   result set
+     */
+    public static void out(OutputStream out, RowSet rowSet)
+    { out(out, rowSet, (PrefixMap)null) ; }
+
+    /**
+     * Output a result set in a text format.
+     * <p>
+     *  This caches the entire results in memory in order to determine the appropriate
+     *  column widths and therefore may exhaust memory for large results
+     *  </p>

Review comment:
       Extra spaces?

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/http/QuerySendMode.java
##########
@@ -0,0 +1,40 @@
+/*
+ * 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.sparql.exec.http;
+
+/** Enum of different ways to send a SPARQL query over HTTP */
+public enum QuerySendMode {
+
+    // Use HTTP GET when below the length limit else POST an HTML Form encoding
+    asGetWithLimitForm,
+
+    // Use HTTP GET when below the length limit else POST the query as application/sparql-query
+    asGetWithLimitBody,
+
+    // Use GET regardless
+    asGetAlways,
+
+    // Use POST HTML Form regardless
+    asPostForm,
+
+    // POST and application/sparql-query
+    asPost;
+
+    public static QuerySendMode systemDefault = QuerySendMode.asGetWithLimitBody;
+}

Review comment:
       Missing newline

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
##########
@@ -0,0 +1,151 @@
+/*
+ * 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.sparql.exec;
+
+import java.io.OutputStream ;
+import java.util.Iterator ;
+
+import org.apache.jena.query.QuerySolution;
+import org.apache.jena.query.ResultSet;
+import org.apache.jena.rdf.model.RDFNode ;
+import org.apache.jena.riot.ResultSetMgr ;
+import org.apache.jena.riot.resultset.ResultSetLang;
+import org.apache.jena.riot.resultset.rw.ResultsWriter;
+import org.apache.jena.riot.system.PrefixMap;
+import org.apache.jena.riot.system.Prefixes;
+import org.apache.jena.shared.PrefixMapping;
+import org.apache.jena.sparql.ARQConstants;
+import org.apache.jena.sparql.core.Prologue ;
+
+/** RowSetFormatter - Convenience ways to call the various output formatters.
+ *  in various formats.
+ *  @see ResultSetMgr
+ */
+
+public class RowSetOps {
+
+    private RowSetOps() {}
+
+    /**
+     * This operation faithfully walks the rowSet but does nothing with the rows.
+     */
+    public static void consume(RowSet rowSet)
+    { count(rowSet); }
+
+    /**
+     * Count the rows in the RowSet (from the current point of RowSet).
+     * This operation consumes the RowSet.
+     */
+    public static long count(RowSet rowSet)
+    { return rowSet.rewindable().size(); }
+
+    /**
+     * Output a result set in a text format.  The result set is consumed.
+     * Use @see{ResultSetFactory.makeRewindable(ResultSet)} for a rewindable one.
+     * <p>
+     *  This caches the entire results in memory in order to determine the appropriate
+     *  column widths and therefore may exhaust memory for large results
+     *  </p>

Review comment:
       Extra leading spaces? ☝️ 

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/http/UpdateSendMode.java
##########
@@ -0,0 +1,31 @@
+/*
+ * 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.sparql.exec.http;
+
+/** Enum of different ways to send a SPARQL update over HTTP */
+public enum UpdateSendMode {
+
+    // POST HTML forms (update=...)
+    asPostForm,
+
+    // POST application/sparql-update
+    asPost ;
+
+    public static UpdateSendMode systemDefault = UpdateSendMode.asPost;
+}

Review comment:
       Missing newline

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
##########
@@ -0,0 +1,151 @@
+/*
+ * 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.sparql.exec;
+
+import java.io.OutputStream ;
+import java.util.Iterator ;
+
+import org.apache.jena.query.QuerySolution;
+import org.apache.jena.query.ResultSet;
+import org.apache.jena.rdf.model.RDFNode ;
+import org.apache.jena.riot.ResultSetMgr ;
+import org.apache.jena.riot.resultset.ResultSetLang;
+import org.apache.jena.riot.resultset.rw.ResultsWriter;
+import org.apache.jena.riot.system.PrefixMap;
+import org.apache.jena.riot.system.Prefixes;
+import org.apache.jena.shared.PrefixMapping;
+import org.apache.jena.sparql.ARQConstants;
+import org.apache.jena.sparql.core.Prologue ;
+
+/** RowSetFormatter - Convenience ways to call the various output formatters.
+ *  in various formats.
+ *  @see ResultSetMgr
+ */
+
+public class RowSetOps {
+
+    private RowSetOps() {}
+
+    /**
+     * This operation faithfully walks the rowSet but does nothing with the rows.
+     */
+    public static void consume(RowSet rowSet)
+    { count(rowSet); }
+
+    /**
+     * Count the rows in the RowSet (from the current point of RowSet).
+     * This operation consumes the RowSet.
+     */
+    public static long count(RowSet rowSet)
+    { return rowSet.rewindable().size(); }
+
+    /**
+     * Output a result set in a text format.  The result set is consumed.
+     * Use @see{ResultSetFactory.makeRewindable(ResultSet)} for a rewindable one.
+     * <p>
+     *  This caches the entire results in memory in order to determine the appropriate
+     *  column widths and therefore may exhaust memory for large results
+     *  </p>
+     * @param rowSet   result set
+     */
+    public static void out(RowSet rowSet)
+    { out(System.out, rowSet) ; }
+
+    /**
+     * Output a result set in a text format.
+     * <p>
+     *  This caches the entire results in memory in order to determine the appropriate
+     *  column widths and therefore may exhaust memory for large results
+     *  </p>
+     * @param out        OutputStream
+     * @param rowSet   result set
+     */
+    public static void out(OutputStream out, RowSet rowSet)
+    { out(out, rowSet, (PrefixMap)null) ; }
+
+    /**
+     * Output a result set in a text format.
+     * <p>
+     *  This caches the entire results in memory in order to determine the appropriate
+     *  column widths and therefore may exhaust memory for large results
+     *  </p>
+     * @param out        OutputStream
+     * @param resultSet  Result set
+     * @param pmap       Prefix mapping for abbreviating URIs.
+     */
+    public static void out(OutputStream out, RowSet resultSet, PrefixMap pmap) {
+        PrefixMapping prefixMapping = (pmap == null) ? null : Prefixes.adapt(pmap);
+        Prologue prologue = new Prologue(prefixMapping);
+        out(out, resultSet, prologue);
+    }
+
+    /**
+     * Output a result set in a text format.  The result set is consumed.
+     * Use @see{ResultSetFactory.makeRewindable(ResultSet)} for a rewindable one.
+     * <p>
+     *  This caches the entire results in memory in order to determine the appropriate
+     *  column widths and therefore may exhaust memory for large results
+     *  </p>

Review comment:
       Extra spaces?

##########
File path: jena-arq/src/main/java/org/apache/jena/http/HttpLib.java
##########
@@ -0,0 +1,711 @@
+/*
+ * 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.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpRequest.Builder;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.net.http.HttpResponse.BodySubscribers;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.InflaterInputStream;
+
+import org.apache.jena.atlas.RuntimeIOException;
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.lib.IRILib;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.atlas.web.TypedInputStream;
+import org.apache.jena.http.auth.AuthEnv;
+import org.apache.jena.http.auth.AuthLib;
+import org.apache.jena.http.sys.HttpRequestModifier;
+import org.apache.jena.http.sys.RegistryRequestModifier;
+import org.apache.jena.query.ARQ;
+import org.apache.jena.riot.web.HttpNames;
+import org.apache.jena.sparql.exec.http.Params;
+import org.apache.jena.sparql.util.Context;
+import org.apache.jena.web.HttpSC;
+
+/**
+ * Operations related to SPARQL HTTP requests - Query, Update and Graph Store protocols.
+ * This class is not considered "API".
+ */
+public class HttpLib {
+
+    private HttpLib() {}
+
+    public static BodyHandler<Void> noBody() { return BodyHandlers.discarding(); }
+
+    public static BodyPublisher stringBody(String str) { return BodyPublishers.ofString(str); }
+
+    private static BodyHandler<InputStream> bodyHandlerInputStream = buildDftBodyHandlerInputStream();
+
+    private static BodyHandler<InputStream> buildDftBodyHandlerInputStream() {
+        return responseInfo -> {
+            return BodySubscribers.ofInputStream();
+        };
+    }
+
+    /** Read the body of a response as a string in UTF-8. */
+    private static Function<HttpResponse<InputStream>, String> bodyInputStreamToString = r-> {
+        try {
+            InputStream in = r.body();
+            String msg = IO.readWholeFileAsUTF8(in);
+            return msg;
+        } catch (Throwable ex) { throw new HttpException(ex); }
+    };
+
+    /**
+     * Calculate basic auth header value. Use with header "Authorization" (constant
+     * {@link HttpNames#hAuthorization}). Best used over https.
+     */
+    public static String basicAuth(String username, String password) {
+        return "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
+    }
+
+    /**
+     * Get the InputStream from an HttpResponse, handling possible compression settings.
+     * The application must consume or close the {@code InputStream} (see {@link #finish(InputStream)}).
+     * Closing the InputStream may close the HTTP connection.
+     * Assumes the status code has been handled e.g. {@link #handleHttpStatusCode} has been called.
+     */
+    public static InputStream getInputStream(HttpResponse<InputStream> httpResponse) {
+        String encoding = httpResponse.headers().firstValue(HttpNames.hContentEncoding).orElse("");
+        InputStream responseInput = httpResponse.body();
+        // Only support "Content-Encoding: <compression>" and not
+        // "Content-Encoding: chunked, <compression>"
+        try {
+            switch (encoding) {
+                case "" :
+                case "identity" : // Proper name for no compression.
+                    return responseInput;
+                case "gzip" :
+                    return new GZIPInputStream(responseInput, 2*1024);
+                case "inflate" :
+                    return new InflaterInputStream(responseInput);
+                case "br" : // RFC7932
+                default :
+                    throw new UnsupportedOperationException("Not supported: Content-Encoding: " + encoding);
+            }
+        } catch (IOException ex) {
+            throw new UncheckedIOException(ex);
+        }
+    }
+
+    /**
+     * Deal with status code and any error message sent as a body in the response.
+     * <p>
+     * It is this handling 4xx/5xx error messages in the body that forces the use of
+     * {@code InputStream}, not generic {@code T}. We don't know until we see the
+     * status code how we are going to process the response body.
+     * <p>
+     * Exits normally without processing the body if the response is 200.
+     * <p>
+     * Throws {@link HttpException} for 3xx (redirection should have happened by
+     * now), 4xx and 5xx, having consumed the body input stream.
+     */
+    public static void handleHttpStatusCode(HttpResponse<InputStream> response) {
+        int httpStatusCode = response.statusCode();
+        // There is no status message in HTTP/2.
+        if ( ! inRange(httpStatusCode, 100, 599) )
+            throw new HttpException("Status code out of range: "+httpStatusCode);
+        else if ( inRange(httpStatusCode, 100, 199) ) {
+            // Informational
+        }
+        else if ( inRange(httpStatusCode, 200, 299) ) {
+            // Success. Continue processing.
+        }
+        else if ( inRange(httpStatusCode, 300, 399) ) {
+            // We had follow redirects on (default client) so it's http->https,
+            // or the application passed on a HttpClient with redirects off.
+            // Either way, we should not continue processing.
+            try {
+                finish(response);
+            } catch (Exception ex) {
+                throw new HttpException("Error discarding body of "+httpStatusCode , ex);
+            }
+            throw new HttpException(httpStatusCode, HttpSC.getMessage(httpStatusCode));
+        }
+        else if ( inRange(httpStatusCode, 400, 499) ) {
+            throw exception(response, httpStatusCode);
+        }
+        else if ( inRange(httpStatusCode, 500, 599) ) {
+            throw exception(response, httpStatusCode);
+        }
+    }
+
+    /**
+     * Handle the HTTP response (see {@link #handleHttpStatusCode(HttpResponse)}) and
+     * return the InputStream if a 200.
+     *
+     * @param httpResponse
+     * @return InputStream
+     */
+    public static InputStream handleResponseInputStream(HttpResponse<InputStream> httpResponse) {
+        handleHttpStatusCode(httpResponse);
+        return getInputStream(httpResponse);
+    }
+
+    /**
+     * Handle the HTTP response (see {@link #handleHttpStatusCode(HttpResponse)}) and
+     * return the TypedInputStream that includes the {@code Content-Type} if a 200.
+     *
+     * @param httpResponse
+     * @return TypedInputStream
+     */
+    public static TypedInputStream handleResponseTypedInputStream(HttpResponse<InputStream> httpResponse) {
+        InputStream input = handleResponseInputStream(httpResponse);
+        String ct = HttpLib.responseHeader(httpResponse, HttpNames.hContentType);
+        return new TypedInputStream(input, ct);
+    }
+
+    /**
+     * Handle the HTTP response and consume the body if a 200.
+     * Otherwise, throw an {@link HttpException}.
+     * @param response
+     */
+    public static void handleResponseNoBody(HttpResponse<InputStream> response) {
+        handleHttpStatusCode(response);
+        finish(response);
+    }
+
+    /**
+     * Handle the HTTP response and read the body to produce a string if a 200.
+     * Otherwise, throw an {@link HttpException}.
+     * @param response
+     * @return String
+     */
+    public static String handleResponseRtnString(HttpResponse<InputStream> response) {
+        InputStream input = handleResponseInputStream(response);
+        try {
+            return IO.readWholeFileAsUTF8(input);
+        } catch (RuntimeIOException e) { throw new HttpException(e); }
+    }
+
+    static HttpException exception(HttpResponse<InputStream> response, int httpStatusCode) {
+
+        URI uri = response.request().uri();
+
+        //long length = HttpLib.getContentLength(response);
+        // Not critical path code. Read body regardless.
+        InputStream in = response.body();
+        String msg;
+        try {
+            msg = IO.readWholeFileAsUTF8(in);
+            if ( msg.isBlank())
+                msg = null;
+        } catch (RuntimeIOException e) {
+            msg = null;
+        }
+        return new HttpException(httpStatusCode, HttpSC.getMessage(httpStatusCode), msg);
+    }
+
+    private static long getContentLength(HttpResponse<InputStream> response) {
+        Optional<String> x = response.headers().firstValue(HttpNames.hContentLength);
+        if ( x.isEmpty() )
+            return -1;
+        try {
+            return Long.parseLong(x.get());
+        } catch (NumberFormatException ex) { return -1; }
+    }
+
+    /** Test x:int in [min, max] */
+    private static boolean inRange(int x, int min, int max) { return min <= x && x <= max; }
+
+    /** Finish with {@code HttpResponse<InputStream>}.
+     * This read and drops any remaining bytes in the response body.
+     * {@code close} may close the underlying HTTP connection.
+     *  See {@link BodySubscribers#ofInputStream()}.
+     */
+    private static void finish(HttpResponse<InputStream> response) {
+        finish(response.body());
+    }
+
+    /** Read to end of {@link InputStream}.
+     *  {@code close} may close the underlying HTTP connection.
+     *  See {@link BodySubscribers#ofInputStream()}.
+     */
+    public static void finish(InputStream input) {
+        consume(input);
+    }
+
+    // This is extracted from commons-io, IOUtils.skip.
+    // Changes:
+    // * No exception.
+    // * Always consumes to the end of stream (or stream throws IOException)
+    // * Larger buffer
+    private static int SKIP_BUFFER_SIZE = 8*1024;
+    private static byte[] SKIP_BYTE_BUFFER = null;
+
+    private static void consume(final InputStream input) {
+        /*
+         * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data
+         * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer
+         * size were variable, we would need to synch. to ensure some other thread did not create a smaller one)
+         */
+        if (SKIP_BYTE_BUFFER == null) {
+            SKIP_BYTE_BUFFER = new byte[SKIP_BUFFER_SIZE];
+        }
+        int bytesRead = 0; // Informational
+        try {
+            for(;;) {
+                // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
+                final long n = input.read(SKIP_BYTE_BUFFER, 0, SKIP_BUFFER_SIZE);
+                if (n < 0) { // EOF
+                    break;
+                }
+                bytesRead += n;
+            }
+        } catch (IOException ex) { /*ignore*/ }
+    }
+
+    /** String to {@link URI}. Throws {@link HttpException} on bad syntax or if the URI isn't absolute. */
+    public static URI toRequestURI(String uriStr) {
+        try {
+            URI uri = new URI(uriStr);
+            if ( ! uri.isAbsolute() )
+                throw new HttpException("Not an absolute URL: <"+uriStr+">");
+            return uri;
+        } catch (URISyntaxException ex) {
+            int idx = ex.getIndex();
+            String msg = (idx<0)
+                ? String.format("Bad URL: %s", uriStr)
+                : String.format("Bad URL: %s starting at character %d", uriStr, idx);
+            throw new HttpException(msg, ex);
+        }
+    }
+
+    // Terminology:
+    // RFC 2616:   Request-Line   = Method SP Request-URI SP HTTP-Version CRLF
+
+    // RFC 7320:   request-line   = method SP request-target SP HTTP-version CRLF
+    // https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1
+
+    // request-target:
+    // https://datatracker.ietf.org/doc/html/rfc7230#section-5.3
+    // When it is for the origin server ==> absolute-path [ "?" query ]
+
+    // EndpointURI: URL for a service, no query string.
+
+    /** Test whether a URI is a service endpoint. It must be absolute, with host and path, and without query string or fragment. */
+    public static boolean isEndpoint(URI uri) {
+        return uri.isAbsolute() &&
+                uri.getHost() != null &&
+                uri.getRawPath() != null &&
+                uri.getRawQuery() == null &&
+                uri.getRawFragment() == null;
+    }
+
+    /**
+     * Return a string (assumed ot be a URI) without query string or fragment.
+     */
+    public static String endpoint(String uriStr) {
+        int idx1 = uriStr.indexOf('?');
+        int idx2 = uriStr.indexOf('#');
+
+        if ( idx1 < 0 && idx2 < 0 )
+            return uriStr;
+
+        int idx = -1;
+        if ( idx1 < 0 && idx2 > 0 )
+            idx = idx2;
+        else if ( idx1 > 0 && idx2 < 0 )
+            idx = idx1;
+        else
+            idx = Math.min(idx1,  idx2);
+        return uriStr.substring(0, idx);
+    }
+
+    /** RFC7320 "request-target", used in digest authentication. */
+    public static String requestTarget(URI uri) {
+        String path = uri.getRawPath();
+        if ( path == null || path.isEmpty() )
+            path = "/";
+        String qs = uri.getQuery();
+        if ( qs == null || qs.isEmpty() )
+            return path;
+        return path+"?"+qs;
+    }
+
+    /** URI, without query string and fragment. */
+    public static URI endpointURI(URI uri) {
+        if ( uri.getRawQuery() == null && uri.getRawFragment() == null )
+            return uri;
+        try {
+            // Same URI except without query strinf an fragment.
+            return new URI(uri.getScheme(), uri.getRawAuthority(), uri.getRawPath(), null, null);
+        } catch (URISyntaxException x) {
+            throw new IllegalArgumentException(x.getMessage(), x);
+        }
+    }
+
+    /** Return a HttpRequest */
+    public static HttpRequest newGetRequest(String url, Consumer<HttpRequest.Builder> modifier) {
+        HttpRequest.Builder builder = HttpLib.requestBuilderFor(url).uri(toRequestURI(url)).GET();
+        if ( modifier != null )
+            modifier.accept(builder);
+        return builder.build();
+    }
+
+    public static <X> X dft(X value, X dftValue) {
+        return (value != null) ? value : dftValue;
+    }
+
+    public static <X> List<X> copyArray(List<X> array) {
+        if ( array == null )
+            return null;
+        return new ArrayList<>(array);
+    }
+
+    /** Encode a string suitable for use in an URL query string */
+    public static String urlEncodeQueryString(String str) {
+        // java.net.URLEncoder is excessive - it encodes / and : which
+        // is not necessary in a query string or fragment.
+        return IRILib.encodeUriQueryFrag(str);
+    }
+
+    /** Query string is assumed to already be encoded. */
+    public static String requestURL(String url, String queryString) {
+        if ( queryString == null || queryString.isEmpty() )
+            // Empty string. Don't add "?"
+            return url;
+        String sep =  url.contains("?") ? "&" : "?";
+        String requestURL = url+sep+queryString;
+        return requestURL;
+    }
+
+    public static HttpRequest.Builder requestBuilderFor(String serviceEndpoint) {
+        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
+        return AuthEnv.get().addAuth(requestBuilder, serviceEndpoint);
+    }
+
+    public static Builder requestBuilder(String url, Map<String, String> httpHeaders, long readTimeout, TimeUnit readTimeoutUnit) {
+        HttpRequest.Builder builder = HttpLib.requestBuilderFor(url);
+        headers(builder, httpHeaders);
+        builder.uri(toRequestURI(url));
+        if ( readTimeout >= 0 )
+            builder.timeout(Duration.ofMillis(readTimeoutUnit.toMillis(readTimeout)));
+        return builder;
+    }
+
+    /** Create a {@code HttpRequest.Builder} from an {@code HttpRequest}. */
+    public static HttpRequest.Builder createBuilder(HttpRequest request) {
+        HttpRequest.Builder builder = HttpRequest.newBuilder()
+                .expectContinue(request.expectContinue())
+                .uri(request.uri());
+        builder.method(request.method(), request.bodyPublisher().orElse(BodyPublishers.noBody()));
+        request.timeout().ifPresent(builder::timeout);
+        request.version().ifPresent(builder::version);
+        request.headers().map().forEach((name, values)->values.forEach(value->builder.header(name, value)));
+        return builder;
+    }
+
+    /** Set the headers from the Map if the map is not null. Returns the Builder. */
+    static Builder headers(Builder builder, Map<String, String> httpHeaders) {
+        if ( httpHeaders != null )
+            httpHeaders.forEach(builder::header);
+        return builder;
+    }
+
+    /** Set the "Accept" header if value is not null. Returns the builder. */
+    public static Builder acceptHeader(Builder builder, String acceptHeader) {
+        if ( acceptHeader != null )
+            builder.header(HttpNames.hAccept, acceptHeader);
+        return builder;
+    }
+
+    /** Set the "Content-Type" header if value is not null. Returns the builder. */
+    public static Builder contentTypeHeader(Builder builder, String contentType) {
+        if ( contentType != null )
+            builder.header(HttpNames.hContentType, contentType);
+        return builder;
+    }
+
+    // Disabled. Don't encourage using compression ("Content-Encoding: gzip") because it interacts with streaming.
+    // Specifically, streaming (unknown Content-Length) needs chunking. Both chunking and compression
+    // encodings use the same HTTP header. Yet they are handled by different layers.
+    // The basic http code handles chunking.
+    // The complete encoding header can get removed resulting in a compressed stream
+    // without any indication being passed to the application.
+//    /**
+//     * Set the "Accept-Encoding" header. Returns the builder.
+//     * See {@link #getInputStream(HttpResponse)}.
+//     */
+//    public
+//    /*package*/ static Builder acceptEncodingCompressed(Builder builder) {
+//        builder.header(HttpNames.hAcceptEncoding, WebContent.acceptEncodingCompressed);
+//        return builder;
+//    }
+
+    /**
+     * Execute a request, return a {@code HttpResponse<InputStream>} which
+     * can be passed to {@link #handleResponseInputStream(HttpResponse)} which will
+     * convert non-2xx status code to {@link HttpException HttpExceptions}.
+     * <p>
+     * This function applies the HTTP authentication challenge support
+     * and will repeat the request if necessary with added authentication.
+     * <p>
+     * See {@link AuthEnv} for authentication registration.
+     * <br/>
+     * See {@link #executeJDK} to execute exactly once without challenge response handling.
+     *
+     * @see AuthEnv AuthEnv for authentic registration
+     * @see #executeJDK executeJDK to execute exacly once.
+     *
+     * @param httpClient
+     * @param httpRequest
+     * @return HttpResponse
+     */
+    public static HttpResponse<InputStream> execute(HttpClient httpClient, HttpRequest httpRequest) {
+        return execute(httpClient, httpRequest, BodyHandlers.ofInputStream());
+    }
+
+    /**
+     * Execute a request, return a {@code HttpResponse<X>} which
+     * can be passed to {@link #handleHttpStatusCode(HttpResponse)} which will
+     * convert non-2xx status code to {@link HttpException HttpExceptions}.
+     * <p>
+     * This function applies the HTTP authentication challenge support
+     * and will repeat the request if necessary with added authentication.
+     * <p>
+     * See {@link AuthEnv} for authentication registration.
+     * <br/>
+     * See {@link #executeJDK} to execute exactly once without challenge response handling.
+     *
+     * @see AuthEnv AuthEnv for authentic registration
+     * @see #executeJDK executeJDK to execute exacly once.
+     *
+     * @param httpClient
+     * @param httpRequest
+     * @param bodyHandler
+     * @return HttpResponse
+     */
+    public
+    /*package*/ static <X> HttpResponse<X> execute(HttpClient httpClient, HttpRequest httpRequest, BodyHandler<X> bodyHandler) {
+        // To run with no jena-supplied authentication handling.
+        if ( false )
+            return executeJDK(httpClient, httpRequest, bodyHandler);
+        URI uri = httpRequest.uri();
+        URI key = null;
+
+        AuthEnv authEnv = AuthEnv.get();
+
+        if ( uri.getUserInfo() != null ) {
+            String[] up = uri.getUserInfo().split(":");
+            if ( up.length == 2 ) {
+                // Only if "user:password@host", not "user@host"
+                key = HttpLib.endpointURI(uri);
+                // The auth key will be with u:p making it specific.
+                authEnv.registerUsernamePassword(key, up[0], up[1]);
+            }
+        }
+        try {
+            return AuthLib.authExecute(httpClient, httpRequest, bodyHandler);
+        } finally {
+            if ( key != null )
+                authEnv.unregisterUsernamePassword(key);
+        }
+    }
+
+    /**
+     * Execute request and return a response without authentication challenge handling.
+     * @param httpClient
+     * @param httpRequest
+     * @param bodyHandler
+     * @return HttpResponse
+     */
+    public static <T> HttpResponse<T> executeJDK(HttpClient httpClient, HttpRequest httpRequest, BodyHandler<T> bodyHandler) {
+        try {
+            // This is the one place all HTTP requests go through.
+            logRequest(httpRequest);
+            HttpResponse<T> httpResponse = httpClient.send(httpRequest, bodyHandler);
+            logResponse(httpResponse);
+            return httpResponse;
+        //} catch (HttpTimeoutException ex) {
+        } catch (IOException | InterruptedException ex) {
+            if ( ex.getMessage() != null ) {
+                // This is silly.
+                // Rather than an HTTP exception, bad authentication becomes IOException("too many authentication attempts");
+                // or IOException("No credentials provided") if the authenticator decides to return null.
+                if ( ex.getMessage().contains("too many authentication attempts") ||
+                     ex.getMessage().contains("No credentials provided") ) {
+                    throw new HttpException(401, HttpSC.getMessage(401));
+                }
+            }
+            throw new HttpException(httpRequest.method()+" "+httpRequest.uri().toString(), ex);
+        }
+    }
+
+    /*package*/ static CompletableFuture<HttpResponse<InputStream>> asyncExecute(HttpClient httpClient, HttpRequest httpRequest) {
+        logAsyncRequest(httpRequest);
+        return httpClient.sendAsync(httpRequest, BodyHandlers.ofInputStream());
+    }
+
+    /** Push data. POST, PUT, PATCH request with no response body data. */
+    public static void httpPushData(HttpClient httpClient, Push style, String url, Consumer<HttpRequest.Builder> modifier, BodyPublisher body) {
+        HttpResponse<InputStream> response = httpPushWithResponse(httpClient, style, url, modifier, body);
+        handleResponseNoBody(response);
+    }
+
+    // Worker
+    /*package*/ static HttpResponse<InputStream> httpPushWithResponse(HttpClient httpClient, Push style, String url,
+                                                                      Consumer<HttpRequest.Builder> modifier, BodyPublisher body) {
+        URI uri = toRequestURI(url);
+        HttpRequest.Builder builder = requestBuilderFor(url);
+        builder.uri(uri);
+        builder.method(style.method(), body);
+        if ( modifier != null )
+            modifier.accept(builder);
+        HttpResponse<InputStream> response = execute(httpClient, builder.build());
+        return response;
+    }
+
+
+    /** Request */
+    private static void logRequest(HttpRequest httpRequest) {
+        // Uses the SystemLogger which defaults to JUL.
+        // Add org.apache.jena.logging:log4j-jpl
+        // (java11 : 11.0.9, if using log4j-jpl, logging prints the request as {0} but response OK)
+//        httpRequest.uri();
+//        httpRequest.method();
+//        httpRequest.headers();
+    }
+
+    /** Async Request */
+    private static void logAsyncRequest(HttpRequest httpRequest) {}
+
+        /** Response (do not touch the body!)  */
+    private static void logResponse(HttpResponse<?> httpResponse) {
+//        httpResponse.uri();
+//        httpResponse.statusCode();
+//        httpResponse.headers();
+//        httpResponse.previousResponse();

Review comment:
       Ditto here on this log method, is it commented for now, but will be logged somewhere later on?

##########
File path: jena-arq/src/main/java/org/apache/jena/http/HttpLib.java
##########
@@ -0,0 +1,711 @@
+/*
+ * 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.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpRequest.Builder;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.net.http.HttpResponse.BodySubscribers;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.InflaterInputStream;
+
+import org.apache.jena.atlas.RuntimeIOException;
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.lib.IRILib;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.atlas.web.TypedInputStream;
+import org.apache.jena.http.auth.AuthEnv;
+import org.apache.jena.http.auth.AuthLib;
+import org.apache.jena.http.sys.HttpRequestModifier;
+import org.apache.jena.http.sys.RegistryRequestModifier;
+import org.apache.jena.query.ARQ;
+import org.apache.jena.riot.web.HttpNames;
+import org.apache.jena.sparql.exec.http.Params;
+import org.apache.jena.sparql.util.Context;
+import org.apache.jena.web.HttpSC;
+
+/**
+ * Operations related to SPARQL HTTP requests - Query, Update and Graph Store protocols.
+ * This class is not considered "API".
+ */
+public class HttpLib {
+
+    private HttpLib() {}
+
+    public static BodyHandler<Void> noBody() { return BodyHandlers.discarding(); }
+
+    public static BodyPublisher stringBody(String str) { return BodyPublishers.ofString(str); }
+
+    private static BodyHandler<InputStream> bodyHandlerInputStream = buildDftBodyHandlerInputStream();
+
+    private static BodyHandler<InputStream> buildDftBodyHandlerInputStream() {
+        return responseInfo -> {
+            return BodySubscribers.ofInputStream();
+        };
+    }
+
+    /** Read the body of a response as a string in UTF-8. */
+    private static Function<HttpResponse<InputStream>, String> bodyInputStreamToString = r-> {
+        try {
+            InputStream in = r.body();
+            String msg = IO.readWholeFileAsUTF8(in);
+            return msg;
+        } catch (Throwable ex) { throw new HttpException(ex); }
+    };
+
+    /**
+     * Calculate basic auth header value. Use with header "Authorization" (constant
+     * {@link HttpNames#hAuthorization}). Best used over https.
+     */
+    public static String basicAuth(String username, String password) {
+        return "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
+    }
+
+    /**
+     * Get the InputStream from an HttpResponse, handling possible compression settings.
+     * The application must consume or close the {@code InputStream} (see {@link #finish(InputStream)}).
+     * Closing the InputStream may close the HTTP connection.
+     * Assumes the status code has been handled e.g. {@link #handleHttpStatusCode} has been called.
+     */
+    public static InputStream getInputStream(HttpResponse<InputStream> httpResponse) {
+        String encoding = httpResponse.headers().firstValue(HttpNames.hContentEncoding).orElse("");
+        InputStream responseInput = httpResponse.body();
+        // Only support "Content-Encoding: <compression>" and not
+        // "Content-Encoding: chunked, <compression>"
+        try {
+            switch (encoding) {
+                case "" :
+                case "identity" : // Proper name for no compression.
+                    return responseInput;
+                case "gzip" :
+                    return new GZIPInputStream(responseInput, 2*1024);
+                case "inflate" :
+                    return new InflaterInputStream(responseInput);
+                case "br" : // RFC7932
+                default :
+                    throw new UnsupportedOperationException("Not supported: Content-Encoding: " + encoding);
+            }
+        } catch (IOException ex) {
+            throw new UncheckedIOException(ex);
+        }
+    }
+
+    /**
+     * Deal with status code and any error message sent as a body in the response.
+     * <p>
+     * It is this handling 4xx/5xx error messages in the body that forces the use of
+     * {@code InputStream}, not generic {@code T}. We don't know until we see the
+     * status code how we are going to process the response body.
+     * <p>
+     * Exits normally without processing the body if the response is 200.
+     * <p>
+     * Throws {@link HttpException} for 3xx (redirection should have happened by
+     * now), 4xx and 5xx, having consumed the body input stream.
+     */
+    public static void handleHttpStatusCode(HttpResponse<InputStream> response) {
+        int httpStatusCode = response.statusCode();
+        // There is no status message in HTTP/2.
+        if ( ! inRange(httpStatusCode, 100, 599) )
+            throw new HttpException("Status code out of range: "+httpStatusCode);
+        else if ( inRange(httpStatusCode, 100, 199) ) {
+            // Informational
+        }
+        else if ( inRange(httpStatusCode, 200, 299) ) {
+            // Success. Continue processing.
+        }
+        else if ( inRange(httpStatusCode, 300, 399) ) {
+            // We had follow redirects on (default client) so it's http->https,
+            // or the application passed on a HttpClient with redirects off.
+            // Either way, we should not continue processing.
+            try {
+                finish(response);
+            } catch (Exception ex) {
+                throw new HttpException("Error discarding body of "+httpStatusCode , ex);
+            }
+            throw new HttpException(httpStatusCode, HttpSC.getMessage(httpStatusCode));
+        }
+        else if ( inRange(httpStatusCode, 400, 499) ) {
+            throw exception(response, httpStatusCode);
+        }
+        else if ( inRange(httpStatusCode, 500, 599) ) {
+            throw exception(response, httpStatusCode);
+        }
+    }
+
+    /**
+     * Handle the HTTP response (see {@link #handleHttpStatusCode(HttpResponse)}) and
+     * return the InputStream if a 200.
+     *
+     * @param httpResponse
+     * @return InputStream
+     */
+    public static InputStream handleResponseInputStream(HttpResponse<InputStream> httpResponse) {
+        handleHttpStatusCode(httpResponse);
+        return getInputStream(httpResponse);
+    }
+
+    /**
+     * Handle the HTTP response (see {@link #handleHttpStatusCode(HttpResponse)}) and
+     * return the TypedInputStream that includes the {@code Content-Type} if a 200.
+     *
+     * @param httpResponse
+     * @return TypedInputStream
+     */
+    public static TypedInputStream handleResponseTypedInputStream(HttpResponse<InputStream> httpResponse) {
+        InputStream input = handleResponseInputStream(httpResponse);
+        String ct = HttpLib.responseHeader(httpResponse, HttpNames.hContentType);
+        return new TypedInputStream(input, ct);
+    }
+
+    /**
+     * Handle the HTTP response and consume the body if a 200.
+     * Otherwise, throw an {@link HttpException}.
+     * @param response
+     */
+    public static void handleResponseNoBody(HttpResponse<InputStream> response) {
+        handleHttpStatusCode(response);
+        finish(response);
+    }
+
+    /**
+     * Handle the HTTP response and read the body to produce a string if a 200.
+     * Otherwise, throw an {@link HttpException}.
+     * @param response
+     * @return String
+     */
+    public static String handleResponseRtnString(HttpResponse<InputStream> response) {
+        InputStream input = handleResponseInputStream(response);
+        try {
+            return IO.readWholeFileAsUTF8(input);
+        } catch (RuntimeIOException e) { throw new HttpException(e); }
+    }
+
+    static HttpException exception(HttpResponse<InputStream> response, int httpStatusCode) {
+
+        URI uri = response.request().uri();
+
+        //long length = HttpLib.getContentLength(response);
+        // Not critical path code. Read body regardless.
+        InputStream in = response.body();
+        String msg;
+        try {
+            msg = IO.readWholeFileAsUTF8(in);
+            if ( msg.isBlank())
+                msg = null;
+        } catch (RuntimeIOException e) {
+            msg = null;
+        }
+        return new HttpException(httpStatusCode, HttpSC.getMessage(httpStatusCode), msg);
+    }
+
+    private static long getContentLength(HttpResponse<InputStream> response) {
+        Optional<String> x = response.headers().firstValue(HttpNames.hContentLength);
+        if ( x.isEmpty() )
+            return -1;
+        try {
+            return Long.parseLong(x.get());
+        } catch (NumberFormatException ex) { return -1; }
+    }
+
+    /** Test x:int in [min, max] */
+    private static boolean inRange(int x, int min, int max) { return min <= x && x <= max; }
+
+    /** Finish with {@code HttpResponse<InputStream>}.
+     * This read and drops any remaining bytes in the response body.
+     * {@code close} may close the underlying HTTP connection.
+     *  See {@link BodySubscribers#ofInputStream()}.
+     */
+    private static void finish(HttpResponse<InputStream> response) {
+        finish(response.body());
+    }
+
+    /** Read to end of {@link InputStream}.
+     *  {@code close} may close the underlying HTTP connection.
+     *  See {@link BodySubscribers#ofInputStream()}.
+     */
+    public static void finish(InputStream input) {
+        consume(input);
+    }
+
+    // This is extracted from commons-io, IOUtils.skip.
+    // Changes:
+    // * No exception.
+    // * Always consumes to the end of stream (or stream throws IOException)
+    // * Larger buffer
+    private static int SKIP_BUFFER_SIZE = 8*1024;
+    private static byte[] SKIP_BYTE_BUFFER = null;
+
+    private static void consume(final InputStream input) {
+        /*
+         * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data
+         * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer
+         * size were variable, we would need to synch. to ensure some other thread did not create a smaller one)
+         */
+        if (SKIP_BYTE_BUFFER == null) {
+            SKIP_BYTE_BUFFER = new byte[SKIP_BUFFER_SIZE];
+        }
+        int bytesRead = 0; // Informational
+        try {
+            for(;;) {
+                // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
+                final long n = input.read(SKIP_BYTE_BUFFER, 0, SKIP_BUFFER_SIZE);
+                if (n < 0) { // EOF
+                    break;
+                }
+                bytesRead += n;
+            }
+        } catch (IOException ex) { /*ignore*/ }
+    }
+
+    /** String to {@link URI}. Throws {@link HttpException} on bad syntax or if the URI isn't absolute. */
+    public static URI toRequestURI(String uriStr) {
+        try {
+            URI uri = new URI(uriStr);
+            if ( ! uri.isAbsolute() )
+                throw new HttpException("Not an absolute URL: <"+uriStr+">");
+            return uri;
+        } catch (URISyntaxException ex) {
+            int idx = ex.getIndex();
+            String msg = (idx<0)
+                ? String.format("Bad URL: %s", uriStr)
+                : String.format("Bad URL: %s starting at character %d", uriStr, idx);
+            throw new HttpException(msg, ex);
+        }
+    }
+
+    // Terminology:
+    // RFC 2616:   Request-Line   = Method SP Request-URI SP HTTP-Version CRLF
+
+    // RFC 7320:   request-line   = method SP request-target SP HTTP-version CRLF
+    // https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1
+
+    // request-target:
+    // https://datatracker.ietf.org/doc/html/rfc7230#section-5.3
+    // When it is for the origin server ==> absolute-path [ "?" query ]
+
+    // EndpointURI: URL for a service, no query string.
+
+    /** Test whether a URI is a service endpoint. It must be absolute, with host and path, and without query string or fragment. */
+    public static boolean isEndpoint(URI uri) {
+        return uri.isAbsolute() &&
+                uri.getHost() != null &&
+                uri.getRawPath() != null &&
+                uri.getRawQuery() == null &&
+                uri.getRawFragment() == null;
+    }
+
+    /**
+     * Return a string (assumed ot be a URI) without query string or fragment.
+     */
+    public static String endpoint(String uriStr) {
+        int idx1 = uriStr.indexOf('?');
+        int idx2 = uriStr.indexOf('#');
+
+        if ( idx1 < 0 && idx2 < 0 )
+            return uriStr;
+
+        int idx = -1;
+        if ( idx1 < 0 && idx2 > 0 )
+            idx = idx2;
+        else if ( idx1 > 0 && idx2 < 0 )
+            idx = idx1;
+        else
+            idx = Math.min(idx1,  idx2);
+        return uriStr.substring(0, idx);
+    }
+
+    /** RFC7320 "request-target", used in digest authentication. */
+    public static String requestTarget(URI uri) {
+        String path = uri.getRawPath();
+        if ( path == null || path.isEmpty() )
+            path = "/";
+        String qs = uri.getQuery();
+        if ( qs == null || qs.isEmpty() )
+            return path;
+        return path+"?"+qs;
+    }
+
+    /** URI, without query string and fragment. */
+    public static URI endpointURI(URI uri) {
+        if ( uri.getRawQuery() == null && uri.getRawFragment() == null )
+            return uri;
+        try {
+            // Same URI except without query strinf an fragment.
+            return new URI(uri.getScheme(), uri.getRawAuthority(), uri.getRawPath(), null, null);
+        } catch (URISyntaxException x) {
+            throw new IllegalArgumentException(x.getMessage(), x);
+        }
+    }
+
+    /** Return a HttpRequest */
+    public static HttpRequest newGetRequest(String url, Consumer<HttpRequest.Builder> modifier) {
+        HttpRequest.Builder builder = HttpLib.requestBuilderFor(url).uri(toRequestURI(url)).GET();
+        if ( modifier != null )
+            modifier.accept(builder);
+        return builder.build();
+    }
+
+    public static <X> X dft(X value, X dftValue) {
+        return (value != null) ? value : dftValue;
+    }
+
+    public static <X> List<X> copyArray(List<X> array) {
+        if ( array == null )
+            return null;
+        return new ArrayList<>(array);
+    }
+
+    /** Encode a string suitable for use in an URL query string */
+    public static String urlEncodeQueryString(String str) {
+        // java.net.URLEncoder is excessive - it encodes / and : which
+        // is not necessary in a query string or fragment.
+        return IRILib.encodeUriQueryFrag(str);
+    }
+
+    /** Query string is assumed to already be encoded. */
+    public static String requestURL(String url, String queryString) {
+        if ( queryString == null || queryString.isEmpty() )
+            // Empty string. Don't add "?"
+            return url;
+        String sep =  url.contains("?") ? "&" : "?";
+        String requestURL = url+sep+queryString;
+        return requestURL;
+    }
+
+    public static HttpRequest.Builder requestBuilderFor(String serviceEndpoint) {
+        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
+        return AuthEnv.get().addAuth(requestBuilder, serviceEndpoint);
+    }
+
+    public static Builder requestBuilder(String url, Map<String, String> httpHeaders, long readTimeout, TimeUnit readTimeoutUnit) {
+        HttpRequest.Builder builder = HttpLib.requestBuilderFor(url);
+        headers(builder, httpHeaders);
+        builder.uri(toRequestURI(url));
+        if ( readTimeout >= 0 )
+            builder.timeout(Duration.ofMillis(readTimeoutUnit.toMillis(readTimeout)));
+        return builder;
+    }
+
+    /** Create a {@code HttpRequest.Builder} from an {@code HttpRequest}. */
+    public static HttpRequest.Builder createBuilder(HttpRequest request) {
+        HttpRequest.Builder builder = HttpRequest.newBuilder()
+                .expectContinue(request.expectContinue())
+                .uri(request.uri());
+        builder.method(request.method(), request.bodyPublisher().orElse(BodyPublishers.noBody()));
+        request.timeout().ifPresent(builder::timeout);
+        request.version().ifPresent(builder::version);
+        request.headers().map().forEach((name, values)->values.forEach(value->builder.header(name, value)));
+        return builder;
+    }
+
+    /** Set the headers from the Map if the map is not null. Returns the Builder. */
+    static Builder headers(Builder builder, Map<String, String> httpHeaders) {
+        if ( httpHeaders != null )
+            httpHeaders.forEach(builder::header);
+        return builder;
+    }
+
+    /** Set the "Accept" header if value is not null. Returns the builder. */
+    public static Builder acceptHeader(Builder builder, String acceptHeader) {
+        if ( acceptHeader != null )
+            builder.header(HttpNames.hAccept, acceptHeader);
+        return builder;
+    }
+
+    /** Set the "Content-Type" header if value is not null. Returns the builder. */
+    public static Builder contentTypeHeader(Builder builder, String contentType) {
+        if ( contentType != null )
+            builder.header(HttpNames.hContentType, contentType);
+        return builder;
+    }
+
+    // Disabled. Don't encourage using compression ("Content-Encoding: gzip") because it interacts with streaming.
+    // Specifically, streaming (unknown Content-Length) needs chunking. Both chunking and compression
+    // encodings use the same HTTP header. Yet they are handled by different layers.
+    // The basic http code handles chunking.
+    // The complete encoding header can get removed resulting in a compressed stream
+    // without any indication being passed to the application.
+//    /**
+//     * Set the "Accept-Encoding" header. Returns the builder.
+//     * See {@link #getInputStream(HttpResponse)}.
+//     */
+//    public
+//    /*package*/ static Builder acceptEncodingCompressed(Builder builder) {
+//        builder.header(HttpNames.hAcceptEncoding, WebContent.acceptEncodingCompressed);
+//        return builder;
+//    }
+
+    /**
+     * Execute a request, return a {@code HttpResponse<InputStream>} which
+     * can be passed to {@link #handleResponseInputStream(HttpResponse)} which will
+     * convert non-2xx status code to {@link HttpException HttpExceptions}.
+     * <p>
+     * This function applies the HTTP authentication challenge support
+     * and will repeat the request if necessary with added authentication.
+     * <p>
+     * See {@link AuthEnv} for authentication registration.
+     * <br/>
+     * See {@link #executeJDK} to execute exactly once without challenge response handling.
+     *
+     * @see AuthEnv AuthEnv for authentic registration
+     * @see #executeJDK executeJDK to execute exacly once.
+     *
+     * @param httpClient
+     * @param httpRequest
+     * @return HttpResponse
+     */
+    public static HttpResponse<InputStream> execute(HttpClient httpClient, HttpRequest httpRequest) {
+        return execute(httpClient, httpRequest, BodyHandlers.ofInputStream());
+    }
+
+    /**
+     * Execute a request, return a {@code HttpResponse<X>} which
+     * can be passed to {@link #handleHttpStatusCode(HttpResponse)} which will
+     * convert non-2xx status code to {@link HttpException HttpExceptions}.
+     * <p>
+     * This function applies the HTTP authentication challenge support
+     * and will repeat the request if necessary with added authentication.
+     * <p>
+     * See {@link AuthEnv} for authentication registration.
+     * <br/>
+     * See {@link #executeJDK} to execute exactly once without challenge response handling.
+     *
+     * @see AuthEnv AuthEnv for authentic registration
+     * @see #executeJDK executeJDK to execute exacly once.
+     *
+     * @param httpClient
+     * @param httpRequest
+     * @param bodyHandler
+     * @return HttpResponse
+     */
+    public
+    /*package*/ static <X> HttpResponse<X> execute(HttpClient httpClient, HttpRequest httpRequest, BodyHandler<X> bodyHandler) {
+        // To run with no jena-supplied authentication handling.
+        if ( false )
+            return executeJDK(httpClient, httpRequest, bodyHandler);
+        URI uri = httpRequest.uri();
+        URI key = null;
+
+        AuthEnv authEnv = AuthEnv.get();
+
+        if ( uri.getUserInfo() != null ) {
+            String[] up = uri.getUserInfo().split(":");
+            if ( up.length == 2 ) {
+                // Only if "user:password@host", not "user@host"
+                key = HttpLib.endpointURI(uri);
+                // The auth key will be with u:p making it specific.
+                authEnv.registerUsernamePassword(key, up[0], up[1]);
+            }
+        }
+        try {
+            return AuthLib.authExecute(httpClient, httpRequest, bodyHandler);
+        } finally {
+            if ( key != null )
+                authEnv.unregisterUsernamePassword(key);
+        }
+    }
+
+    /**
+     * Execute request and return a response without authentication challenge handling.
+     * @param httpClient
+     * @param httpRequest
+     * @param bodyHandler
+     * @return HttpResponse
+     */
+    public static <T> HttpResponse<T> executeJDK(HttpClient httpClient, HttpRequest httpRequest, BodyHandler<T> bodyHandler) {
+        try {
+            // This is the one place all HTTP requests go through.
+            logRequest(httpRequest);
+            HttpResponse<T> httpResponse = httpClient.send(httpRequest, bodyHandler);
+            logResponse(httpResponse);
+            return httpResponse;
+        //} catch (HttpTimeoutException ex) {
+        } catch (IOException | InterruptedException ex) {
+            if ( ex.getMessage() != null ) {
+                // This is silly.
+                // Rather than an HTTP exception, bad authentication becomes IOException("too many authentication attempts");
+                // or IOException("No credentials provided") if the authenticator decides to return null.
+                if ( ex.getMessage().contains("too many authentication attempts") ||
+                     ex.getMessage().contains("No credentials provided") ) {
+                    throw new HttpException(401, HttpSC.getMessage(401));
+                }
+            }
+            throw new HttpException(httpRequest.method()+" "+httpRequest.uri().toString(), ex);
+        }
+    }
+
+    /*package*/ static CompletableFuture<HttpResponse<InputStream>> asyncExecute(HttpClient httpClient, HttpRequest httpRequest) {
+        logAsyncRequest(httpRequest);
+        return httpClient.sendAsync(httpRequest, BodyHandlers.ofInputStream());
+    }
+
+    /** Push data. POST, PUT, PATCH request with no response body data. */
+    public static void httpPushData(HttpClient httpClient, Push style, String url, Consumer<HttpRequest.Builder> modifier, BodyPublisher body) {
+        HttpResponse<InputStream> response = httpPushWithResponse(httpClient, style, url, modifier, body);
+        handleResponseNoBody(response);
+    }
+
+    // Worker
+    /*package*/ static HttpResponse<InputStream> httpPushWithResponse(HttpClient httpClient, Push style, String url,
+                                                                      Consumer<HttpRequest.Builder> modifier, BodyPublisher body) {
+        URI uri = toRequestURI(url);
+        HttpRequest.Builder builder = requestBuilderFor(url);
+        builder.uri(uri);
+        builder.method(style.method(), body);
+        if ( modifier != null )
+            modifier.accept(builder);
+        HttpResponse<InputStream> response = execute(httpClient, builder.build());
+        return response;
+    }
+
+
+    /** Request */
+    private static void logRequest(HttpRequest httpRequest) {
+        // Uses the SystemLogger which defaults to JUL.
+        // Add org.apache.jena.logging:log4j-jpl
+        // (java11 : 11.0.9, if using log4j-jpl, logging prints the request as {0} but response OK)
+//        httpRequest.uri();
+//        httpRequest.method();
+//        httpRequest.headers();
+    }
+
+    /** Async Request */
+    private static void logAsyncRequest(HttpRequest httpRequest) {}
+
+        /** Response (do not touch the body!)  */
+    private static void logResponse(HttpResponse<?> httpResponse) {
+//        httpResponse.uri();
+//        httpResponse.statusCode();
+//        httpResponse.headers();
+//        httpResponse.previousResponse();
+    }
+
+    /**
+     * Allow setting additional/optional query parameters on a per remote service (including for SERVICE).
+     * <ul>
+     * <li>ARQ.httpRequestModifer - the specific modifier</li>
+     * <li>ARQ.httpRegistryRequestModifer - the registry, keyed by service URL.</li>
+     * </ul>
+     */
+    /*package*/ public static void modifyByService(String serviceURI, Context context, Params params, Map<String, String> httpHeaders) {
+        HttpRequestModifier modifier = context.get(ARQ.httpRequestModifer);
+        if ( modifier != null ) {
+            modifier.modify(params, httpHeaders);
+            return;
+        }
+        RegistryRequestModifier modifierRegistry = context.get(ARQ.httpRegistryRequestModifer);
+        if ( modifierRegistry == null )
+            modifierRegistry = RegistryRequestModifier.get();
+        if ( modifierRegistry != null ) {
+            HttpRequestModifier mods = modifierRegistry.find(serviceURI);
+            if ( mods != null )
+                mods.modify(params, httpHeaders);
+        }
+    }
+
+    /**
+     * Return a modifier that will set the Accept header to the value.
+     * An argument of "null" means "no action".
+     */
+    public static Consumer<HttpRequest.Builder> setHeaders(Map<String, String> headers) {
+        if ( headers == null )
+            return (x)->{};
+        return x->headers.forEach(x::header);
+    }
+
+    /**
+     * Return a modifier that will set the Accept header to the value.
+     * An argument of "null" means "no action".
+     */
+    static Consumer<HttpRequest.Builder> setAcceptHeader(String acceptHeader) {
+        if ( acceptHeader == null )
+            return (x)->{};
+        return header(HttpNames.hAccept, acceptHeader);
+    }
+
+    /**
+     * Return a modifier that will set the Content-Type header to the value.
+     * An argument of "null" means "no action".
+     */
+    static Consumer<HttpRequest.Builder> setContentTypeHeader(String contentType) {
+        if ( contentType == null )
+            return (x)->{};
+        return header(HttpNames.hContentType, contentType);
+    }
+
+    /**
+     * Return a modifier that will set the named header to the value.
+     */
+    static Consumer<HttpRequest.Builder> header(String headerName, String headerValue) {
+        return x->x.header(headerName, headerValue);
+    }
+
+    /** Return the first header of the given name, or null if none */
+    public static String responseHeader(HttpResponse<?> response, String headerName) {
+        Objects.requireNonNull(response);
+        Objects.requireNonNull(headerName);
+        return response.headers().firstValue(headerName).orElse(null);
+    }
+
+    // HTTP response header inserted to aid tracking.
+    public static String FusekiRequestIdHeader = "Fuseki-Request-Id";
+
+    /**
+     * Test whether a URL identifies a Fuseki server. This operation can not guarantee to
+     * detect a Fuseki server - for example, it may be behind a reverse proxy that masks
+     * the signature.
+     */
+    public static boolean isFuseki(String datasetURL) {
+        HttpRequest.Builder builder =
+                HttpRequest.newBuilder().uri(toRequestURI(datasetURL)).method(HttpNames.METHOD_HEAD, BodyPublishers.noBody());
+        HttpRequest request = builder.build();
+        HttpClient httpClient = HttpEnv.getDftHttpClient();
+        HttpResponse<InputStream> response = execute(httpClient, request);
+        handleResponseNoBody(response);
+
+        Optional<String> value1 = response.headers().firstValue(FusekiRequestIdHeader);
+        if ( value1.isPresent() )
+            return true;
+        Optional<String> value2 = response.headers().firstValue("Server");
+        if ( value2.isEmpty() )
+            return false;
+        String headerValue = value2.get();
+        boolean isFuseki = headerValue.startsWith("Apache Jena Fuseki");
+        if ( !isFuseki )
+            isFuseki = headerValue.toLowerCase().contains("fuseki");

Review comment:
       ```suggestion
           boolean isFuseki = headerValue.startsWith("Apache Jena Fuseki") || headerValue.toLowerCase().contains("fuseki");
   ```

##########
File path: jena-arq/src/main/java/org/apache/jena/http/auth/AuthStringTokenizer.java
##########
@@ -0,0 +1,138 @@
+/**
+ * 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.http.auth;
+
+import java.util.* ;
+import java.util.regex.Matcher ;
+import java.util.regex.Pattern ;
+
+/** Parser for authentication header strings.
+ * More forgiving than necessary.
+ */
+class AuthStringTokenizer {
+
+    // Terms:
+    //   "quoted string"
+    //   delimiters( , or =)
+    //   an unquoted string, no spaces.
+    private static String regex = "(\"[^\"=]*\"|,|=|[^=, \"]+)";
+    private static Pattern pattern = Pattern.compile(regex) ;
+    private static String nullString = "" ;
+
+    static Map<String, String> parse(String string) {
+        try {
+            return parse$(string) ;
+        } catch (AuthStringException ex) { return null ; }
+    }
+
+    private static Map<String, String> parse$(String string) {
+        // Phase one - split into tokens.
+        List<String> tokens = tokenize(string) ;
+
+        Map<String, String> map = new HashMap<>() ;
+        if ( ! tokens.isEmpty() ) {
+            String s = tokens.get(0);
+            if ( "Digest".equalsIgnoreCase(s) )
+                map.put(AuthChallenge.SCHEME, s);
+            if ( "Basic".equalsIgnoreCase(s) )
+                map.put(AuthChallenge.SCHEME, s);
+        }
+
+        // Phase two : assign to the map.
+        String word1 = null ;
+        boolean seenEquals = false ;
+        for ( String s : tokens ) {
+            if ( s == null )
+                continue ;
+            if ( s.equals(",") ) {
+                if ( word1 != null )
+                    record(map, word1, null) ;
+                word1 = null ;
+                continue ;
+            }
+
+            if ( s.equals("=") ) {
+                seenEquals = true ;
+                continue ;
+            }
+
+            if (word1 == null ) {
+                if ( seenEquals )
+                    // Two = =
+                    throw new AuthStringException() ;
+                word1 = s ;
+                continue ;
+            }
+
+            // new word, word1 seen.
+            //if ( word1 != null ) {
+            if ( ! seenEquals ) {
+                record(map, word1, null) ;
+                word1 = s ;
+            } else {
+                record(map, word1, s) ;
+                word1 = null ;
+                seenEquals = false ;
+                continue ;
+            }
+        }
+
+        if (word1 != null )
+            record(map, word1, null) ;
+
+        return map ;
+
+    }
+
+    /** Tokenize. Quoted strings retain the "" */
+    /*package*/ static List<String> tokenize(String string) {
+        List<String> list = new ArrayList<String>();
+        Matcher m = pattern.matcher(string);
+        while (m.find()) {
+            // First non-null match.
+            for ( int i = 1 ; i <= m.groupCount() ; i++ ) {
+                if ( m.group(i) != null ) {
+                    list.add(m.group(i));
+                    break ;
+                }
+            }
+        }
+        return list ;
+    }
+
+    private static boolean isQuoted(String string) {
+        return string.startsWith("\"") && string.endsWith("\"") ;
+    }
+
+    private static boolean maybeQuoted(String string) {
+        return string.startsWith("\"") || string.endsWith("\"") ;
+    }
+
+    private static void record(Map<String, String> map, String word1, String word2) {
+        if ( word1 == null || word1.isEmpty() || maybeQuoted(word1) )
+            throw new AuthStringException() ;
+        word1 = word1.toLowerCase() ;
+        if ( word2 == null )
+            word2 = nullString ;
+        else if ( isQuoted(word2) )
+            word2 = word2.substring(1, word2.length()-1) ;
+
+        map.put(word1, word2) ;
+    }
+}

Review comment:
       Missing new line

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkFuseki.java
##########
@@ -0,0 +1,134 @@
+/*
+ * 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.rdflink;
+
+import java.net.http.HttpClient;
+
+import org.apache.jena.rdflink.RDFLinkFuseki;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFFormat;
+import org.apache.jena.riot.resultset.ResultSetLang;
+import org.apache.jena.sparql.core.Transactional;
+
+/**
+ * Implementation of the {@link RDFLink} interface for connecting to an Apache Jena Fuseki.
+ * <p>
+ * This adds the ability to work with blank nodes across the network.
+ */
+public class RDFLinkFuseki extends RDFLinkHTTP {
+
+    /**
+     * Create a connection builder which is initialized for the default Fuseki
+     * configuration. The application must call
+     * {@link RDFLinkHTTPBuilder#destination(String)} to set the URL of the remote
+     * dataset.
+     * @return RDFLinkRemoteBuilder
+     */
+    public static RDFLinkHTTPBuilder newBuilder() {
+        return setupForFuseki(RDFLinkHTTP.newBuilder());
+    }
+
+    /**
+     * Create a connection builder which is initialized from an existing {@code RDFLinkFuseki}.
+     * @param other The RDFLinkFuseki to clone.
+     * @return RDFLinkRemoteBuilder
+     */
+    public static RDFLinkHTTPBuilder from(RDFLinkFuseki other) {
+        return setupCreator(RDFLinkHTTP.from(other));
+    }
+
+    /** Fuseki settings */
+    private static RDFLinkHTTPBuilder setupForFuseki(RDFLinkHTTPBuilder builder) {
+        String ctRDFThrift = Lang.RDFTHRIFT.getHeaderString();
+        String acceptHeaderSPARQL = String.join(","
+                            , ResultSetLang.RS_Thrift.getHeaderString()
+                            , ResultSetLang.RS_JSON.getHeaderString()+";q=0.9"
+                            , Lang.RDFTHRIFT.getHeaderString());
+        return builder
+            .quadsFormat(RDFFormat.RDF_THRIFT)
+            .triplesFormat(RDFFormat.RDF_THRIFT)
+            .acceptHeaderGraph(ctRDFThrift)
+            .acceptHeaderDataset(ctRDFThrift)
+            .acceptHeaderSelectQuery(ResultSetLang.RS_Thrift.getHeaderString())
+            .acceptHeaderAskQuery(ResultSetLang.RS_JSON.getHeaderString())
+            .acceptHeaderQuery(acceptHeaderSPARQL)
+            .parseCheckSPARQL(false)
+            // Create object of this class.
+            .creator((b)->fusekiMaker(b));
+    }
+
+    private static RDFLinkHTTPBuilder setupCreator(RDFLinkHTTPBuilder builder) {
+        return builder.creator((b)->fusekiMaker(b));
+    }
+
+    static RDFLinkFuseki fusekiMaker(RDFLinkHTTPBuilder builder) {
+        return new RDFLinkFuseki(builder);
+    }
+
+    protected RDFLinkFuseki(RDFLinkHTTPBuilder base) {
+        this(base.txnLifecycle, base.httpClient,
+            base.destination, base.queryURL, base.updateURL, base.gspURL,
+            base.outputQuads, base.outputTriples,
+            base.acceptDataset, base.acceptGraph,
+            base.acceptSparqlResults, base.acceptSelectResult, base.acceptAskResult,
+            base.parseCheckQueries, base.parseCheckUpdates);
+    }
+
+    protected RDFLinkFuseki(Transactional txnLifecycle, HttpClient httpClient, String destination,
+                            String queryURL, String updateURL, String gspURL, RDFFormat outputQuads, RDFFormat outputTriples,
+                            String acceptDataset, String acceptGraph,
+                            String acceptSparqlResults, String acceptSelectResult, String acceptAskResult,
+                            boolean parseCheckQueries, boolean parseCheckUpdates) {
+        super(txnLifecycle, httpClient,
+              destination, queryURL, updateURL, gspURL,
+              outputQuads, outputTriples,
+              acceptDataset, acceptGraph,
+              acceptSparqlResults, acceptSelectResult, acceptAskResult, parseCheckQueries, parseCheckUpdates);
+    }
+
+    // Fuseki specific operations.
+
+//    /**
+//     * Return a {@link Model} that is proxy for a remote model in a Fuseki server. This
+//     * support the model operations of accessing statements and changing the model.
+//     * <p>
+//     * This provide low level access to the remote data. The application will be working
+//     * with and manipulating the remote model directly which may involve a significant
+//     * overhead for every {@code Model} API operation.
+//     * <p>
+//     * <b><em>Warning</em>:</b> This is <b>not</b> performant for bulk changes.
+//     * <p>
+//     * Getting the model, using {@link #fetch()}, which copies the whole model into a local
+//     * {@code Model} object, maniupulating it and putting it back with {@link #put(Model)}
+//     * provides another way to work with remote data.
+//     *
+//     * @return Graph
+//     */
+//    public Graph getGraphProxy() { return null; }
+//    public Graph getGraphProxy(String graphName) { return null; }
+//
+//    public DatasetGraph getDatasetProxy() { return null; }
+//
+//    // Or remote RDFStorage?
+//    public Stream<Triple> findStream(Node s, Node p , Node o) { return null; }
+//    public Stream<Quad> findStream(Node g, Node s, Node p , Node o) { return null; }
+
+    // Send Patch
+}

Review comment:
       This commented code is still WIP, or for future work?

##########
File path: jena-arq/src/main/java/org/apache/jena/web/AuthSetup.java
##########
@@ -29,22 +29,27 @@
     public final String user;
     public final String password;
     public final String realm;
-    
+
     public AuthSetup(String host, Integer port, String user, String password, String realm) {
         this.host = any(host, AuthScope.ANY_HOST);
-        this.port = (port == null || port <= 0 ) ? AuthScope.ANY_PORT : port; 
+        this.port = (port == null || port <= 0 ) ? AuthScope.ANY_PORT : port;
         this.user = user;
         this.password = password;
         this.realm = any(host, AuthScope.ANY_REALM);
     }
-    
+
     public AuthScope authScope() {
         return new AuthScope(host, port, realm, AuthScope.ANY_SCHEME);
     }
-    
+
     private <X> X any(X value, X anyVal) {
         if ( value == null )
             return anyVal;
         return value;
     }
+
+    @Override
+    public String toString() {
+        return "AuthSetup [host=" + host + ", port=" + port + ", user=" + user + ", password=" + password + ", realm=" + realm + "]";

Review comment:
       Shouldn't we mask the password here??? Probably not safe to have it in the `toString` even if we are not using it IMO

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecutionCompat.java
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.sparql.exec;
+
+import java.util.Iterator;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.jena.atlas.json.JsonArray;
+import org.apache.jena.atlas.json.JsonObject;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.query.*;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.sparql.core.Quad;
+import org.apache.jena.sparql.engine.binding.Binding;
+import org.apache.jena.sparql.engine.binding.BindingLib;
+import org.apache.jena.sparql.util.Context;
+
+/**
+ * Query execution that delays making the QueryExecution until needed by exec* This
+ * means timeout and initialBinds can still be set.
+ *
+ * @see QueryExecution
+ */
+public class QueryExecutionCompat extends QueryExecutionAdapter {
+    private final QueryExecMod qExecBuilder;
+    private QueryExec qExecHere = null;
+    private final Dataset datasetHere;
+    private final Query queryHere;
+
+    public static QueryExecution compatibility(QueryExecMod qExec, Dataset dataset, Query query, String queryString) {
+        return new QueryExecutionCompat(qExec, dataset, query);
+    }
+
+    private QueryExecutionCompat(QueryExecMod qExecBuilder, Dataset dataset, Query query) {
+        super(null);
+        this.qExecBuilder = qExecBuilder;
+        this.datasetHere = dataset;
+        this.queryHere = query;
+    }
+
+    @Override
+    protected QueryExec get() { return qExecHere; }
+
+    private void execution() {
+        // Delay until used so setTimeout,setInitialBindings work.
+        // Also - rebuild allowed!
+        if ( qExecHere == null )
+            qExecHere = qExecBuilder.build();
+    }
+
+    @Override
+    public void setInitialBinding(Binding binding) {
+        if ( qExecBuilder instanceof QueryExecDatasetBuilder)
+            ((QueryExecDatasetBuilder)qExecBuilder).initialBinding(binding);
+        else
+            throw new UnsupportedOperationException("setInitialBinding");
+    }
+
+    @Override
+    public Dataset getDataset() {
+        return datasetHere;
+    }
+
+    @Override
+    public Context getContext() {
+        return qExecBuilder.getContext();
+    }
+
+    @Override
+    public Query getQuery() {
+        if ( queryHere != null )
+            return queryHere;
+        // Have to build (and hope! It may be a queryString with non-jena extensions).

Review comment:
       🙏 😄 

##########
File path: jena-arq/src/main/java/org/apache/jena/query/ModelStore.java
##########
@@ -0,0 +1,299 @@
+/*
+ * 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.query;
+
+import java.net.http.HttpClient;
+
+import org.apache.jena.graph.Graph;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.rdf.model.ModelFactory;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFFormat;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.exec.http.GSP;
+
+/**
+ * Client for the
+ * <a href="https://www.w3.org/TR/sparql11-http-rdf-update/"
+ * >SPARQL 1.1 Graph Store Protocol</a>
+ * working at the Model/Resource API level.
+ * <p>
+ * This is extended to include operations GET, POST and PUT on RDF Datasets.
+ * <p>
+ * Examples:
+ * <pre>
+ *   // Get the default graph.
+ *   Model model = ModelStore.service("http://example/dataset").defaultModel().GET();
+ * </pre>
+ * <pre>
+ *   // Get a named graph.
+ *   Model model = ModelStore.service("http://example/dataset").namedGraph("http://my/graph").GET();
+ * </pre>
+ * <pre>
+ *   // POST (add) to a named graph.
+ *   Model myData = ...;
+ *   ModelStore.request("http://example/dataset").namedGraph("http://my/graph").POST(myData);
+ * </pre>
+ *
+ * @see GSP
+ */
+public class ModelStore {
+
+    /** Create a request to the remote service (without Graph Store Protocol naming).
+     *  Call {@link #defaultModel()} or {@link #namedGraph(String)} to select the target graph.

Review comment:
       Extras spaces before "Call"

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/util/ContextAccumulator.java
##########
@@ -0,0 +1,157 @@
+/*
+ * 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.sparql.util;
+
+import java.util.function.Supplier;
+
+import org.apache.jena.query.ARQ;
+
+/**
+ * Context builder component.
+ * <p>
+ * Use in a buildee either inherited, or use as a component

Review comment:
       s/buildee/builder?

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetMem.java
##########
@@ -0,0 +1,134 @@
+/*
+ * 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.sparql.exec;
+
+import java.util.ArrayList ;
+import java.util.List ;
+
+import org.apache.jena.atlas.iterator.PeekIterator ;
+import org.apache.jena.sparql.core.Var;
+import org.apache.jena.sparql.engine.binding.Binding ;
+
+/**
+ *  A row set held in-memory which is rewindable and peekable
+ */
+
+public class RowSetMem implements RowSetRewindable
+{
+    protected final List<Binding> rows;
+    protected final List<Var> vars ;
+
+    private int rowNumber = 0 ;
+    private PeekIterator<Binding> iterator = null ;
+
+    public static RowSetRewindable create(RowSet rowSet) {
+        if ( rowSet instanceof RowSetMem )
+            return new RowSetMem((RowSetMem)rowSet);
+        else
+            return new RowSetMem(rowSet);
+    }
+
+    /** Create an in-memory result set from another one
+     *
+     * @param other     The other RowSetMem object
+     */
+    private RowSetMem(RowSetMem other) {
+        // Should be no need to isolate the rows list.
+        this(other, false);
+    }
+
+    /**
+     * Create an in-memory result set from another one
+     *
+     * @param other
+     *            The other ResultSetMem object
+     * @param takeCopy
+     *            Should we copy the rows?
+     */
+
+    private RowSetMem(RowSetMem other, boolean takeCopy) {
+        vars = other.vars;
+        if ( takeCopy )
+            rows = new ArrayList<>(other.rows);
+        else
+            // Share results (not the iterator).
+            rows = other.rows;
+        reset();
+    }
+
+    /**
+     * Create an in-memory result set from any RowSet object. If the
+     * ResultSet is an in-memory one already, then no copying is done - the
+     * necessary internal datastructures are shared. This operation destroys
+     * (uses up) a RowSet object that is not an in-memory one.
+     */
+
+    private RowSetMem(RowSet other) {
+        this.rows = new ArrayList<>();
+        other.forEachRemaining(rows::add);
+        this.vars = other.getResultVars();
+        reset();
+    }
+
+    /**
+     * Is there another possibility?
+     */
+    @Override
+    public boolean hasNext() { return iterator.hasNext() ; }
+
+    /**
+     * Moves onto the next result possibility.
+     */
+    @Override
+    public Binding next()  { rowNumber++ ; return iterator.next() ; }
+
+    /** Reset this result set back to the beginning */
+    public void rewind() {
+        reset();
+    }
+
+    @Override
+    public void reset() {
+        iterator = new PeekIterator<>(rows.iterator());
+        rowNumber = 0;
+    }
+
+    /** Return the "row" number for the current iterator item
+     */
+    @Override
+    public long getRowNumber() { return rowNumber ; }
+
+    /**
+     *  Return the number of rows
+     */
+    @Override
+    public long size() { return rows.size() ; }
+
+    /** Get the variable names for the projection
+     */
+    @Override
+    public List<Var> getResultVars() { return vars ; }
+
+    public Binding peek() {
+        return iterator.element();
+    }
+
+    @Override
+    public void close() {}
+}

Review comment:
       Missing newline

##########
File path: jena-arq/src/main/java/org/apache/jena/http/sys/RegistryRequestModifier.java
##########
@@ -0,0 +1,37 @@
+/*
+ * 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.http.sys;
+
+import java.util.function.Function;
+
+/**
+ * A service registry is a set of actions to take to modify an HTTP request before
+ * sending it to a specific endpoint.
+ *
+ * The key can be a prefix which must end in "/"

Review comment:
       Add a `<p>` before and the period?

##########
File path: jena-arq/src/main/java/org/apache/jena/update/UpdateExecutionAdpater.java
##########
@@ -0,0 +1,23 @@
+/*
+ * 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.update;
+
+public class UpdateExecutionAdpater {

Review comment:
       Empty? Also typo in class name, `UpdateExecutionAdpater ` Adpater

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkFuseki.java
##########
@@ -0,0 +1,134 @@
+/*
+ * 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.rdflink;
+
+import java.net.http.HttpClient;
+
+import org.apache.jena.rdflink.RDFLinkFuseki;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFFormat;
+import org.apache.jena.riot.resultset.ResultSetLang;
+import org.apache.jena.sparql.core.Transactional;
+
+/**
+ * Implementation of the {@link RDFLink} interface for connecting to an Apache Jena Fuseki.
+ * <p>
+ * This adds the ability to work with blank nodes across the network.

Review comment:
       👍 I think this is solving that JIRA issue where a user requested the ability to access blank nodes? Great job!

##########
File path: jena-cmds/src/main/java/arq/rsparql.java
##########
@@ -18,77 +18,64 @@
 
 package arq;
 
+import arq.cmdline.CmdARQ ;
+import arq.cmdline.ModQueryIn ;
+import arq.cmdline.ModRemote ;
+import arq.cmdline.ModResultsOut ;
 import org.apache.jena.cmd.CmdException;
 import org.apache.jena.query.Query ;
-import org.apache.jena.query.QueryExecution ;
-import org.apache.jena.query.QueryExecutionFactory ;
 import org.apache.jena.query.Syntax ;
-import org.apache.jena.sparql.engine.http.HttpQuery ;
 import org.apache.jena.sparql.engine.http.QueryExceptionHTTP ;
+import org.apache.jena.sparql.exec.http.QueryExecutionHTTP;
+import org.apache.jena.sparql.exec.http.QueryExecutionHTTPBuilder;
+import org.apache.jena.sparql.exec.http.QuerySendMode;
 import org.apache.jena.sparql.util.QueryExecUtils ;
 
-import arq.cmdline.CmdARQ ;
-import arq.cmdline.ModQueryIn ;
-import arq.cmdline.ModRemote ;
-import arq.cmdline.ModResultsOut ;
-
 public class rsparql extends CmdARQ
 {
     protected ModQueryIn    modQuery =      new ModQueryIn(Syntax.syntaxSPARQL_11) ;
     protected ModRemote     modRemote =     new ModRemote() ;
     protected ModResultsOut modResults =    new ModResultsOut() ;
 
-    public static void main (String... argv)
-    {
-        new rsparql(argv).mainRun() ;
+    public static void main(String...argv) {
+        new rsparql(argv).mainRun();
     }
 
-
-    public rsparql(String[] argv)
-    {
-        super(argv) ;
-        super.addModule(modRemote) ;
-        super.addModule(modQuery) ;
-        super.addModule(modResults) ;
+    public rsparql(String[] argv) {
+        super(argv);
+        super.addModule(modRemote);
+        super.addModule(modQuery);
+        super.addModule(modResults);
     }
-    
-    
+
     @Override
-    protected void processModulesAndArgs()
-    {
-        super.processModulesAndArgs() ;
+    protected void processModulesAndArgs() {
+        super.processModulesAndArgs();
         if ( modRemote.getServiceURL() == null )
-            throw new CmdException("No SPARQL endpoint specificied") ;
+            throw new CmdException("No SPARQL endpoint specificied");
     }
-    
+
     @Override
-    protected void exec()
-    {
-        Query query = modQuery.getQuery() ;
+    protected void exec() {
+        Query query = modQuery.getQuery();
 
         try {
-            String serviceURL = modRemote.getServiceURL() ;
-            QueryExecution qe = QueryExecutionFactory.sparqlService(serviceURL, query) ;
-            if ( modRemote.usePost() )
-                HttpQuery.urlLimit = 0 ;
+            String serviceURL = modRemote.getServiceURL();
+            QuerySendMode sendMode = modRemote.usePost() ? QuerySendMode.asPost : QuerySendMode.systemDefault;
 
-            QueryExecUtils.executeQuery(query, qe, modResults.getResultsFormat()) ;
-        } catch (QueryExceptionHTTP ex)
-        {
-            throw new CmdException("HTTP Exeception", ex) ;
-        }
-        catch (Exception ex)
-        {
-            System.out.flush() ;
-            ex.printStackTrace(System.err) ;
+            QueryExecutionHTTP qe = QueryExecutionHTTPBuilder.create().endpoint(serviceURL).query(query).sendMode(sendMode).build();
+
+            QueryExecUtils.executeQuery(query, qe, modResults.getResultsFormat());
+        } catch (QueryExceptionHTTP ex) {
+            throw new CmdException("HTTP Exeception", ex);

Review comment:
       s/Exeception/Exception

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSet.java
##########
@@ -0,0 +1,77 @@
+/*
+ * 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.sparql.exec;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.jena.query.ResultSet;
+import org.apache.jena.sparql.core.Var;
+import org.apache.jena.sparql.engine.QueryIterator;
+import org.apache.jena.sparql.engine.binding.Binding;
+
+public interface RowSet extends Iterator<Binding> {
+
+    @Override public boolean hasNext() ;
+
+    @Override public Binding next() ;
+
+    public List<Var> getResultVars() ;
+
+    /**
+     * Create a {@link RowSetRewindable} from the current position to the end.
+     * This consumes this RowSet - the iterator will have ended after a call to this method.
+     */
+    public default RowSetRewindable rewindable() {
+        return RowSetMem.create(this);
+    }
+
+    /**
+     * Return a {@code RowSet} that is not connected to the original source.
+     * This consumes this ResultSet and produces another one.
+     */
+    public default RowSet materialize() {
+        return rewindable();
+    }
+
+    /** Return the row number. The first row is row 1. */
+    public long getRowNumber();
+
+    public static RowSet adapt(ResultSet resultSet) {
+        if ( resultSet instanceof ResultSetAdapter )
+            return ((ResultSetAdapter)resultSet).get();
+        return new RowSetAdapter(resultSet);
+    }
+
+    /**
+     * Turn a {@link QueryIterator} into a RowSet.
+     * This operation does not materialize the QueryIterator.
+     */
+    public static RowSet create(QueryIterator qIter, List<Var> vars) {
+        return new RowSetStream(vars, qIter);
+    }
+
+    /**
+     * Normally a RowSet is process until complete which implicitly closes any

Review comment:
       s/process/processed

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLink.java
##########
@@ -0,0 +1,491 @@
+/*
+ * 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.rdflink;
+
+import java.util.function.Consumer;
+
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.query.QueryFactory;
+import org.apache.jena.rdfconnection.JenaConnectionException;
+import org.apache.jena.rdfconnection.RDFConnection;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.Quad;
+import org.apache.jena.sparql.core.Transactional;
+import org.apache.jena.sparql.engine.binding.Binding;
+import org.apache.jena.sparql.exec.QueryExec;
+import org.apache.jena.sparql.exec.QueryExecBuilder;
+import org.apache.jena.sparql.exec.RowSet;
+import org.apache.jena.system.Txn;
+import org.apache.jena.update.Update;
+import org.apache.jena.update.UpdateFactory;
+import org.apache.jena.update.UpdateRequest;
+
+/**
+ * Interface for SPARQL operations on a datasets, whether local or remote.
+ * Operations can performed via this interface or via the various
+ * interfaces for a subset of the operations.
+ *
+ * <ul>
+ * <li>query ({@link LinkSparqlQuery})
+ * <li>update ({@link LinkSparqlUpdate})
+ * <li>graph store protocol ({@link LinkDatasetGraph} and read-only {@link LinkDatasetGraphAccess}).
+ * </ul>
+ *
+ * For remote operations, the
+ * <a href="http://www.w3.org/TR/sparql11-protocol/">SPARQL Protocol</a> is used
+ * for query and updates and
+ * <a href="http://www.w3.org/TR/sparql11-http-rdf-update/">SPARQL Graph Store
+ * Protocol</a> for the graph operations and in addition, there are analogous
+ * operations on datasets (fetch, load, put; but not delete).
+ *
+ * {@code RDFLink} provides transaction boundaries. If not in a
+ * transaction, an implicit transactional wrapper is applied ("autocommit").
+ *
+ * Remote SPARQL operations are atomic but without additional capabilities from
+ * the remote server, multiple operations are not combined into a single
+ * transaction.
+ *
+ * Not all implementations may implement all operations.
+ * See the implementation notes for details.
+ *
+ * @see RDFConnection
+ * @see RDFLinkFactory
+ * @see RDFLinkDataset
+ * @see RDFLinkHTTP
+ * @see LinkSparqlQuery
+ * @see LinkSparqlUpdate
+ * @see LinkDatasetGraph
+ */
+
+public interface RDFLink extends
+        LinkSparqlQuery, LinkSparqlUpdate, LinkDatasetGraph,
+        Transactional, AutoCloseable {
+    // Default implementations could be pushed up but then they can't be mentioned here
+    // and the javadoc for RDFLink is not in one place.
+    // Inheriting interfaces and re-mentioning gets the javadoc in one place.
+
+    // ---- SparqlQueryConnection
+    // Where the argument is a query string, this code avoids simply parsing it and calling
+    // the Query object form. This allows RDFLinkRemote to pass the query string
+    // untouched to the connection depending in the internal setting to parse/check
+    // queries.
+    // Java9 introduces private methods for interfaces which could clear the duplication up by passing in a Creator<QueryExecution>.
+    // (Alternatively, add RDFLinkBase with protected query(String, Query)
+    // See RDFLinkRemote.
+
+    /**
+     * Execute a SELECT query and process the RowSet with the handler code.
+     * @param queryString
+     * @param rowSetAction
+     */
+    @Override
+    public default void queryRowSet(String queryString, Consumer<RowSet> rowSetAction) {
+        Txn.executeRead(this, ()->{
+            try ( QueryExec qExec = query(queryString) ) {
+                RowSet rs = qExec.select();
+                rowSetAction.accept(rs);
+            }
+        } );
+    }
+
+    /**
+     * Execute a SELECT query and process the RowSet with the handler code.
+     * @param query
+     * @param rowSetAction
+     */
+    @Override
+    public default void queryRowSet(Query query, Consumer<RowSet> rowSetAction) {
+        if ( ! query.isSelectType() )
+            throw new JenaConnectionException("Query is not a SELECT query");
+        Txn.executeRead(this, ()->{
+            try ( QueryExec qExec = query(query) ) {
+                RowSet rs = qExec.select();
+                rowSetAction.accept(rs);
+            }
+        } );
+    }
+
+    private static void forEachRow(RowSet rowSet, Consumer<Binding> rowAction) {
+        rowSet.forEachRemaining(rowAction);
+    }
+
+    /**
+     * Execute a SELECT query and process the rows of the results with the handler code.
+     * @param queryString
+     * @param rowAction
+     */
+    @Override
+    public default void querySelect(String queryString, Consumer<Binding> rowAction) {
+        Txn.executeRead(this, ()->{
+            try ( QueryExec qExec = query(queryString) ) {
+                forEachRow(qExec.select(), rowAction);
+            }
+        } );
+    }
+
+    /**
+     * Execute a SELECT query and process the rows of the results with the handler code.
+     * @param query
+     * @param rowAction
+     */
+    @Override
+    public default void querySelect(Query query, Consumer<Binding> rowAction) {
+        if ( ! query.isSelectType() )
+            throw new JenaConnectionException("Query is not a SELECT query");
+        Txn.executeRead(this, ()->{
+            try ( QueryExec qExec = query(query) ) {
+                forEachRow(qExec.select(), rowAction);
+            }
+        } );
+    }
+
+    /** Execute a CONSTRUCT query and return as a Graph */
+    @Override
+    public default Graph queryConstruct(String queryString) {
+        return
+            Txn.calculateRead(this, ()->{
+                try ( QueryExec qExec = query(queryString) ) {
+                    return qExec.construct();
+                }
+            } );
+    }
+
+    /** Execute a CONSTRUCT query and return as a DatasetGraph */
+    //@Override
+    public default DatasetGraph queryConstructDataset(Query query) {
+        return
+            Txn.calculateRead(this, ()->{
+                try ( QueryExec qExec = query(query) ) {
+                    return qExec.constructDataset();
+                }
+            } );
+    }
+
+    /** Execute a CONSTRUCT query and return as a Graph */
+    //@Override
+    public default DatasetGraph queryConstructDataset(String queryString) {
+        return
+            Txn.calculateRead(this, ()->{
+                try ( QueryExec qExec = query(queryString) ) {
+                    return qExec.constructDataset();
+                }
+            } );
+    }
+
+    /** Execute a CONSTRUCT query and return as a Graph */
+    @Override
+    public default Graph queryConstruct(Query query) {
+        return
+            Txn.calculateRead(this, ()->{
+                try ( QueryExec qExec = query(query) ) {
+                    return qExec.construct();
+                }
+            } );
+    }
+
+
+
+    /** Execute a DESCRIBE query and return as a Graph */
+    @Override
+    public default Graph queryDescribe(String queryString) {
+        return
+            Txn.calculateRead(this, ()->{
+                try ( QueryExec qExec = query(queryString) ) {
+                    return qExec.describe();
+                }
+            } );
+    }
+
+    /** Execute a DESCRIBE query and return as a Graph */
+    @Override
+    public default Graph queryDescribe(Query query) {
+        return
+            Txn.calculateRead(this, ()->{
+                try ( QueryExec qExec = query(query) ) {
+                    return qExec.describe();
+                }
+            } );
+    }
+
+    /** Execute a ASK query and return a boolean */
+    @Override
+    public default boolean queryAsk(String queryString) {
+        return
+            Txn.calculateRead(this, ()->{
+                try ( QueryExec qExec = query(queryString) ) {
+                    return qExec.ask();
+                }
+            } );
+    }
+
+    /** Execute a ASK query and return a boolean */
+    @Override
+    public default boolean queryAsk(Query query) {
+        return
+            Txn.calculateRead(this, ()->{
+                try ( QueryExec qExec = query(query) ) {
+                    return qExec.ask();
+                }
+            } );
+    }
+
+    /** Setup a SPARQL query execution.
+     *
+     *  See also {@link #querySelect(Query, Consumer)}, {@link #queryConstruct(Query)},
+     *  {@link #queryDescribe(Query)}, {@link #queryAsk(Query)}
+     *  for ways to execute queries for of a specific form.
+     *
+     * @param query
+     * @return QueryExecution
+     */
+    @Override
+    public QueryExec query(Query query);
+
+    /** Setup a SPARQL query execution.
+     * This is a low-level operation.
+     * Handling the {@link QueryExecution} should be done with try-resource.
+     * Some {@link QueryExecution QueryExecutions}, such as ones connecting to a remote server,
+     * need to be properly closed to release system resources.
+     *
+     *  See also {@link #querySelect(String, Consumer)}, {@link #queryConstruct(String)},
+     *  {@link #queryDescribe(String)}, {@link #queryAsk(String)}
+     *  for ways to execute queries of a specific form.

Review comment:
       Extra spaces? ☝️ 

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdflink/LibRDFLink.java
##########
@@ -0,0 +1,109 @@
+/*
+ * 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.rdflink;
+
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.http.HttpLib;
+import org.apache.jena.query.Dataset;
+import org.apache.jena.query.DatasetFactory;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.rdf.model.ModelFactory;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.Quad;
+
+/** package-wide utilities etc */
+class LibRDFLink {
+    private static String dftName =  "default" ;
+
+    /*package*/ static boolean isDefault(Node name) {
+        return name == null || Quad.isDefaultGraph(name);
+    }
+
+    private static String queryStringForGraph(String ch, Node graphName) {
+        return
+            ch +
+                (LibRDFLink.isDefault(graphName)
+                ? "default"
+                : "graph="+encode(graphName));
+    }
+
+    private static String encode(Node node) {
+        return HttpLib.urlEncodeQueryString(node.getURI());
+    }
+
+    /*package*/ static String urlForGraph(String graphStoreProtocolService, Node graphName) {
+        // If query string
+        String ch = "?";
+        if ( graphStoreProtocolService.contains("?") )
+            // Already has a query string, append with "&"
+            ch = "&";
+        return graphStoreProtocolService + queryStringForGraph(ch, graphName);
+    }
+
+    /*package*/ static Model graph2model(Graph graph) {
+        return ModelFactory.createModelForGraph(graph);
+    }
+
+    /*package*/ static Graph model2graph(Model model) {
+        return model.getGraph();
+    }
+
+    /*package*/ static Dataset asDataset(DatasetGraph dsg) {
+        return DatasetFactory.wrap(dsg);
+    }
+
+    /*package*/ static DatasetGraph asDatasetGraph(Dataset dataset) {
+        return dataset.asDatasetGraph();
+    }
+
+    /*package*/ static Node name(String graphName) {
+        if ( graphName == null || graphName.equals("default") )
+            return Quad.defaultGraphIRI;
+        return NodeFactory.createURI(graphName);
+    }
+
+    /*package*/ static String formServiceURL(String destination, String srvEndpoint) {
+        if ( srvEndpoint == null )
+            return null;
+        if ( srvEndpoint == RDFLinkHTTPBuilder.SameAsDestination )
+            return destination;
+        if ( destination == null )
+            return srvEndpoint;
+
+        // If the srvEndpoint looks like an absolute URL, use as given.
+        if ( srvEndpoint.startsWith("http:/") || srvEndpoint.startsWith("https:/") )

Review comment:
       I assume the URL is validated elsewhere, so `http:/localhost:3030/....` won't create any issues here (i.e. user forgot and used just 1 slash). It will probably fail elsewhere first or it's harmless I think?

##########
File path: jena-arq/src/main/java/org/apache/jena/http/sys/AbstractRegistryWithPrefix.java
##########
@@ -0,0 +1,90 @@
+/*
+ * 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.http.sys;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import org.apache.jena.atlas.lib.Trie;
+
+/**
+ * Abstract base class for registries with exact and prefix lookup..
+ * <p>
+ * The lookup ({@link #find}) is by exact match then by longest prefix. e.g. a registration of
+ * "http://someHost/" or "http://someHost/dataset" will apply to
+ * "http://someHost/dataset/sparql" and "http://someHost/dataset/update" but not to
+ *  "https://someHost/..." which uses "https".
+ */
+public abstract class AbstractRegistryWithPrefix<X, T> {
+
+    private final Map<String, T> exactMap = new ConcurrentHashMap<>();
+    private final Trie<T> trie = new Trie<>();
+    private final Function<X, String> generateKey;
+
+    protected AbstractRegistryWithPrefix(Function<X, String> genKey) {
+        this.generateKey = genKey;
+    }
+
+    public void add(X service, T value) {
+        String key = generateKey.apply(service);
+        exactMap.put(key, value);
+    }
+
+    /** Add a prefix. The prefix must end in "/" */
+    public void addPrefix(X service, T value) {
+        String key = generateKey.apply(service);
+        if ( ! key.endsWith("/") )
+            throw new IllegalArgumentException("Prefix must end in \"/\"");
+
+        //if ( key.endsWith("/") )
+            trie.add(key, value);

Review comment:
       Indentation intentionally left with the commented `if`, or development left-over?

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLink.java
##########
@@ -0,0 +1,491 @@
+/*
+ * 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.rdflink;
+
+import java.util.function.Consumer;
+
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.query.QueryFactory;
+import org.apache.jena.rdfconnection.JenaConnectionException;
+import org.apache.jena.rdfconnection.RDFConnection;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.Quad;
+import org.apache.jena.sparql.core.Transactional;
+import org.apache.jena.sparql.engine.binding.Binding;
+import org.apache.jena.sparql.exec.QueryExec;
+import org.apache.jena.sparql.exec.QueryExecBuilder;
+import org.apache.jena.sparql.exec.RowSet;
+import org.apache.jena.system.Txn;
+import org.apache.jena.update.Update;
+import org.apache.jena.update.UpdateFactory;
+import org.apache.jena.update.UpdateRequest;
+
+/**
+ * Interface for SPARQL operations on a datasets, whether local or remote.

Review comment:
       on a dataset or on datasets?

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
##########
@@ -0,0 +1,151 @@
+/*
+ * 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.sparql.exec;
+
+import java.io.OutputStream ;
+import java.util.Iterator ;
+
+import org.apache.jena.query.QuerySolution;
+import org.apache.jena.query.ResultSet;
+import org.apache.jena.rdf.model.RDFNode ;
+import org.apache.jena.riot.ResultSetMgr ;
+import org.apache.jena.riot.resultset.ResultSetLang;
+import org.apache.jena.riot.resultset.rw.ResultsWriter;
+import org.apache.jena.riot.system.PrefixMap;
+import org.apache.jena.riot.system.Prefixes;
+import org.apache.jena.shared.PrefixMapping;
+import org.apache.jena.sparql.ARQConstants;
+import org.apache.jena.sparql.core.Prologue ;
+
+/** RowSetFormatter - Convenience ways to call the various output formatters.
+ *  in various formats.
+ *  @see ResultSetMgr
+ */
+
+public class RowSetOps {
+
+    private RowSetOps() {}
+
+    /**
+     * This operation faithfully walks the rowSet but does nothing with the rows.
+     */
+    public static void consume(RowSet rowSet)
+    { count(rowSet); }
+
+    /**
+     * Count the rows in the RowSet (from the current point of RowSet).
+     * This operation consumes the RowSet.
+     */
+    public static long count(RowSet rowSet)
+    { return rowSet.rewindable().size(); }
+
+    /**
+     * Output a result set in a text format.  The result set is consumed.
+     * Use @see{ResultSetFactory.makeRewindable(ResultSet)} for a rewindable one.
+     * <p>
+     *  This caches the entire results in memory in order to determine the appropriate
+     *  column widths and therefore may exhaust memory for large results
+     *  </p>
+     * @param rowSet   result set
+     */
+    public static void out(RowSet rowSet)
+    { out(System.out, rowSet) ; }
+
+    /**
+     * Output a result set in a text format.
+     * <p>
+     *  This caches the entire results in memory in order to determine the appropriate
+     *  column widths and therefore may exhaust memory for large results
+     *  </p>

Review comment:
       Extra spaces ☝️ 

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdfconnection/RDFConnectionRemote.java
##########
@@ -18,677 +18,17 @@
 
 package org.apache.jena.rdfconnection;
 
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.util.Objects;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-import org.apache.http.HttpEntity;
-import org.apache.http.client.HttpClient;
-import org.apache.http.entity.ByteArrayEntity;
-import org.apache.http.entity.ContentType;
-import org.apache.http.entity.EntityTemplate;
-import org.apache.http.entity.FileEntity;
-import org.apache.http.protocol.HttpContext;
-import org.apache.jena.atlas.io.IO;
-import org.apache.jena.atlas.lib.InternalErrorException;
-import org.apache.jena.atlas.web.HttpException;
-import org.apache.jena.atlas.web.TypedInputStream;
-import org.apache.jena.graph.Graph;
-import org.apache.jena.query.*;
-import org.apache.jena.rdf.model.Model;
-import org.apache.jena.rdf.model.ModelFactory;
-import org.apache.jena.riot.*;
-import org.apache.jena.riot.web.HttpCaptureResponse;
-import org.apache.jena.riot.web.HttpOp;
-import org.apache.jena.riot.web.HttpOp.CaptureInput;
-import org.apache.jena.riot.web.HttpResponseLib;
-import org.apache.jena.sparql.ARQException;
-import org.apache.jena.sparql.core.DatasetGraph;
-import org.apache.jena.sparql.core.Transactional;
-import org.apache.jena.sparql.core.TransactionalLock;
-import org.apache.jena.sparql.engine.http.QueryEngineHTTP;
-import org.apache.jena.system.Txn;
-import org.apache.jena.update.UpdateFactory;
-import org.apache.jena.update.UpdateRequest;
-import org.apache.jena.web.HttpSC;
-
-/**
- * Implementation of the {@link RDFConnection} interface using remote SPARQL operations.
- */
-public class RDFConnectionRemote implements RDFConnection {
-    // Adds a Builder to help with HTTP details.
-
-    private static final String fusekiDftSrvQuery   = "sparql";
-    private static final String fusekiDftSrvUpdate  = "update";
-    private static final String fusekiDftSrvGSP     = "data";
-
-    private boolean isOpen = true;
-    protected final String destination;
-    protected final String svcQuery;
-    protected final String svcUpdate;
-    protected final String svcGraphStore;
-
-    protected final Transactional txnLifecycle;
-    protected final HttpClient httpClient;
-    protected final HttpContext httpContext;
-
-    // On-the-wire settings.
-    protected final RDFFormat outputQuads;
-    protected final RDFFormat outputTriples;
-    protected final String acceptGraph;
-    protected final String acceptDataset;
-    protected final String acceptSelectResult;
-    protected final String acceptAskResult;
-    // All purpose SPARQL results header used if above specific cases do not apply.
-    protected final String acceptSparqlResults;
-
-    // Whether to check SPARQL queries given as strings by parsing them.
-    protected final boolean parseCheckQueries;
-    // Whether to check SPARQL updates given as strings by parsing them.
-    protected final boolean parseCheckUpdates;
-
-    /** Create a {@link RDFConnectionRemoteBuilder}. */
-    public static RDFConnectionRemoteBuilder create() {
-        return new RDFConnectionRemoteBuilder();
-    }
-
+public interface RDFConnectionRemote extends RDFConnection {
     /**
-     * Create a {@link RDFConnectionRemoteBuilder} initialized with the
-     * settings of another {@code RDFConnectionRemote}.
+     * Create a {@link RDFConnectionRemoteBuilder}.
      */
-    public static RDFConnectionRemoteBuilder create(RDFConnectionRemote base) {
-        return new RDFConnectionRemoteBuilder(base);
-    }
-
-    // Used by the builder.
-    protected RDFConnectionRemote(Transactional txnLifecycle, HttpClient httpClient, HttpContext httpContext, String destination,
-                                   String queryURL, String updateURL, String gspURL, RDFFormat outputQuads, RDFFormat outputTriples,
-                                   String acceptDataset, String acceptGraph,
-                                   String acceptSparqlResults,
-                                   String acceptSelectResult, String acceptAskResult,
-                                   boolean parseCheckQueries, boolean parseCheckUpdates) {
-        this.httpClient = httpClient;
-        this.httpContext = httpContext;
-        this.destination = destination;
-        this.svcQuery = queryURL;
-        this.svcUpdate = updateURL;
-        this.svcGraphStore = gspURL;
-        if ( txnLifecycle == null )
-            txnLifecycle  = TransactionalLock.createMRPlusSW();
-        this.txnLifecycle = txnLifecycle;
-        this.outputQuads = outputQuads;
-        this.outputTriples = outputTriples;
-        this.acceptDataset = acceptDataset;
-        this.acceptGraph = acceptGraph;
-        this.acceptSparqlResults = acceptSparqlResults;
-        this.acceptSelectResult = acceptSelectResult;
-        this.acceptAskResult = acceptAskResult;
-        this.parseCheckQueries = parseCheckQueries;
-        this.parseCheckUpdates = parseCheckUpdates;
-    }
+    public static RDFConnectionRemoteBuilder create() { return newBuilder(); }
 
-    /** Return the {@link HttpClient} in-use. */
-    public HttpClient getHttpClient() {
-        return httpClient;
-    }
-
-    /** Return the {@link HttpContext} in-use. */
-    public HttpContext getHttpContext() {
-        return httpContext;
-    }
-
-    /** Return the destination URL for the connection. */
-    public String getDestination() {
-        return destination;
-    }
-
-    // For custom content negotiation.
-
-    // This class overrides each of these to pass down the query type as well.
-    // Then we can derive the accept header if customized without needing to parse
-    // the query. This allows an arbitrary string for a query and allows the remote
-    // server to have custom syntax extensions or interpretations of comments.
-
-    /**
-     * Execute a SELECT query and process the ResultSet with the handler code.
-     * @param queryString
-     * @param resultSetAction
-     */
-    @Override
-    public void queryResultSet(String queryString, Consumer<ResultSet> resultSetAction) {
-        Txn.executeRead(this, ()->{
-            try ( QueryExecution qExec = query(queryString, QueryType.SELECT) ) {
-                ResultSet rs = qExec.execSelect();
-                resultSetAction.accept(rs);
-            }
-        } );
-    }
-
-    /**
-     * Execute a SELECT query and process the rows of the results with the handler code.
-     * @param queryString
-     * @param rowAction
-     */
-    @Override
-    public void querySelect(String queryString, Consumer<QuerySolution> rowAction) {
-        Txn.executeRead(this, ()->{
-            try ( QueryExecution qExec = query(queryString, QueryType.SELECT) ) {
-                qExec.execSelect().forEachRemaining(rowAction);
-            }
-        } );
-    }
-
-    /** Execute a CONSTRUCT query and return as a Model */
-    @Override
-    public Model queryConstruct(String queryString) {
-        return
-            Txn.calculateRead(this, ()->{
-                try ( QueryExecution qExec = query(queryString, QueryType.CONSTRUCT) ) {
-                    return qExec.execConstruct();
-                }
-            } );
-    }
-
-    /** Execute a DESCRIBE query and return as a Model */
-    @Override
-    public Model queryDescribe(String queryString) {
-        return
-            Txn.calculateRead(this, ()->{
-                try ( QueryExecution qExec = query(queryString, QueryType.DESCRIBE) ) {
-                    return qExec.execDescribe();
-                }
-            } );
-    }
-
-    /** Execute a ASK query and return a boolean */
-    @Override
-    public boolean queryAsk(String queryString) {
-        return
-            Txn.calculateRead(this, ()->{
-                try ( QueryExecution qExec = query(queryString, QueryType.ASK) ) {
-                    return qExec.execAsk();
-                }
-            } );
-    }
-
-    /**
-     * Operation that passed down the query type so the accept header can be set without parsing the query string.
-     * @param queryString
-     * @param queryType
-     * @return QueryExecution
-     */
-    protected QueryExecution query(String queryString, QueryType queryType) {
-        Objects.requireNonNull(queryString);
-        return queryExec(null, queryString, queryType);
-    }
-
-    @Override
-    public QueryExecution query(String queryString) {
-        Objects.requireNonNull(queryString);
-        return queryExec(null, queryString, null);
-    }
-
-    @Override
-    public QueryExecution query(Query query) {
-        Objects.requireNonNull(query);
-        return queryExec(query, null, null);
-    }
-
-    private QueryExecution queryExec(Query query, String queryString, QueryType queryType) {
-        checkQuery();
-        if ( query == null && queryString == null )
-            throw new InternalErrorException("Both query and query string are null");
-        if ( query == null ) {
-            if ( parseCheckQueries )
-                QueryFactory.create(queryString);
-        }
-
-        // Use the query string as provided if possible, otherwise serialize the query.
-        String queryStringToSend = ( queryString != null ) ? queryString : query.toString();
-        return exec(()-> createQueryExecution(query, queryStringToSend, queryType));
-    }
-
-    // Create the QueryExecution
-    private QueryExecution createQueryExecution(Query query, String queryStringToSend, QueryType queryType) {
-        QueryExecution qExec = new QueryEngineHTTP(svcQuery, queryStringToSend, httpClient, httpContext);
-        QueryEngineHTTP qEngine = (QueryEngineHTTP)qExec;
-        QueryType qt = queryType;
-        if ( query != null && qt == null )
-            qt = query.queryType();
-        if ( qt == null )
-            qt = QueryType.UNKNOWN;
-        // Set the accept header - use the most specific method.
-        switch(qt) {
-            case SELECT :
-                if ( acceptSelectResult != null )
-                    qEngine.setAcceptHeader(acceptSelectResult);
-                break;
-            case ASK :
-                if ( acceptAskResult != null )
-                    qEngine.setAcceptHeader(acceptAskResult);
-                break;
-            case DESCRIBE :
-            case CONSTRUCT :
-                if ( acceptGraph != null )
-                    qEngine.setAcceptHeader(acceptGraph);
-                break;
-            case UNKNOWN:
-                // All-purpose content type.
-                if ( acceptSparqlResults != null )
-                    qEngine.setAcceptHeader(acceptSparqlResults);
-                else
-                    // No idea! Set an "anything" and hope.
-                    // (Reasonable chance this is going to end up as HTML though.)
-                    qEngine.setAcceptHeader("*/*");
-            default :
-                break;
-        }
-        // Make sure it was set somehow.
-        if ( qEngine.getAcceptHeader() == null )
-            throw new JenaConnectionException("No Accept header");
-        return qExec ;
-    }
-
-    private void acc(StringBuilder sBuff, String acceptString) {
-        if ( acceptString == null )
-            return;
-        if ( sBuff.length() != 0 )
-            sBuff.append(", ");
-        sBuff.append(acceptString);
-    }
-
-    @Override
-    public void update(String updateString) {
-        Objects.requireNonNull(updateString);
-        updateExec(null, updateString);
-    }
-
-    @Override
-    public void update(UpdateRequest update) {
-        Objects.requireNonNull(update);
-        updateExec(update, null);
-    }
-
-    private void updateExec(UpdateRequest update, String updateString ) {
-        checkUpdate();
-        if ( update == null && updateString == null )
-            throw new InternalErrorException("Both update request and update string are null");
-        if ( update == null ) {
-            if ( parseCheckUpdates )
-                UpdateFactory.create(updateString);
-        }
-        // Use the update string as provided if possible, otherwise serialize the update.
-        String updateStringToSend = ( updateString != null ) ? updateString  : update.toString();
-        exec(()->HttpOp.execHttpPost(svcUpdate, WebContent.contentTypeSPARQLUpdate, updateStringToSend, this.httpClient, this.httpContext));
-    }
-
-    @Override
-    public Model fetch(String graphName) {
-        checkGSP();
-        String url = LibRDFConn.urlForGraph(svcGraphStore, graphName);
-        Graph graph = fetch$(url);
-        return ModelFactory.createModelForGraph(graph);
-    }
-
-    @Override
-    public Model fetch() {
-        checkGSP();
-        return fetch(null);
-    }
-
-    private Graph fetch$(String url) {
-        HttpCaptureResponse<Graph> graph = HttpResponseLib.graphHandler();
-        exec(()->HttpOp.execHttpGet(url, acceptGraph, graph, this.httpClient, this.httpContext));
-        return graph.get();
-    }
-
-    @Override
-    public void load(String graph, String file) {
-        checkGSP();
-        upload(graph, file, false);
-    }
-
-    @Override
-    public void load(String file) {
-        checkGSP();
-        upload(null, file, false);
-    }
-
-    @Override
-    public void load(Model model) {
-        doPutPost(model, null, false);
-    }
-
-    @Override
-    public void load(String graphName, Model model) {
-        doPutPost(model, graphName, false);
-    }
-
-    @Override
-    public void put(String graph, String file) {
-        checkGSP();
-        upload(graph, file, true);
-    }
-
-    @Override
-    public void put(String file) {
-        checkGSP();
-        upload(null, file, true);
-    }
-
-    @Override
-    public void put(String graphName, Model model) {
-        checkGSP();
-        doPutPost(model, graphName, true);
-    }
-
-    @Override
-    public void put(Model model) {
-        checkGSP();
-        doPutPost(model, null, true);
-    }
-
-    /** Send a file to named graph (or "default" or null for the default graph).
-     * <p>
-     * The Content-Type is inferred from the file extension.
-     * <p>
-     * "Replace" means overwrite existing data, otherwise the date is added to the target.
-     */
-    protected void upload(String graph, String file, boolean replace) {
-        // if triples
-        Lang lang = RDFLanguages.filenameToLang(file);
-        if ( RDFLanguages.isQuads(lang) )
-            throw new ARQException("Can't load quads into a graph");
-        if ( ! RDFLanguages.isTriples(lang) )
-            throw new ARQException("Not an RDF format: "+file+" (lang="+lang+")");
-        String url = LibRDFConn.urlForGraph(svcGraphStore, graph);
-        doPutPost(url, file, lang, replace);
-    }
-
-    /** Send a file to named graph (or "default" or null for the default graph).
-     * <p>
-     * The Content-Type is taken from the given {@code Lang}.
-     * <p>
-     * "Replace" means overwrite existing data, otherwise the date is added to the target.
-     */
-    protected void doPutPost(String url, String file, Lang lang, boolean replace) {
-        File f = new File(file);
-        long length = f.length();
-
-        // Leave RDF/XML to the XML parse, else it's UTF-8.
-        String charset = (lang.equals(Lang.RDFXML) ? null : WebContent.charsetUTF8);
-        // HttpClient Content type.
-        ContentType ct = ContentType.create(lang.getContentType().getContentTypeStr(), charset);
-
-        exec(()->{
-            HttpEntity entity = fileToHttpEntity(file, lang);
-            if ( replace )
-                HttpOp.execHttpPut(url, entity, httpClient, httpContext);
-            else
-                HttpOp.execHttpPost(url, entity, httpClient, httpContext);
-        });
-
-        // This is non-repeatable so does not work with authentication.
-//        InputStream source = IO.openFile(file);
-//        exec(()->{
-//            HttpOp.execHttpPost(url, null);
-//
-//            if ( replace )
-//                HttpOp.execHttpPut(url, lang.getContentType().getContentType(), source, length, httpClient, this.httpContext);
-//            else
-//                HttpOp.execHttpPost(url, lang.getContentType().getContentType(), source, length, null, null, httpClient, this.httpContext);
-//        });
-    }
-
-    /** Send a model to named graph (or "default" or null for the default graph).
-     * <p>
-     * The Content-Type is taken from the given {@code Lang}.
-     * <p>
-     * "Replace" means overwrite existing data, otherwise the date is added to the target.
-     */
-    protected void doPutPost(Model model, String name, boolean replace) {
-        String url = LibRDFConn.urlForGraph(svcGraphStore, name);
-        exec(()->{
-            Graph graph = model.getGraph();
-            if ( replace )
-                HttpOp.execHttpPut(url, graphToHttpEntity(graph), httpClient, httpContext);
-            else
-                HttpOp.execHttpPost(url, graphToHttpEntity(graph), null, null, httpClient, httpContext);
-        });
-    }
-
-    @Override
-    public void delete(String graph) {
-        checkGSP();
-        String url = LibRDFConn.urlForGraph(svcGraphStore, graph);
-        exec(()->HttpOp.execHttpDelete(url));
-    }
-
-    @Override
-    public void delete() {
-        checkGSP();
-        delete(null);
-    }
-
-    @Override
-    public Dataset fetchDataset() {
-        if ( destination == null )
-            throw new ARQException("Dataset operations not available - no dataset URL provided");
-        Dataset ds = DatasetFactory.createTxnMem();
-        Txn.executeWrite(ds, ()->{
-            HttpCaptureResponse<TypedInputStream> handler = new CaptureInput();
-            exec(()->HttpOp.execHttpGet(destination, acceptDataset, handler, this.httpClient, this.httpContext));
-            TypedInputStream s = handler.get();
-            Lang lang = RDFLanguages.contentTypeToLang(s.getContentType());
-            RDFDataMgr.read(ds, s, lang);
-        });
-        return ds;
-    }
-
-    @Override
-    public void loadDataset(String file) {
-        if ( destination == null )
-            throw new ARQException("Dataset operations not available - no dataset URL provided");
-        doPutPostDataset(file, false);
-    }
-
-    @Override
-    public void loadDataset(Dataset dataset) {
-        if ( destination == null )
-            throw new ARQException("Dataset operations not available - no dataset URL provided");
-        doPutPostDataset(dataset, false);
-    }
-
-    @Override
-    public void putDataset(String file) {
-        if ( destination == null )
-            throw new ARQException("Dataset operations not available - no dataset URL provided");
-        doPutPostDataset(file, true);
-    }
-
-    @Override
-    public void putDataset(Dataset dataset) {
-        if ( destination == null )
-            throw new ARQException("Dataset operations not available - no dataset URL provided");
-        doPutPostDataset(dataset, true);
-    }
-
-    /** Do a PUT or POST to a dataset, sending the contents of the file.
-     * <p>
-     * The Content-Type is inferred from the file extension.
-     * <p>
-     * "Replace" implies PUT, otherwise a POST is used.
-     */
-    protected void doPutPostDataset(String file, boolean replace) {
-        Lang lang = RDFLanguages.filenameToLang(file);
-        File f = new File(file);
-        long length = f.length();
-        exec(()->{
-            HttpEntity entity = fileToHttpEntity(file, lang);
-            if ( replace )
-                HttpOp.execHttpPut(destination, entity, httpClient, httpContext);
-            else
-                HttpOp.execHttpPost(destination, entity, httpClient, httpContext);
-        });
-    }
-
-    /** Do a PUT or POST to a dataset, sending the contents of a dataset.
-     * The Content-Type is {@code application/n-quads}.
-     * <p>
-     * "Replace" implies PUT, otherwise a POST is used.
-     */
-    protected void doPutPostDataset(Dataset dataset, boolean replace) {
-        exec(()->{
-            DatasetGraph dsg = dataset.asDatasetGraph();
-            if ( replace )
-                HttpOp.execHttpPut(destination, datasetToHttpEntity(dsg), httpClient, null);
-            else
-                HttpOp.execHttpPost(destination, datasetToHttpEntity(dsg), httpClient, null);
-        });
-    }
-
-    protected void checkQuery() {
-        checkOpen();
-        if ( svcQuery == null )
-            throw new ARQException("No query service defined for this RDFConnection");
-    }
-
-    protected void checkUpdate() {
-        checkOpen();
-        if ( svcUpdate == null )
-            throw new ARQException("No update service defined for this RDFConnection");
-    }
-
-    protected void checkGSP() {
-        checkOpen();
-        if ( svcGraphStore == null )
-            throw new ARQException("No SPARQL Graph Store service defined for this RDFConnection");
-    }
-
-    protected void checkDataset() {
-        checkOpen();
-        if ( destination == null )
-            throw new ARQException("Dataset operations not available - no dataset URL provided");
-    }
-
-    protected void checkOpen() {
-        if ( ! isOpen )
-            throw new ARQException("closed");
-    }
-
-    @Override
-    public void close() {
-        isOpen = false;
-    }
-
-    @Override
-    public boolean isClosed() {
-        return ! isOpen;
-    }
-
-    protected HttpEntity fileToHttpEntity(String filename, Lang lang) {
-        // Leave RDF/XML to the XML parse, else it's UTF-8.
-        String charset = (lang.equals(Lang.RDFXML) ? null : WebContent.charsetUTF8);
-        // HttpClient Content type.
-        ContentType ct = ContentType.create(lang.getContentType().getContentTypeStr(), charset);
-        // Repeatable.
-        return new FileEntity(new File(filename), ct);
-    }
-
-    /** Create an HttpEntity for the graph */
-    protected HttpEntity graphToHttpEntity(Graph graph) {
-        return graphToHttpEntity(graph, outputTriples);
-    }
-
-    /** Create an HttpEntity for the graph. */
-    protected HttpEntity graphToHttpEntity(Graph graph, RDFFormat syntax) {
-        // Length - leaves connection reusable.
-        return graphToHttpEntityWithLength(graph, syntax);
-    }
-
-    /**
-     * Create an HttpEntity for the graph. The HTTP entity will have the length but this
-     * requires serialising the graph at the point when this function is called.
-     */
-    private HttpEntity graphToHttpEntityWithLength(Graph graph, RDFFormat syntax) {
-        String ct = syntax.getLang().getContentType().toHeaderString();
-        ByteArrayOutputStream out = new ByteArrayOutputStream(128*1024);
-        RDFDataMgr.write(out, graph, syntax);
-        IO.close(out);
-        ByteArrayEntity entity = new ByteArrayEntity(out.toByteArray());
-        entity.setContentType(ct);
-        return entity;
-    }
-
-    /**
-     * Create an HttpEntity for the graph. The bytes for the graph are written
-     * directly the HTTP stream but the length of the entity will be -1 (unknown).
-     * This does not work over cached connections which need to know when
-     * a request body is finished.
-     */
-    private HttpEntity graphToHttpEntityStream(Graph graph, RDFFormat syntax) {
-        EntityTemplate entity = new EntityTemplate((out)->RDFDataMgr.write(out, graph, syntax));
-        String ct = syntax.getLang().getContentType().toHeaderString();
-        entity.setContentType(ct);
-        return entity;
-    }
-
-    /** Create an HttpEntity for the dataset */
-    protected HttpEntity datasetToHttpEntity(DatasetGraph dataset) {
-        return datasetToHttpEntity(dataset, outputQuads);
-    }
-
-    /** Create an HttpEntity for the dataset */
-    protected HttpEntity datasetToHttpEntity(DatasetGraph dataset, RDFFormat syntax) {
-        // Length - leaves connection reusable.
-        return datasetToHttpEntityWithLength(dataset, syntax);
-    }
-
-    private HttpEntity datasetToHttpEntityWithLength(DatasetGraph dataset, RDFFormat syntax) {
-        String ct = syntax.getLang().getContentType().toHeaderString();
-        ByteArrayOutputStream out = new ByteArrayOutputStream(128*1024);
-        RDFDataMgr.write(out, dataset, syntax);
-        IO.close(out);
-        ByteArrayEntity entity = new ByteArrayEntity(out.toByteArray());
-        entity.setContentType(ct);
-        return entity;
-    }
-
-    private HttpEntity datasetToHttpEntityStream(DatasetGraph dataset, RDFFormat syntax) {
-        EntityTemplate entity = new EntityTemplate((out)->RDFDataMgr.write(out, dataset, syntax));
-        String ct = syntax.getLang().getContentType().toHeaderString();
-        entity.setContentType(ct);
-        return entity;
-    }
-
-    /** Convert HTTP status codes to exceptions */
-    static protected void exec(Runnable action)  {
-        try { action.run(); }
-        catch (HttpException ex) { handleHttpException(ex, false); }
-    }
-
-    /** Convert HTTP status codes to exceptions */
-    static protected <X> X exec(Supplier<X> action)  {
-        try { return action.get(); }
-        catch (HttpException ex) { handleHttpException(ex, true); return null;}
-    }
+    /** Create a {@link RDFConnectionRemoteBuilder}. */
+    public static RDFConnectionRemoteBuilder newBuilder() { return new RDFConnectionRemoteBuilder(); }
 
-    private static void handleHttpException(HttpException ex, boolean ignore404) {
-        if ( ex.getStatusCode() == HttpSC.NOT_FOUND_404 && ignore404 )
-            return ;
-        throw ex;
+    /** Create a {@link RDFConnectionRemoteBuilder} for a nremnote destination. */

Review comment:
       s/nremnote /remote?

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecApp.java
##########
@@ -0,0 +1,189 @@
+/*
+ * 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.sparql.exec;
+
+import java.util.Iterator;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.jena.atlas.json.JsonArray;
+import org.apache.jena.atlas.json.JsonObject;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.query.Query;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.Quad;
+import org.apache.jena.sparql.util.Context;
+
+/**
+ * {@link QueryExec} that delays making the QueryExec until needed by a query operation
+ * This means timeouts and initialBinding can still be set via the {@link QueryExecMod}.
+ *
+ * @see QueryExec
+ */
+public class QueryExecApp implements QueryExec {
+    private final QueryExecMod qExecBuilder;
+    private QueryExec qExecHere = null;
+    // Frozen elements of the build.
+    private final DatasetGraph datasetHere;
+    private final Query queryHere;
+    private final String queryStringHere;
+
+    public static QueryExec create(QueryExecMod qExec, DatasetGraph dataset, Query query, String queryString) {
+        return new QueryExecApp(qExec, dataset, query, queryString);
+    }
+
+    private QueryExecApp(QueryExecMod qExecBuilder, DatasetGraph dataset, Query query, String queryString) {
+        // In normal use, one of query and queryString should be non-null
+        // (If being used as a carrier for QueryExecMod

Review comment:
       Dangling `(`? Missing its `)` pair?

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkDataset.java
##########
@@ -0,0 +1,328 @@
+/*
+ * 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.rdflink;
+
+import static org.apache.jena.riot.other.G.clear;
+import static org.apache.jena.riot.other.G.copyGraphSrcToDst;
+
+import java.util.Objects;
+
+import org.apache.jena.atlas.lib.InternalErrorException;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.ReadWrite;
+import org.apache.jena.query.TxnType;
+import org.apache.jena.rdfconnection.Isolation;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.riot.RDFLanguages;
+import org.apache.jena.sparql.ARQException;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.core.DatasetGraphReadOnly;
+import org.apache.jena.sparql.exec.QueryExec;
+import org.apache.jena.sparql.exec.QueryExecApp;
+import org.apache.jena.sparql.exec.QueryExecBuilder;
+import org.apache.jena.sparql.exec.UpdateExecDatasetBuilder;
+import org.apache.jena.sparql.graph.GraphFactory;
+import org.apache.jena.sparql.graph.GraphReadOnly;
+import org.apache.jena.system.Txn;
+import org.apache.jena.update.UpdateRequest;
+
+/**
+ * Implement of {@link RDFLink} over a {@link Graph} in the same JVM.
+ * <p>
+ * Multiple levels of {@link Isolation} are provided, The default {@code COPY} level makes a local
+ * {@link RDFLink} behave like a remote connection. This should be the normal use in
+ * testing.
+ * <ul>
+ * <li>{@code COPY} &ndash; {@code Graph}s and {@code Dataset}s are copied.
+ *     This is most like a remote connection.
+ * <li>{@code READONLY} &ndash; Read-only wrappers are added but changes to
+ *     the underlying graph or dataset will be seen.
+ * <li>{@code NONE} (default) &ndash; Changes to the returned {@code Graph}s or {@code Dataset}s act on the original object.
+ * </ul>
+ */
+
+public class RDFLinkDataset implements RDFLink {
+    private ThreadLocal<Boolean> transactionActive = ThreadLocal.withInitial(()->false);
+
+    private DatasetGraph dataset;
+    private final Isolation isolation;
+
+    private RDFLinkDataset(DatasetGraph dataset) {
+        this(dataset, Isolation.NONE);
+    }
+
+    /*package*/ RDFLinkDataset(DatasetGraph dataset, Isolation isolation) {
+        this.dataset = dataset;
+        this.isolation = isolation;
+    }
+
+    @Override
+    public QueryExec query(Query query) {
+        checkOpen();
+        //return QueryExec.newBuilder().dataset(dataset).query(query).build();
+        // Delayed.
+        return QueryExecApp.create(QueryExec.dataset(dataset).query(query),
+                                   dataset,
+                                   query,
+                                   null);
+    }
+
+    @Override
+    public QueryExecBuilder newQuery() {
+        return QueryExec.dataset(dataset);
+    }
+
+    @Override
+    public void update(UpdateRequest update) {
+        checkOpen();
+        Txn.executeWrite(dataset, ()->UpdateExecDatasetBuilder.create().update(update).execute(dataset));
+    }
+
+    @Override
+    public void load(Node graphName, String file) {
+        checkOpen();
+        doPutPost(graphName, file, false);
+    }
+
+    @Override
+    public void load(String file) {
+        checkOpen();
+        doPutPost(null, file, false);
+    }
+
+    @Override
+    public void load(Node graphName, Graph graphSrc) {
+        checkOpen();
+        Txn.executeWrite(dataset, ()-> {
+            Graph graphDst = graphFor(graphName);
+            copyGraphSrcToDst(graphSrc, graphDst);
+        });
+    }
+
+    @Override
+    public void load(Graph graph) {
+        load(null, graph);
+    }
+
+    @Override
+    public Graph get(Node graphName) {
+        return Txn.calculateRead(dataset, ()-> {

Review comment:
       Missing `checkOpen`?

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkDataset.java
##########
@@ -0,0 +1,328 @@
+/*
+ * 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.rdflink;
+
+import static org.apache.jena.riot.other.G.clear;
+import static org.apache.jena.riot.other.G.copyGraphSrcToDst;
+
+import java.util.Objects;
+
+import org.apache.jena.atlas.lib.InternalErrorException;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.ReadWrite;
+import org.apache.jena.query.TxnType;
+import org.apache.jena.rdfconnection.Isolation;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.riot.RDFLanguages;
+import org.apache.jena.sparql.ARQException;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.core.DatasetGraphReadOnly;
+import org.apache.jena.sparql.exec.QueryExec;
+import org.apache.jena.sparql.exec.QueryExecApp;
+import org.apache.jena.sparql.exec.QueryExecBuilder;
+import org.apache.jena.sparql.exec.UpdateExecDatasetBuilder;
+import org.apache.jena.sparql.graph.GraphFactory;
+import org.apache.jena.sparql.graph.GraphReadOnly;
+import org.apache.jena.system.Txn;
+import org.apache.jena.update.UpdateRequest;
+
+/**
+ * Implement of {@link RDFLink} over a {@link Graph} in the same JVM.
+ * <p>
+ * Multiple levels of {@link Isolation} are provided, The default {@code COPY} level makes a local
+ * {@link RDFLink} behave like a remote connection. This should be the normal use in

Review comment:
       that behaves

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdfconnection/SparqlQueryConnection.java
##########
@@ -25,81 +25,88 @@
 import org.apache.jena.sparql.core.Transactional;
 
 /** SPARQL Query Operations on a connection.
- * 
+ *
  * @see RDFConnection
  * @see RDFConnectionFactory
- */  
+ */
 public interface SparqlQueryConnection extends Transactional, AutoCloseable
 {
     /**
-     * Execute a SELECT query and process the ResultSet with the handler code.  
+     * Execute a SELECT query and process the ResultSet with the handler code.
      * @param query
      * @param resultSetAction
      */
     public void queryResultSet(String query, Consumer<ResultSet> resultSetAction);
-    
+
     /**
-     * Execute a SELECT query and process the ResultSet with the handler code.  
+     * Execute a SELECT query and process the ResultSet with the handler code.
      * @param query
      * @param resultSetAction
      */
-    public void queryResultSet(Query query, Consumer<ResultSet> resultSetAction); 
+    public void queryResultSet(Query query, Consumer<ResultSet> resultSetAction);
 
     /**
-     * Execute a SELECT query and process the rows of the results with the handler code.  
+     * Execute a SELECT query and process the rows of the results with the handler code.
      * @param query
      * @param rowAction
      */
     public void querySelect(String query, Consumer<QuerySolution> rowAction);
-    
+
     /**
-     * Execute a SELECT query and process the rows of the results with the handler code.  
+     * Execute a SELECT query and process the rows of the results with the handler code.
      * @param query
      * @param rowAction
      */
-    public void querySelect(Query query, Consumer<QuerySolution> rowAction); 
+    public void querySelect(Query query, Consumer<QuerySolution> rowAction);
 
     /** Execute a CONSTRUCT query and return as a Model */
     public Model queryConstruct(String query);
-    
+
     /** Execute a CONSTRUCT query and return as a Model */
     public Model queryConstruct(Query query);
 
     /** Execute a DESCRIBE query and return as a Model */
     public Model queryDescribe(String query);
-    
+
     /** Execute a DESCRIBE query and return as a Model */
     public Model queryDescribe(Query query);
-    
+
     /** Execute a ASK query and return a boolean */
     public boolean queryAsk(String query);
 
     /** Execute a ASK query and return a boolean */
     public boolean queryAsk(Query query);
-    
+
     /** Setup a SPARQL query execution.
-     * 
-     *  See also {@link #querySelect(Query, Consumer)}, {@link #queryConstruct(Query)}, 
+     *
+     *  See also {@link #querySelect(Query, Consumer)}, {@link #queryConstruct(Query)},
      *  {@link #queryDescribe(Query)}, {@link #queryAsk(Query)}
      *  for ways to execute queries for of a specific form.
-     * 
+     *
      * @param query
      * @return QueryExecution
      */
     public QueryExecution query(Query query);
 
     /** Setup a SPARQL query execution.
-     * 
-     *  See also {@link #querySelect(String, Consumer)}, {@link #queryConstruct(String)}, 
+     *
+     *  See also {@link #querySelect(String, Consumer)}, {@link #queryConstruct(String)},
      *  {@link #queryDescribe(String)}, {@link #queryAsk(String)}
      *  for ways to execute queries for of a specific form.
-     * 
-     * @param queryString 
+     *
+     * @param queryString
      * @return QueryExecution
      */
     public QueryExecution query(String queryString);
-    
-    /** Close this connection.  Use with try-resource. */ 
+
+    /**
+     * Return a execution builder initialized with the RDFConnection setup.

Review comment:
       an execution

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkDataset.java
##########
@@ -0,0 +1,328 @@
+/*
+ * 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.rdflink;
+
+import static org.apache.jena.riot.other.G.clear;
+import static org.apache.jena.riot.other.G.copyGraphSrcToDst;
+
+import java.util.Objects;
+
+import org.apache.jena.atlas.lib.InternalErrorException;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.ReadWrite;
+import org.apache.jena.query.TxnType;
+import org.apache.jena.rdfconnection.Isolation;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.riot.RDFLanguages;
+import org.apache.jena.sparql.ARQException;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.core.DatasetGraphReadOnly;
+import org.apache.jena.sparql.exec.QueryExec;
+import org.apache.jena.sparql.exec.QueryExecApp;
+import org.apache.jena.sparql.exec.QueryExecBuilder;
+import org.apache.jena.sparql.exec.UpdateExecDatasetBuilder;
+import org.apache.jena.sparql.graph.GraphFactory;
+import org.apache.jena.sparql.graph.GraphReadOnly;
+import org.apache.jena.system.Txn;
+import org.apache.jena.update.UpdateRequest;
+
+/**
+ * Implement of {@link RDFLink} over a {@link Graph} in the same JVM.
+ * <p>
+ * Multiple levels of {@link Isolation} are provided, The default {@code COPY} level makes a local

Review comment:
       are provided. The default?

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkDataset.java
##########
@@ -0,0 +1,328 @@
+/*
+ * 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.rdflink;
+
+import static org.apache.jena.riot.other.G.clear;
+import static org.apache.jena.riot.other.G.copyGraphSrcToDst;
+
+import java.util.Objects;
+
+import org.apache.jena.atlas.lib.InternalErrorException;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.ReadWrite;
+import org.apache.jena.query.TxnType;
+import org.apache.jena.rdfconnection.Isolation;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.riot.RDFLanguages;
+import org.apache.jena.sparql.ARQException;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.core.DatasetGraphReadOnly;
+import org.apache.jena.sparql.exec.QueryExec;
+import org.apache.jena.sparql.exec.QueryExecApp;
+import org.apache.jena.sparql.exec.QueryExecBuilder;
+import org.apache.jena.sparql.exec.UpdateExecDatasetBuilder;
+import org.apache.jena.sparql.graph.GraphFactory;
+import org.apache.jena.sparql.graph.GraphReadOnly;
+import org.apache.jena.system.Txn;
+import org.apache.jena.update.UpdateRequest;
+
+/**
+ * Implement of {@link RDFLink} over a {@link Graph} in the same JVM.
+ * <p>
+ * Multiple levels of {@link Isolation} are provided, The default {@code COPY} level makes a local
+ * {@link RDFLink} behave like a remote connection. This should be the normal use in
+ * testing.
+ * <ul>
+ * <li>{@code COPY} &ndash; {@code Graph}s and {@code Dataset}s are copied.
+ *     This is most like a remote connection.
+ * <li>{@code READONLY} &ndash; Read-only wrappers are added but changes to
+ *     the underlying graph or dataset will be seen.
+ * <li>{@code NONE} (default) &ndash; Changes to the returned {@code Graph}s or {@code Dataset}s act on the original object.
+ * </ul>
+ */
+
+public class RDFLinkDataset implements RDFLink {
+    private ThreadLocal<Boolean> transactionActive = ThreadLocal.withInitial(()->false);
+
+    private DatasetGraph dataset;
+    private final Isolation isolation;
+
+    private RDFLinkDataset(DatasetGraph dataset) {
+        this(dataset, Isolation.NONE);
+    }
+
+    /*package*/ RDFLinkDataset(DatasetGraph dataset, Isolation isolation) {
+        this.dataset = dataset;
+        this.isolation = isolation;
+    }
+
+    @Override
+    public QueryExec query(Query query) {
+        checkOpen();
+        //return QueryExec.newBuilder().dataset(dataset).query(query).build();
+        // Delayed.
+        return QueryExecApp.create(QueryExec.dataset(dataset).query(query),
+                                   dataset,
+                                   query,
+                                   null);
+    }
+
+    @Override
+    public QueryExecBuilder newQuery() {
+        return QueryExec.dataset(dataset);
+    }
+
+    @Override
+    public void update(UpdateRequest update) {
+        checkOpen();
+        Txn.executeWrite(dataset, ()->UpdateExecDatasetBuilder.create().update(update).execute(dataset));
+    }
+
+    @Override
+    public void load(Node graphName, String file) {
+        checkOpen();
+        doPutPost(graphName, file, false);
+    }
+
+    @Override
+    public void load(String file) {
+        checkOpen();
+        doPutPost(null, file, false);
+    }
+
+    @Override
+    public void load(Node graphName, Graph graphSrc) {
+        checkOpen();
+        Txn.executeWrite(dataset, ()-> {
+            Graph graphDst = graphFor(graphName);
+            copyGraphSrcToDst(graphSrc, graphDst);
+        });
+    }
+
+    @Override
+    public void load(Graph graph) {
+        load(null, graph);
+    }
+
+    @Override
+    public Graph get(Node graphName) {
+        return Txn.calculateRead(dataset, ()-> {
+            Graph graph = graphFor(graphName);
+            return isolate(graph);
+        });
+    }
+
+    @Override
+    public Graph get() {
+        checkOpen();
+        return get(null);
+    }
+
+    @Override
+    public void put(String file) {
+        checkOpen();
+        doPutPost(null, file, true);
+    }
+
+    @Override
+    public void put(Node graphName, String file) {
+        checkOpen();
+        doPutPost(graphName, file, true);
+    }
+
+    @Override
+    public void put(Graph graph) {
+        put(null, graph);
+    }
+
+    @Override
+    public void put(Node graphName, Graph graph) {
+        checkOpen();
+        Txn.executeWrite(dataset, ()-> {
+            Graph graphDst = graphFor(graphName);
+            clear(graphDst);
+            copyGraphSrcToDst(graph, graphDst);
+        });
+    }
+
+    @Override
+    public void delete(Node graphName) {
+        checkOpen();
+        Txn.executeWrite(dataset,() ->{
+            if ( LibRDFLink.isDefault(graphName) )
+                clear(dataset.getDefaultGraph());
+            else
+                clear(dataset.getGraph(graphName));
+        });
+    }
+
+    @Override
+    public void delete() {
+        checkOpen();
+        delete(null);
+    }
+
+    private void doPutPost(Node graphName, String file, boolean replace) {
+        Objects.requireNonNull(file);
+        Lang lang = RDFLanguages.filenameToLang(file);
+
+        Txn.executeWrite(dataset,() ->{
+            if ( RDFLanguages.isTriples(lang) ) {
+                Graph graph = LibRDFLink.isDefault(graphName) ? dataset.getDefaultGraph() : dataset.getGraph(graphName);
+                if ( replace )
+                    clear(graph);
+                RDFDataMgr.read(graph, file);
+            }
+            else if ( RDFLanguages.isQuads(lang) ) {
+                if ( replace )
+                    dataset.clear();
+                // Try to POST to the dataset.
+                RDFDataMgr.read(dataset, file);
+            }
+            else
+                throw new ARQException("Not an RDF format: "+file+" (lang="+lang+")");
+        });
+    }
+
+    /**
+     * Called to isolate a graph from it's storage.
+     * Must be inside a transaction.
+     */
+    private Graph isolate(Graph graph) {
+        switch(isolation) {
+            case COPY: {
+                // Copy - the graph is completely isolated from the original.
+                Graph graph2 = GraphFactory.createDefaultGraph();
+                copyGraphSrcToDst(graph, graph2);
+                return graph2;
+            }
+            case READONLY : {
+                Graph graph2 = new GraphReadOnly(graph);
+                return graph2;
+            }
+            case NONE :
+                return graph;
+        }
+        throw new InternalErrorException();
+    }
+
+    /**
+     * Called to isolate a dataset from it's storage.

Review comment:
       s/it's/its

##########
File path: jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkFactory.java
##########
@@ -0,0 +1,176 @@
+/*
+ * 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.rdflink;
+
+import org.apache.jena.rdfconnection.Isolation;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sys.JenaSystem;
+
+public class RDFLinkFactory {
+    static { JenaSystem.init(); }
+
+    /**
+     * Create a connection to a remote location by URL. This is the URL for the
+     * dataset. This call assumes the SPARQL Query endpoint, SPARQL Update endpoint
+     * and SPARQL Graph Store Protocol endpoinst are the same URL.

Review comment:
       endpoinst endpoint?

##########
File path: jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilder.java
##########
@@ -0,0 +1,101 @@
+/*
+ * 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.sparql.exec;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.query.ARQ;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.Syntax;
+import org.apache.jena.sparql.core.Var;
+import org.apache.jena.sparql.engine.binding.Binding;
+import org.apache.jena.sparql.util.Context;
+import org.apache.jena.sparql.util.Symbol;
+
+/** The common elements of a {@link QueryExec} builder. */
+public interface QueryExecBuilder extends QueryExecMod {
+
+    /** Set the query. */
+    public QueryExecBuilder query(Query query);
+
+    /** Set the query. */
+    public QueryExecBuilder query(String queryString);
+
+    /** Set the query. */
+    public QueryExecBuilder query(String queryString, Syntax syntax);
+
+    /** Set a context entry. */
+    public QueryExecBuilder set(Symbol symbol, Object value);
+
+    /** Set a context entry. */
+    public QueryExecBuilder set(Symbol symbol, boolean value);
+
+    /**
+     * Set the context. if not set, defaults to the system context

Review comment:
       s/if not set/If not set




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@jena.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@jena.apache.org
For additional commands, e-mail: pr-help@jena.apache.org