JENA-1435: Better FusekiServer.Builder; tests for custom operations

Project: http://git-wip-us.apache.org/repos/asf/jena/repo
Commit: http://git-wip-us.apache.org/repos/asf/jena/commit/07409cda
Tree: http://git-wip-us.apache.org/repos/asf/jena/tree/07409cda
Diff: http://git-wip-us.apache.org/repos/asf/jena/diff/07409cda

Branch: refs/heads/master
Commit: 07409cda5b44fa83ac00c9de7f4bf9f27ac51160
Parents: 2a78ef8
Author: Andy Seaborne <[email protected]>
Authored: Sat Nov 25 22:04:11 2017 +0000
Committer: Andy Seaborne <[email protected]>
Committed: Mon Dec 11 14:56:08 2017 +0000

----------------------------------------------------------------------
 .../fuseki/server/DataAccessPointRegistry.java  |   4 +
 .../servlets/ServiceDispatchRegistry.java       |   9 +
 .../jena/fuseki/embedded/FusekiServer.java      | 298 +++++++++++--------
 .../jena/fuseki/embedded/CustomService.java     |  81 +++++
 .../jena/fuseki/embedded/TS_EmbeddedFuseki.java |   1 +
 .../embedded/TestFusekiCustomOperation.java     | 165 ++++++++++
 6 files changed, 435 insertions(+), 123 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jena/blob/07409cda/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataAccessPointRegistry.java
