Merge pull request #745 from afs/graph-target

JENA-1897: Extract and share GSPTarget -> GraphTarget
diff --git a/jena-arq/src/main/java/org/apache/jena/riot/system/IRIResolver.java b/jena-arq/src/main/java/org/apache/jena/riot/system/IRIResolver.java
index a9cc216..3ab2625 100644
--- a/jena-arq/src/main/java/org/apache/jena/riot/system/IRIResolver.java
+++ b/jena-arq/src/main/java/org/apache/jena/riot/system/IRIResolver.java
@@ -274,10 +274,13 @@
         return exceptions(globalResolver.resolve(uriStr));
     }
 
-    /*
-     * No exception thrown by this method.
+    /**
+     * Resolve a string against a base.
+     * <p>
+     * No exceptions thrown by this method; the application should test the returned
+     * IRI for violations with {@link IRI#hasViolation(boolean)}.
      */
-    static private IRI resolveIRI(String relStr, String baseStr) {
+    public static IRI resolveIRI(String relStr, String baseStr) {
         IRI i = iriFactory().create(relStr);
         if (i.isAbsolute())
             // removes excess . segments
diff --git a/jena-arq/src/main/java/org/apache/jena/riot/web/HttpNames.java b/jena-arq/src/main/java/org/apache/jena/riot/web/HttpNames.java
index d340dd8..286b740 100644
--- a/jena-arq/src/main/java/org/apache/jena/riot/web/HttpNames.java
+++ b/jena-arq/src/main/java/org/apache/jena/riot/web/HttpNames.java
@@ -26,7 +26,7 @@
     public static final String hAcceptEncoding      = "Accept-Encoding" ;
     public static final String hAcceptCharset       = "Accept-Charset" ;
     public static final String hAcceptRanges        = "Accept-Ranges" ;
-    
+
     public static final String hAllow               = "Allow" ;
     public static final String hAuthorization       = "Authorization";
     public static final String hContentEncoding     = "Content-Encoding" ;
@@ -41,7 +41,7 @@
     public static final String hLocation            = "Location" ; 
     public static final String hVary                = "Vary" ;
     public static final String charset              = "charset" ;
-    
+
     // CORS: 
     //   http://www.w3.org/TR/cors/  http://esw.w3.org/CORS_Enabled
     public static final String hAccessControlAllowOrigin        = "Access-Control-Allow-Origin" ;
@@ -53,8 +53,8 @@
     public static final String hOrigin                          = "Origin" ;
     public static final String hAccessControlRequestMethod      = "Access-Control-Request-Method" ;
     public static final String hAccessControlRequestHeaders     = "Access-Control-Request-Headers" ;
-    
-    // Fuseki parameter names 
+
+    // GSP parameter names 
     public static final String paramGraph           = "graph" ;
     public static final String paramGraphDefault    = "default" ;
 
@@ -62,7 +62,7 @@
     public static final String paramQueryRef        = "query-ref" ;
     public static final String paramDefaultGraphURI = "default-graph-uri" ;
     public static final String paramNamedGraphURI   = "named-graph-uri" ;
-    
+
     public static final String paramStyleSheet      = "stylesheet" ;
     public static final String paramAccept          = "accept" ;
     public static final String paramOutput1         = "output" ;        // See Yahoo! developer: http://developer.yahoo.net/common/json.html 
@@ -87,6 +87,8 @@
 
     public static final String HEADER_IFMODSINCE    = "If-Modified-Since";
     public static final String HEADER_LASTMOD       = "Last-Modified";
-    
-    public static final String valueDefault    = "default" ;
+
+    // Special names for GSP targets (use in ?graph=)
+    public static final String graphTargetDefault   = "default" ;
+    public static final String graphTargetUnion     = "union" ;
 }
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 0c1ac15..b8c45d6 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
@@ -18,9 +18,39 @@
 
 package org.apache.jena.fuseki.servlets;
 
+import java.util.Map;
+
 import javax.servlet.http.HttpServletRequest;
 
+import org.apache.jena.riot.web.HttpNames;
+
 public class GSPLib {
+    
+    /** Test whether the operation has either of the GSP parameters. */
+    public static boolean hasGSPParams(HttpAction action) {
+        if ( action.request.getQueryString() == null )
+            return false;
+        boolean hasParamGraphDefault = action.request.getParameter(HttpNames.paramGraphDefault) != null;
+        if ( hasParamGraphDefault )
+            return true;
+        boolean hasParamGraph = action.request.getParameter(HttpNames.paramGraph) != null;
+        if ( hasParamGraph )
+            return true;
+        return false;
+    }
+
+    /** Test whether the operation has exactly one GSP parameter and no other parameters. */ 
+    public static boolean hasGSPParamsStrict(HttpAction action) {
+        if ( action.request.getQueryString() == null )
+            return false;
+        Map<String, String[]> params = action.request.getParameterMap();
+        if ( params.size() != 1 )
+            return false;
+        boolean hasParamGraphDefault = GSPLib.hasExactlyOneValue(action, HttpNames.paramGraphDefault);
+        boolean hasParamGraph = GSPLib.hasExactlyOneValue(action, HttpNames.paramGraph);
+        // Java XOR
+        return hasParamGraph ^ hasParamGraphDefault;
+    }
 
     /** Check whether there is exactly one HTTP header value */
     public static boolean hasExactlyOneValue(HttpAction action, String name) {
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSPTarget.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSPTarget.java
deleted file mode 100644
index df30e50..0000000
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSPTarget.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * 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.fuseki.servlets;
-
-import org.apache.jena.graph.Graph;
-import org.apache.jena.graph.Node;
-import org.apache.jena.sparql.core.DatasetGraph;
-
-/** Target of GSP operations */
-final class GSPTarget {
-    final boolean      isDefault;
-    final DatasetGraph dsg;
-    private Graph      _graph;
-    final String       name;
-    final Node         graphName;
-
-    static GSPTarget createNamed(DatasetGraph dsg, String name, Node graphName) {
-        return new GSPTarget(false, dsg, name, graphName);
-    }
-
-    static GSPTarget createDefault(DatasetGraph dsg) {
-        return new GSPTarget(true, dsg, null, null);
-    }
-
-    /**
-     * Create a new Target which is like the original but aimed at a different
-     * DatasetGraph
-     */
-    static GSPTarget retarget(GSPTarget target, DatasetGraph dsg) {
-        GSPTarget target2 = new GSPTarget(target, dsg);
-        target2._graph = null;
-        return target2;
-    }
-
-    private GSPTarget(boolean isDefault, DatasetGraph dsg, String name, Node graphName) {
-        this.isDefault = isDefault;
-        this.dsg = dsg;
-        this._graph = null;
-        this.name = name;
-        this.graphName = graphName;
-
-        if ( isDefault ) {
-            if ( name != null || graphName != null )
-                throw new IllegalArgumentException("Inconsistent: default and a graph name/node");
-        } else {
-            if ( name == null || graphName == null )
-                throw new IllegalArgumentException("Inconsistent: not default and/or no graph name/node");
-        }
-    }
-
-    private GSPTarget(GSPTarget other, DatasetGraph dsg) {
-        this.isDefault = other.isDefault;
-        this.dsg = dsg; // other.dsg;
-        this._graph = other._graph;
-        this.name = other.name;
-        this.graphName = other.graphName;
-    }
-
-    /**
-     * Get a graph for the action - this may create a graph in the dataset - this is
-     * not a test for graph existence
-     */
-    public Graph graph() {
-        if ( !isGraphSet() ) {
-            if ( isDefault )
-                _graph = dsg.getDefaultGraph();
-            else
-                _graph = dsg.getGraph(graphName);
-        }
-        return _graph;
-    }
-
-    public boolean exists() {
-        if ( isDefault )
-            return true;
-        return dsg.containsGraph(graphName);
-    }
-
-    public boolean isGraphSet() {
-        return _graph != null;
-    }
-
-    @Override
-    public String toString() {
-        if ( isDefault )
-            return "default";
-        return name;
-    }
-}
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_Base.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_Base.java
index f2118cf..8b4b916 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_Base.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GSP_Base.java
@@ -22,13 +22,7 @@
 
 import javax.servlet.http.HttpServletRequest;
 
-import org.apache.jena.fuseki.Fuseki;
-import org.apache.jena.graph.Node;
-import org.apache.jena.graph.NodeFactory;
-import org.apache.jena.riot.RiotException;
-import org.apache.jena.riot.system.IRIResolver;
 import org.apache.jena.riot.web.HttpNames;
-import org.apache.jena.sparql.core.DatasetGraph;
 
 public abstract class GSP_Base extends ActionREST {
 
@@ -85,58 +79,4 @@
                 ServletOps.errorBadRequest("Multiple parameters '" + h + "'");
         }
     }
-    
-    protected final static GSPTarget determineTarget(DatasetGraph dsg, HttpAction action) {
-        // Inside a transaction.
-        if ( dsg == null )
-            ServletOps.errorOccurred("Internal error : No action graph (not in a transaction?)");
-//        if ( ! dsg.isInTransaction() )
-//            ServletOps.errorOccurred("Internal error : No transaction");
-
-        boolean dftGraph = GSPLib.getOneOnly(action.request, HttpNames.paramGraphDefault) != null;
-        String uri = GSPLib.getOneOnly(action.request, HttpNames.paramGraph);
-
-        if ( !dftGraph && uri == null ) {
-            // No params - direct naming.
-            if ( !Fuseki.GSP_DIRECT_NAMING )
-                ServletOps.errorBadRequest("Neither default graph nor named graph specified");
-
-            // Direct naming.
-            String directName = action.request.getRequestURL().toString();
-            if ( action.request.getRequestURI().equals(action.getDatasetName()) )
-                // No name (should have been a quads operations).
-                ServletOps.errorBadRequest("Neither default graph nor named graph specified and no direct name");
-            Node gn = NodeFactory.createURI(directName);
-            return namedTarget(dsg, directName);
-        }
-
-        if ( dftGraph )
-            return GSPTarget.createDefault(dsg);
-
-        // Named graph
-        if ( uri.equals(HttpNames.valueDefault) )
-            // But "named" default
-            return GSPTarget.createDefault(dsg);
-
-        // Strictly, a bit naughty on the URI resolution. But more sensible.
-        // Base is dataset.
-
-        String base = action.request.getRequestURL().toString(); // wholeRequestURL(request);
-        // Make sure it ends in "/", ie. dataset as container.
-        if ( action.request.getQueryString() != null && !base.endsWith("/") )
-            base = base + "/";
-        String absUri = null;
-        try {
-            absUri = IRIResolver.resolveString(uri, base);
-        } catch (RiotException ex) {
-            // Bad IRI
-            ServletOps.errorBadRequest("Bad IRI: " + ex.getMessage());
-        }
-        return namedTarget(dsg, absUri);
-    }
-
-    private static GSPTarget namedTarget(DatasetGraph dsg, String graphName) {
-        Node gn = NodeFactory.createURI(graphName);
-        return GSPTarget.createNamed(dsg, graphName, gn);
-    }
 }
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 606ab0f..7d0d2a0 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
@@ -19,6 +19,7 @@
 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;
 
@@ -102,15 +103,13 @@
                             action.id, mediaType.getContentType(), mediaType.getCharset(), lang.getName()));
         try {
             DatasetGraph dsg = decideDataset(action);
-            GSPTarget target = determineTarget(dsg, 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.name+">");
+                ServletOps.errorNotFound("No such graph: "+target.label());
             Graph g = target.graph();
-            if ( ! target.isDefault && g.isEmpty() )
-                ServletOps.errorNotFound("No such graph: <"+target.name+">");
             // If we want to set the Content-Length, we need to buffer.
             //response.setContentLength(??);
             String ct = lang.getContentType().toHeaderString();
@@ -165,12 +164,12 @@
         action.beginRead();
         try {
             DatasetGraph dsg = decideDataset(action);
-            GSPTarget target = determineTarget(dsg, 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.name+">");
+                ServletOps.errorNotFound("No such graph: "+target.label());
             ServletOps.success(action);
         } finally { action.endRead(); }
     }
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 48ac2d2..b96b4f5 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
@@ -18,12 +18,10 @@
 
 package org.apache.jena.fuseki.servlets;
 
