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