This is an automated email from the ASF dual-hosted git repository.

afs pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git

commit ee459bdeb9829751d14d4f528a670226922e2e64
Author: Andy Seaborne <[email protected]>
AuthorDate: Tue May 26 10:15:30 2026 +0100

    GH-3947: GSP Direct Naming
---
 .../java/org/apache/jena/sparql/exec/http/GSP.java |  41 +--
 .../main/java/org/apache/jena/fuseki/Fuseki.java   |  22 +-
 .../fuseki/server/DataAccessPointRegistry.java     |  16 +
 .../org/apache/jena/fuseki/server/Dispatcher.java  |  64 +++-
 .../apache/jena/fuseki/server/FusekiVocabG.java    |  29 +-
 .../org/apache/jena/fuseki/server/Operation.java   |   3 +
 .../jena/fuseki/server/OperationRegistry.java      |  10 +-
 .../apache/jena/fuseki/servlets/ActionExecLib.java |  43 ++-
 .../org/apache/jena/fuseki/servlets/ActionLib.java |   8 +-
 .../org/apache/jena/fuseki/servlets/GSPLib.java    | 253 +++++++++++++++-
 .../apache/jena/fuseki/servlets/GSP_Direct_R.java  | 110 +++++++
 .../apache/jena/fuseki/servlets/GSP_Direct_RW.java |  80 +++++
 .../org/apache/jena/fuseki/servlets/GSP_R.java     |  70 +----
 .../org/apache/jena/fuseki/servlets/GSP_RW.java    | 154 +---------
 .../apache/jena/fuseki/servlets/GraphTarget.java   |  17 +-
 .../apache/jena/fuseki/servlets/HttpAction.java    |   4 +
 .../jena/fuseki/server/TestDispatchOnURI.java      |   4 +-
 .../jena/sparql/exec/http/TS_SparqlExecHttp.java   |   1 +
 .../jena/sparql/exec/http/TestGSPDirect.java       | 336 +++++++++++++++++++++
 19 files changed, 967 insertions(+), 298 deletions(-)

diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/GSP.java 
b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/GSP.java
index de97382a0f..22a10b96ed 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/GSP.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/GSP.java
@@ -66,11 +66,14 @@ import org.apache.jena.sparql.graph.GraphFactory;
  */
 public class GSP extends StoreProtocol<GSP> {
 
-    // One, and only one of these two, must be set at the point the 
terminating operation is called.
+    // One, and only one of these three, must be set at the point the 
terminating operation is called.
     // 1 - Graph operation, GSP naming, default graph
-    private boolean             defaultGraph    = false;
-    // 2 - Graph operation, GSP naming, graph name.
-    private String              graphName       = null;
+    private boolean defaultGraph    = false;
+    // 2 - Graph operation, GSP indirect naming, graph name.
+    private String  graphName       = null;
+    // 3 - Graph operation, GSP direct naming
+    // This must align to the service URL.
+    private String  directGraphName = null;
 
     /**
      * Create a request to the remote serviceURL (without a URL query string).
@@ -79,6 +82,7 @@ public class GSP extends StoreProtocol<GSP> {
      * @param service
      */
     public static GSP service(String service) {
+        Objects.requireNonNull(service);
         return new GSP().endpoint(service);
     }
 
@@ -101,7 +105,6 @@ public class GSP extends StoreProtocol<GSP> {
     public GSP graphName(String graphName) {
         Objects.requireNonNull(graphName);
         this.graphName = graphName;
-        this.defaultGraph = false;
         return this;
     }
 
@@ -113,7 +116,6 @@ public class GSP extends StoreProtocol<GSP> {
             throw exception("Not an acceptable graph name: "+this.graphName);
         Node gn = RiotLib.blankNodeToIri(graphName);
         this.graphName = gn.getURI();
-        this.defaultGraph = false;
         return this;
     }
 
@@ -124,14 +126,25 @@ public class GSP extends StoreProtocol<GSP> {
         return this;
     }
 
+    /** Send request for a named graph using GSP direct naming. */
+    public GSP directGraphName(String directGraphName) {
+        Objects.requireNonNull(directGraphName);
+        if ( ! directGraphName.startsWith(serviceEndpoint) )
+            throw exception("Graph name for GSP direct naming does not start 
with the service URL: "+directGraphName);
+        clearOperation();
+        this.directGraphName = directGraphName;
+        return this;
+    }
+
     private void clearOperation() {
         this.defaultGraph = false;
         this.graphName = null;
+        this.directGraphName = null;
     }
 
     final protected void validateGraphOperation() {
         Objects.requireNonNull("Service Endpoint", serviceEndpoint);
-        if ( ! defaultGraph && graphName == null )
+        if ( ! defaultGraph && graphName == null && directGraphName == null )
             throw exception("Need either default graph or a graph name");
     }
 
@@ -306,21 +319,15 @@ public class GSP extends StoreProtocol<GSP> {
     // Only valid when the request has been correctly setup.
 
     final protected String graphName()           { return graphName; }
-    final protected boolean isDefaultGraph()     { return graphName == null; }
-    final protected boolean isGraphOperation()   { return defaultGraph || 
graphName != null; }
+    final protected boolean isDefaultGraph()     { return defaultGraph; }
+    final protected boolean isDirectOperation()  { return directGraphName != 
null ; }
 
     private String graphRequestURL() {
+        if ( directGraphName != null )
+            return directGraphName;
         return HttpLib.requestURL(serviceEndpoint, 
queryStringForGraph(graphName));
     }
 
-    final protected void validateDatasetOperation() {
-        Objects.requireNonNull("Service Endpoint", serviceEndpoint);
-        if ( defaultGraph )
-            throw exception("Default graph specified for dataset operation");
-        if ( graphName != null )
-            throw exception("A graph name specified for dataset operation");
-    }
-
     /** Send a file of triples to a URL. */
     private static void uploadTriples(HttpClient httpClient, String gspUrl, 
String file, String fileExtContentType,
                                       Map<String, String> headers, Push mode) {
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/Fuseki.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/Fuseki.java
index 3c46008b16..d4b742d6b2 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/Fuseki.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/Fuseki.java
@@ -65,27 +65,33 @@ public class Fuseki {
     /** Version of this Fuseki instance */
     static public final String  VERSION           = 
Version.versionForClass(Fuseki.class).orElse("<development>");
 
-    /** Supporting Graph Store Protocol direct naming.
+    /**
+     * Supporting Graph Store Protocol direct naming.
      * <p>
      *  A GSP "direct name" is a request, not using ?default or ?graph=, that 
names the graph
      *  by the request URL so it is of the form {@code 
http://server/dataset/graphname...}.
      *  There are two cases: looking like a service {@code 
http://server/dataset/service} and
      *  a longer URL that can't be a service {@code 
http://server/dataset/segment/segment/...}.
      *  <p>
-     *  GSP "direct name" is usually off.  It is a rare feature and because of 
hard wiring to the URL
-     *  quite sensitive to request route.
+     *  GSP "direct name" is not part of the standard default Fuseki 
configuration.
+     *  It needs to enabled by configuration using {@code fuseki:operation 
fuseki:gsp-direct-r}
+     *  or {@code  fuseki:operation fuseki:gsp-direct-rw}.
+     *
+     *  It conflicts with having static file and overalpping service endpoint 
names.
+     *  Service endpoint names takes precidence.
      *  <p>
      *  The following places use this switch:
      *  <ul>
-     *  <li>{@code FusekiFilter} for the "clearly not a service" case
-     *  <li>{@code ServiceRouterServlet}, end of dispatch (after checking for 
http://server/dataset/service)
-     *  <li>{@code SPARQL_GSP.determineTarget} This is all-purpose code - 
should not get there because of other checks.
+     *  <li>{@code Dispatcher}.
+     *  <li>{@code GraphTarget.determineTargetGSP} This is all-purpose code - 
should not get there because of other checks.
+     *  <li>{@code OperationRegistry} where it endbles operation registry.
+     *  <li>{@code FusekiServer.Builder.applyAccessControl}
      *  </ul>
      *  <p>
      * <b>Note</b><br/>
-     * GSP Direct Naming was implemented to provide two implementations for 
the SPARQL 1.1 implementation report.
+     * GSP Direct Naming was primarily implemented to provide two 
implementations for the SPARQL 1.1 implementation report.
      */
-    static public final boolean GSP_DIRECT_NAMING = false;
+    static public final boolean GSP_DIRECT_NAMING = true;
 
     /** Are we in development mode?  That means a SNAPSHOT, or no VERSION
      * because maven has not filtered the fuseki-properties.xml file.
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataAccessPointRegistry.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataAccessPointRegistry.java
index 5d29bf4c70..ed58a48bb3 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataAccessPointRegistry.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataAccessPointRegistry.java
@@ -73,6 +73,22 @@ public class DataAccessPointRegistry extends 
Registry<String, DataAccessPoint>
         return super.get(key);
     }
 
+    /**
+     * Find a {@link DataAccessPoint} which as {@code uri} as a prefix to its 
name.
+     * If there are multiple possible matches, which is returned is not 
defined.
+     */
+    public DataAccessPoint getByPrefix(String prefix) {
+        if ( prefix == null )
+            return null;
+        for ( String dapName : keys() ) {
+            if ( prefix.startsWith(dapName) ) {
+                // First match.
+                return get(dapName);
+            }
+        }
+        return null;
+    }
+
     // Debugging
     public void print() {
         print(null);
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Dispatcher.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Dispatcher.java
index 6476568bd2..f41492e9c9 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Dispatcher.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Dispatcher.java
@@ -29,6 +29,8 @@ import static org.apache.jena.fuseki.server.Operation.Query;
 import static org.apache.jena.fuseki.server.Operation.Update;
 import static org.apache.jena.fuseki.servlets.ActionExecLib.allocHttpAction;
 
+import java.util.List;
+
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 
@@ -133,7 +135,6 @@ public class Dispatcher {
      *   </ul>
      * <li>Allow authentication for the dispatch choice.
      * </ul>
-     *
      */
     public static boolean dispatch(HttpServletRequest request, 
HttpServletResponse response) {
         DataAccessPointRegistry registry = 
DataAccessPointRegistry.get(request.getServletContext());
@@ -144,8 +145,14 @@ public class Dispatcher {
         if ( dap == null ) {
             if ( LogDispatch )
                 LOG.debug("No dispatch for '"+request.getRequestURI()+"'");
+            // Don't handle.
             return false;
         }
+        // Data access point found.
+        // The request HttpAction is created and logging started.
+        // Only if the service endpoint can not be found will this return 
false;
+        // that enables static files under the dataset URL prefix.
+
         // The execution code is in ActionExecLib.execAction
         return process(dap, request, response);
     }
@@ -170,24 +177,32 @@ public class Dispatcher {
             LOG.info("Filter: Request URI = " + request.getRequestURI());
             LOG.info("Filter: Action URI  = " + uri);
         }
-        DataAccessPoint dap = locateDataAccessPoint(uri, registry);
+        boolean hasQueryString = (request.getQueryString() != null);
+        DataAccessPoint dap = locateDataAccessPoint(uri, registry, 
hasQueryString);
         // At this point, we are going to dispatch to the DataAccessPoint.
         // It still may not have a handler for the service on this dataset.
         // See #chooseProcessor(HttpAction) for locating the endpoint.
         return dap;
     }
 
+
+    /*package:testing*/ static DataAccessPoint 
locateDataAccessPointTest(String uri, DataAccessPointRegistry registry) {
+        return locateDataAccessPoint(uri, registry, false);
+    }
+
     /**
-     * Identity the Fuseki service for the request
+     * Identity the Fuseki service for the request.
      */
-    /*package:testing*/ static DataAccessPoint locateDataAccessPoint(String 
uri, DataAccessPointRegistry registry) {
+    private static DataAccessPoint locateDataAccessPoint(String uri, 
DataAccessPointRegistry registry, boolean hasQueryString) {
         // Cases:
         // /dataset
         // /dataset/endpoint
         // /path/.../dataset/
         // /path/.../dataset/endpoint
+        // if these fail,
+        // optionally, try GSP_DIRECT_NAMING
 
-        // Direct match.
+        // Dataset match.
         if ( registry.isRegistered(uri) )
             // Cases: /, /dataset and /path/dataset where /path is not the 
servlet context path.
             return registry.get(uri);
@@ -202,7 +217,20 @@ public class Dispatcher {
         if ( registry.isRegistered(datasetUri) )
             // Cases: /dataset/sparql and /path/dataset/sparql
             return registry.get(datasetUri);
+        // Not found by the fixed patterns of /../dataset and 
/../dataset/service
 
+        if ( Fuseki.GSP_DIRECT_NAMING && ! hasQueryString ) {
+            // Precaution: Reserved for the Fuseki server admin operations.
+            if ( uri.startsWith("/$/") )
+                return null;
+            DataAccessPoint dap = registry.getByPrefix(uri);
+            if ( dap == null )
+                return null;
+            if ( dap.getDataService().hasOperation(Operation.GSP_Direct_RW) )
+              return dap;
+            if ( dap.getDataService().hasOperation(Operation.GSP_Direct_R) )
+                return dap;
+        }
         return null;
     }
 
@@ -232,6 +260,7 @@ public class Dispatcher {
         return dispatchAction(action);
     }
 
+    // Collapse.
     /**
      * Determine and call the {@link ActionProcessor} to handle this
      * {@link HttpAction}, including access control at the dataset and service 
levels.
@@ -243,10 +272,12 @@ public class Dispatcher {
     }
 
     /**
-     * Find the ActionProcessor or return null if there can't determine one.
-     *
+     * Find the ActionProcessor or return null if we can't determine one.
+     * <p>
+     * The data access point has already been found and the request action 
created.
+     * Code form here either executes a request or rejects it. It does not 
pass it along the servlet filter chain.
      * This function sends the appropriate HTTP error response on failure to 
choose an endpoint.
-     *
+     * <p>
      * Returning null indicates an HTTP error response, and the HTTP response 
has been done.
      *
      * Process:
@@ -282,6 +313,23 @@ public class Dispatcher {
         // There may be multiple operations for an endpointName of this data 
service.
 
         Endpoint endpoint = chooseEndpoint(action, dataService, endpointName);
+
+        if ( endpoint == null && Fuseki.GSP_DIRECT_NAMING) {
+            if ( true ) { //! action.hasRequestQueryString() ) {
+                // If GSP direct naming is enabled, it can hide static files 
with the same prefix.
+                // Find operation.
+                // It should exist because the DataAccessPoint decision in 
locateDataAccessPoint checked.
+                List<Endpoint> x1 = 
dataService.getEndpoints(Operation.GSP_Direct_RW);
+                if ( ! x1.isEmpty() ) {
+                    endpoint = x1.getFirst();
+                } else {
+                    List<Endpoint> x2 = 
dataService.getEndpoints(Operation.GSP_Direct_R);
+                    if ( ! x2.isEmpty() )
+                        endpoint = x2.getFirst();
+                }
+            }
+        }
+
         if ( endpoint == null ) {
             if ( DEBUG )
                 FmtLog.info(LOG, "Dispatch: no endpoint");
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/FusekiVocabG.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/FusekiVocabG.java
index a4b6facc80..a4f8a29a8a 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/FusekiVocabG.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/FusekiVocabG.java
@@ -76,23 +76,24 @@ public class FusekiVocabG
     public static final Node pServiceShaclEP                = 
property("serviceShacl");
     public static final Node pServiceReadWriteGraphStoreEP  = 
property("serviceReadWriteGraphStore");
     public static final Node pServiceReadGraphStoreEP       = 
property("serviceReadGraphStore");
-    // No longer used.
-//    public static final Node pServiceReadWriteQuadsEP       = 
property("serviceReadWriteQuads");
-//    public static final Node pServiceReadQuadsEP            = 
property("serviceReadQuads");
 
     // Operation names : the standard operations.
     // "alt" names are the same but using "_" not "-".
-    public static final Node opQuery       = resource("query");
-    public static final Node opUpdate      = resource("update");
-    public static final Node opUpload      = resource("upload");
-    public static final Node opGSP_r       = resource("gsp-r");
-    public static final Node opGSP_r_alt   = resource("gsp_r");
-    public static final Node opGSP_rw      = resource("gsp-rw");
-    public static final Node opGSP_rw_alt  = resource("gsp_rw");
-    public static final Node opNoOp        = resource("no-op");
-    public static final Node opNoOp_alt    = resource("no_op");
-    public static final Node opShacl       = resource("shacl");
-    public static final Node opPatch       = resource("patch");
+    public static final Node opQuery            = resource("query");
+    public static final Node opUpdate           = resource("update");
+    public static final Node opUpload           = resource("upload");
+    public static final Node opGSP_r            = resource("gsp-r");
+    public static final Node opGSP_r_alt        = resource("gsp_r");
+    public static final Node opGSP_rw           = resource("gsp-rw");
+    public static final Node opGSP_rw_alt       = resource("gsp_rw");
+
+    public static final Node opGSP_direct_r     = resource("gsp-direct-r");
+    public static final Node opGSP_direct_rw    = resource("gsp-direct-rw");
+
+    public static final Node opNoOp             = resource("no-op");
+    public static final Node opNoOp_alt         = resource("no_op");
+    public static final Node opShacl            = resource("shacl");
+    public static final Node opPatch            = resource("patch");
 
     public static final Node opPREFIXES_R       = resource("prefixes-r");
     public static final Node opPREFIXES_RW      = resource("prefixes-rw");
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Operation.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Operation.java
index 0e627baf79..a16ec01319 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Operation.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Operation.java
@@ -82,6 +82,9 @@ public class Operation {
     public static final Operation GSP_R    = alloc(FusekiVocabG.opGSP_r,   
"gsp-r",   "Graph Store Protocol (Read)");
     public static final Operation GSP_RW   = alloc(FusekiVocabG.opGSP_rw,  
"gsp-rw",  "Graph Store Protocol");
 
+    public static final Operation GSP_Direct_R    = 
alloc(FusekiVocabG.opGSP_direct_r,   "gsp-direct-r",   "Graph Store Protocol 
(Direct naming, Read)");
+    public static final Operation GSP_Direct_RW   = 
alloc(FusekiVocabG.opGSP_direct_rw,  "gsp-direct-rw",  "Graph Store Protocol 
(Direct naming)");
+
     public static final Operation Shacl    = alloc(FusekiVocabG.opShacl,   
"SHACL",   "SHACL Validation");
     public static final Operation Upload   = alloc(FusekiVocabG.opUpload,  
"upload",  "File Upload");
     public static final Operation Patch    = alloc(FusekiVocabG.opPatch,   
"patch",   "RDF Patch");
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/OperationRegistry.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/OperationRegistry.java
index 71a2f2838f..015c112f55 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/OperationRegistry.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/OperationRegistry.java
@@ -26,7 +26,6 @@ import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 
 import jakarta.servlet.ServletContext;
-
 import org.apache.jena.atlas.logging.Log;
 import org.apache.jena.fuseki.Fuseki;
 import org.apache.jena.fuseki.servlets.*;
@@ -49,6 +48,10 @@ public class OperationRegistry {
     private static final ActionService uploadServlet   = new UploadRDF();
     private static final ActionService gspServlet_R    = new GSP_R();
     private static final ActionService gspServlet_RW   = new GSP_RW();
+    private static final ActionService gspDirect_R     = new GSP_Direct_R();
+    private static final ActionService gspDirect_RW    = new GSP_Direct_RW();
+
+
     private static final ActionService rdfPatch        = new PatchApply();
     private static final ActionService noOperation     = new 
NoOpActionService();
     private static final ActionService shaclValidation = new 
SHACL_Validation();
@@ -74,6 +77,11 @@ public class OperationRegistry {
         stdOpReg.register(Operation.GSP_R,   null, gspServlet_R);
         stdOpReg.register(Operation.GSP_RW,  null, gspServlet_RW);
 
+        if ( Fuseki.GSP_DIRECT_NAMING ) {
+            stdOpReg.register(Operation.GSP_Direct_R,  null, gspDirect_R);
+            stdOpReg.register(Operation.GSP_Direct_RW, null, gspDirect_RW);
+        }
+
         stdOpReg.register(Operation.Patch,   WebContent.contentTypePatch, 
rdfPatch);
         stdOpReg.register(Operation.Shacl,   null, shaclValidation);
         stdOpReg.register(Operation.Upload,  null, uploadServlet);
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionExecLib.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionExecLib.java
index 261f13a467..217ef94ed8 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionExecLib.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionExecLib.java
@@ -69,7 +69,7 @@ public class ActionExecLib {
     }
 
     /**
-     * Standard execution lifecycle for a SPARQL Request.
+     * Direct call to the standard execution lifecycle for a SPARQL Request.
      * <ul>
      * <li>{@link #startRequest(HttpAction)}</li>
      * <li>initial statistics,</li>
@@ -83,8 +83,10 @@ public class ActionExecLib {
      * servlet directly outside the Fuseki dispatch process ({@link 
ServletAction}
      * for special case like {@link SPARQL_QueryGeneral} which directly holds 
the {@link ActionProcessor}
      * and {@link ServletProcessor} for administration actions.
-     * <p>
-     * Return false if the ActionProcessor is null.
+     * <p>For execution choosing the processor from the data access point
+     * See {@link #execAction(HttpAction, Supplier)}
+     *
+     * @returns false if the ActionProcessor is not found.
      */
     public static void execAction(HttpAction action, ActionProcessor 
processor) {
         boolean b = execAction(action, ()->processor);
@@ -93,12 +95,27 @@ public class ActionExecLib {
     }
 
     /**
-     * execAction, allowing for a choice of {@link ActionProcessor} within the 
logging and error handling.
-     * Return false if there was no ActionProcessor to handle the action.
+     * Standard execution lifecycle for a SPARQL Request with dynamic choice 
of processor.
+     * <ul>
+     * <li>{@link #startRequest(HttpAction)}</li>
+     * <li>initial statistics,</li>
+     * <li>{@link ActionLifecycle#validate(HttpAction)} request,</li>
+     * <li>{@link ActionLifecycle#execute(HttpAction)} request,</li>
+     * <li>completion/error statistics,</li>
+     * <li>{@link #finishRequest(HttpAction)}
+     * </ul>
+     * Common process for handling HTTP requests with logging and Java error
+     * handling. This is the case where the ActionProcessor is defined by or 
is the
+     * servlet directly outside the Fuseki dispatch process ({@link 
ServletAction}
+     * for special case like {@link SPARQL_QueryGeneral} which directly holds 
the {@link ActionProcessor}
+     * and {@link ServletProcessor} for administration actions.
+     *
+     * @returns false if the ActionProcessor is not found.
      */
-    public static boolean execAction(HttpAction action, 
Supplier<ActionProcessor> processor) {
+    public static boolean execAction(HttpAction action, 
Supplier<ActionProcessor> processorSelector) {
         try {
-            return execActionSub(action, processor);
+            // Protect call to the worker.
+            return execActionSub(action, processorSelector);
         } catch (Throwable th) {
             // This really should not catch anything.
             FmtLog.error(action.log, th, "Internal error");
@@ -106,7 +123,8 @@ public class ActionExecLib {
         }
     }
 
-    private static boolean execActionSub(HttpAction action, 
Supplier<ActionProcessor> processor) {
+    // processorSelector - find endpoint and handler code within the data 
access point after the action starts.
+    private static boolean execActionSub(HttpAction action, 
Supplier<ActionProcessor> processorSelector) {
         logRequest(action);
         action.setStartTime();
         initResponse(action);
@@ -115,13 +133,13 @@ public class ActionExecLib {
         startRequest(action);
         try {
             // Get the processor inside the startRequest - error handling - 
finishRequest sequence.
-            ActionProcessor proc = processor.get();
+            ActionProcessor proc = processorSelector.get();
             if ( proc == null ) {
+                // Can't find the service within the data access point.
                 // Only for the logging.
                 finishRequest(action);
-                logNoResponse(action);
+                logNoDispatch(action);
                 archiveHttpAction(action);
-                // Can't find the URL (the /dataset/service case) - not 
handled here.
                 return false;
             }
             proc.process(action);
@@ -180,7 +198,6 @@ public class ActionExecLib {
             ServletOps.responseSendError(response, 
HttpSC.INTERNAL_SERVER_ERROR_500, ex.getMessage());
         } catch (Throwable ex) {
             // This should not happen.
-            //ex.printStackTrace(System.err);
             FmtLog.warn(action.log, ex, "[%d] RC = %d : %s", action.id, 
HttpSC.INTERNAL_SERVER_ERROR_500, ex.getMessage());
             ServletOps.responseSendError(response, 
HttpSC.INTERNAL_SERVER_ERROR_500, ex.getMessage());
         } finally {
@@ -307,7 +324,7 @@ public class ActionExecLib {
     /**
      * Log when we don't handle this request.
      */
-    public static void logNoResponse(HttpAction action) {
+    private static void logNoDispatch(HttpAction action) {
         FmtLog.info(action.log,"[%d] No Fuseki dispatch %s", action.id, 
action.getActionURI());
     }
 
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java
index b4b6d96903..884c50de4c 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java
@@ -149,14 +149,10 @@ public class ActionLib {
         String uri = request.getRequestURI();
         ServletContext servletCxt = request.getServletContext();
         if ( servletCxt == null )
-            return request.getRequestURI();
-
-        String contextPath = servletCxt.getContextPath();
-        if ( contextPath == null )
             return uri;
-        if ( contextPath.isEmpty())
+        String contextPath = servletCxt.getContextPath();
+        if ( contextPath == null || contextPath.isEmpty() )
             return uri;
-
         String x = uri;
         if ( uri.startsWith(contextPath) )
             x = uri.substring(contextPath.length());
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSPLib.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSPLib.java
index a93593a69f..6829bb7793 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSPLib.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSPLib.java
@@ -21,16 +21,39 @@
 
 package org.apache.jena.fuseki.servlets;
 
+import static java.lang.String.format;
+import static org.apache.jena.fuseki.servlets.GraphTarget.determineTargetGSP;
+
+import java.io.IOException;
+import java.io.OutputStream;
 import java.util.Map;
+import java.util.function.Function;
 
 import jakarta.servlet.http.HttpServletRequest;
-
+import org.apache.jena.atlas.web.MediaType;
+import org.apache.jena.fuseki.FusekiConfigException;
+import org.apache.jena.fuseki.server.Validators;
+import org.apache.jena.fuseki.system.DataUploader;
+import org.apache.jena.fuseki.system.FusekiNetLib;
+import org.apache.jena.fuseki.system.UploadDetails;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFLanguages;
+import org.apache.jena.riot.RiotException;
+import org.apache.jena.riot.RiotParseException;
+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.shared.JenaException;
+import org.apache.jena.shared.OperationDeniedException;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.graph.GraphFactory;
+import org.apache.jena.web.HttpSC;
+
+/*package*/ class GSPLib {
 
-public class GSPLib {
-    
     /** Test whether the operation has either of the GSP parameters. */
-    public static boolean hasGSPParams(HttpAction action) {
+    /*package*/ static boolean hasGSPParams(HttpAction action) {
         if ( action.getRequestQueryString() == null )
             return false;
         boolean hasParamGraphDefault = 
action.getRequestParameter(HttpNames.paramGraphDefault) != null;
@@ -42,8 +65,8 @@ public class GSPLib {
         return false;
     }
 
-    /** Test whether the operation has exactly one GSP parameter and no other 
parameters. */ 
-    public static boolean hasGSPParamsStrict(HttpAction action) {
+    /** Test whether the operation has exactly one GSP parameter and no other 
parameters. */
+    /*package*/ static boolean hasGSPParamsStrict(HttpAction action) {
         if ( action.getRequestQueryString() == null )
             return false;
         Map<String, String[]> params = action.getRequestParameterMap();
@@ -56,7 +79,7 @@ public class GSPLib {
     }
 
     /** Check whether there is exactly one HTTP header value */
-    public static boolean hasExactlyOneValue(HttpAction action, String name) {
+    /*package*/ static boolean hasExactlyOneValue(HttpAction action, String 
name) {
         String[] values = action.getRequestParameterValues(name);
         if ( values == null )
             return false;
@@ -71,7 +94,7 @@ public class GSPLib {
      * Multiple values causes an exception.
      * No value returns null.
      */
-    public static String getOneOnly(HttpServletRequest request, String name) {
+    /*package*/ static String getOneOnly(HttpServletRequest request, String 
name) {
         String[] values = request.getParameterValues(name);
         if ( values == null )
             return null;
@@ -81,4 +104,218 @@ public class GSPLib {
             ServletOps.errorBadRequest("Multiple occurrences of '" + name + 
"'");
         return values[0];
     }
+
+    /*package*/ static String fullURI(HttpAction action) {
+        return action.getRequest().getRequestURL().toString();
+    }
+
+    /*package*/ static void execGetGSP(HttpAction action, Function<HttpAction, 
DatasetGraph> selectDataset) {
+        ActionLib.setCommonHeaders(action);
+        MediaType mediaType = ActionLib.contentNegotationRDF(action);
+
+        OutputStream output;
+        try { output = action.getResponseOutputStream(); }
+        catch (IOException ex) { ServletOps.errorOccurred(ex); output = null; }
+
+        Lang lang = 
RDFLanguages.contentTypeToLang(mediaType.getContentTypeStr());
+        if ( lang == null )
+            lang = RDFLanguages.TURTLE;
+
+        action.beginRead();
+        if ( action.verbose )
+            action.log.info(format("[%d]   Get: Content-Type=%s, Charset=%s => 
%s",
+                            action.id, mediaType.getContentTypeStr(), 
mediaType.getCharset(), lang.getName()));
+        try {
+            DatasetGraph dsg = selectDataset.apply(action);
+            GraphTarget target = determineTargetGSP(dsg, action);
+            if ( action.log.isDebugEnabled() )
+                action.log.debug("GET->"+target);
+            boolean exists = target.exists();
+            if ( ! exists )
+                ServletOps.errorNotFound("No such graph: "+target.label());
+            Graph graph = target.graph();
+            try {
+                // Use the preferred MIME type.
+                ActionLib.graphResponse(action, graph, lang);
+                ServletOps.success(action);
+            } catch (OperationDeniedException ex) {
+                throw ex;
+            } catch (JenaException ex) {
+                // ActionLib.graphResponse has special handling for RDF/XML 
but for
+                // other syntax forms unexpected errors mean we may or may not 
have
+                // written some output because of output buffering.
+                // Attempt to send an error - which may not work.
+                // "406 Not Acceptable" - Accept header issue; target is fine.
+                ServletOps.error(HttpSC.NOT_ACCEPTABLE_406, "Failed to write 
output: "+ex.getMessage());
+            }
+        } finally { action.endRead(); }
+    }
+
+
+
+    /** Directly add data in a transaction.
+     * Assumes recovery from parse errors by transaction abort.
+     * Return whether the target existed before.
+     */
+    /*package*/ static UploadDetails triplesPutPostTxn(HttpAction action, 
boolean replaceOperation, Function<HttpAction, DatasetGraph> selectDataset) {
+        action.beginWrite();
+        try {
+            DatasetGraph dsg = selectDataset.apply(action);
+            GraphTarget target = GraphTarget.determineTargetGSP(dsg, action);
+            if ( action.log.isDebugEnabled() )
+                
action.log.debug(action.getRequestMethod().toUpperCase()+"->"+target);
+            if ( target.isUnion() )
+                ServletOps.errorBadRequest("Can't load into the union graph");
+            // Check URI.
+            if ( ! target.isDefault() && target.graphName() != null && ! 
target.graphName().isBlank()) {
+                String uri = target.graphName().getURI();
+                try {
+                    Validators.graphName(uri);
+                } catch (FusekiConfigException ex) {
+                    ServletOps.errorBadRequest("Bad URI: "+uri);
+                    return null;
+                }
+            }
+
+            boolean existedBefore = target.exists();
+            Graph g = target.graph();
+            if ( replaceOperation && existedBefore )
+                clearGraph(target);
+            StreamRDF sink = StreamRDFLib.graph(g);
+            UploadDetails upload = DataUploader.incomingData(action, sink);
+            upload.setExistedBefore(existedBefore);
+            action.commit();
+            return upload;
+        } catch (RiotParseException ex) {
+            action.abortSilent();
+            ServletOps.errorParseError(ex);
+            return null;
+        } catch (RiotException ex) {
+            // Parse error
+            action.abortSilent();
+            ServletOps.errorBadRequest(ex.getMessage());
+            return null;
+        } catch (OperationDeniedException ex) {
+            action.abortSilent();
+            throw ex;
+        } catch (ActionErrorException ex) {
+            // Any ServletOps.error from calls in the try{} block.
+            action.abortSilent();
+            throw ex;
+        } catch (Exception ex) {
+            // Something unexpected.
+            action.abortSilent();
+            ServletOps.errorOccurred(ex.getMessage());
+            return null;
+        } finally {
+            action.endWrite();
+        }
+    }
+
+    /**
+     * Add data where the destination does not support full transactions.
+     * In particular, with no abort, and actions probably going to the real 
storage
+     * parse errors can lead to partial updates.  Instead, parse to a temporary
+     * graph, then insert that data.
+     */
+    /*package*/ static UploadDetails triplesPutPostNonTxn(HttpAction action, 
boolean replaceOperation, Function<HttpAction, DatasetGraph> selectDataset) {
+        Graph graphTmp = GraphFactory.createGraphMem();
+        StreamRDF dest = StreamRDFLib.graph(graphTmp);
+
+        UploadDetails details;
+        try { details = DataUploader.incomingData(action, dest); }
+        catch (RiotParseException ex) {
+            ServletOps.errorParseError(ex);
+            return null;
+        }
+        // Now insert into dataset
+        action.beginWrite();
+        try {
+            DatasetGraph dsg = selectDataset.apply(action);
+            GraphTarget target = GraphTarget.determineTargetGSP(dsg, action);
+            if ( action.log.isDebugEnabled() )
+                action.log.debug("  ->"+target);
+            if ( target.isUnion() )
+                ServletOps.errorBadRequest("Can't load into the union graph");
+            boolean existedBefore = target.exists();
+            if ( replaceOperation && existedBefore )
+                clearGraph(target);
+            FusekiNetLib.addDataInto(graphTmp, target.dataset(), 
target.graphName());
+            details.setExistedBefore(existedBefore);
+            action.commit();
+            return details;
+        } catch (OperationDeniedException ex) {
+            action.abortSilent();
+            throw ex;
+        } catch (Exception ex) {
+            // We parsed into a temporary graph so an exception at this point
+            // is not because of a parse error.
+            // We're in the non-transactional branch, this probably will not 
work
+            // but it might and there is no harm safely trying.
+            action.abortSilent();
+            ServletOps.errorOccurred(ex.getMessage());
+            return null;
+        } finally { action.endWrite(); }
+    }
+
+    /*package*/ static void execHeadGSP(HttpAction action, 
Function<HttpAction, DatasetGraph> selectDataset) {
+        ActionLib.setCommonHeaders(action);
+        MediaType mediaType = ActionLib.contentNegotationRDF(action);
+        if ( action.verbose )
+            action.log.info(format("[%d]   Head: Content-Type=%s", action.id, 
mediaType.getContentTypeStr()));
+        // Check graph not 404.
+        action.beginRead();
+        try {
+            DatasetGraph dsg = selectDataset.apply(action);
+            GraphTarget target = determineTargetGSP(dsg, action);
+            if ( action.log.isDebugEnabled() )
+                action.log.debug("HEAD->"+target);
+            boolean exists = target.exists();
+            if ( ! exists )
+                ServletOps.errorNotFound("No such graph: "+target.label());
+            ServletOps.success(action);
+        } finally { action.endRead(); }
+    }
+
+    /*package*/ static  void execDeleteGSP(HttpAction action, 
Function<HttpAction, DatasetGraph> selectDataset) {
+        action.beginWrite();
+        boolean haveCommited = false;
+        try {
+            DatasetGraph dsg = selectDataset.apply(action);
+            GraphTarget target = determineTargetGSP(dsg, action);
+            if ( action.log.isDebugEnabled() )
+                action.log.debug("DELETE->"+target);
+            if ( target.isUnion() )
+                ServletOps.errorBadRequest("Can't delete the union graph");
+            boolean existedBefore = target.exists();
+            if ( !existedBefore ) {
+                // Commit, not abort, because locking "transactions" don't 
support abort.
+                action.commit();
+                haveCommited = true;
+                ServletOps.errorNotFound("No such graph: "+target.label());
+            }
+            deleteGraph(target);
+            action.commit();
+            haveCommited = true;
+        }
+        catch (ActionErrorException ex) { throw ex; }
+        catch (Exception ex) { action.abortSilent(); }
+        finally { action.endWrite(); }
+        ServletOps.successNoContent(action);
+    }
+
+    /*package*/ static void deleteGraph(GraphTarget target) {
+        if ( target.isDefault() )
+            clearGraph(target);
+        else
+            target.dataset().removeGraph(target.graphName());
+    }
+
+    /** Clear a graph - this leaves the storage choice and setup in-place */
+    /*package*/ static void clearGraph(GraphTarget target) {
+        Graph g = target.graph();
+        g.getPrefixMapping().clearNsPrefixMap();
+        g.clear();
+    }
+
 }
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_Direct_R.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_Direct_R.java
new file mode 100644
index 0000000000..83591d5feb
--- /dev/null
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_Direct_R.java
@@ -0,0 +1,110 @@
+/*
+ * 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
+ *
+ *   https://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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.apache.jena.fuseki.servlets;
+
+import static java.lang.String.format;
+import static org.apache.jena.fuseki.servlets.GraphTarget.determineTargetGSP;
+
+import java.util.function.Function;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.apache.jena.atlas.web.MediaType;
+import org.apache.jena.riot.web.HttpNames;
+import org.apache.jena.sparql.core.DatasetGraph;
+
+public class GSP_Direct_R extends ActionREST {
+
+    private static String defaultDirect = "$default$" ;
+
+    // triples only!
+    public GSP_Direct_R() {}
+
+    @Override
+    public void validate(HttpAction action) {
+        // No query string allowed
+        HttpServletRequest request = action.getRequest();
+        if ( request.getQueryString() != null )
+            ServletOps.errorBadRequest("Query string not allowed");
+    }
+
+    @Override
+    protected void doGet(HttpAction action) {
+        GSPLib.execGetGSP(action, this::decideDataset);
+    }
+
+    private void notSupported(HttpAction action) {
+        ServletOps.errorMethodNotAllowed(action.getRequestMethod()+" 
"+action.getDatasetName());
+    }
+
+    protected void readOnly(HttpAction action) {
+        ServletOps.errorMethodNotAllowed(action.getRequestMethod()+" : 
Read-only");
+    }
+
+    // Share?
+    @Override
+    protected void doHead(HttpAction action) {
+        execHeadGSP(action, this::decideDataset);
+    }
+
+    // Share with GSP_R via GSP_Ops
+    protected static void execHeadGSP(HttpAction action, Function<HttpAction, 
DatasetGraph> decideDataset) {
+        ActionLib.setCommonHeaders(action);
+        MediaType mediaType = ActionLib.contentNegotationRDF(action);
+        if ( action.verbose )
+            action.log.info(format("[%d]   Head: Content-Type=%s", action.id, 
mediaType.getContentTypeStr()));
+        // Check graph not 404.
+        action.beginRead();
+        try {
+            DatasetGraph dsg = decideDataset.apply(action);
+            GraphTarget target = determineTargetGSP(dsg, action);
+            if ( action.log.isDebugEnabled() )
+                action.log.debug("HEAD->"+target);
+            boolean exists = target.exists();
+            if ( ! exists )
+                ServletOps.errorNotFound("No such graph: "+target.label());
+            ServletOps.success(action);
+        } finally { action.endRead(); }
+    }
+
+    @Override
+    protected void doOptions(HttpAction action) {
+        ActionLib.setCommonHeadersForOptions(action);
+        action.setResponseHeader(HttpNames.hAllow, "GET,HEAD,OPTIONS");
+        ServletOps.success(action);
+    }
+
+    @Override
+    protected void doPost(HttpAction action)
+    { readOnly(action); }
+
+    @Override
+    protected void doPut(HttpAction action)
+    { readOnly(action); }
+
+    @Override
+    protected void doDelete(HttpAction action)
+    { readOnly(action); }
+
+    @Override
+    protected void doPatch(HttpAction action)
+    { notSupported(action); }
+}
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_Direct_RW.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_Direct_RW.java
new file mode 100644
index 0000000000..2e87c22a4a
--- /dev/null
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_Direct_RW.java
@@ -0,0 +1,80 @@
+/*
+ * 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
+ *
+ *   https://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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.apache.jena.fuseki.servlets;
+
+import org.apache.jena.atlas.web.ContentType;
+import org.apache.jena.fuseki.system.UploadDetails;
+import org.apache.jena.riot.web.HttpNames;
+
+public class GSP_Direct_RW extends GSP_Direct_R {
+
+    // triples only!
+    public GSP_Direct_RW() {}
+
+    @Override
+    protected void doPost(HttpAction action) {
+        doPutPostDirect(action, false);
+    }
+
+    @Override
+    protected void doPut(HttpAction action) {
+        doPutPostDirect(action, true);
+    }
+
+   private void doPutPostDirect(HttpAction action, boolean replace) {
+        ContentType ct = ActionLib.getContentType(action);
+        if ( ct == null )
+            ServletOps.errorBadRequest("No Content-Type:");
+
+        UploadDetails details;
+        if ( action.isTransactional() )
+            details = GSPLib.triplesPutPostTxn(action, replace, 
this::decideDataset);
+        else
+            details = GSPLib.triplesPutPostNonTxn(action, replace, 
this::decideDataset);
+        ServletOps.uploadResponse(action, details);
+    }
+
+    @Override
+    protected void doDelete(HttpAction action) {
+        GSPLib.execDeleteGSP(action, this::decideDataset);
+    }
+
+    @Override
+    protected void doHead(HttpAction action) {
+        GSPLib.execHeadGSP(action, this::decideDataset);
+    }
+
+    @Override
+    protected void doOptions(HttpAction action) {
+        ActionLib.setCommonHeadersForOptions(action);
+        action.setResponseHeader(HttpNames.hAllow, 
"GET,HEAD,OPTIONS,PUT,DELETE,POST");
+        ServletOps.success(action);
+    }
+
+    @Override
+    protected void doPatch(HttpAction action)
+    { notSupported(action); }
+
+    private void notSupported(HttpAction action) {
+        ServletOps.errorMethodNotAllowed(action.getRequestMethod()+" 
"+action.getDatasetName());
+    }
+}
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_R.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_R.java
index a5acde7bc4..020374ebf8 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_R.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_R.java
@@ -22,15 +22,11 @@
 package org.apache.jena.fuseki.servlets;
 
 import static java.lang.String.format;
-import static org.apache.jena.fuseki.servlets.GraphTarget.determineTargetGSP;
 
 import java.io.IOException;
-import java.io.OutputStream;
 
 import jakarta.servlet.ServletOutputStream;
-
 import org.apache.jena.atlas.web.MediaType;
-import org.apache.jena.graph.Graph;
 import org.apache.jena.riot.Lang;
 import org.apache.jena.riot.RDFLanguages;
 import org.apache.jena.riot.web.HttpNames;
@@ -48,7 +44,7 @@ public class GSP_R extends GSP_Base {
         if ( isQuads(action) )
             execGetQuads(action);
         else
-            execGetGSP(action);
+            GSPLib.execGetGSP(action, this::decideDataset);
     }
 
     protected void execGetQuads(HttpAction action) {
@@ -88,49 +84,6 @@ public class GSP_R extends GSP_Base {
         }
     }
 
-    protected void execGetGSP(HttpAction action) {
-        ActionLib.setCommonHeaders(action);
-        MediaType mediaType = ActionLib.contentNegotationRDF(action);
-
-        OutputStream output;
-        try { output = action.getResponseOutputStream(); }
-        catch (IOException ex) { ServletOps.errorOccurred(ex); output = null; }
-
-        Lang lang = 
RDFLanguages.contentTypeToLang(mediaType.getContentTypeStr());
-        if ( lang == null )
-            lang = RDFLanguages.TURTLE;
-
-        action.beginRead();
-        if ( action.verbose )
-            action.log.info(format("[%d]   Get: Content-Type=%s, Charset=%s => 
%s",
-                            action.id, mediaType.getContentTypeStr(), 
mediaType.getCharset(), lang.getName()));
-        try {
-            DatasetGraph dsg = decideDataset(action);
-            GraphTarget target = determineTargetGSP(dsg, action);
-            if ( action.log.isDebugEnabled() )
-                action.log.debug("GET->"+target);
-            boolean exists = target.exists();
-            if ( ! exists )
-                ServletOps.errorNotFound("No such graph: "+target.label());
-            Graph graph = target.graph();
-            // Special case RDF/XML to be the plain (faster, less readable) 
form
-            try {
-                // Use the preferred MIME type.
-                ActionLib.graphResponse(action, graph, lang);
-                ServletOps.success(action);
-            } catch (OperationDeniedException ex) {
-                throw ex;
-            } catch (JenaException ex) {
-                // ActionLib.graphResponse has special handling for RDF/XML 
but for
-                // other syntax forms unexpected errors mean we may or may not 
have
-                // written some output because of output buffering.
-                // Attempt to send an error - which may not work.
-                // "406 Not Acceptable" - Accept header issue; target is fine.
-                ServletOps.error(HttpSC.NOT_ACCEPTABLE_406, "Failed to write 
output: "+ex.getMessage());
-            }
-        } finally { action.endRead(); }
-    }
-
     @Override
     protected void doOptions(HttpAction action) {
         ActionLib.setCommonHeadersForOptions(action);
@@ -143,7 +96,7 @@ public class GSP_R extends GSP_Base {
         if ( isQuads(action) )
             execHeadQuads(action);
         else
-            execHeadGSP(action);
+            GSPLib.execHeadGSP(action, this::decideDataset);
     }
 
     protected void execHeadQuads(HttpAction action) {
@@ -154,25 +107,6 @@ public class GSP_R extends GSP_Base {
         ServletOps.success(action);
     }
 
-    protected void execHeadGSP(HttpAction action) {
-        ActionLib.setCommonHeaders(action);
-        MediaType mediaType = ActionLib.contentNegotationRDF(action);
-        if ( action.verbose )
-            action.log.info(format("[%d]   Head: Content-Type=%s", action.id, 
mediaType.getContentTypeStr()));
-        // Check graph not 404.
-        action.beginRead();
-        try {
-            DatasetGraph dsg = decideDataset(action);
-            GraphTarget target = determineTargetGSP(dsg, action);
-            if ( action.log.isDebugEnabled() )
-                action.log.debug("HEAD->"+target);
-            boolean exists = target.exists();
-            if ( ! exists )
-                ServletOps.errorNotFound("No such graph: "+target.label());
-            ServletOps.success(action);
-        } finally { action.endRead(); }
-    }
-
     @Override
     protected void doPost(HttpAction action)
     { ServletOps.errorMethodNotAllowed("POST : Read-only"); }
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_RW.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_RW.java
index 58bce75b37..d42359b48c 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_RW.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_RW.java
@@ -24,20 +24,10 @@ package org.apache.jena.fuseki.servlets;
 import static org.apache.jena.fuseki.servlets.GraphTarget.determineTargetGSP;
 
 import org.apache.jena.atlas.web.ContentType;
-import org.apache.jena.fuseki.FusekiConfigException;
-import org.apache.jena.fuseki.server.Validators;
-import org.apache.jena.fuseki.system.DataUploader;
-import org.apache.jena.fuseki.system.FusekiNetLib;
 import org.apache.jena.fuseki.system.UploadDetails;
 import org.apache.jena.graph.Graph;
-import org.apache.jena.riot.RiotException;
-import org.apache.jena.riot.RiotParseException;
-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.shared.OperationDeniedException;
 import org.apache.jena.sparql.core.DatasetGraph;
-import org.apache.jena.sparql.graph.GraphFactory;
 
 public class GSP_RW extends GSP_R {
 
@@ -58,8 +48,8 @@ public class GSP_RW extends GSP_R {
         if ( isQuads(action) )
             execDeleteQuads(action);
         else
-            execDeleteGSP(action);
-   }
+            GSPLib.execDeleteGSP(action, this::decideDataset);
+    }
 
     @Override
     protected void doPut(HttpAction action) {
@@ -85,33 +75,6 @@ public class GSP_RW extends GSP_R {
 
     protected void execPutQuads(HttpAction action) { doPutPostQuads(action, 
true); }
 
-    protected void execDeleteGSP(HttpAction action) {
-        action.beginWrite();
-        boolean haveCommited = false;
-        try {
-            DatasetGraph dsg = decideDataset(action);
-            GraphTarget target = determineTargetGSP(dsg, action);
-            if ( action.log.isDebugEnabled() )
-                action.log.debug("DELETE->"+target);
-            if ( target.isUnion() )
-                ServletOps.errorBadRequest("Can't delete the union graph");
-            boolean existedBefore = target.exists();
-            if ( !existedBefore ) {
-                // Commit, not abort, because locking "transactions" don't 
support abort.
-                action.commit();
-                haveCommited = true;
-                ServletOps.errorNotFound("No such graph: "+target.label());
-            }
-            deleteGraph(dsg, action);
-            action.commit();
-            haveCommited = true;
-        }
-        catch (ActionErrorException ex) { throw ex; }
-        catch (Exception ex) { action.abortSilent(); }
-        finally { action.endWrite(); }
-        ServletOps.successNoContent(action);
-    }
-
     protected void execDeleteQuads(HttpAction action) {
         // Don't allow whole-database DELETE.
         ServletOps.errorMethodNotAllowed("DELETE");
@@ -135,6 +98,7 @@ public class GSP_RW extends GSP_R {
         g.clear();
     }
 
+    // ---- Quads to dataset
     protected void doPutPostQuads(HttpAction action, boolean replaceOperation) 
{
         ContentType ct = ActionLib.getContentType(action);
         if ( ct == null )
@@ -142,9 +106,9 @@ public class GSP_RW extends GSP_R {
 
         UploadDetails details;
         if ( action.isTransactional() )
-            details = UploadRDF.quadsPutPostTxn(action, a->decideDataset(a), 
replaceOperation);
+            details = UploadRDF.quadsPutPostTxn(action, this::decideDataset, 
replaceOperation);
         else
-            details = UploadRDF.quadsPutPostNonTxn(action, 
a->decideDataset(a), replaceOperation);
+            details = UploadRDF.quadsPutPostNonTxn(action, 
this::decideDataset, replaceOperation);
         ServletOps.uploadResponse(action, details);
     }
 
@@ -157,113 +121,9 @@ public class GSP_RW extends GSP_R {
 
         UploadDetails details;
         if ( action.isTransactional() )
-            details = triplesPutPostTxn(action, overwrite);
+            details = GSPLib.triplesPutPostTxn(action, overwrite, 
this::decideDataset);
         else
-            details = triplesPutPostNonTxn(action, overwrite);
+            details = GSPLib.triplesPutPostNonTxn(action, overwrite, 
this::decideDataset);
         ServletOps.uploadResponse(action, details);
     }
-
-    /** Directly add data in a transaction.
-     * Assumes recovery from parse errors by transaction abort.
-     * Return whether the target existed before.
-     */
-    private UploadDetails triplesPutPostTxn(HttpAction action, boolean 
replaceOperation) {
-        action.beginWrite();
-        try {
-            DatasetGraph dsg = decideDataset(action);
-            GraphTarget target = determineTargetGSP(dsg, action);
-            if ( action.log.isDebugEnabled() )
-                
action.log.debug(action.getRequestMethod().toUpperCase()+"->"+target);
-            if ( target.isUnion() )
-                ServletOps.errorBadRequest("Can't load into the union graph");
-            // Check URI.
-            if ( ! target.isDefault() && target.graphName() != null && ! 
target.graphName().isBlank()) {
-                String uri = target.graphName().getURI();
-                try {
-                    Validators.graphName(uri);
-                } catch (FusekiConfigException ex) {
-                    ServletOps.errorBadRequest("Bad URI: "+uri);
-                    return null;
-                }
-            }
-
-            boolean existedBefore = target.exists();
-            Graph g = target.graph();
-            if ( replaceOperation && existedBefore )
-                clearGraph(target);
-            StreamRDF sink = StreamRDFLib.graph(g);
-            UploadDetails upload = DataUploader.incomingData(action, sink);
-            upload.setExistedBefore(existedBefore);
-            action.commit();
-            return upload;
-        } catch (RiotParseException ex) {
-            action.abortSilent();
-            ServletOps.errorParseError(ex);
-            return null;
-        } catch (RiotException ex) {
-            // Parse error
-            action.abortSilent();
-            ServletOps.errorBadRequest(ex.getMessage());
-            return null;
-        } catch (OperationDeniedException ex) {
-            action.abortSilent();
-            throw ex;
-        } catch (ActionErrorException ex) {
-            // Any ServletOps.error from calls in the try{} block.
-            action.abortSilent();
-            throw ex;
-        } catch (Exception ex) {
-            // Something unexpected.
-            action.abortSilent();
-            ServletOps.errorOccurred(ex.getMessage());
-            return null;
-        } finally {
-            action.endWrite();
-        }
-    }
-
-    /** Add data where the destination does not support full transactions.
-     *  In particular, with no abort, and actions probably going to the real 
storage
-     *  parse errors can lead to partial updates.  Instead, parse to a 
temporary
-     *  graph, then insert that data.
-     */
-    private UploadDetails triplesPutPostNonTxn(HttpAction action, boolean 
replaceOperation) {
-        Graph graphTmp = GraphFactory.createGraphMem();
-        StreamRDF dest = StreamRDFLib.graph(graphTmp);
-
-        UploadDetails details;
-        try { details = DataUploader.incomingData(action, dest); }
-        catch (RiotParseException ex) {
-            ServletOps.errorParseError(ex);
-            return null;
-        }
-        // Now insert into dataset
-        action.beginWrite();
-        try {
-            DatasetGraph dsg = decideDataset(action);
-            GraphTarget target = determineTargetGSP(dsg, action);
-            if ( action.log.isDebugEnabled() )
-                action.log.debug("  ->"+target);
-            if ( target.isUnion() )
-                ServletOps.errorBadRequest("Can't load into the union graph");
-            boolean existedBefore = target.exists();
-            if ( replaceOperation && existedBefore )
-                clearGraph(target);
-            FusekiNetLib.addDataInto(graphTmp, target.dataset(), 
target.graphName());
-            details.setExistedBefore(existedBefore);
-            action.commit();
-            return details;
-        } catch (OperationDeniedException ex) {
-            action.abortSilent();
-            throw ex;
-        } catch (Exception ex) {
-            // We parsed into a temporary graph so an exception at this point
-            // is not because of a parse error.
-            // We're in the non-transactional branch, this probably will not 
work
-            // but it might and there is no harm safely trying.
-            action.abortSilent();
-            ServletOps.errorOccurred(ex.getMessage());
-            return null;
-        } finally { action.endWrite(); }
-    }
 }
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GraphTarget.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GraphTarget.java
index ef4a34355c..9effce1f5b 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GraphTarget.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GraphTarget.java
@@ -36,9 +36,9 @@ import org.apache.jena.riot.web.HttpNames;
 import org.apache.jena.sparql.core.DatasetGraph;
 
 /**
- * Target of GSP operations.<br/>
+ * Target of GSP operations.
+ * <p>
  * Extensions: "?graph=union" and "?graph=default"
- *
  */
 public class GraphTarget {
 
@@ -66,11 +66,16 @@ public class GraphTarget {
             if ( ! allowDirectNaming )
                 ServletOps.errorBadRequest("Neither default graph nor named 
graph specified");
 
-            // Direct naming.
-            String directName = action.getRequestRequestURL().toString();
-            if ( action.getRequestRequestURI().equals(action.getDatasetName()) 
)
-                // No name (should have been a quads operations).
+            // GSP Direct Naming.
+            String directName = GSPLib.fullURI(action);
+            String requestURI = action.getRequestRequestURI();
+
+            // NB Use URI here - no host/port.
+            if ( directName == null || 
requestURI.equals(action.getDatasetName()) )
                 ServletOps.errorBadRequest("Neither default graph nor named 
graph specified and no direct name");
+            if ( action.getDatasetName().startsWith(requestURI) )
+                ServletOps.errorBadRequest("Direct name does not have the 
dataset name as a prefix: "+requestURI);
+
             Node gn = NodeFactory.createURI(directName);
             return createNamed(dsg, gn);
         }
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/HttpAction.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/HttpAction.java
index bf2ae9ce49..bd296c14d0 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/HttpAction.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/HttpAction.java
@@ -710,6 +710,10 @@ public class HttpAction
         return request.getInputStream();
     }
 
+    public boolean hasRequestQueryString() {
+        return request.getQueryString() != null;
+    }
+
     public String getRequestQueryString() {
         return request.getQueryString();
     }
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/test/java/org/apache/jena/fuseki/server/TestDispatchOnURI.java
 
b/jena-fuseki2/jena-fuseki-core/src/test/java/org/apache/jena/fuseki/server/TestDispatchOnURI.java
index 176b1d9d30..052a17da1f 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/test/java/org/apache/jena/fuseki/server/TestDispatchOnURI.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/test/java/org/apache/jena/fuseki/server/TestDispatchOnURI.java
@@ -119,12 +119,12 @@ public class TestDispatchOnURI {
     }
 
     private void testNoDispatch(String requestURI, DataAccessPointRegistry 
registry) {
-        DataAccessPoint dap = Dispatcher.locateDataAccessPoint(requestURI, 
registry);
+        DataAccessPoint dap = Dispatcher.locateDataAccessPointTest(requestURI, 
registry);
         assertNull(dap, "Expected no dispatch for "+requestURI);
     }
 
     private void testDispatch(String requestURI, DataAccessPointRegistry 
registry, String expectedDataset, String expectedEndpoint) {
-        DataAccessPoint dap = Dispatcher.locateDataAccessPoint(requestURI, 
registry);
+        DataAccessPoint dap = Dispatcher.locateDataAccessPointTest(requestURI, 
registry);
         if ( dap == null ) {
             if ( expectedDataset != null )
                 fail("No DataAccessPoint: expected to find a match: 
"+requestURI+" -> ("+expectedDataset+", "+expectedEndpoint+")");
diff --git 
a/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TS_SparqlExecHttp.java
 
b/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TS_SparqlExecHttp.java
index 0cb38f3033..fa19024e83 100644
--- 
a/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TS_SparqlExecHttp.java
+++ 
b/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TS_SparqlExecHttp.java
@@ -28,6 +28,7 @@ import org.junit.platform.suite.api.Suite;
 @SelectClasses({
     TestGSP.class
     , TestDSP.class
+    , TestGSPDirect.class
     , TestModelStore.class
     //, TestModelStore2.class
     , TestQueryExecHTTP.class
diff --git 
a/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestGSPDirect.java
 
b/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestGSPDirect.java
new file mode 100644
index 0000000000..c21ec642b9
--- /dev/null
+++ 
b/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestGSPDirect.java
@@ -0,0 +1,336 @@
+/*
+ * 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
+ *
+ *   https://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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.apache.jena.sparql.exec.http;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
+
+import org.apache.jena.atlas.lib.StrUtils;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.FusekiTestLib;
+import org.apache.jena.fuseki.server.DataService;
+import org.apache.jena.fuseki.server.Operation;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.http.HttpOp;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFParser;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.graph.GraphFactory;
+import org.apache.jena.sparql.sse.SSE;
+import org.apache.jena.web.HttpSC;
+
+/** Tests specifically for GSP direct naming. */
+@EnabledIf(value="isEnabled_GSPDirectNaming", disabledReason 
="GSP_DIRECT_NAMING not enabled")
+public class TestGSPDirect {
+
+    static boolean isEnabled_GSPDirectNaming() {
+        return Fuseki.GSP_DIRECT_NAMING;
+    }
+
+    private FusekiServer server = null;
+    private final boolean verbose = false;
+
+    private final String dsName = "/graphs";
+    private final String endpoint = "graphs";
+
+    private static String configNamedService = """
+            PREFIX :        <http://test/config#>
+            PREFIX fuseki:  <http://jena.apache.org/fuseki#>
+            PREFIX rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+            PREFIX ja:      <http://jena.hpl.hp.com/2005/11/Assembler#>
+
+            :service rdf:type fuseki:Service ;
+                fuseki:name "/gsp-direct" ;
+                fuseki:endpoint [ fuseki:operation fuseki:gsp-direct-rw ; ] ;
+                fuseki:endpoint [ fuseki:operation fuseki:query ; ] ;
+                fuseki:dataset [ rdf:type ja:MemoryDataset; ]
+            .
+            """;
+    private static String configTwoServices = """
+            PREFIX :        <http://test/config#>
+            PREFIX fuseki:  <http://jena.apache.org/fuseki#>
+            PREFIX rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+            PREFIX ja:      <http://jena.hpl.hp.com/2005/11/Assembler#>
+
+            ## Load data with indirect GSP
+            :service1 rdf:type fuseki:Service ;
+                fuseki:name "/gsp-rw" ;
+                fuseki:endpoint [ fuseki:operation fuseki:gsp-rw ; ] ;
+                fuseki:dataset :dataset ;
+                .
+            ## Access with direct
+            :service2 rdf:type fuseki:Service ;
+                fuseki:name "/gsp-direct-r" ;
+                fuseki:endpoint [ fuseki:operation fuseki:gsp-direct-r ; ] ;
+                fuseki:dataset :dataset ;
+                .
+
+            :dataset rdf:type ja:MemoryDataset .
+            """;
+    //At the root!
+    private static String configRootService = """
+            PREFIX :        <http://test/config#>
+            PREFIX fuseki:  <http://jena.apache.org/fuseki#>
+            PREFIX rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+            PREFIX ja:      <http://jena.hpl.hp.com/2005/11/Assembler#>
+
+            :service rdf:type fuseki:Service ;
+                fuseki:name "/" ;
+                fuseki:endpoint [ fuseki:operation fuseki:gsp-direct-rw ; ] ;
+                fuseki:endpoint [ fuseki:operation fuseki:query ; ] ;
+                fuseki:dataset [ rdf:type ja:MemoryDataset; ]
+            .
+            """;
+
+
+    // Not @BeforeEach - some servers are different.
+    public void makeServer() {
+        server = makeServer(dsName, endpoint, verbose);
+    }
+
+    private static FusekiServer makeServer(String dsName, String gspEndpoint, 
boolean verbose) {
+        DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+        DataService dSrv = DataService.newBuilder(dsg)
+                // Direct
+                .addEndpoint(Operation.GSP_Direct_RW)
+                // Indirect (i.e. ?default, ?graph=)
+                .addEndpoint(Operation.GSP_RW)
+                .build();
+        FusekiServer.create().add(dsName, dSrv).build();
+
+        FusekiServer server = FusekiServer.create()
+                .port(0)
+                .verbose(verbose)
+                .enablePing(true)
+                .add(gspEndpoint, dSrv)
+                .build()
+                .start();
+        return server;
+    }
+
+    @AfterEach
+    public void releaseServer() {
+        if ( server != null )
+            server.stop();
+    }
+
+    @Test
+    public void gsp_direct_POST_GET () {
+        makeServer();
+        String serviceURL = server.serverURL()+endpoint;
+        String URL1 = serviceURL+"/dir/file";
+        String URL2 = serviceURL+"/dir/noSuchData";
+
+        Graph data1 = GraphFactory.createDefaultGraph();
+        data1.add(SSE.parseTriple("(:s :p 123)"));
+
+        GSP.service(serviceURL).directGraphName(URL1).POST(data1);
+
+        Graph data2 = GraphFactory.createDefaultGraph();
+        data2.add(SSE.parseTriple("(:s :p 456)"));
+        GSP.service(serviceURL).directGraphName(URL1).POST(data2);
+
+        Graph g1 = GSP.service(serviceURL).directGraphName(URL1).GET();
+        assertEquals(2, g1.size());
+    }
+
+    @Test
+    public void gsp_direct_PUT_GET () {
+        makeServer();
+        String serviceURL = server.serverURL()+endpoint;
+        String URL1 = serviceURL+"/dir/file";
+        String URL2 = serviceURL+"/dir/noSuchData";
+
+        Graph data1 = GraphFactory.createDefaultGraph();
+        data1.add(SSE.parseTriple("(:s :p 123)"));
+
+        GSP.service(serviceURL).directGraphName(URL1).PUT(data1);
+
+        Graph data2 = GraphFactory.createDefaultGraph();
+        data2.add(SSE.parseTriple("(:s :p 456)"));
+        GSP.service(serviceURL).directGraphName(URL1).PUT(data2);
+
+        Graph fetch = GSP.service(serviceURL).directGraphName(URL1).GET();
+        assertEquals(1, fetch.size());
+    }
+
+    @Test
+    public void gsp_direct_PUT_DELETE_GET () {
+        makeServer();
+        String serviceURL = server.serverURL()+endpoint;
+        String URL1 = serviceURL+"/dir/file";
+        String URL2 = serviceURL+"/dir/noSuchData";
+
+        Graph data1 = GraphFactory.createDefaultGraph();
+        data1.add(SSE.parseTriple("(:s :p 123)"));
+        HttpException ex = assertThrows(HttpException.class, 
()->GSP.service(serviceURL).directGraphName(URL1).DELETE());
+        assertEquals(HttpSC.NOT_FOUND_404, ex.getStatusCode());
+    }
+
+
+    @Test
+    public void gsp_direct_POST_GET_different () {
+        makeServer();
+        String serviceURL = server.serverURL()+endpoint;
+        String URL1 = serviceURL+"/dir/file";
+        String URL2 = serviceURL+"/dir/noSuchData";
+
+        Graph data = GraphFactory.createDefaultGraph();
+        data.add(SSE.parseTriple("(:s :p :o)"));
+        GSP.service(serviceURL).directGraphName(URL1).POST(data);
+        FusekiTestLib.expect404(()-> 
GSP.service(serviceURL).directGraphName(URL2).GET() );
+    }
+
+    @Test
+    public void gsp_direct_POST_GET_byQuads () {
+        makeServer();
+        String serviceURL = server.serverURL()+endpoint;
+        String URL1 = serviceURL+"/dir/file1";
+
+        Graph data = GraphFactory.createDefaultGraph();
+        data.add(SSE.parseTriple("(:s :p :o)"));
+
+        GSP.service(serviceURL).directGraphName(URL1).POST(data);
+
+        DatasetGraph dsg = DSP.service(serviceURL).GET();
+        assertTrue(dsg.getDefaultGraph().isEmpty(), "Dataset default graph is 
not empty");
+
+        Node gn = NodeFactory.createURI(URL1);
+        assertEquals(1, dsg.getGraph(gn).size());
+    }
+
+    @Test
+    public void gsp_direct_POST_GET_usingDSP () {
+        makeServer();
+        // Fake using DSP.
+        String URL = server.serverURL()+endpoint+"/dir/file";
+        String service = server.serverURL()+endpoint;
+
+        Graph data = GraphFactory.createDefaultGraph();
+        data.add(SSE.parseTriple("(:s :p :o)"));
+
+        GSP.service(service).graphName(URL).POST(data);
+        // Retrieve
+
+        Graph g  = DSP.service(URL).GET().getDefaultGraph();
+        assertEquals(1, g.size());
+    }
+
+    @Test
+    public void gsp_direct_bad_URL() {
+        makeServer();
+        String serviceURL = server.serverURL()+endpoint;
+        String URL1 = "http://somewhere.test/dir/file1";;
+        HttpException ex = assertThrows(HttpException.class, 
()->GSP.service(serviceURL).directGraphName(URL1));
+        // Local error.
+        assertEquals(-1, ex.getStatusCode());
+        assertTrue(StrUtils.containsIgnoreCase(ex.getMessage(),"direct"));
+    }
+
+    @Test
+    public void gsp_direct_config_1() {
+        DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+        Graph configGraph = RDFParser.fromString(configNamedService, 
Lang.TTL).toGraph();
+        server = FusekiServer.create()
+                .port(0)
+                .verbose(verbose)
+                .enablePing(true)
+                .parseConfig(configGraph)
+                .build()
+                .start();
+        String pingStr = HttpOp.httpGetString(server.serverURL()+"$/ping");
+        assertNotNull(pingStr);
+
+        String serviceURL = server.datasetURL("/gsp-direct");
+        String URL1 = serviceURL+"/dir/file";
+        String URL2 = serviceURL+"/dir/noSuchData";
+        Graph data = GraphFactory.createDefaultGraph();
+        data.add(SSE.parseTriple("(:s :p :o)"));
+        GSP.service(serviceURL).directGraphName(URL1).POST(data);
+        FusekiTestLib.expect404(()-> 
GSP.service(serviceURL).directGraphName(URL2).GET() );
+    }
+
+    @Test
+    public void gsp_direct_config_2() {
+        DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+        Graph configGraph = RDFParser.fromString(configRootService, 
Lang.TTL).toGraph();
+        server = FusekiServer.create()
+                .port(0)
+                .verbose(verbose)
+                .enablePing(true)
+                .parseConfig(configGraph)
+                .build()
+                .start();
+        String serviceURL = server.datasetURL("/");
+        String URL1 = serviceURL+"dir/file";
+        String URL2 = serviceURL+"dir/noSuchData";
+        Graph data = GraphFactory.createDefaultGraph();
+        data.add(SSE.parseTriple("(:s :p :o)"));
+        GSP.service(serviceURL).directGraphName(URL1).POST(data);
+        FusekiTestLib.expect404(()-> 
GSP.service(serviceURL).directGraphName(URL2).GET() );
+
+        String pingStr = HttpOp.httpGetString(server.serverURL()+"$/ping");
+        assertNotNull(pingStr);
+    }
+
+    @Test
+    public void gsp_direct_config_3() {
+        DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+        Graph configGraph = RDFParser.fromString(configTwoServices, 
Lang.TTL).toGraph();
+        server = FusekiServer.create()
+                .port(0)
+                .verbose(verbose)
+                .parseConfig(configGraph)
+                .build()
+                .start();
+        String serviceURL1 = server.datasetURL("/gsp-rw");
+        String serviceURL2 = server.datasetURL("/gsp-direct-r");
+        String URL = serviceURL2+"/dir/file";
+
+        Graph data = GraphFactory.createDefaultGraph();
+        data.add(SSE.parseTriple("(:s :p 'abc')"));
+
+        // Write using GS indirect naming - graph name is use naming scheme of 
the read service.
+        GSP.service(serviceURL1).graphName(URL).POST(data);
+
+        // access
+        Graph read = GSP.service(serviceURL2).directGraphName(URL).GET();
+        assertEquals(1,read.size());
+
+        // attempt to delete
+        int sc = FusekiTestLib.expectFail(()-> 
GSP.service(serviceURL2).directGraphName(URL).DELETE() );
+        assertEquals(sc, HttpSC.METHOD_NOT_ALLOWED_405);
+    }
+
+
+}

Reply via email to