+import static org.apache.jena.fuseki.servlets.GraphTarget.determineTargetGSP;
 import static org.apache.jena.riot.WebContent.ctMultipartMixed;
 import static org.apache.jena.riot.WebContent.matchContentType;
 
-import java.util.Map;
-import java.util.function.Supplier;
-
 import org.apache.jena.atlas.web.ContentType;
 import org.apache.jena.fuseki.system.FusekiNetLib;
 import org.apache.jena.fuseki.system.Upload;
@@ -46,7 +44,7 @@
     @Override
     protected void doOptions(HttpAction action) {
         ActionLib.setCommonHeadersForOptions(action.response);
-        if ( hasGSPParams(action) )
+        if ( GSPLib.hasGSPParams(action) )
             action.response.setHeader(HttpNames.hAllow, "GET,HEAD,OPTIONS,PUT,DELETE,POST");
         else
             action.response.setHeader(HttpNames.hAllow, "GET,HEAD,OPTIONS,PUT,POST");
@@ -85,23 +83,22 @@
 
     protected void execPutQuads(HttpAction action) { doPutPostQuads(action, true); }
 
-    protected void execDelete(Supplier<DatasetGraph> dataset, HttpAction action) {
-    }
-
-    public void execDeleteGSP(HttpAction action) {
+    protected void execDeleteGSP(HttpAction action) {
         action.beginWrite();
         boolean haveCommited = false;
         try {
             DatasetGraph dsg = decideDataset(action);
-            GSPTarget target = determineTarget(dsg, 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.name);
+                ServletOps.errorNotFound("No such graph: "+target.label());
             }
             deleteGraph(dsg, action);
             action.commit();
@@ -118,32 +115,6 @@
         ServletOps.errorMethodNotAllowed("DELETE");
     }
 
-    /** Test whether the operation has either of the GSP parameters. */ 
-    public static boolean hasGSPParams(HttpAction action) {
-        if ( action.request.getQueryString() == null )
-            return false;
-        boolean hasParamGraphDefault = action.request.getParameter(HttpNames.paramGraphDefault) != null;
-        if ( hasParamGraphDefault )
-            return true;
-        boolean hasParamGraph = action.request.getParameter(HttpNames.paramGraph) != null;
-        if ( hasParamGraph )
-            return true;
-        return false;
-    }
-
-    /** Test whether the operation has exactly one GSP parameter and no other parameters. */ 
-    public static boolean hasGSPParamsStrict(HttpAction action) {
-        if ( action.request.getQueryString() == null )
-            return false;
-        Map<String, String[]> params = action.request.getParameterMap();
-        if ( params.size() != 1 )
-            return false;
-        boolean hasParamGraphDefault = GSPLib.hasExactlyOneValue(action, HttpNames.paramGraphDefault);
-        boolean hasParamGraph = GSPLib.hasExactlyOneValue(action, HttpNames.paramGraph);
-        // Java XOR
-        return hasParamGraph ^ hasParamGraphDefault;
-    }
-
     protected void doPutPostGSP(HttpAction action, boolean overwrite) {
         ContentType ct = ActionLib.getContentType(action);
         if ( ct == null )
@@ -177,9 +148,11 @@
         action.beginWrite();
         try {
             DatasetGraph dsg = decideDataset(action);
-            GSPTarget target = determineTarget(dsg, action);
+            GraphTarget target = determineTargetGSP(dsg, action);
             if ( action.log.isDebugEnabled() )
                 action.log.debug(action.request.getMethod().toUpperCase()+"->"+target);
+            if ( target.isUnion() )
+                ServletOps.errorBadRequest("Can't delete the union graph");
             boolean existedBefore = target.exists();
             Graph g = target.graph();
             if ( overwrite && existedBefore )
@@ -234,13 +207,15 @@
         action.beginWrite();
         try {
             DatasetGraph dsg = decideDataset(action);
-            GSPTarget target = determineTarget(dsg, action);
+            GraphTarget target = determineTargetGSP(dsg, action);
             if ( action.log.isDebugEnabled() )
                 action.log.debug("  ->"+target);
+            if ( target.isUnion() )
+                ServletOps.errorBadRequest("Can't delete the union graph");
             boolean existedBefore = target.exists();
             if ( overwrite && existedBefore )
                 clearGraph(target);
-            FusekiNetLib.addDataInto(graphTmp, target.dsg, target.graphName);
+            FusekiNetLib.addDataInto(graphTmp, target.dataset(), target.graphName());
             details.setExistedBefore(existedBefore);
             action.commit();
             return details;
@@ -259,15 +234,15 @@
      * The default graph is cleared, not removed.
      */
     protected static void deleteGraph(DatasetGraph dsg, HttpAction action) {
-        GSPTarget target = determineTarget(dsg, action);
-        if ( target.isDefault )
+        GraphTarget target = determineTargetGSP(dsg, action);
+        if ( target.isDefault() )
             clearGraph(target);
         else
-            dsg.removeGraph(target.graphName);
+            target.dataset().removeGraph(target.graphName());
     }
 
     /** Clear a graph - this leaves the storage choice and setup in-place */
-    protected static void clearGraph(GSPTarget target) {
+    protected 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/GraphTarget.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GraphTarget.java
new file mode 100644
index 0000000..4010bf6
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/GraphTarget.java
@@ -0,0 +1,226 @@
+/*
+ * 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.fuseki.servlets;
+
+import static java.lang.String.format;
+
+import org.apache.jena.atlas.logging.FmtLog;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.server.Endpoint;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.iri.IRI;
+import org.apache.jena.riot.RiotException;
+import org.apache.jena.riot.out.NodeFmtLib;
+import org.apache.jena.riot.system.IRIResolver;
+import org.apache.jena.riot.web.HttpNames;
+import org.apache.jena.sparql.core.DatasetGraph;
+
+/**
+ * Target of GSP operations.<br/>
+ * Extensions: "?graph=union" and "?graph=default"
+ *
+ */
+public class GraphTarget {
+    
+    public final static GraphTarget determineTarget(DatasetGraph dsg, HttpAction action) {
+        return determineTarget(dsg, action, false);
+    }
+    
+    /** With GSP direct naming. */
+    public final static GraphTarget determineTargetGSP(DatasetGraph dsg, HttpAction action) {
+        return determineTarget(dsg, action, Fuseki.GSP_DIRECT_NAMING);
+    }
+        
+    private final static GraphTarget determineTarget(DatasetGraph dsg, HttpAction action, boolean allowDirectNaming) {
+        // Inside a transaction.
+        if ( dsg == null )
+            ServletOps.errorOccurred("Internal error : No action graph (not in a transaction?)");
+//        if ( ! dsg.isInTransaction() )
+//            ServletOps.errorOccurred("Internal error : No transaction");
+
+        boolean dftGraph = GSPLib.getOneOnly(action.request, HttpNames.paramGraphDefault) != null;
+        String uri = GSPLib.getOneOnly(action.request, HttpNames.paramGraph);
+
+        if ( !dftGraph && uri == null ) {
+            // No params - direct naming?
+            if ( ! allowDirectNaming )
+                ServletOps.errorBadRequest("Neither default graph nor named graph specified");
+
+            // Direct naming.
+            String directName = action.request.getRequestURL().toString();
+            if ( action.request.getRequestURI().equals(action.getDatasetName()) )
+                // No name (should have been a quads operations).
+                ServletOps.errorBadRequest("Neither default graph nor named graph specified and no direct name");
+            Node gn = NodeFactory.createURI(directName);
+            return createNamed(dsg, gn);
+        }
+
+        if ( dftGraph )
+            return createDefault(dsg);
+        // Named graph as default
+        if ( uri.equals(HttpNames.graphTargetDefault) )
+            // But "named" default
+            return createDefault(dsg);
+        // Named graph - union
+        if ( uri.equals(HttpNames.graphTargetUnion) )
+            return createUnion(dsg);
+
+        String absUri = resolve0(uri, action);
+        return createNamed(dsg, absUri);
+    }
+
+    
+    // Resolving a relative URI in ?graph= is a bit murky.
+    //   Whether to use the base is the dataset or the dataset+service endpoint name.
+    //   How to find the dataset URL when service can be on the dataset directly or a named endpoint.
+    //   And will it match in the dataset named graphs anyway?
+    
+    /** Check URI, require it to be absolute. */
+    private static String resolve0(String uri, HttpAction action) {
+        IRI iri = IRIResolver.parseIRI(uri);
+        if ( iri.hasViolation(false) ) {
+            action.log.warn(format("[%d] Bad URI <%s> : %s", 
+                action.id, uri, iri.violations(false).next().getShortMessage()));
+        }
+        if ( ! iri.isAbsolute() ) {
+            action.log.warn(format("[%d] URI is not abolute: <%s>", action.id, uri));
+        }
+        return uri;
+    }
+
+    /** Resolve URI, Calculate a base URI as the dataset URI and resolve uri against that.*/ 
+    private static String resolve(String uri, HttpAction action) {
+        // Strictly, a bit naughty on the URI resolution, but more sensible.
+        // Make the base the URI of the dataset.
+        // Strictly, the base includes service and query string but that is unhelpful.
+        // wholeRequestURL(request);
+        String base = action.request.getRequestURL().toString();
+        Endpoint ep = action.getEndpoint();
+        if ( ! ep.isUnnamed() && base.endsWith(ep.getName()) ) {
+            // Remove endpoint name
+            base = base.substring(0, base.length()-ep.getName().length());
+        }
+        // Make sure it ends in "/", treating the dataset as a container.
+        if ( !base.endsWith("/") )
+            base = base + "/";
+        try {
+            IRI abs = IRIResolver.resolveIRI(uri, base);
+            if ( abs.hasViolation(false) ) {
+                FmtLog.warn(Fuseki.actionLog, "Bad URI: '"+uri+"' : "+abs.violations(false).next().getShortMessage());
+            }
+            return abs.toString();
+        } catch (RiotException ex) {
+            // Bad IRI
+            ServletOps.errorBadRequest("Bad IRI: " + ex.getMessage());
+            return null;
+        }
+    }
+
+    final private boolean      isDefault;
+    final private boolean      isUnion;
+    final private DatasetGraph dsg;
+    final private Node         graphName;
+
+    static GraphTarget createNamed(DatasetGraph dsg, String graphName) {
+        return createNamed(dsg, NodeFactory.createURI(graphName)); 
+    }
+
+    static GraphTarget createNamed(DatasetGraph dsg, Node graphName) {
+        return new GraphTarget(false, false, dsg, graphName);
+    }
+
+    static GraphTarget createDefault(DatasetGraph dsg) {
+        return new GraphTarget(true, false, dsg, null);
+    }
+
+    static GraphTarget createUnion(DatasetGraph dsg) {
+        return new GraphTarget(false, true, dsg, null);
+    }
+
+    /**
+     * Create a new GraphTarget which is like the original but aimed at a different
+     * DatasetGraph
+     */
+    static GraphTarget retarget(GraphTarget target, DatasetGraph dsg) {
+        GraphTarget target2 = new GraphTarget(target, dsg);
+        return target2;
+    }
+
+    private GraphTarget(boolean isDefault, boolean isUnion, DatasetGraph dsg, Node graphName) {
+        if ( ! isUnion && !isDefault && graphName == null )
+            throw new IllegalArgumentException("Inconsistent: not default, union nor graph name");
+        else if ( isDefault && graphName != null )
+            throw new IllegalArgumentException("Inconsistent: default and a graph name");
+        else if ( isUnion && graphName != null )
+            throw new IllegalArgumentException("Inconsistent: union and a graph name");
+        else if ( isDefault && isUnion )
+            throw new IllegalArgumentException("Inconsistent: default and union graph");
+        
+        this.isDefault = isDefault;
+        this.isUnion = isUnion;
+        this.dsg = dsg;
+        this.graphName = graphName;
+    }
+
+    private GraphTarget(GraphTarget other, DatasetGraph dsg) {
+        this.dsg = dsg; // other.dsg; // Retarget
+        this.isDefault = other.isDefault;
+        this.isUnion = other.isUnion;
+        this.graphName = other.graphName;
+    }
+
+    public DatasetGraph dataset() { return dsg; }
+    public boolean isDefault()    { return isDefault; }
+    public boolean isUnion()      { return isUnion; }
+    public Node graphName()       { return graphName; }
+
+    /**
+     * Get a graph for the action -  this is not a test for graph existence.
+     * May return null or an empty graph.
+     */
+    public Graph graph() {
+        if ( isDefault )
+            return dsg.getDefaultGraph();
+        if ( isUnion )
+            return dsg.getUnionGraph();
+        return dsg.getGraph(graphName);
+    }
+    
+    public boolean exists() {
+        if ( isDefault || isUnion )
+            return true;
+        Graph g = graph();
+        return g != null && ! g.isEmpty();
+    }
+
+    public String label() {
+        if ( isDefault )
+            return "default";
+        if ( isUnion )
+            return "union";
+        return NodeFmtLib.str(graphName);
+    }
+
+    @Override
+    public String toString() {
+        return "target:"+label();
+    }
+}
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SHACL_Validation.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SHACL_Validation.java
index d22f40a..3fa1a2e 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SHACL_Validation.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SHACL_Validation.java
@@ -19,20 +19,16 @@
 package org.apache.jena.fuseki.servlets;
 
 import static java.lang.String.format;
-import static org.apache.jena.fuseki.servlets.ActionLib.getOneHeader;
+import static org.apache.jena.fuseki.servlets.GraphTarget.determineTarget;
 
 import org.apache.jena.atlas.web.MediaType;
 import org.apache.jena.fuseki.DEF;
 import org.apache.jena.graph.Graph;
-import org.apache.jena.graph.Node;
-import org.apache.jena.graph.NodeFactory;
 import org.apache.jena.riot.Lang;
 import org.apache.jena.riot.RDFLanguages;
-import org.apache.jena.riot.web.HttpNames;
 import org.apache.jena.shacl.ShaclValidator;
 import org.apache.jena.shacl.Shapes;
 import org.apache.jena.shacl.ValidationReport;
-import org.apache.jena.sparql.core.DatasetGraph;
 import org.apache.jena.web.HttpSC;
 
 /**
@@ -55,7 +51,10 @@
 
         action.beginRead();
         try {
-            Graph data = determineTarget(action.getActiveDSG(), action);
+            GraphTarget target = determineTarget(action.getActiveDSG(), action);
+            if ( ! target.exists() )
+                ServletOps.errorNotFound("No data graph: "+target.label());
+            Graph data = target.graph();
             Graph shapesGraph = ActionLib.readFromRequest(action, Lang.TTL);
             Shapes shapes = Shapes.parse(shapesGraph);
             ValidationReport report = ShaclValidator.get().validate(shapesGraph, data);
@@ -71,23 +70,4 @@
             action.endRead();
         }
     }
-
-    protected final static Graph determineTarget(DatasetGraph dsg, HttpAction action) {
-        boolean dftGraph = getOneHeader(action.request, HttpNames.paramGraphDefault) != null ;
-        String graphName = getOneHeader(action.request, HttpNames.paramGraph) ;
-        if ( dftGraph && graphName != null )
-            ServletOps.errorBadRequest("Both default graph and named graph specified") ;
-        if ( dftGraph )
-            graphName = HttpNames.valueDefault;
-        if ( graphName == null )
-            graphName = HttpNames.valueDefault;
-        // ?graph=
-        if ( graphName.equals(HttpNames.valueDefault ) )
-            return dsg.getDefaultGraph();
-        if ( graphName.equals("union") )
-            return dsg.getUnionGraph();
-        Node gn = NodeFactory.createURI(graphName);
-        return dsg.getGraph(gn);
-    }
-
 }
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Upload.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Upload.java
index 62b2f39..b2358e7 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Upload.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Upload.java
@@ -128,7 +128,7 @@
 
         Node gn = null;
         if ( graphName != null ) {
-            gn = graphName.equals(HttpNames.valueDefault)
+            gn = graphName.equals(HttpNames.graphTargetDefault)
                 ? Quad.defaultGraphNodeGenerated
                 : NodeFactory.createURI(graphName);
         }
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/system/Upload.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/system/Upload.java
index 8e7878c..ae619fd 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/system/Upload.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/system/Upload.java
@@ -206,7 +206,7 @@
                     String value = Streams.asString(input, "UTF-8");
                     if ( fieldName.equals(HttpNames.paramGraph) ) {
                         graphName = value;
-                        if ( graphName != null && !graphName.equals("") && !graphName.equals(HttpNames.valueDefault) ) {
+                        if ( graphName != null && !graphName.equals("") && !graphName.equals(HttpNames.graphTargetDefault) ) {
                             // -- Check IRI with additional checks.
                             IRI iri = IRIResolver.parseIRI(value);
                             if ( iri.hasViolation(false) )
@@ -272,7 +272,7 @@
             }
 
             if ( graphName == null || graphName.equals("") )
-                graphName = HttpNames.valueDefault;
+                graphName = HttpNames.graphTargetDefault;
             if ( isQuads )
                 graphName = null;
             return new UploadDetailsWithName(graphName, dsgTmp, count);
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiShaclValidation.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiShaclValidation.java
index 9ea2b44..cc73682 100644
--- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiShaclValidation.java
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiShaclValidation.java
@@ -75,14 +75,9 @@
     public void shacl_default_graph() {
         try ( RDFConnection conn = RDFConnectionFactory.connect(serverURL+"/ds")) {
             conn.put(DIR+"data1.ttl");
-            
             ValidationReport report = validateReport(serverURL+"/ds/shacl?graph=default", DIR+"shapes1.ttl");
             assertNotNull(report);
             assertEquals(2, report.getEntries().size());
-            
-            ValidationReport report2 = validateReport(serverURL+"/ds/shacl?graph=urn:x:noGraph", DIR+"shapes1.ttl");
-            assertNotNull(report);
-            assertEquals(0, report2.getEntries().size());
             conn.update("CLEAR ALL");
         }
     }
@@ -91,10 +86,13 @@
     public void shacl_no_data_graph() {
         try ( RDFConnection conn = RDFConnectionFactory.connect(serverURL+"/ds")) {
             conn.put(DIR+"data1.ttl");
-            ValidationReport report = validateReport(serverURL+"/ds/shacl?graph=urn:x:noGraph", DIR+"shapes1.ttl");
-            assertNotNull(report);
-            assertEquals(0, report.getEntries().size());
-            conn.update("CLEAR ALL");
+            try {
+                FusekiTestLib.expect404(()->{
+                    ValidationReport report = validateReport(serverURL+"/ds/shacl?graph=urn:abc:noGraph", DIR+"shapes1.ttl");
+                });
+            } finally {
+                conn.update("CLEAR ALL");
+            }
         }
     }
 
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestAdmin.java b/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestAdmin.java
index 260a103..5b73ba3 100644
--- a/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestAdmin.java
+++ b/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestAdmin.java
@@ -535,13 +535,13 @@
     */
    private static boolean waitForTasksToFinish(int pauseMillis, int maxWaitMillis) {
        // Wait for them to finish.
-       // Divide into
+       // Divide into chunks
        if ( pauseMillis > 0 )
            Lib.sleep(pauseMillis);
        int waited = 0;
        final int INTERVALS = 10;
        for (int i = 0 ; i < INTERVALS ; i++ ) {
-           System.err.println("Wait: "+i);
+           //System.err.println("Wait: "+i);
            List<String> x = runningTasks();
            if ( x.isEmpty() )
                return true;