----------------------------------------------------------------------
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 965ed0f..5097cf0 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
@@ -28,6 +28,10 @@ public class DataAccessPointRegistry extends 
Registry<String, DataAccessPoint>
 {
     public DataAccessPointRegistry() {}
     
+    public DataAccessPointRegistry(DataAccessPointRegistry other) {
+        other.forEach((name, accessPoint)->register(name, accessPoint));
+    }
+    
     // Add error checking.
     public void register(String name, DataAccessPoint accessPt) {
         if ( isRegistered(name) )

http://git-wip-us.apache.org/repos/asf/jena/blob/07409cda/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServiceDispatchRegistry.java
----------------------------------------------------------------------
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServiceDispatchRegistry.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServiceDispatchRegistry.java
index 3d7303a..bf52647 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServiceDispatchRegistry.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServiceDispatchRegistry.java
@@ -53,6 +53,11 @@ public class ServiceDispatchRegistry {
      */  
     private final Map<Operation, ActionService> operationToHandler = new 
ConcurrentHashMap<>();
     
+    public ServiceDispatchRegistry(ServiceDispatchRegistry other) {
+        contentTypeToOperation.putAll(other.contentTypeToOperation);
+        operationToHandler.putAll(other.operationToHandler);
+    }
+    
     public ServiceDispatchRegistry(boolean includeStdConfig) {
         if ( includeStdConfig ) {
             register(Operation.Query, WebContent.contentTypeSPARQLQuery, 
queryServlet);
@@ -79,6 +84,10 @@ public class ServiceDispatchRegistry {
         return operationToHandler.get(operation);
     }
     
+    public boolean isRegistered(Operation operation) {
+        return operationToHandler.containsKey(operation);
+    }
+
     /**
      * Register a new {@link Operation}, with its {@code Content-Type} (may be 
null,
      * meaning no dispatch by content type), and the implementation handler.

http://git-wip-us.apache.org/repos/asf/jena/blob/07409cda/jena-fuseki2/jena-fuseki-embedded/src/main/java/org/apache/jena/fuseki/embedded/FusekiServer.java
----------------------------------------------------------------------
diff --git 
a/jena-fuseki2/jena-fuseki-embedded/src/main/java/org/apache/jena/fuseki/embedded/FusekiServer.java
 
b/jena-fuseki2/jena-fuseki-embedded/src/main/java/org/apache/jena/fuseki/embedded/FusekiServer.java
index f7c837a..40d6cff 100644
--- 
a/jena-fuseki2/jena-fuseki-embedded/src/main/java/org/apache/jena/fuseki/embedded/FusekiServer.java
+++ 
b/jena-fuseki2/jena-fuseki-embedded/src/main/java/org/apache/jena/fuseki/embedded/FusekiServer.java
@@ -18,38 +18,38 @@
 
 package org.apache.jena.fuseki.embedded;
 
-import java.util.ArrayList;
-import java.util.HashMap ;
-import java.util.List ;
-import java.util.Map ;
+import java.util.*;
+import static java.util.Objects.requireNonNull;
 
-import javax.servlet.ServletContext ;
+import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServlet;
 
 import org.apache.jena.atlas.lib.Pair;
-import org.apache.jena.fuseki.Fuseki ;
-import org.apache.jena.fuseki.FusekiConfigException ;
-import org.apache.jena.fuseki.FusekiException ;
-import org.apache.jena.fuseki.build.FusekiBuilder ;
-import org.apache.jena.fuseki.build.FusekiConfig ;
-import org.apache.jena.fuseki.jetty.FusekiErrorHandler1 ;
-import org.apache.jena.fuseki.mgt.ActionStats ;
-import org.apache.jena.fuseki.server.DataAccessPoint ;
-import org.apache.jena.fuseki.server.DataAccessPointRegistry ;
-import org.apache.jena.fuseki.server.DataService ;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.FusekiConfigException;
+import org.apache.jena.fuseki.FusekiException;
+import org.apache.jena.fuseki.build.FusekiBuilder;
+import org.apache.jena.fuseki.build.FusekiConfig;
+import org.apache.jena.fuseki.jetty.FusekiErrorHandler1;
+import org.apache.jena.fuseki.mgt.ActionStats;
+import org.apache.jena.fuseki.server.DataAccessPoint;
+import org.apache.jena.fuseki.server.DataAccessPointRegistry;
+import org.apache.jena.fuseki.server.DataService;
+import org.apache.jena.fuseki.server.Operation;
 import org.apache.jena.fuseki.servlets.ServiceDispatchRegistry;
-import org.apache.jena.fuseki.servlets.FusekiFilter ;
-import org.apache.jena.query.Dataset ;
+import org.apache.jena.fuseki.servlets.ActionService;
+import org.apache.jena.fuseki.servlets.FusekiFilter;
+import org.apache.jena.query.Dataset;
 import org.apache.jena.riot.WebContent;
-import org.apache.jena.sparql.core.DatasetGraph ;
+import org.apache.jena.sparql.core.DatasetGraph;
 import org.eclipse.jetty.http.MimeTypes;
 import org.eclipse.jetty.security.SecurityHandler;
-import org.eclipse.jetty.server.HttpConnectionFactory ;
-import org.eclipse.jetty.server.Server ;
-import org.eclipse.jetty.server.ServerConnector ;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
 import org.eclipse.jetty.servlet.DefaultServlet;
-import org.eclipse.jetty.servlet.FilterHolder ;
-import org.eclipse.jetty.servlet.ServletContextHandler ;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.eclipse.jetty.servlet.ServletHolder;
 
 /**
@@ -64,24 +64,24 @@ import org.eclipse.jetty.servlet.ServletHolder;
  * (it wil run in the background : see {@link #join}).
  * <p>Example:
  * <pre>
- *      DatasetGraph dsg = ... ;
+ *      DatasetGraph dsg = ...;
  *      FusekiServer server = FusekiServer.create()
  *          .setPort(1234)
  *          .add("/ds", dsg)
- *          .build() ;
- *       server.start() ;
+ *          .build();
+ *       server.start();
  * </pre>
  * Compact form (use the builder pattern above to get more flexibility):
  * <pre>
- *    FusekiServer.make(1234, "/ds", dsg).start() ;
+ *    FusekiServer.make(1234, "/ds", dsg).start();
  * </pre>
  * 
  */
 public class FusekiServer {
     static { 
-        //FusekiEnv.mode = FusekiEnv.INIT.EMBEDDED ;
+        //FusekiEnv.mode = FusekiEnv.INIT.EMBEDDED;
         // Stop anything accidently resetting Fuseki server logging. 
-        //FusekiLogging.allowLoggingReset(false) ;
+        //FusekiLogging.allowLoggingReset(false);
     }
     
     /** Construct a Fuseki server for one dataset.
@@ -92,21 +92,21 @@ public class FusekiServer {
             .setPort(port)
             .setLoopback(true)
             .add(name, dsg)
-            .build() ;
+            .build();
     }
     
     public static Builder create() {
-        return new Builder() ;
+        return new Builder();
     }
     
-    public final Server server ;
-    private int port ;
+    public final Server server;
+    private int port;
     
     private FusekiServer(int port, Server server) {
-        this.server = server ;
+        this.server = server;
         // This should be the same.
-        //this.port = ((ServerConnector)server.getConnectors()[0]).getPort() ;
-        this.port = port ;
+        //this.port = ((ServerConnector)server.getConnectors()[0]).getPort();
+        this.port = port;
     }
     
     /** 
@@ -115,33 +115,33 @@ public class FusekiServer {
      * the one actually allocated if the port was 0 ("choose a free port").
      */
     public int getPort() {
-        return port ; 
+        return port; 
     }
 
     /** Get the underlying Jetty server which has also been set up. */ 
     public Server getJettyServer() {
-        return server ; 
+        return server; 
     }
     
     /** Get the {@link ServletContext}.
      * Adding new servlets is possible with care.
      */ 
     public ServletContext getServletContext() {
-        return 
((ServletContextHandler)server.getHandler()).getServletContext() ;
+        return 
((ServletContextHandler)server.getHandler()).getServletContext();
     }
 
     /** Get the {@link DataAccessPointRegistry}.
      * This method is intended for inspecting the registry.
      */ 
     public DataAccessPointRegistry getDataAccessPointRegistry() {
-        return DataAccessPointRegistry.get(getServletContext()) ;
+        return DataAccessPointRegistry.get(getServletContext());
     }
 
     /** Get the {@link DataAccessPointRegistry}.
      * This method is intended for inspecting the registry.
      */ 
     public ServiceDispatchRegistry getServiceDispatchRegistry() {
-        return ServiceDispatchRegistry.get(getServletContext()) ;
+        return ServiceDispatchRegistry.get(getServletContext());
     }
 
     /** Start the server - the server continues to run after this call returns.
@@ -149,76 +149,81 @@ public class FusekiServer {
      */
     public FusekiServer start() { 
         try { server.start(); }
-        catch (Exception e) { throw new FusekiException(e) ; }
+        catch (Exception e) { throw new FusekiException(e); }
         if ( port == 0 )
-            port = ((ServerConnector)server.getConnectors()[0]).getLocalPort() 
;
+            port = ((ServerConnector)server.getConnectors()[0]).getLocalPort();
         Fuseki.serverLog.info("Start Fuseki (port="+port+")");
-        return this ;
+        return this;
     }
 
     /** Stop the server. */
     public void stop() { 
         Fuseki.serverLog.info("Stop Fuseki (port="+port+")");
         try { server.stop(); }
-        catch (Exception e) { throw new FusekiException(e) ; }
+        catch (Exception e) { throw new FusekiException(e); }
     }
     
     /** Wait for the server to exit. This call is blocking. */
     public void join() {
         try { server.join(); }
-        catch (Exception e) { throw new FusekiException(e) ; }
+        catch (Exception e) { throw new FusekiException(e); }
     }
     
     /** FusekiServer.Builder */
     public static class Builder {
-        private Map<String, DataService> map                = new HashMap<>() ;
+        private DataAccessPointRegistry  dataAccessPoints   = new 
DataAccessPointRegistry();
+        private ServiceDispatchRegistry  serviceDispatch    = new 
ServiceDispatchRegistry(true);
         // Default values.
-        private int                      port               = 3330 ;
-        private boolean                  loopback           = false ;
-        private boolean                  withStats          = false ;
-        private boolean                  verbose            = false ;
+        private int                      port               = 3330;
+        private boolean                  loopback           = false;
+        private boolean                  withStats          = false;
+        private boolean                  verbose            = false;
         // Other servlets to add.
-        private List<Pair<String, HttpServlet>> other = new ArrayList<>();
-        private String                   contextPath        = "/" ;
-        private String                   staticContentDir   = null ;
-        private SecurityHandler          securityHandler    = null ;
+        private List<Pair<String, HttpServlet>> other       = new 
ArrayList<>();
+        private String                   contextPath        = "/";
+        private String                   staticContentDir   = null;
+        private SecurityHandler          securityHandler    = null;
 
         /** Set the port to run on. */ 
         public Builder setPort(int port) {
             if ( port < 0 )
-                throw new IllegalArgumentException("Illegal port="+port+" : 
Port must be greater than or equal to zero.") ;
-            this.port = port ;
-            return this ;
+                throw new IllegalArgumentException("Illegal port="+port+" : 
Port must be greater than or equal to zero.");
+            this.port = port;
+            return this;
         }
         
         /** Context path to Fuseki.  If it's "/" then Fuseki URL look like
          * "http://host:port/dataset/query"; else 
"http://host:port/path/dataset/query"; 
          */
         public Builder setContextPath(String path) {
-            this.contextPath = path ;
-            return this ;
+            requireNonNull(path, "path");
+            this.contextPath = path;
+            return this;
         }
         
         /** Restrict the server to only responding to the localhost interface. 
*/ 
         public Builder setLoopback(boolean loopback) {
             this.loopback = loopback;
-            return this ;
+            return this;
         }
 
         /** Set the location (filing system directory) to serve static file 
from. */ 
         public Builder setStaticFileBase(String directory) {
+            requireNonNull(directory, "directory");
             this.staticContentDir = directory;
-            return this ;
+            return this;
         }
         
         /** Set a Jetty SecurityHandler.
+         * <p>
          *  By default, the server runs with no security.
          *  This is more for using the basic server for testing.
-         *  The full Fuseki server provides secjurity with Apache Shiro
-         *  and a defensive reverse proxy (e.g. Apache httpd) in front og the 
Jetty server
-         *  can also be used, which provides a wide varient of proven security 
options.   
+         *  The full Fuseki server provides security with Apache Shiro
+         *  and a defensive reverse proxy (e.g. Apache httpd) in front of the 
Jetty server
+         *  can also be used, which provides a wide varity of proven security 
options.   
          */
         public Builder setSecurityHandler(SecurityHandler securityHandler) {
+            requireNonNull(securityHandler, "securityHandler");
             this.securityHandler = securityHandler;
             return this;
         }
@@ -234,43 +239,54 @@ public class FusekiServer {
          */ 
         public Builder enableStats(boolean withStats) {
             this.withStats = withStats;
-            return this ;
+            return this;
         }
 
         /** Add the dataset with given name and a default set of services 
including update */  
-        public Builder add(String name, Dataset ds) {
-            return add(name, ds.asDatasetGraph()) ;
+        public Builder add(String name, Dataset dataset) {
+            requireNonNull(name, "name");
+            requireNonNull(dataset, "dataset");
+            return add(name, dataset.asDatasetGraph());
         }
 
         /** Add the dataset with given name and a default set of services 
including update */  
-        public Builder add(String name, DatasetGraph dsg) {
-            return add(name, dsg, true) ;
+        public Builder add(String name, DatasetGraph dataset) {
+            requireNonNull(name, "name");
+            requireNonNull(dataset, "dataset");
+            return add(name, dataset, true);
         }
 
         /** Add the dataset with given name and a default set of services. */  
-        public Builder add(String name, Dataset ds, boolean allowUpdate) {
-            return add(name, ds.asDatasetGraph(), allowUpdate) ;
+        public Builder add(String name, Dataset dataset, boolean allowUpdate) {
+            requireNonNull(name, "name");
+            requireNonNull(dataset, "dataset");
+            return add(name, dataset.asDatasetGraph(), allowUpdate);
         }
         
         /** Add the dataset with given name and a default set of services. */  
-        public Builder add(String name, DatasetGraph dsg, boolean allowUpdate) 
{
-            DataService dSrv = FusekiBuilder.buildDataServiceStd(dsg, 
allowUpdate) ; 
-            return add(name, dSrv) ;
+        public Builder add(String name, DatasetGraph dataset, boolean 
allowUpdate) {
+            requireNonNull(name, "name");
+            requireNonNull(dataset, "dataset");
+            DataService dSrv = FusekiBuilder.buildDataServiceStd(dataset, 
allowUpdate); 
+            return add(name, dSrv);
         }
         
         /** Add a data service that includes dataset and service names.
          * A {@link DataService} allows for choices of the various endpoint 
names.
          */  
         public Builder add(String name, DataService dataService) {
-            return add$(name, dataService) ; 
+            requireNonNull(name, "name");
+            requireNonNull(dataService, "dataService");
+            return add$(name, dataService); 
         }
         
         private Builder add$(String name, DataService dataService) {
-            name = DataAccessPoint.canonical(name) ;
-            if ( map.containsKey(name) )
-                throw new FusekiConfigException("Data service name already 
registered: "+name) ;
-            map.put(name, dataService) ;
-            return this ;
+            name = DataAccessPoint.canonical(name);
+            if ( dataAccessPoints.isRegistered(name) )
+                throw new FusekiConfigException("Data service name already 
registered: "+name);
+            DataAccessPoint dap = new DataAccessPoint(name, dataService);
+            dataAccessPoints.register(name, dap);
+            return this;
         }
 
         /** Read and parse a Fuseki services/datasets file.
@@ -280,10 +296,11 @@ public class FusekiServer {
          *  includes a text index.     
          */
         public Builder parseConfigFile(String filename) {
-            List<DataAccessPoint> x = 
FusekiConfig.readConfigurationFile(filename) ;
+            requireNonNull(filename, "filename");
+            List<DataAccessPoint> x = 
FusekiConfig.readConfigurationFile(filename);
             // Unbundle so that they accumulate.
-            x.forEach(dap-> add(dap.getName(), dap.getDataService())) ;
-            return this ;
+            x.forEach(dap-> add(dap.getName(), dap.getDataService()));
+            return this;
         }
 
         /**
@@ -292,49 +309,84 @@ public class FusekiServer {
          * handler (which is the last servlet) used for {@link 
#setStaticFileBase(String)}.
          */
         public Builder addServlet(String pathSpec, HttpServlet servlet) {
+            requireNonNull(pathSpec, "pathSpec");
+            requireNonNull(servlet, "servlet");
             other.add(Pair.create(pathSpec, servlet));
             return this;
         }
         
-        /** Build a server according to the current description */ 
-        public FusekiServer build() {
-            ServiceDispatchRegistry serviceRegistry = new 
ServiceDispatchRegistry(true);            
-            DataAccessPointRegistry registry = new DataAccessPointRegistry() ;
+        /**
+         * Add an operation to the server, together with its triggering 
Content-Type (may be null) and servlet handler.
+         * <p>
+         * To associate an operation with a daatsets, call {@link 
#addOperation} after adding the dataset. 
+         * <p>
+         * (Advanced and experimental option: see 
+         * <a 
href="http://jena.apache.org/documentation/fuseki2/extend.html>Extending 
Fuseki</a>)
+         * @see #addOperation 
+         */
+        public Builder registerOperation(Operation operation, String 
contentType, ActionService handler) {
+            Objects.requireNonNull(operation, "operation");
+            Objects.requireNonNull(handler, "handler");
+            if ( serviceDispatch.isRegistered(operation) )
+                throw new FusekiConfigException("Handler for operation already 
registered: "+operation.getName());
+            serviceDispatch.register(operation, contentType, handler);
+            return this;
+        }
+        
+        /** 
+         * Create an endpoint on the dataset. The operation must already be 
registered with the builder.
+         * @see #registerOperation 
+         */
+        public Builder addOperation(String datasetName, Operation operation, 
String endpointName) {
+            Objects.requireNonNull(datasetName, "datasetName");
+            Objects.requireNonNull(operation, "operation");
+            Objects.requireNonNull(endpointName, "endpointName");
             
-            map.forEach((name, dSrv) -> {
-                DataAccessPoint dap = new DataAccessPoint(name, dSrv) ;
-                registry.put(name, dap) ;
-            }) ;
-            ServletContextHandler handler = buildServletContext(contextPath, 
registry) ;
-
-            Fuseki.setVerbose(handler.getServletContext(), verbose);
+            String name = DataAccessPoint.canonical(datasetName);
             
-            ServiceDispatchRegistry.set(handler.getServletContext(), 
serviceRegistry);
+            if ( ! serviceDispatch.isRegistered(operation) )
+                throw new FusekiConfigException("Operation not registered: 
"+operation.getName());
             
+            if ( ! dataAccessPoints.isRegistered(name) )
+                throw new FusekiConfigException("Dataset not registered: 
"+datasetName);
+            DataAccessPoint dap = dataAccessPoints.get(name);
+            FusekiBuilder.addServiceEP(dap.getDataService(), operation, 
endpointName);
+            return this;
+        }
+
+        /**
+         * Build a server according to the current description.
+         */
+        public FusekiServer build() {
+            ServletContextHandler handler = buildServletContext(contextPath);
+
+            ServletContext cxt = handler.getServletContext();
+            Fuseki.setVerbose(cxt, verbose);
+            // Clone to isolate from any future changes. 
+            ServiceDispatchRegistry.set(cxt, new 
ServiceDispatchRegistry(serviceDispatch));
+            DataAccessPointRegistry.set(cxt, new 
DataAccessPointRegistry(dataAccessPoints));
             setMimeTypes(handler);
             servlets(handler);
             
-            DataAccessPointRegistry.set(handler.getServletContext(), registry) 
;
-
-            Server server = jettyServer(port, loopback) ;
+            Server server = jettyServer(port, loopback);
             server.setHandler(handler);
-            return new FusekiServer(port, server) ;
+            return new FusekiServer(port, server);
         }
 
         /** Build a ServletContextHandler with the Fuseki router : {@link 
FusekiFilter} */
-        private ServletContextHandler buildServletContext(String contextPath, 
DataAccessPointRegistry registry) {
+        private ServletContextHandler buildServletContext(String contextPath) {
             if ( contextPath == null || contextPath.isEmpty() )
-                contextPath = "/" ;
+                contextPath = "/";
             else if ( !contextPath.startsWith("/") )
-                contextPath = "/" + contextPath ;
-            ServletContextHandler context = new ServletContextHandler() ;
-            context.setDisplayName(Fuseki.servletRequestLogName) ;
-            context.setErrorHandler(new FusekiErrorHandler1()) ;
-            context.setContextPath(contextPath) ;
+                contextPath = "/" + contextPath;
+            ServletContextHandler context = new ServletContextHandler();
+            context.setDisplayName(Fuseki.servletRequestLogName);
+            context.setErrorHandler(new FusekiErrorHandler1());
+            context.setContextPath(contextPath);
             if ( securityHandler != null )
                 context.setSecurityHandler(securityHandler);
             
-            return context ;
+            return context;
         }
         
         private static void setMimeTypes(ServletContextHandler context) {
@@ -368,44 +420,44 @@ public class FusekiServer {
 
         private void servlets(ServletContextHandler context) {
             // Fuseki dataset services filter
-            FusekiFilter ff = new FusekiFilter() ;
-            FilterHolder h = new FilterHolder(ff) ;
-            context.addFilter(h, "/*", null) ;
+            FusekiFilter ff = new FusekiFilter();
+            FilterHolder h = new FilterHolder(ff);
+            context.addFilter(h, "/*", null);
 
             other.forEach(p->addServlet(context, p.getLeft(), p.getRight()));
             
             if ( withStats )
-                addServlet(context, "/$/stats", new ActionStats()) ;
+                addServlet(context, "/$/stats", new ActionStats());
             
             if ( staticContentDir != null ) {
-                DefaultServlet staticServlet = new DefaultServlet() ;
-                ServletHolder staticContent = new ServletHolder(staticServlet) 
;
-                staticContent.setInitParameter("resourceBase", 
staticContentDir) ;
+                DefaultServlet staticServlet = new DefaultServlet();
+                ServletHolder staticContent = new ServletHolder(staticServlet);
+                staticContent.setInitParameter("resourceBase", 
staticContentDir);
                 context.addServlet(staticContent, "/");
             }
         }
 
         private static void addServlet(ServletContextHandler context, String 
pathspec, HttpServlet httpServlet) {
             ServletHolder sh = new ServletHolder(httpServlet);
-            context.addServlet(sh, pathspec) ;
+            context.addServlet(sh, pathspec);
         }
 
         /** Jetty server */
         private static Server jettyServer(int port, boolean loopback) {
-            Server server = new Server() ;
-            HttpConnectionFactory f1 = new HttpConnectionFactory() ;
+            Server server = new Server();
+            HttpConnectionFactory f1 = new HttpConnectionFactory();
             // Some people do try very large operations ... really, should use 
POST.
             f1.getHttpConfiguration().setRequestHeaderSize(512 * 1024);
-            f1.getHttpConfiguration().setOutputBufferSize(1024 * 1024) ;
+            f1.getHttpConfiguration().setOutputBufferSize(1024 * 1024);
             // Do not add "Server: Jetty(....) when not a development system.
             if ( ! Fuseki.outputJettyServerHeader )
-                f1.getHttpConfiguration().setSendServerVersion(false) ;
-            ServerConnector connector = new ServerConnector(server, f1) ;
-            connector.setPort(port) ;
+                f1.getHttpConfiguration().setSendServerVersion(false);
+            ServerConnector connector = new ServerConnector(server, f1);
+            connector.setPort(port);
             server.addConnector(connector);
             if ( loopback )
                 connector.setHost("localhost");
-            return server ;
+            return server;
         }
     }
 }

http://git-wip-us.apache.org/repos/asf/jena/blob/07409cda/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/CustomService.java
----------------------------------------------------------------------
diff --git 
a/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/CustomService.java
 
b/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/CustomService.java
new file mode 100644
index 0000000..5f4ec2d
--- /dev/null
+++ 
b/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/CustomService.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.fuseki.embedded;
+
+import java.io.IOException;
+
+import org.apache.jena.fuseki.servlets.ActionREST;
+import org.apache.jena.fuseki.servlets.HttpAction;
+import org.apache.jena.fuseki.servlets.ServletOps;
+import org.apache.jena.riot.WebContent;
+import org.apache.jena.web.HttpSC;
+
+public class CustomService extends ActionREST {
+
+    // do* -- the operations to accept
+    
+    @Override
+    protected void doGet(HttpAction action) {
+        action.response.setStatus(HttpSC.OK_200);
+        try {
+            action.response.setContentType(WebContent.contentTypeTextPlain);
+            action.response.getOutputStream().println("    ** Hello world 
(GET) **");
+        }
+        catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    protected void doHead(HttpAction action) {
+        action.response.setStatus(HttpSC.OK_200);
+        action.response.setContentType(WebContent.contentTypeTextPlain);
+    }
+
+    @Override
+    protected void doPost(HttpAction action) {
+        action.response.setStatus(HttpSC.OK_200);
+        try {
+            action.response.setContentType(WebContent.contentTypeTextPlain);
+            action.response.getOutputStream().println("    ** Hello world 
(POST) **");
+        }
+        catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    protected void doPatch(HttpAction action) { notSupported(action); }
+
+    @Override
+    protected void doDelete(HttpAction action) { notSupported(action); }
+
+    @Override
+    protected void doPut(HttpAction action) { notSupported(action); }
+
+    @Override
+    protected void doOptions(HttpAction action) { notSupported(action); }
+
+    @Override
+    protected void validate(HttpAction action) { }
+    
+    private void notSupported(HttpAction action) {
+        ServletOps.errorMethodNotAllowed(action.getMethod()+" 
"+action.getDatasetName());
+    }
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/07409cda/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TS_EmbeddedFuseki.java
----------------------------------------------------------------------
diff --git 
a/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TS_EmbeddedFuseki.java
 
b/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TS_EmbeddedFuseki.java
index dd2d8e9..d8ec581 100644
--- 
a/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TS_EmbeddedFuseki.java
+++ 
b/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TS_EmbeddedFuseki.java
@@ -32,6 +32,7 @@ import org.junit.runners.Suite.SuiteClasses ;
   , TestMultipleEmbedded.class
   , TestFusekiTestServer.class
   , TestFusekiTestAuth.class
+  , TestFusekiCustomOperation.class
 })
 public class TS_EmbeddedFuseki {
     @BeforeClass public static void setupForFusekiServer() {

http://git-wip-us.apache.org/repos/asf/jena/blob/07409cda/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TestFusekiCustomOperation.java
----------------------------------------------------------------------
diff --git 
a/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TestFusekiCustomOperation.java
 
b/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TestFusekiCustomOperation.java
new file mode 100644
index 0000000..8b037b6
--- /dev/null
+++ 
b/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TestFusekiCustomOperation.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.fuseki.embedded;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.atlas.web.TypedInputStream;
+import org.apache.jena.fuseki.FusekiConfigException;
+import org.apache.jena.fuseki.FusekiLib;
+import org.apache.jena.fuseki.build.FusekiBuilder;
+import org.apache.jena.fuseki.server.DataService;
+import org.apache.jena.fuseki.server.Operation;
+import org.apache.jena.fuseki.servlets.ActionService;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.rdfconnection.RDFConnection;
+import org.apache.jena.rdfconnection.RDFConnectionFactory;
+import org.apache.jena.riot.web.HttpOp;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.junit.Test;
+
+/** Test for adding a new operation */
+public class TestFusekiCustomOperation {
+    private static final Operation newOp = Operation.register("Special", 
"Custom operation");
+    private static final String contentType = "application/special";
+    private static final String endpointName = "special";
+
+    private final ActionService customHandler = new CustomService();
+    private final int port = FusekiLib.choosePort();
+    private final String url = "http://localhost:"+port;
+
+    @Test
+    public void cfg_dataservice() {
+        // Create a DataService and add the endpoint -> operation association.
+        // This still needs the server to have the operation registered.  
+        DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+        DataService dataService = new DataService(dsg);
+        FusekiBuilder.populateStdServices(dataService, true);
+        FusekiBuilder.addServiceEP(dataService, newOp, endpointName);
+        
+        FusekiServer server = 
+            FusekiServer.create()
+                .setPort(port)
+                .registerOperation(newOp, contentType, customHandler)
+                .add("/ds", dataService)
+                .build();
+        testServer(server, true);
+    }
+        
+    @Test
+    public void cfg_builder_CT() {
+        FusekiServer server = 
+            FusekiServer.create()
+                .setPort(port)
+                .registerOperation(newOp, contentType, customHandler)
+                .add("/ds", DatasetGraphFactory.createTxnMem(), true)
+                .addOperation("/ds", newOp, endpointName)
+                .build();
+        testServer(server, true);
+    }
+
+    @Test
+    public void cfg_builder_noCT() {
+        FusekiServer server = 
+            FusekiServer.create()
+                .setPort(port)
+                .registerOperation(newOp, null, customHandler)
+                .add("/ds", DatasetGraphFactory.createTxnMem(), true)
+                .addOperation("/ds", newOp, endpointName)
+                .build();
+        testServer(server, false);
+    }
+    
+    @Test(expected=FusekiConfigException.class)
+    public void cfg_bad_01() {
+        FusekiServer.create()
+        .setPort(port)
+        .registerOperation(newOp, null, customHandler)
+        .addOperation("/UNKNOWN", newOp, endpointName);
+        //.build();
+    }
+    
+    @Test(expected=FusekiConfigException.class)
+    public void cfg_bad_02() {
+        FusekiServer.create()
+        .setPort(port)
+        //.registerOperation(newOp, null, customHandler)
+        .add("/ds", DatasetGraphFactory.createTxnMem(), true)
+        // Unregistered.
+        .addOperation("/ds", newOp, endpointName);
+        //.build();
+    }
+
+    private void testServer(FusekiServer server, boolean withContentType) {
+        try { 
+            server.start();
+            // Try  query (no extension required)
+            try(RDFConnection rconn = RDFConnectionFactory.connect(url+"/ds")) 
{
+                try(QueryExecution qExec = rconn.query("ASK {}")) {
+                    qExec.execAsk();
+                }
+            }
+
+            // Service endpoint name : GET
+            String s1 = HttpOp.execHttpGetString(url+"/ds/"+endpointName);
+
+            // Service endpoint name : POST
+            try ( TypedInputStream stream = 
HttpOp.execHttpPostStream(url+"/ds/"+endpointName, "ignored", "", "text/plain") 
) {
+                String x = IOUtils.toString(stream, StandardCharsets.UTF_8);
+                assertNotNull(x);
+            } catch (IOException ex) {
+                IO.exception(ex);
+            }
+            
+            if ( withContentType ) {
+                // Content-type
+                try ( TypedInputStream stream = 
HttpOp.execHttpPostStream(url+"/ds", contentType, "", "text/plain") ) {
+                    String x = IOUtils.toString(stream, 
StandardCharsets.UTF_8);
+                    assertNotNull(x);
+                } catch (IOException ex) {
+                    IO.exception(ex);
+                }
+            } else {
+                // No Content-Type
+                try ( TypedInputStream stream = 
HttpOp.execHttpPostStream(url+"/ds", contentType, "", "text/plain") ) {
+                    fail("Managed to use Content-Type");
+                } catch (HttpException ex) {} 
+
+                // Service endpoint name. DELETE -> fails 405
+                try { 
+                    HttpOp.execHttpDelete(url+"/ds/"+endpointName);
+                    throw new IllegalStateException("DELETE succeeded");
+                } catch (HttpException ex) {
+                    assertEquals(405, ex.getResponseCode());   
+                }
+            }
+        } finally {
+            server.stop();
+        }
+    }
+}

Reply via email to