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