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