Merge pull request #657 from afs/jena-1804-fuseki-404
JENA-1804: Pass through non-matching service requests
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 105af8c..c5fd7c1 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
@@ -65,7 +65,7 @@
*
* If the request URL matches a registered dataset, process the request, and send
* the response.
- *
+ *
* This function is called by {@link FusekiFilter#doFilter}.
*
* @param request
@@ -96,37 +96,39 @@
return false;
}
DataAccessPoint dap = registry.get(datasetUri);
- process(dap, request, response);
- return true;
+ return process(dap, request, response);
}
/** Set up and handle a HTTP request for a dataset. */
- private static void process(DataAccessPoint dap, HttpServletRequest request, HttpServletResponse response) {
+ private static boolean process(DataAccessPoint dap, HttpServletRequest request, HttpServletResponse response) {
HttpAction action = allocHttpAction(dap, Fuseki.actionLog, request, response);
- dispatchAction(action);
+ return dispatchAction(action);
}
/**
* Determine and call the {@link ActionProcessor} to handle this
* {@link HttpAction}, including access control at the dataset and service levels.
*/
- public static void dispatchAction(HttpAction action) {
- ActionExecLib.execAction(action, ()->chooseProcessor(action));
+ public static boolean dispatchAction(HttpAction action) {
+ return ActionExecLib.execAction(action, ()->chooseProcessor(action));
}
/**
* Find the ActionProcessor or return null if there can't determine one.
- *
+ *
* This function sends the appropriate HTTP error response.
- *
+ *
* Returning null indicates an HTTP error response, and the HTTP response has been done.
- *
+ *
* Process
- * <li> mapRequestToEndpointName -> endpoint name
+ * <li> mapRequestToEndpointName -> endpoint name
* <li> chooseEndpoint(action, dataService, endpointName) -> Endpoint.
* <li> Endpoint to Operation (endpoint carries Operation).
- * <li> target(action, operation) -> ActionProcess.
- *
+ * <li> target(action, operation) -> ActionProcess.
+ *
+ * @param action
+ * @return ActionProcessor or null if the request URI does not name a service or the dataset.
+ *
*/
private static ActionProcessor chooseProcessor(HttpAction action) {
// "return null" indicates that processing failed to find a ActionProcessor
@@ -145,8 +147,12 @@
if ( endpoint == null ) {
if ( isEmpty(endpointName) )
ServletOps.errorBadRequest("No operation for request: "+action.getActionURI());
- else
- ServletOps.errorNotFound("No endpoint: "+action.getActionURI());
+ else {
+ // No dispatch - the filter passes these through if the ActionProcessor is null.
+ return null;
+ // If this is used, resources (servlets, sttaic files) under "/dataset/" are not accessible.
+ //ServletOps.errorNotFound("No endpoint: "+action.getActionURI());
+ }
return null;
}
@@ -157,7 +163,7 @@
}
action.setEndpoint(endpoint);
-
+
// ---- Authorization
// -- Server-level authorization.
// Checking was carried out by servlet filter AuthFilter.
@@ -218,14 +224,14 @@
/**
* Choose an endpoint. This can be with or without endpointName.
* If there is no endpoint and the action is on the data service itself (unnamed endpoint)
- * look for a named endpoint that supplies the operation.
- */
+ * look for a named endpoint that supplies the operation.
+ */
private static Endpoint chooseEndpoint(HttpAction action, DataService dataService, String endpointName) {
Endpoint ep = chooseEndpointNoLegacy(action, dataService, endpointName);
if ( ep != null )
return ep;
// No dispatch so far.
-
+
if ( ! isEmpty(endpointName) )
return ep;
// [DISPATCH LEGACY]
@@ -239,7 +245,7 @@
ep = findEndpointForOperation(action, dataService, operation, true);
return ep;
}
-
+
/**
* Choose an endpoint.
* <ul>
@@ -249,16 +255,16 @@
* - processor implmentations must be defensive).</li>
* <li>If multiple choices, classify the operation
* (includes custom content-type) and look up by operation.</li>
- * <li>Return a match wit a r
+ * <li>Return a match wit a r
* </ul>
*/
private static Endpoint chooseEndpointNoLegacy(HttpAction action, DataService dataService, String endpointName) {
EndpointSet epSet = isEmpty(endpointName) ? dataService.getEndpointSet() : dataService.getEndpointSet(endpointName);
-
+
if ( epSet == null || epSet.isEmpty() )
// No matches by name.
return null;
-
+
// If there is one endpoint, dispatch there directly.
Endpoint ep = epSet.getOnly();
if ( ep != null )
@@ -274,9 +280,9 @@
return ep;
}
- /**
+ /**
* Find an endpoint for an operation.
- * This searches all endpoints of a {@link DataService} that provide the {@link Operation}.
+ * This searches all endpoints of a {@link DataService} that provide the {@link Operation}.
* This understands that GSP_RW can service GSP_R.
* Used for legacy dispatch.
*/
@@ -284,8 +290,8 @@
Endpoint ep = findEndpointForOperationExact(dataService, operation, preferUnnamed);
if ( ep != null )
return ep;
- // Try to find "R" functionality from an RW.
- if ( GSP_R.equals(operation) )
+ // Try to find "R" functionality from an RW.
+ if ( GSP_R.equals(operation) )
return findEndpointForOperationExact(dataService, GSP_RW, preferUnnamed);
// Instead of 404, return 405 if asked for RW but only R available.
if ( GSP_RW.equals(operation) && dataService.hasOperation(GSP_R) )
@@ -296,7 +302,7 @@
/** Find a matching endpoint for exactly this operation.
* If multiple choices, prefer either named or unnamed according
* to the flag {@code preferUnnamed}.
- */
+ */
private static Endpoint findEndpointForOperationExact(DataService dataService, Operation operation, boolean preferUnnamed) {
List<Endpoint> eps = dataService.getEndpoints(operation);
if ( eps == null || eps.isEmpty() )
@@ -319,7 +325,7 @@
/**
* Identify the operation being requested.
- * It is analysing the HTTP request using global configuration.
+ * It is analysing the HTTP request using global configuration.
* The decision is based on
* <ul>
* <li>Query parameters (URL query string or HTML form)</li>
@@ -329,11 +335,11 @@
* The HTTP Method is not considered.
* <p>
* The operation is not guaranteed to be supported on every {@link DataService}
- * nor that access control will allow it to be performed.
+ * nor that access control will allow it to be performed.
*/
public static Operation chooseOperation(HttpAction action) {
HttpServletRequest request = action.getRequest();
-
+
// ---- Dispatch based on HttpParams : Query, Update, GSP.
// -- Query
boolean isQuery = request.getParameter(HttpNames.paramQuery) != null;
@@ -345,13 +351,13 @@
if ( isUpdate )
// The SPARQL_Update servlet will deal with using GET.
return Update;
-
+
// -- SPARQL Graph Store Protocol
boolean hasParamGraph = request.getParameter(HttpNames.paramGraph) != null;
boolean hasParamGraphDefault = request.getParameter(HttpNames.paramGraphDefault) != null;
if ( hasParamGraph || hasParamGraphDefault )
return gspOperation(action, request);
-
+
// -- Any other queryString
// Place for an extension point.
boolean hasParams = request.getParameterMap().size() > 0;
@@ -359,11 +365,11 @@
// Unrecognized ?key=value
ServletOps.errorBadRequest("Malformed request: unrecognized query string parameters: " + request.getQueryString());
}
-
+
// ---- Content-type
// We don't wire in all the RDF syntaxes.
// Instead, "Quads" drops through to the default operation.
-
+
// This does not have the ";charset="
String ct = request.getContentType();
if ( ct != null ) {
@@ -371,7 +377,7 @@
if ( operation != null )
return operation;
}
-
+
// ---- No registered content type, no query parameters.
// Plain HTTP operation on the dataset handled as quads or rejected.
return quadsOperation(action, request);
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 4a347c9..3de11e4 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
@@ -75,12 +75,15 @@
* @param action
* @param processor
*/
- public static void execAction(HttpAction action, ActionProcessor processor) {
- execAction(action, ()->processor);
+ public static boolean execAction(HttpAction action, ActionProcessor processor) {
+ boolean b = execAction(action, ()->processor);
+ if ( !b )
+ ServletOps.errorNotFound("Not found: "+action.getActionURI());
+ return true;
}
/** execAction, allowing for a choice of {@link ActionProcessor} within the logging and error handling. */
- public static void execAction(HttpAction action, Supplier<ActionProcessor> processor) {
+ public static boolean execAction(HttpAction action, Supplier<ActionProcessor> processor) {
try {
logRequest(action);
action.setStartTime();
@@ -88,10 +91,18 @@
HttpServletResponse response = action.response;
startRequest(action);
-
+
try {
- // Get the processor inside the startRequest - error handling - finishRequest sequence.
+ // Get the processor inside the startRequest - error handling - finishRequest sequence.
ActionProcessor proc = processor.get();
+ if ( proc == null ) {
+ // Only for the logging.
+ finishRequest(action);
+ logNoResponse(action);
+ archiveHttpAction(action);
+ // Can't find the URL (the /dataset/service case) - not handled here.
+ return false;
+ }
proc.process(action);
} catch (QueryCancelledException ex) {
// To put in the action timeout, need (1) global, (2) dataset and (3) protocol settings.
@@ -127,10 +138,14 @@
action.setFinishTime();
finishRequest(action);
}
+ // Handled - including sending back errors.
logResponse(action);
archiveHttpAction(action);
+ return true;
} catch (Throwable th) {
+ // This really should not catch anything.
FmtLog.error(action.log, th, "Internal error");
+ return true;
}
}
@@ -198,7 +213,10 @@
}
}
- /** Log an {@link HttpAction} response. */
+ /**
+ * Log an {@link HttpAction} response.
+ * This includes a message to the action log and also on to the standard format Combined NCSA log.
+ */
public static void logResponse(HttpAction action) {
long time = action.getTime();
@@ -225,10 +243,22 @@
action.id, action.statusCode, HttpSC.getMessage(action.statusCode), timeStr);
else
FmtLog.info(action.log,"[%d] %d %s (%s)", action.id, action.statusCode, action.message, timeStr);
+ // Standard format NCSA log.
+ if ( Fuseki.requestLog != null && Fuseki.requestLog.isInfoEnabled() ) {
+ String s = RequestLog.combinedNCSA(action);
+ Fuseki.requestLog.info(s);
+ }
// See also HttpAction.finishRequest - request logging happens there.
}
+ /**
+ * Log when we don't handle this request.
+ */
+ public static void logNoResponse(HttpAction action) {
+ FmtLog.info(action.log,"[%d] No Fuseki dispatch %s", action.id, action.getActionURI());
+ }
+
/** Set headers for the response. */
public static void initResponse(HttpAction action) {
ServletBase.setCommonHeaders(action.response);
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 0e6e345..7db1610 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
@@ -122,7 +122,7 @@
* Initialization after action creation, during lifecycle setup. This is "set
* once" (in other words, constructor-like but delayed because the information is
* not yet available at the point we want to create the HttpAction).
- *
+ *
* This method sets the action dataset for service requests. Does not apply to "admin" and
* "ctl" servlets. Setting will replace any existing {@link DataAccessPoint} and
* {@link DataService}, as the {@link DatasetGraph} of the current HTTP Action.
@@ -150,7 +150,7 @@
this.dataService = dService;
setDataset(dService.getDataset());
}
-
+
/** Minimum initialization using just a dataset.
* <p>
* the HTTP Action will change its transactional state and
@@ -347,11 +347,6 @@
public final void finishRequest() {
if ( dataAccessPoint != null )
dataAccessPoint.finishRequest(this);
- // Standard logging goes here.
- if ( Fuseki.requestLog != null && Fuseki.requestLog.isInfoEnabled() ) {
- String s = RequestLog.combinedNCSA(this);
- Fuseki.requestLog.info(s);
- }
}
/** If inside the transaction for the action, return the active {@link DatasetGraph},
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestHTTP.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestHTTP.java
index cceb07d..5edf601 100644
--- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestHTTP.java
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestHTTP.java
@@ -20,6 +20,13 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
import org.apache.jena.atlas.lib.StrUtils;
import org.apache.jena.atlas.web.WebLib;
@@ -67,10 +74,22 @@
FusekiServer server = FusekiServer.create()
.add("/ds", dsg)
.port(port)
+ .addServlet("/ds/myServlet", new MyServlet())
+ .staticFileBase("testing/Files")
.build();
server.start();
}
+ // Test : responds to GET
+ private static class MyServlet extends HttpServlet {
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.setContentType("text/plain");
+ resp.getOutputStream().print("SERVLET");
+ resp.setStatus(200);
+ }
+ }
+
@AfterClass
public static void afterClass() {
if ( server != null )
@@ -158,4 +177,16 @@
assertEquals(ct, h);
});
}
+
+ // Servlet - mounted at /ds/myServlet, but not a service that Fuseki dispatches.
+ @Test public void plainServlet() {
+ String x = HttpOp.execHttpGetString(URL+"/myServlet");
+ assertEquals(x, "SERVLET");
+ }
+
+ // Files - a static file /ds/file.txt is visible.
+ @Test public void plainFile() {
+ String x = HttpOp.execHttpGetString(URL+"/file.txt");
+ assertTrue(x.contains("CONTENT"));
+ }
}
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Files/ds/file.txt b/jena-fuseki2/jena-fuseki-main/testing/Files/ds/file.txt
new file mode 100644
index 0000000..a980c83
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/Files/ds/file.txt
@@ -0,0 +1,2 @@
+# Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0
+CONTENT