You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by an...@apache.org on 2018/09/21 10:16:07 UTC

[56/70] [abbrv] jena git commit: JENA-1597: Modules jena-fuseki-main and jena-fuseki-server

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/JettyServer.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/JettyServer.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/JettyServer.java
new file mode 100644
index 0000000..61a2e11
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/JettyServer.java
@@ -0,0 +1,369 @@
+/*
+ * 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.main;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.Filter;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.jena.atlas.lib.Pair;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.server.DataAccessPointRegistry;
+import org.apache.jena.fuseki.servlets.ActionBase;
+import org.apache.jena.fuseki.servlets.ServiceDispatchRegistry;
+import org.apache.jena.riot.web.HttpNames;
+import org.apache.jena.web.HttpSC;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.security.SecurityHandler;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Jetty server for servlets, including being able to run Fuseki {@link ActionBase} derived servlets.
+ * Static RDF types by file extension can be enabled.
+ */
+public class JettyServer {
+    // Use this for the super class of FusekiServer or as implementation inheritance.
+    // Caution : there are small differences e.g. in building where order matters.
+
+    private static Logger LOG = LoggerFactory.getLogger("HTTP");
+
+    protected final Server server;
+    protected int port;
+
+    public static Builder create() {
+        return new Builder();
+    }
+
+    protected JettyServer(int port, Server server) {
+        this.server = server;
+        this.port = port;
+    }
+
+    /**
+     * Return the port begin used.
+     * This will be the give port, which defaults to 3330, or
+     * the one actually allocated if the port was 0 ("choose a free port").
+     */
+    public int getPort() {
+        return port;
+    }
+
+    /** Get the underlying Jetty server which has also been set up. */
+    public Server getJettyServer() {
+        return server;
+    }
+
+    /** Get the {@link ServletContext}.
+     * Adding new servlets is possible with care.
+     */
+    public ServletContext getServletContext() {
+        return ((ServletContextHandler)server.getHandler()).getServletContext();
+    }
+
+    /** Start the server - the server continues to run after this call returns.
+     *  To synchronise with the server stopping, call {@link #join}.
+     */
+    public JettyServer start() {
+        try { server.start(); }
+        catch (Exception e) { throw new RuntimeException(e); }
+        if ( port == 0 )
+            port = ((ServerConnector)server.getConnectors()[0]).getLocalPort();
+        logStart();
+        return this;
+    }
+
+    protected void logStart() {
+        LOG.info("Start (port="+port+")");
+    }
+
+    /** Stop the server. */
+    public void stop() {
+        logStop();
+        try { server.stop(); }
+        catch (Exception e) { throw new RuntimeException(e); }
+    }
+
+    protected void logStop() {
+        LOG.info("Stop (port="+port+")");
+    }
+
+    /** Wait for the server to exit. This call is blocking. */
+    public void join() {
+        try { server.join(); }
+        catch (Exception e) { throw new RuntimeException(e); }
+    }
+
+    /** One line error handler */
+    public static class PlainErrorHandler extends ErrorHandler {
+        // c.f. FusekiErrorHandler1
+        public PlainErrorHandler() {}
+
+        @Override
+        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+        {
+            String method = request.getMethod();
+
+            if ( !method.equals(HttpMethod.GET.asString())
+                 && !method.equals(HttpMethod.POST.asString())
+                 && !method.equals(HttpMethod.HEAD.asString()) )
+                return ;
+
+            response.setContentType(MimeTypes.Type.TEXT_PLAIN_UTF_8.asString()) ;
+            response.setHeader(HttpNames.hCacheControl, "must-revalidate,no-cache,no-store");
+            response.setHeader(HttpNames.hPragma, "no-cache");
+            int code = response.getStatus() ;
+            String message=(response instanceof Response)?((Response)response).getReason(): HttpSC.getMessage(code) ;
+            response.getOutputStream().print(format("Error %d: %s\n", code, message)) ;
+        }
+    }
+
+    protected static class Builder {
+        private int                      port               = -1;
+        private boolean                  loopback           = false;
+        protected boolean                verbose            = false;
+        // Other servlets to add.
+        private List<Pair<String, HttpServlet>> servlets    = new ArrayList<>();
+        private List<Pair<String, Filter>> filters          = new ArrayList<>();
+
+        private String                   contextPath        = "/";
+        private String                   servletContextName = "Jetty";
+        private String                   staticContentDir   = null;
+        private SecurityHandler          securityHandler    = null;
+        private ErrorHandler             errorHandler       = new PlainErrorHandler();
+        private Map<String, Object>      servletAttr        = new HashMap<>();
+
+        /** Set the port to run on. */
+        public Builder port(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;
+        }
+
+        /**
+         * Context path.  If it's "/" then Server URL will look like
+         * "http://host:port/" else "http://host:port/path/"
+         * (or no port if :80).
+         */
+        public Builder contextPath(String path) {
+            requireNonNull(path, "path");
+            this.contextPath = path;
+            return this;
+        }
+
+        /**
+         * ServletContextName.
+         */
+        public Builder servletContextName(String name) {
+            requireNonNull(name, "name");
+            this.servletContextName = name;
+            return this;
+        }
+
+        /** Restrict the server to only responding to the localhost interface. */
+        public Builder loopback(boolean loopback) {
+            this.loopback = loopback;
+            return this;
+        }
+
+        /** Set the location (filing system directory) to serve static file from. */
+        public Builder staticFileBase(String directory) {
+            requireNonNull(directory, "directory");
+            this.staticContentDir = directory;
+            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 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 securityHandler(SecurityHandler securityHandler) {
+            requireNonNull(securityHandler, "securityHandler");
+            this.securityHandler = securityHandler;
+            return this;
+        }
+
+        /** Set an {@link ErrorHandler}.
+         * <p>
+         *  By default, the server runs with error handle that prints the code and message.
+         */
+        public Builder errorHandler(ErrorHandler errorHandler) {
+            requireNonNull(errorHandler, "securityHandler");
+            this.errorHandler = errorHandler;
+            return this;
+        }
+
+        /** Set verbose logging */
+        public Builder verbose(boolean verbose) {
+            this.verbose = verbose;
+            return this;
+        }
+
+        /**
+         * Add the given servlet with the pathSpec. These are added so that they are
+         * before the static content handler (which is the last servlet)
+         * used for {@link #staticFileBase(String)}.
+         */
+        public Builder addServlet(String pathSpec, HttpServlet servlet) {
+            requireNonNull(pathSpec, "pathSpec");
+            requireNonNull(servlet, "servlet");
+            servlets.add(Pair.create(pathSpec, servlet));
+            return this;
+        }
+
+        /**
+         * Add a servlet attribute. Pass a value of null to remove any existing binding.
+         */
+        public Builder addServletAttribute(String attrName, Object value) {
+            requireNonNull(attrName, "attrName");
+            if ( value != null )
+                servletAttr.put(attrName, value);
+            else
+                servletAttr.remove(attrName);
+            return this;
+        }
+
+        /**
+         * Add the given filter with the pathSpec.
+         * It is applied to all dispatch types.
+         */
+        public Builder addFilter(String pathSpec, Filter filter) {
+            requireNonNull(pathSpec, "pathSpec");
+            requireNonNull(filter, "filter");
+            filters.add(Pair.create(pathSpec, filter));
+            return this;
+        }
+
+        /**
+         * Build a server according to the current description.
+         */
+        public JettyServer build() {
+            ServletContextHandler handler = buildServletContext();
+            // Use HandlerCollection for several ServletContextHandlers and thus several ServletContext.
+            Server server = jettyServer(port, loopback);
+            server.setHandler(handler);
+            return new JettyServer(port, server);
+        }
+
+        /** Build a ServletContextHandler : one servlet context */
+        private ServletContextHandler buildServletContext() {
+            ServletContextHandler handler = buildServletContext(contextPath);
+            ServletContext cxt = handler.getServletContext();
+            adjustForFuseki(cxt);
+            servletAttr.forEach((n,v)->cxt.setAttribute(n, v));
+            servletsAndFilters(handler);
+            return handler;
+        }
+
+        private void adjustForFuseki(ServletContext cxt) {
+            // For Fuseki servlets added directly.
+            // This enables servlets inheriting from {@link ActionBase} to work in the
+            // plain Jetty server, e.g. to use Fuseki logging.
+            try {
+                Fuseki.setVerbose(cxt, verbose);
+                ServiceDispatchRegistry.set(cxt, new ServiceDispatchRegistry(false));
+                DataAccessPointRegistry.set(cxt, new DataAccessPointRegistry());
+            } catch (NoClassDefFoundError err) {
+                LOG.info("Fuseki classes not found");
+            }
+        }
+
+        /** Build a ServletContextHandler. */
+        private ServletContextHandler buildServletContext(String contextPath) {
+            if ( contextPath == null || contextPath.isEmpty() )
+                contextPath = "/";
+            else if ( !contextPath.startsWith("/") )
+                contextPath = "/" + contextPath;
+            ServletContextHandler context = new ServletContextHandler();
+            context.setDisplayName(servletContextName);
+            context.setErrorHandler(errorHandler);
+            context.setContextPath(contextPath);
+            if ( securityHandler != null )
+                context.setSecurityHandler(securityHandler);
+
+            return context;
+        }
+
+        /** Add servlets and servlet filters */
+        private void servletsAndFilters(ServletContextHandler context) {
+            servlets.forEach(p-> addServlet(context, p.getLeft(), p.getRight()) );
+            filters.forEach (p-> addFilter (context, p.getLeft(), p.getRight()) );
+
+            if ( staticContentDir != null ) {
+                DefaultServlet staticServlet = new DefaultServlet();
+                ServletHolder staticContent = new ServletHolder(staticServlet);
+                staticContent.setInitParameter("resourceBase", staticContentDir);
+                context.addServlet(staticContent, "/");
+            }
+        }
+
+        protected static void addServlet(ServletContextHandler context, String pathspec, HttpServlet httpServlet) {
+            ServletHolder sh = new ServletHolder(httpServlet);
+            context.addServlet(sh, pathspec);
+        }
+
+        protected void addFilter(ServletContextHandler context, String pathspec, Filter filter) {
+            FilterHolder h = new FilterHolder(filter);
+            context.addFilter(h, pathspec, null);
+        }
+
+        /** Jetty server */
+        private static Server jettyServer(int port, boolean loopback) {
+            Server server = new Server();
+            HttpConnectionFactory f1 = new HttpConnectionFactory();
+
+            //f1.getHttpConfiguration().setRequestHeaderSize(512 * 1024);
+            //f1.getHttpConfiguration().setOutputBufferSize(1024 * 1024);
+            f1.getHttpConfiguration().setSendServerVersion(false);
+            ServerConnector connector = new ServerConnector(server, f1);
+            connector.setPort(port);
+            server.addConnector(connector);
+            if ( loopback )
+                connector.setHost("localhost");
+            return server;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiMain.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiMain.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiMain.java
new file mode 100644
index 0000000..702b4f6
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiMain.java
@@ -0,0 +1,498 @@
+/*
+ * 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.main.cmds;
+
+import java.net.BindException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import arq.cmdline.CmdARQ;
+import arq.cmdline.ModAssembler;
+import arq.cmdline.ModDatasetAssembler;
+import jena.cmd.ArgDecl;
+import jena.cmd.CmdException;
+import org.apache.jena.assembler.exceptions.AssemblerException;
+import org.apache.jena.atlas.lib.DateTimeUtils;
+import org.apache.jena.atlas.lib.FileOps;
+import org.apache.jena.atlas.logging.FmtLog;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.FusekiException;
+import org.apache.jena.fuseki.main.FusekiServer;
+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.servlets.SPARQL_QueryGeneral;
+import org.apache.jena.fuseki.validation.DataValidator;
+import org.apache.jena.fuseki.validation.IRIValidator;
+import org.apache.jena.fuseki.validation.QueryValidator;
+import org.apache.jena.fuseki.validation.UpdateValidator;
+import org.apache.jena.query.ARQ;
+import org.apache.jena.query.Dataset;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.riot.RDFLanguages;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sys.JenaSystem;
+import org.apache.jena.system.Txn;
+import org.apache.jena.tdb.TDB;
+import org.apache.jena.tdb.TDBFactory;
+import org.apache.jena.tdb.transaction.TransactionManager;
+import org.apache.jena.tdb2.DatabaseMgr;
+import org.slf4j.Logger;
+
+public class FusekiMain extends CmdARQ {
+        private static int defaultPort = 3030;
+        
+        private static ArgDecl  argMem          = new ArgDecl(ArgDecl.NoValue,  "mem");
+        private static ArgDecl  argUpdate       = new ArgDecl(ArgDecl.NoValue,  "update", "allowUpdate");
+        private static ArgDecl  argFile         = new ArgDecl(ArgDecl.HasValue, "file");
+
+        private static ArgDecl  argTDB2mode     = new ArgDecl(ArgDecl.NoValue,  "tdb2");
+        private static ArgDecl  argMemTDB       = new ArgDecl(ArgDecl.NoValue,  "memtdb", "memTDB", "tdbmem");
+        private static ArgDecl  argTDB          = new ArgDecl(ArgDecl.HasValue, "loc", "location", "tdb");
+        
+        // No SPARQL dataset or services
+        private static ArgDecl  argEmpty        = new ArgDecl(ArgDecl.NoValue,  "empty", "no-dataset");
+        private static ArgDecl  argPort         = new ArgDecl(ArgDecl.HasValue, "port");
+        private static ArgDecl  argLocalhost    = new ArgDecl(ArgDecl.NoValue,  "localhost", "local");
+        private static ArgDecl  argTimeout      = new ArgDecl(ArgDecl.HasValue, "timeout");
+        private static ArgDecl  argConfig       = new ArgDecl(ArgDecl.HasValue, "config", "conf");
+        private static ArgDecl  argGZip         = new ArgDecl(ArgDecl.HasValue, "gzip");
+        private static ArgDecl  argBase         = new ArgDecl(ArgDecl.HasValue, "base", "files");
+        private static ArgDecl  argGeneralQuerySvc = new ArgDecl(ArgDecl.HasValue, "general");
+        
+        // Same as --empty --validators --general=/sparql, --files=ARG
+        
+        private static ArgDecl  argSparqler     = new ArgDecl(ArgDecl.HasValue, "sparqler");
+
+        private static ArgDecl  argValidators   = new ArgDecl(ArgDecl.NoValue,  "validators");
+        // private static ModLocation modLocation = new ModLocation();
+        private static ModDatasetAssembler modDataset      = new ModDatasetAssembler();
+
+        private final ServerConfig serverConfig  = new ServerConfig();
+        private boolean useTDB2;
+        
+        /** Build, but do not start, a server based on command line syntax. */  
+        public static FusekiServer build(String... argv) {
+            FusekiMain inner = new FusekiMain(argv);
+            inner.process();
+            return inner.buildServer();
+        }
+
+        static void innerMain(String... argv) {
+            JenaSystem.init();
+            new FusekiMain(argv).mainRun();
+        }
+
+        protected FusekiMain(String... argv) {
+            super(argv);
+
+            if ( false )
+                // Consider ...
+                TransactionManager.QueueBatchSize = TransactionManager.QueueBatchSize / 2;
+
+            getUsage().startCategory("Fuseki");
+            addModule(modDataset);
+            add(argMem, "--mem",
+                "Create an in-memory, non-persistent dataset for the server");
+            add(argFile, "--file=FILE",
+                "Create an in-memory, non-persistent dataset for the server, initialised with the contents of the file");
+            add(argTDB2mode, "--tdb2",
+                "Create command line persistent datasets with TDB2");
+            add(argTDB, "--loc=DIR",
+                "Use an existing TDB database (or create if does not exist)");
+            add(argMemTDB, "--memTDB",
+                "Create an in-memory, non-persistent dataset using TDB (testing only)");
+//            add(argEmpty, "--empty",
+//                "Run with no datasets and services (validators only)");
+            add(argEmpty); // Hidden for now.
+            add(argPort, "--port",
+                "Listen on this port number");
+            add(argLocalhost, "--localhost",
+                "Listen only on the localhost interface");
+            add(argTimeout, "--timeout=",
+                "Global timeout applied to queries (value in ms) -- format is X[,Y] ");
+            add(argUpdate, "--update",
+                "Allow updates (via SPARQL Update and SPARQL HTTP Update)");
+            add(argConfig, "--config=",
+                "Use a configuration file to determine the services");
+            add(argGZip, "--gzip=on|off",
+                "Enable GZip compression (HTTP Accept-Encoding) if request header set");
+            add(argBase, "--base=DIR",
+                "Directory for static content");
+            add(argSparqler, "--sparqler=DIR",
+                "Run with SPARQLer services Directory for static content");
+            add(argValidators, "--validators", "Install validators");
+
+            super.modVersion.addClass(TDB.class);
+            super.modVersion.addClass(Fuseki.class);
+        }
+
+        static String argUsage = "[--config=FILE] [--mem|--desc=AssemblerFile|--file=FILE] [--port PORT] /DatasetPathName";
+
+        @Override
+        protected String getSummary() {
+            return getCommandName() + " " + argUsage;
+        }
+
+        @Override
+        protected void processModulesAndArgs() {
+            boolean allowEmpty = contains(argEmpty) || contains(argSparqler);
+
+            // ---- Checking consistency
+            int numDefinitions = 0;
+
+            if ( contains(argMem) )             
+                numDefinitions++;
+            if ( contains(argFile) )
+                numDefinitions++;
+            if ( contains(ModAssembler.assemblerDescDecl) )
+                numDefinitions++;
+            if ( contains(argTDB) )
+                numDefinitions++;
+            if ( contains(argMemTDB) )
+                numDefinitions++;
+            if ( contains(argConfig) )
+                numDefinitions++;
+
+            if ( numDefinitions == 0 && ! allowEmpty )
+                throw new CmdException("No dataset specified on the command line.");
+
+            if ( numDefinitions > 1 )
+                throw new CmdException("Multiple ways providing a dataset. Only one of --mem, --file, --loc or --desc");
+
+            if ( numDefinitions > 0 && allowEmpty )
+                throw new CmdException("Dataset provided but 'no dataset' flag given");
+
+            //---- check: Invalid: --conf + service name.
+            if ( contains(argConfig) ) {
+                if ( getPositional().size() != 0 )
+                    throw new CmdException("Can't have both a configutation file and a service name");
+            } else if ( ! allowEmpty ) {
+                if ( getPositional().size() == 0 )
+                    throw new CmdException("Missing service name");
+                if ( getPositional().size() > 1 )
+                    throw new CmdException("Multiple dataset path names given");
+                serverConfig.datasetPath = DataAccessPoint.canonical(getPositionalArg(0));
+            }
+            
+            serverConfig.datasetDescription = "<unset>";
+            
+            // ---- check: Invalid: --update + --conf
+            if ( contains(argUpdate) && contains(argConfig) )
+                throw new CmdException("--update and a configuration file does not make sense (control using the configuration file only)");
+            boolean allowUpdate = contains(argUpdate);
+            serverConfig.allowUpdate = allowUpdate;
+
+            // ---- Port
+            serverConfig.port = defaultPort;
+            
+            if ( contains(argPort) ) {
+                String portStr = getValue(argPort);
+                try {
+                    int port = Integer.parseInt(portStr);
+                    serverConfig.port = port;
+                } catch (NumberFormatException ex) {
+                    throw new CmdException(argPort.getKeyName() + " : bad port number: " + portStr);
+                }
+            }
+            if ( contains(argLocalhost) )
+                serverConfig.loopback = true;
+
+            // ---- Dataset
+            // Only one of these is choose from the checking above.
+            
+            // Which TDB to use to create a command line TDB database. 
+            useTDB2 = contains(argTDB2mode);
+            String tag = useTDB2 ? "TDB2" : "TDB";
+            
+            if ( allowEmpty ) {
+                serverConfig.empty = true;
+                serverConfig.datasetDescription = "No dataset";
+            }                
+
+            // Fuseki config file 
+            if ( contains(argConfig) ) {
+                String file = getValue(argConfig);
+                if ( file.startsWith("file:") )
+                    file = file.substring("file:".length());
+                
+                Path path = Paths.get(file);
+                if ( ! Files.exists(path) )
+                    throw new CmdException("File not found: "+file);
+                if ( Files.isDirectory(path) )
+                    throw new CmdException("Is a directory: "+file);
+                serverConfig.datasetDescription = "Configuration: "+path.toAbsolutePath();
+                serverConfig.serverConfig = getValue(argConfig);
+            }
+            
+            // Ways to setup a dataset.
+            if ( contains(argMem) ) {
+                serverConfig.datasetDescription = "in-memory";
+                // Only one setup should be called by the test above but to be safe
+                // and in case of future changes, clear the configuration.  
+                serverConfig.dsg = DatasetGraphFactory.createTxnMem();
+                // Always allow, else you can't do very much! 
+                serverConfig.allowUpdate = true;
+            }
+
+            if ( contains(argFile) ) {
+                String filename = getValue(argFile);
+                String pathname = filename;
+                if ( filename.startsWith("file:") )
+                    pathname = filename.substring("file:".length());
+
+                serverConfig.datasetDescription = "file:"+filename;
+                if ( !FileOps.exists(pathname) )
+                    throw new CmdException("File not found: " + filename);
+                serverConfig.dsg = DatasetGraphFactory.createTxnMem();
+                
+                // INITIAL DATA.
+                Lang language = RDFLanguages.filenameToLang(filename);
+                if ( language == null )
+                    throw new CmdException("Can't guess language for file: " + filename);
+                Txn.executeWrite(serverConfig.dsg,  ()->RDFDataMgr.read(serverConfig.dsg, filename));
+            }
+
+            if ( contains(argMemTDB) ) {
+                serverConfig.datasetDescription = tag+" dataset in-memory";
+                serverConfig.dsg =
+                    useTDB2
+                    ? DatabaseMgr.createDatasetGraph()
+                    : TDBFactory.createDatasetGraph();
+                serverConfig.allowUpdate = true;
+            }
+
+            if ( contains(argTDB) ) {
+                String dir = getValue(argTDB);
+                serverConfig.datasetDescription = tag+" dataset: "+dir;
+                serverConfig.dsg = 
+                    useTDB2
+                    ? DatabaseMgr.connectDatasetGraph(dir)
+                    : TDBFactory.createDatasetGraph(dir);
+            }
+
+            if ( contains(ModAssembler.assemblerDescDecl) ) {
+                serverConfig.datasetDescription = "Assembler: "+ getValue(ModAssembler.assemblerDescDecl);
+                // Need to add service details.
+                Dataset ds = modDataset.createDataset();
+                serverConfig.dsg = ds.asDatasetGraph();
+            }
+
+            // ---- Misc features.
+            if ( contains(argTimeout) ) {
+                String str = getValue(argTimeout);
+                ARQ.getContext().set(ARQ.queryTimeout, str);
+            }
+            
+            if ( contains(argSparqler) ) {
+                String filebase = getValue(argSparqler);
+                if ( ! FileOps.exists(filebase) )
+                    throw new CmdException("File area not found: "+filebase); 
+                serverConfig.contentDirectory = filebase;
+                serverConfig.addGeneral = "/sparql";
+                serverConfig.empty = true;
+                serverConfig.validators = true;
+            }
+            
+            if ( contains(argGeneralQuerySvc) ) {
+                String z = getValue(argGeneralQuerySvc);
+                if ( ! z.startsWith("/") )
+                    z = "/"+z;
+                serverConfig.addGeneral = z;
+            }
+
+            if ( contains(argValidators) ) {
+                serverConfig.validators = true;
+            }
+                
+            if ( contains(argBase) ) {
+                // Static files.
+                String filebase = getValue(argBase);
+                if ( ! FileOps.exists(filebase) ) {
+                    throw new CmdException("File area not found: "+filebase); 
+                    //FmtLog.warn(Fuseki.configLog, "File area not found: "+filebase);  
+                }
+                serverConfig.contentDirectory = filebase;
+            }
+
+//            if ( contains(argGZip) ) {
+//                if ( !hasValueOfTrue(argGZip) && !hasValueOfFalse(argGZip) )
+//                    throw new CmdException(argGZip.getNames().get(0) + ": Not understood: " + getValue(argGZip));
+//                jettyServerConfig.enableCompression = super.hasValueOfTrue(argGZip);
+//            }
+        }
+        
+        @Override
+        protected void exec() {
+            try {
+                FusekiServer server = buildServer(serverConfig);
+                info(server, serverConfig);
+                try {
+                    server.start();
+                } catch (FusekiException ex) {
+                    if ( ex.getCause() instanceof BindException ) {
+                        Fuseki.serverLog.error("Failed to start server: "+ex.getCause().getMessage()+ ": port="+serverConfig.port) ;
+                        System.exit(1);
+                    }
+                    throw ex;
+                } catch (Exception ex) {
+                    throw new FusekiException("Failed to start server: " + ex.getMessage(), ex) ;
+                }
+                server.join();
+                System.exit(0);
+            }
+            catch (AssemblerException ex) {
+                if ( ex.getCause() != null )
+                    System.err.println(ex.getCause().getMessage());
+                else
+                    System.err.println(ex.getMessage());
+            }
+        }
+
+        private FusekiServer buildServer() {
+            return buildServer(serverConfig);
+        }
+
+        // ServerConfig -> Setup the builder.
+        private FusekiServer buildServer(ServerConfig serverConfig) {
+            FusekiServer.Builder builder = builder();
+            return buildServer(builder, serverConfig);
+        }
+        
+        protected FusekiServer.Builder builder() {
+            return FusekiServer.create();
+        }
+        
+        private static FusekiServer buildServer(FusekiServer.Builder builder, ServerConfig serverConfig) {
+            builder.port(serverConfig.port);
+            builder.loopback(serverConfig.loopback);
+            
+            if ( serverConfig.addGeneral != null )
+                builder.addServlet(serverConfig.addGeneral,  new SPARQL_QueryGeneral());
+            
+            if ( serverConfig.validators ) {
+                // Validators.
+                builder.addServlet("/validate/query",  new QueryValidator());
+                builder.addServlet("/validate/update", new UpdateValidator());
+                builder.addServlet("/validate/iri",    new IRIValidator());
+                builder.addServlet("/validate/data",   new DataValidator());
+            }
+            
+            if ( ! serverConfig.empty ) {
+                if ( serverConfig.serverConfig != null )
+                    // Config file.
+                    builder.parseConfigFile(serverConfig.serverConfig);
+                else
+                    // One dataset.
+                    builder.add(serverConfig.datasetPath, serverConfig.dsg, serverConfig.allowUpdate);
+            }
+            
+            if ( serverConfig.contentDirectory != null )
+                builder.staticFileBase(serverConfig.contentDirectory) ;
+
+            return builder.build();
+        }
+
+        private void info(FusekiServer server, ServerConfig serverConfig) {
+            if ( super.isQuiet() )
+                return;
+
+            Logger log = Fuseki.serverLog;
+
+            String version = Fuseki.VERSION;
+            String buildDate = Fuseki.BUILD_DATE ;
+
+            if ( version != null && version.equals("${project.version}") )
+                version = null ;
+            if ( buildDate != null && buildDate.equals("${build.time.xsd}") )
+                buildDate = DateTimeUtils.nowAsXSDDateTimeString() ;
+            
+            String name = Fuseki.NAME;
+            name = name +" (basic server)";
+            
+            if ( version != null ) {
+                if ( Fuseki.developmentMode && buildDate != null )
+                    FmtLog.info(log, "%s %s %s", name, version, buildDate) ;
+                else
+                    FmtLog.info(log, "%s %s", name, version);
+            }
+            
+            // Dataset -> Endpoints
+            Map<String, List<String>> mapDatasetEndpoints = description(DataAccessPointRegistry.get(server.getServletContext()));
+            
+            if ( serverConfig.empty ) {
+                FmtLog.info(log, "No SPARQL datasets services"); 
+            } else {
+                if ( serverConfig.datasetPath == null && serverConfig.serverConfig == null )
+                    log.error("No dataset path nor server configuration file");
+            }
+            
+            if ( serverConfig.datasetPath != null ) {
+                if ( mapDatasetEndpoints.size() != 1 )
+                    log.error("Expected only one dataset");
+                List<String> endpoints = mapDatasetEndpoints.get(serverConfig.datasetPath); 
+                FmtLog.info(log,  "Dataset Type = %s", serverConfig.datasetDescription);
+                FmtLog.info(log,  "Path = %s; Services = %s", serverConfig.datasetPath, endpoints);
+            }
+            if ( serverConfig.serverConfig != null ) {
+                // May be many datasets and services.
+                FmtLog.info(log,  "Configuration file %s", serverConfig.serverConfig);
+                mapDatasetEndpoints.forEach((path, endpoints)->{
+                    FmtLog.info(log,  "Path = %s; Services = %s", path, endpoints);
+                });
+            }
+            
+            if ( serverConfig.contentDirectory != null )
+                FmtLog.info(log,  "Static files = %s", serverConfig.contentDirectory);
+                
+            if ( super.isVerbose() )
+                PlatformInfo.logDetailsVerbose(log);
+            else if ( !super.isQuiet() )
+                PlatformInfo.logDetails(log);
+        }
+
+        private static Map<String, List<String>> description(DataAccessPointRegistry reg) {
+            Map<String, List<String>> desc = new LinkedHashMap<>();
+            reg.forEach((ds,dap)->{
+                List<String> endpoints = new ArrayList<>();
+                desc.put(ds, endpoints);
+                DataService dSrv = dap.getDataService();
+                dSrv.getOperations().forEach((op)->{
+                    dSrv.getEndpoints(op).forEach(ep-> {
+                        String x = ep.getEndpoint();
+                        if ( x.isEmpty() )
+                            x = "quads";
+                        endpoints.add(x);   
+                    });
+                });
+            });
+            return desc;
+        }
+        
+        @Override
+        protected String getCommandName() {
+            return "fuseki";
+        }
+    }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiMainCmd.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiMainCmd.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiMainCmd.java
new file mode 100644
index 0000000..a7ae54b
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiMainCmd.java
@@ -0,0 +1,46 @@
+/*
+ * 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.main.cmds;
+
+import org.apache.jena.fuseki.system.FusekiLogging;
+
+/** Fuseki command that runs a Fuseki server without the admin UI, just SPARQL services.
+ * <p>
+ * Use {@code --conf=} for multiple datasets and specific service names. 
+ * <p>
+ * The command line dataset setup only supports a single dataset.
+ */
+
+public class FusekiMainCmd {
+    // This class wraps FusekiBasicMain so that it can take control of logging setup.
+    // It does not depend via inheritance on any Jena code - FusekiBasicMain does.
+    // Inheritance causes initialization in the super class first, before class
+    // initialization code in this class.
+    
+    static { FusekiLogging.setLogging(); }
+
+    /**
+     * Build and run, a server based on command line syntax. This operation does not
+     * return. See {@link FusekiMain#build} to build a server using command line
+     * syntax but not start it.
+     */
+    static public void main(String... argv) {
+        FusekiMain.innerMain(argv);
+    }
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/PlatformInfo.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/PlatformInfo.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/PlatformInfo.java
new file mode 100644
index 0000000..143a43d
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/PlatformInfo.java
@@ -0,0 +1,135 @@
+/*
+ * 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.main.cmds;
+
+import java.io.IOException;
+import java.util.function.Function;
+
+import org.apache.jena.atlas.logging.FmtLog;
+import org.slf4j.Logger;
+
+public class PlatformInfo {
+    
+    public static void main(String ...args) throws IOException {
+        long maxMem = Runtime.getRuntime().maxMemory();
+        long totalMem = Runtime.getRuntime().totalMemory();
+        long freeMem = Runtime.getRuntime().freeMemory();
+        long usedMem = totalMem - freeMem;
+        Function<Long, String> f = PlatformInfo::strNumMixed;
+        
+        System.out.printf("max=%s  total=%s  used=%s  free=%s\n", f.apply(maxMem), f.apply(totalMem), f.apply(usedMem), f.apply(freeMem));
+    }
+    
+    /** Essential information about the runtime environment. */
+    
+    public static void logDetails(Logger log) {
+        logDetails(log, "  ");
+    }
+    
+    /**
+     * Essential information about the runtime environment
+     * @param log
+     * @param prefix String to add at the start of the log message.
+     */
+    public static void logDetails(Logger log, String prefix) {
+        if ( prefix == null )
+            prefix = "";
+        long maxMem = Runtime.getRuntime().maxMemory();
+        long totalMem = Runtime.getRuntime().totalMemory();
+        long freeMem = Runtime.getRuntime().freeMemory();
+        long usedMem = totalMem - freeMem;
+        Function<Long, String> f = PlatformInfo::strNum2;
+
+        long pid = getProcessId();
+        FmtLog.info(log, "%sMemory: %s", prefix, f.apply(maxMem));
+        //FmtLog.info(log, "%sMemory: max=%s  total=%s  used=%s  free=%s", prefix, f.apply(maxMem), f.apply(totalMem), f.apply(usedMem), f.apply(freeMem));
+        FmtLog.info(log, "%sJava:   %s", prefix, System.getProperty("java.version"));
+        FmtLog.info(log, "%sOS:     %s %s %s", prefix, System.getProperty("os.name"), System.getProperty("os.version"), System.getProperty("os.arch"));
+        if ( pid != -1)
+            FmtLog.info(log, "%sPID:    %s", prefix, pid);
+    }
+
+    public static void logDetailsVerbose(Logger log) {
+        logDetails(log);
+        logOne(log, "java.vendor");
+        logOne(log, "java.home");
+        logOne(log, "java.runtime.version");
+        logOne(log, "java.runtime.name");
+        //logOne(log, "java.endorsed.dirs");
+        logOne(log, "user.language");
+        logOne(log, "user.timezone");
+        logOne(log, "user.country");
+        logOne(log, "user.dir");
+        //logOne(log, "file.encoding");
+    }
+    
+    private static void logOne(Logger log, String property) {
+        FmtLog.info(log, "    %-20s = %s", property, System.getProperty(property));
+    }
+
+    /** Create a human-friendly string for a number based on Kilo/Mega/Giga/Tera (powers of 2) */
+    public static String strNumMixed(long x) {
+        // https://en.wikipedia.org/wiki/Kibibyte
+        if ( x < 1024 )
+            return Long.toString(x);
+        if ( x < 1024*1024 )
+            return String.format("%.1fK", x/1024.0);
+        if ( x < 1024*1024*1024 )
+            return String.format("%.1fM", x/(1024.0*1024));
+        if ( x < 1024L*1024*1024*1024 )
+            return String.format("%.1fG", x/(1024.0*1024*1024));
+        return String.format("%.1fT", x/(1024.0*1024*1024*1024));
+    }
+    
+    private static long getProcessId() {
+        // Java9
+        //long pid = ProcessHandle.current().getPid();
+        try { 
+            String x = java.lang.management.ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
+            return Long.parseLong(x);
+        } catch (NumberFormatException ex) { return -1 ; }
+    }
+    
+    /** Create a human-friendly string for a number based on Kilo/Mega/Giga/Tera (powers of 10) */
+    public static String strNum10(long x) {
+        if ( x < 1_000 )
+            return Long.toString(x);
+        if ( x < 1_000_000 )
+            return String.format("%.1fK", x/1000.0);
+        if ( x < 1_000_000_000 )
+            return String.format("%.1fM", x/(1000.0*1000));
+        if ( x < 1_000_000_000_000L )
+            return String.format("%.1fG", x/(1000.0*1000*1000));
+        return String.format("%.1fT", x/(1000.0*1000*1000*1000));
+    }
+    
+    /** Create a human-friendly string for a number based on Kibi/Mebi/Gibi/Tebi (powers of 2) */
+    public static String strNum2(long x) {
+        // https://en.wikipedia.org/wiki/Kibibyte
+        if ( x < 1024 )
+            return Long.toString(x);
+        if ( x < 1024*1024 )
+            return String.format("%.1f KiB", x/1024.0);
+        if ( x < 1024*1024*1024 )
+            return String.format("%.1f MiB", x/(1024.0*1024));
+        if ( x < 1024L*1024*1024*1024 )
+            return String.format("%.1f GiB", x/(1024.0*1024*1024));
+        return String.format("%.1fTiB", x/(1024.0*1024*1024*1024));
+    }
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/ServerConfig.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/ServerConfig.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/ServerConfig.java
new file mode 100644
index 0000000..2376a50
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/ServerConfig.java
@@ -0,0 +1,51 @@
+/*
+ * 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.main.cmds;
+
+import org.apache.jena.sparql.core.DatasetGraph;
+
+/** Setup details (command line, config file) from command line processing.
+ *  This is built by {@link FusekiMain#exec}.
+ *  This is processed by {@link FusekiMain#buildServer}.
+ */
+class ServerConfig {
+    /** Server port */
+    public int port;
+    /** Loopback */
+    public boolean loopback           = false;
+    /** The dataset name */
+    public String    datasetPath      = null; 
+    /** Allow update */
+    public boolean   allowUpdate      = false;
+    
+    // This is set ...
+    public DatasetGraph dsg           = null;
+    // ... or this.
+    public String serverConfig        = null;
+    
+    /** No registered datasets without it being an error. */
+    public boolean empty         = false ;
+    /** General query processor servlet */
+    public String addGeneral          = null ;
+    
+    public boolean validators         = false ;
+    /** An informative label */
+    public String datasetDescription;
+    public String contentDirectory    = null;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/CustomService.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/CustomService.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/CustomService.java
new file mode 100644
index 0000000..94eec39
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/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.main;
+
+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/7e6d03af/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/FusekiTestAuth.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/FusekiTestAuth.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/FusekiTestAuth.java
new file mode 100644
index 0000000..8342571
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/FusekiTestAuth.java
@@ -0,0 +1,178 @@
+/**
+ * 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.main;
+
+import java.util.Objects;
+
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.fuseki.FusekiLib;
+import org.apache.jena.fuseki.jetty.JettyLib;
+import org.apache.jena.sparql.core.DatasetGraph ;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.web.HttpSC;
+import org.eclipse.jetty.security.*;
+import org.eclipse.jetty.security.authentication.BasicAuthenticator;
+import org.eclipse.jetty.util.security.Constraint;
+import org.junit.Assert;
+
+/**
+ * Testing HTTP athentication.
+ * <p>
+ * {@code FusekiTestAuth} provides helper code for before/after (any of suite/class/test).
+ * The pattern of usage is:
+ * <pre>
+ * 
+ * &#64;BeforeClass
+ * public static void beforeClassAuth() {
+ *     SecurityHandler sh = FusekiTestAuth.makeSimpleSecurityHandler("/*", "USER", "PASSWORD");
+ *     FusekiTestAuth.setupServer(true, sh);
+ * }
+ * 
+ * &#64;AfterClass
+ * public static void afterClassAuth() {
+ *     FusekiTestAuth.teardownServer();
+ *     // Clear up any pooled connections.
+ *     HttpOp.setDefaultHttpClient(HttpOp.createPoolingHttpClient());
+ * }
+ * 
+ * &#64;Test
+ * public void myAuthTest() {
+ *     BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
+ *     Credentials credentials = new UsernamePasswordCredentials("USER", "PASSWORD");
+ *     credsProvider.setCredentials(AuthScope.ANY, credentials);
+ *     HttpClient client = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build();
+ *     try (TypedInputStream in = HttpOp.execHttpGet(ServerCtl.urlDataset(), "* /*", client, null)) {}
+ * }
+ * 
+ * &#64;Test
+ * public void myAuthTestRejected() {
+ *     BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
+ *     Credentials credentials = new UsernamePasswordCredentials("USER", "PASSWORD");
+ *     credsProvider.setCredentials(AuthScope.ANY, credentials);
+ *     HttpClient client = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build();
+ *     try (TypedInputStream in = HttpOp.execHttpGet(ServerCtl.urlDataset(), "* /*", client, null)) {}
+ *     catch (HttpException ex) {
+ *         throw assertAuthHttpException(ex);
+ *     }
+ * }
+ * </pre>
+ * 
+ * {@code @BeforeClass} can be {@code @Before} but server stop-start is expensive so a
+ * large test suite may end up quite slow.
+ */
+public class FusekiTestAuth {
+    private static int currentPort = FusekiLib.choosePort() ;
+    
+    public static int port() {
+        return currentPort ;
+    }
+    
+    static boolean CLEAR_DSG_DIRECTLY = true ;
+    static private DatasetGraph dsgTesting ;
+    
+    // Abstraction that runs a SPARQL server for tests.
+    public static final String urlRoot()            { return "http://localhost:"+port()+"/" ; }
+    public static final String datasetPath()        { return "/dataset" ; }
+    public static final String urlDataset()         { return "http://localhost:"+port()+datasetPath() ; }
+    public static final DatasetGraph getDataset()   { return dsgTesting ; }
+    
+    public static final String serviceUpdate()      { return "http://localhost:"+port()+datasetPath()+"/update" ; } 
+    public static final String serviceQuery()       { return "http://localhost:"+port()+datasetPath()+"/query" ; }
+    public static final String serviceGSP()         { return "http://localhost:"+port()+datasetPath()+"/data" ; }
+    
+    private static FusekiServer server ;
+
+    /** Setup a testing server, using the given  Jetty {@link SecurityHandler} for authentication. 
+     * The server will have an empty, in-emory transactional dataset.
+     */
+    public static void setupServer(boolean updateable, SecurityHandler sh) {
+        setupServer(updateable, sh, DatasetGraphFactory.createTxnMem());
+    }
+    
+    /** Setup a testing server, using the given  Jetty {@link SecurityHandler} for authentication. 
+     */
+    public static void setupServer(boolean updateable, SecurityHandler sh, DatasetGraph dsg) {
+        dsgTesting = dsg;
+        server = FusekiServer.create()
+            .add(datasetPath(), dsgTesting)
+            .port(port())
+            .securityHandler(sh)
+            .build()
+            .start();
+    }
+    
+    /** Shutdown the server.*/
+    public static void teardownServer() {
+        if ( server != null ) {
+            server.stop() ;
+            server = null ;
+            dsgTesting = null;
+        }
+    }
+
+    /** Create a Jetty {@link SecurityHandler} for basic authentication, one user/password/role. */
+    public static SecurityHandler makeSimpleSecurityHandler(String pathSpec, String user, String password) {
+            return makeSimpleSecurityHandler(pathSpec, null, user, password, "FusekiTestRole");
+    }
+
+    /** Create a Jetty {@link SecurityHandler} for basic authentication, one user/password/role. */
+    public static SecurityHandler makeSimpleSecurityHandler(String pathSpec, String realm, String user, String password, String role) {
+        Objects.requireNonNull(user);
+        Objects.requireNonNull(password);
+        Objects.requireNonNull(role);
+        
+        Constraint constraint = new Constraint() ;
+        constraint.setName(Constraint.__BASIC_AUTH) ;
+        String[] roles = new String[]{role};
+        constraint.setRoles(roles) ;
+        constraint.setAuthenticate(true) ;
+
+        ConstraintMapping mapping = new ConstraintMapping() ;
+        mapping.setConstraint(constraint) ;
+        mapping.setPathSpec("/*") ;
+
+        IdentityService identService = new DefaultIdentityService() ;
+        
+        ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler() ;
+        securityHandler.addConstraintMapping(mapping) ;
+        securityHandler.setIdentityService(identService) ;
+        
+        UserStore userStore = JettyLib.makeUserStore(user, password, role);
+        
+        HashLoginService loginService = new HashLoginService("Fuseki Authentication") ;
+        loginService.setUserStore(userStore);
+        loginService.setIdentityService(identService) ;
+        
+        securityHandler.setLoginService(loginService) ;
+        securityHandler.setAuthenticator(new BasicAuthenticator()) ;
+        if ( realm != null )
+            securityHandler.setRealmName(realm);
+        
+        return securityHandler;
+    }
+
+    /** Assert that an {@code HttpException} ias an authorization failure.
+     * This is normally 403.  401 indicates no retryu with credentials.
+     */
+    public static HttpException assertAuthHttpException(HttpException ex) {
+        int rc = ex.getResponseCode();
+        Assert.assertTrue(rc == HttpSC.FORBIDDEN_403 || rc == HttpSC.UNAUTHORIZED_401 );
+        return ex;
+    }
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/FusekiTestServer.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/FusekiTestServer.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/FusekiTestServer.java
new file mode 100644
index 0000000..58983c9
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/FusekiTestServer.java
@@ -0,0 +1,261 @@
+/**
+ * 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.main;
+
+import static org.apache.jena.fuseki.main.FusekiTestServer.ServerScope.CLASS;
+import static org.apache.jena.fuseki.main.FusekiTestServer.ServerScope.SUITE;
+import static org.apache.jena.fuseki.main.FusekiTestServer.ServerScope.TEST;
+
+import java.util.concurrent.atomic.AtomicInteger ;
+
+import org.apache.http.client.HttpClient ;
+import org.apache.http.impl.client.CloseableHttpClient ;
+import org.apache.jena.atlas.io.IO ;
+import org.apache.jena.fuseki.FusekiLib;
+import org.apache.jena.riot.web.HttpOp ;
+import org.apache.jena.sparql.core.DatasetGraph ;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.modify.request.Target ;
+import org.apache.jena.sparql.modify.request.UpdateDrop ;
+import org.apache.jena.system.Txn ;
+import org.apache.jena.update.Update ;
+import org.apache.jena.update.UpdateExecutionFactory ;
+import org.apache.jena.update.UpdateProcessor ;
+
+/**
+ * Manage a single server for use with tests. It supports three modes:
+ * <ul>
+ * <li>One server for a whole test suite
+ * <li>One server per test class
+ * <li>One server per individual test
+ * </ul>
+ * One server per individual test can be troublesome due to connections not closing down
+ * fast enough (left in TCP state {@code TIME_WAIT} which is 2 minutes) and also can be
+ * slow. One server per test class is a good compromise.
+ * <p>
+ * The data in the server is always reseet between tests.
+ * <p>
+ * Using a connection pooling HttpClient (see {@link HttpOp#createPoolingHttpClient()}) is
+ * important, both for test performance and for reducing the TCP connection load on the
+ * operating system.
+ * <p>
+ * Usage:
+ * </p>
+ * <p>
+ * In the test suite, put:
+ * 
+ * <pre>
+ *  {@literal @BeforeClass} static public void beforeSuiteClass() { FusekiTestServer.ctlBeforeTestSuite(); } 
+ *  {@literal @AfterClass}  static public void afterSuiteClass()  { FusekiTestServer.ctlAfterTestSuite(); }
+ * </pre>
+ * <p>
+ * In the test class, put:
+ * 
+ * <pre>
+ * {@literal @BeforeClass} public static void ctlBeforeClass() { FusekiTestServer.ctlBeforeClass(); }
+ * {@literal @AfterClass}  public static void ctlAfterClass()  { FusekiTestServer.ctlAfterClass(); }
+ * {@literal @Before}      public void ctlBeforeTest()         { FusekiTestServer.ctlBeforeTest(); }
+ * {@literal @After}       public void ctlAfterTest()          { FusekiTestServer.ctlAfterTest(); }
+ * </pre>
+ * 
+ * Much of this machinery is unnessecary for just running a sever in the background:
+ * 
+ * <pre>
+ *   private static FusekiServer server ;
+ *   private static DatasetGraph serverdsg = DatasetGraphFactory.createTxnMem() ;
+ *
+ *   &#64;BeforeClass
+ *   public static void beforeClass() {
+ *       server = FusekiServer.create()
+ *           .setPort(....)
+ *           .add("/ds", serverdsg)
+ *           .build()
+ *           .start();
+ *   }
+ *
+ *   &#64;Before
+ *   public void beforeTest() {
+ *       // Clear up data in server servers
+ *       Txn.executeWrite(serverdsg, ()->serverdsg.clear()) ;
+ *   }
+ *       
+ *   &#64;AfterClass
+ *   public static void afterClass() {
+ *       server.stop(); 
+ *   }
+ * </pre>
+ */
+public class FusekiTestServer {
+    /* Cut&Paste versions:
+
+    Test suite (TS_*)
+    @BeforeClass static public void beforeSuiteClass() { FusekiTestServer.ctlBeforeTestSuite(); } 
+    @AfterClass  static public void afterSuiteClass()  { FusekiTestServer.ctlAfterTestSuite(); }
+
+    Test class (Test*)
+    @BeforeClass public static void ctlBeforeClass() { FusekiTestServer.ctlBeforeClass(); }
+    @AfterClass  public static void ctlAfterClass()  { FusekiTestServer.ctlAfterClass(); }
+    @Before      public void ctlBeforeTest()         { FusekiTestServer.ctlBeforeTest(); }
+    @After       public void ctlAfterTest()          { FusekiTestServer.ctlAfterTest(); }
+     
+    */
+
+    // Note: it is important to cleanly close a PoolingHttpClient across server restarts
+    // otherwise the pooled connections remain for the old server. 
+    
+    /*package : for import static */ enum ServerScope { SUITE, CLASS, TEST }
+    private static ServerScope serverScope = ServerScope.CLASS ;
+    private static int currentPort = FusekiLib.choosePort() ;
+    
+    public static int port() {
+        return currentPort ;
+    }
+
+    // Whether to use a transaction on the dataset or to use SPARQL Update. 
+    static boolean CLEAR_DSG_DIRECTLY = true ;
+    static private DatasetGraph dsgTesting ;
+    
+    // Abstraction that runs a SPARQL server for tests.
+    public static final String urlRoot()        { return "http://localhost:"+port()+"/" ; }
+    public static final String datasetPath()    { return "/dataset" ; }
+    public static final String urlDataset()     { return "http://localhost:"+port()+datasetPath() ; }
+    
+    public static final String serviceUpdate()  { return "http://localhost:"+port()+datasetPath()+"/update" ; } 
+    public static final String serviceQuery()   { return "http://localhost:"+port()+datasetPath()+"/query" ; }
+    public static final String serviceGSP()     { return "http://localhost:"+port()+datasetPath()+"/data" ; }
+    
+    public static void ctlBeforeTestSuite() {
+        if ( serverScope == SUITE  ) {
+            setPoolingHttpClient() ;
+            allocServer();
+        }
+    }
+    
+    public static void ctlAfterTestSuite()  {
+        if ( serverScope == SUITE  ) {
+            freeServer();
+            resetDefaultHttpClient() ;
+        }
+    }
+    
+    /**
+     * Setup for the tests by allocating a Fuseki instance to work with
+     */
+    public static void ctlBeforeClass() {
+        if ( serverScope == CLASS  ) {
+            setPoolingHttpClient() ;
+            allocServer();
+        }
+    }
+    
+    /**
+     * Clean up after tests by de-allocating the Fuseki instance
+     */
+    public static void ctlAfterClass() {
+        if ( serverScope == CLASS  ) {
+            freeServer();
+            resetDefaultHttpClient() ;
+        }
+    }
+
+    /**
+     * Placeholder.
+     */
+    public static void ctlBeforeTest() {
+        if ( serverScope == TEST  ) {
+            setPoolingHttpClient() ;
+            allocServer();
+        }
+    }
+
+    /**
+     * Clean up after each test by resetting the Fuseki dataset
+     */
+    public static void ctlAfterTest() {
+        if ( serverScope == TEST  ) {
+            freeServer();
+            resetDefaultHttpClient() ;
+        } else
+            resetServer();
+    }
+
+    /** Set a PoolingHttpClient */
+    public static void setPoolingHttpClient() {
+        setHttpClient(HttpOp.createPoolingHttpClient()) ;
+    }
+
+    /** Restore the original setup */
+    private static void resetDefaultHttpClient() {
+        setHttpClient(HttpOp.createDefaultHttpClient());
+    }
+    
+    /** Set the HttpClient - close the old one if appropriate */
+    public static void setHttpClient(HttpClient newHttpClient) {
+        HttpClient hc = HttpOp.getDefaultHttpClient() ;
+        if ( hc instanceof CloseableHttpClient )
+            IO.close((CloseableHttpClient)hc) ;
+        HttpOp.setDefaultHttpClient(newHttpClient) ;
+    }
+    
+    // reference count of start/stop server
+    private static AtomicInteger countServer = new AtomicInteger() ; 
+    private static FusekiServer server        = null ;
+    
+    /*package*/ static void allocServer() {
+        if ( countServer.getAndIncrement() == 0 )
+            setupServer(true) ;
+    }
+    
+    /*package*/ static void freeServer() {
+        if ( countServer.decrementAndGet() == 0 )
+            teardownServer() ;
+    }
+    
+    /*package*/ static void setupServer(boolean updateable) {
+        dsgTesting = DatasetGraphFactory.createTxnMem();
+        server = FusekiServer.create()
+            .add(datasetPath(), dsgTesting)
+            .port(port())
+            .build()
+            .start();
+    }
+    
+    /*package*/ static void teardownServer() {
+        if ( server != null ) {
+            server.stop() ;
+            server = null ;
+        }
+    }
+
+    /*package*/ static void resetServer() {
+        if (countServer.get() == 0)  
+            throw new RuntimeException("No server started!");
+        if ( CLEAR_DSG_DIRECTLY ) {
+            Txn.executeWrite(dsgTesting, ()->dsgTesting.clear()) ;   
+        } else {
+            Update clearRequest = new UpdateDrop(Target.ALL) ;
+            UpdateProcessor proc = UpdateExecutionFactory.createRemote(clearRequest, serviceUpdate()) ;
+            try {proc.execute() ; }
+            catch (Throwable e) {e.printStackTrace(); throw e;}
+        }
+    }
+    
+    // ---- Helper code.
+
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TS_EmbeddedFuseki.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TS_EmbeddedFuseki.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TS_EmbeddedFuseki.java
new file mode 100644
index 0000000..c10c8d5
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TS_EmbeddedFuseki.java
@@ -0,0 +1,51 @@
+/*
+ * 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.main;
+
+import org.apache.jena.atlas.logging.LogCtl ;
+import org.apache.jena.fuseki.Fuseki ;
+import org.junit.BeforeClass ;
+import org.junit.runner.RunWith ;
+import org.junit.runners.Suite ;
+import org.junit.runners.Suite.SuiteClasses ;
+
+@RunWith(Suite.class)
+@SuiteClasses({
+  TestEmbeddedFuseki.class
+  , TestMultipleEmbedded.class
+  , TestFusekiTestServer.class
+  , TestFusekiTestAuth.class
+  , TestFusekiCustomOperation.class
+})
+public class TS_EmbeddedFuseki {
+    @BeforeClass public static void setupForFusekiServer() {
+        LogCtl.setLevel(Fuseki.serverLogName,        "WARN");
+        LogCtl.setLevel(Fuseki.actionLogName,        "WARN");
+        LogCtl.setLevel(Fuseki.requestLogName,       "WARN");
+        LogCtl.setLevel(Fuseki.adminLogName,         "WARN");
+        LogCtl.setLevel("org.eclipse.jetty",         "WARN");
+        
+        // Shouldn't see these in the embedded server.
+//        LogCtl.setLevel("org.apache.shiro",          "WARN") ;
+//        LogCtl.setLevel(Fuseki.configLogName,        "WARN");
+
+//        LogCtl.setLevel(Fuseki.builderLogName,       "WARN");
+//        LogCtl.setLevel(Fuseki.servletRequestLogName,"WARN");
+    }
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestEmbeddedFuseki.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestEmbeddedFuseki.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestEmbeddedFuseki.java
new file mode 100644
index 0000000..6499444
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestEmbeddedFuseki.java
@@ -0,0 +1,321 @@
+/*
+ * 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.main;
+
+import static org.junit.Assert.assertEquals ;
+import static org.junit.Assert.assertFalse ;
+import static org.junit.Assert.assertNotNull ;
+import static org.junit.Assert.assertNull ;
+import static org.junit.Assert.assertTrue ;
+
+import java.io.OutputStream ;
+import java.util.function.Consumer ;
+
+import org.apache.http.HttpEntity ;
+import org.apache.http.entity.ContentProducer ;
+import org.apache.http.entity.EntityTemplate ;
+import org.apache.jena.atlas.web.ContentType ;
+import org.apache.jena.atlas.web.HttpException ;
+import org.apache.jena.atlas.web.TypedInputStream ;
+import org.apache.jena.fuseki.FusekiLib;
+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.graph.Graph ;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.query.QueryExecutionFactory;
+import org.apache.jena.query.ResultSet;
+import org.apache.jena.query.ResultSetFormatter;
+import org.apache.jena.riot.RDFDataMgr ;
+import org.apache.jena.riot.RDFFormat ;
+import org.apache.jena.riot.RDFLanguages ;
+import org.apache.jena.riot.web.HttpOp ;
+import org.apache.jena.sparql.core.DatasetGraph ;
+import org.apache.jena.sparql.core.DatasetGraphFactory ;
+import org.apache.jena.sparql.core.Quad ;
+import org.apache.jena.sparql.graph.GraphFactory ;
+import org.apache.jena.sparql.sse.SSE ;
+import org.apache.jena.system.Txn ;
+import org.apache.jena.update.UpdateExecutionFactory ;
+import org.apache.jena.update.UpdateFactory ;
+import org.apache.jena.update.UpdateRequest ;
+import org.apache.jena.web.HttpSC ;
+import org.junit.Test ;
+
+public class TestEmbeddedFuseki {
+    
+    private static final String DIR = "testing/FusekiEmbedded/" ;
+
+    // Test - build on default port. 
+    @Test public void embedded_01() {
+        DatasetGraph dsg = dataset() ;
+        int port = 3330 ;   // Default port.
+        FusekiServer server = FusekiServer.create().add("/ds", dsg).build() ;
+        assertTrue(server.getDataAccessPointRegistry().isRegistered("/ds")) ;
+        server.start() ;
+        query("http://localhost:"+port+"/ds/query", "SELECT * { ?s ?p ?o}", qExec-> {
+            ResultSet rs = qExec.execSelect() ; 
+            assertFalse(rs.hasNext()) ;
+        }) ;
+        server.stop() ;
+    }
+    
+    // Different dataset name.
+    @Test public void embedded_02() {
+        DatasetGraph dsg = dataset() ;
+        int port = 0 ;//FusekiEnv.choosePort() ;
+        FusekiServer server = FusekiServer.make(port, "/ds2", dsg) ;
+        DataAccessPointRegistry registry = server.getDataAccessPointRegistry() ;
+        // But no /ds
+        assertEquals(1, registry.size()) ;
+        assertTrue(registry.isRegistered("/ds2")) ;
+        assertFalse(registry.isRegistered("/ds")) ;
+        try {
+            server.start() ;
+        } finally { server.stop() ; }
+    }
+    
+    // Different dataset name.
+    @Test public void embedded_03() {
+        DatasetGraph dsg = dataset() ;
+        int port = FusekiLib.choosePort() ;
+        FusekiServer server = FusekiServer.create()
+            .port(port)
+            .add("/ds1", dsg) 
+            .build() ;
+        server.start() ;
+        try {
+            // Add while live.
+            Txn.executeWrite(dsg,  ()->{
+                Quad q = SSE.parseQuad("(_ :s :p _:b)") ;
+                dsg.add(q); 
+            }) ;
+            query("http://localhost:"+port+"/ds1/query", "SELECT * { ?s ?p ?o}", qExec->{
+                ResultSet rs = qExec.execSelect() ; 
+                int x = ResultSetFormatter.consume(rs) ;
+                assertEquals(1, x) ;
+            }) ;
+        } finally { server.stop() ; }
+    }
+    
+    
+    @Test public void embedded_04() {
+        DatasetGraph dsg = dataset() ;
+        Txn.executeWrite(dsg,  ()->{
+            Quad q = SSE.parseQuad("(_ :s :p _:b)") ;
+            dsg.add(q); 
+        }) ;
+
+        // A service with just being able to do quads operations
+        // That is, GET, POST, PUT on  "/data" in N-quads and TriG. 
+        DataService dataService = new DataService(dsg) ;
+        dataService.addEndpoint(Operation.Quads_RW, "");
+        dataService.addEndpoint(Operation.Query, "");
+        dataService.addEndpoint(Operation.Update, "");
+        int port = FusekiLib.choosePort() ;
+        
+        FusekiServer server = FusekiServer.create()
+            .port(port)
+            .add("/data", dataService)
+            .build() ;
+        server.start() ;
+        try {
+            // Put data in.
+            String data = "(graph (:s :p 1) (:s :p 2) (:s :p 3))" ;
+            Graph g = SSE.parseGraph(data) ;
+            HttpEntity e = graphToHttpEntity(g) ;
+            HttpOp.execHttpPut("http://localhost:"+port+"/data", e) ;
+    
+            // Get data out.
+            try ( TypedInputStream in = HttpOp.execHttpGet("http://localhost:"+port+"/data") ) { 
+                Graph g2 = GraphFactory.createDefaultGraph() ;
+                RDFDataMgr.read(g2, in, RDFLanguages.contentTypeToLang(in.getContentType())) ;
+                assertTrue(g.isIsomorphicWith(g2)) ;
+            }
+            // Query.
+            query("http://localhost:"+port+"/data", "SELECT * { ?s ?p ?o}", qExec->{
+                ResultSet rs = qExec.execSelect() ; 
+                int x = ResultSetFormatter.consume(rs) ;
+                assertEquals(3, x) ;
+            }) ;
+            // Update
+            UpdateRequest req = UpdateFactory.create("CLEAR DEFAULT") ;
+            UpdateExecutionFactory.createRemote(req, "http://localhost:"+port+"/data").execute(); 
+            // Query again.
+            query("http://localhost:"+port+"/data", "SELECT * { ?s ?p ?o}", qExec-> {
+                ResultSet rs = qExec.execSelect() ; 
+                int x = ResultSetFormatter.consume(rs) ;
+                assertEquals(0, x) ;
+            }) ;
+        } finally { server.stop() ; }
+    }
+    
+    @Test public void embedded_05() {
+        DatasetGraph dsg = dataset() ;
+        int port = FusekiLib.choosePort() ;
+        FusekiServer server = FusekiServer.create()
+            .port(port)
+            .add("/ds0", dsg) 
+            .build() ;
+        server.start() ;
+        try {
+            // No stats
+            String x = HttpOp.execHttpGetString("http://localhost:"+port+"/$/stats") ;
+            assertNull(x) ;  
+        } finally { server.stop() ; }
+    }
+    
+    @Test public void embedded_06() {
+        DatasetGraph dsg = dataset() ;
+        int port = FusekiLib.choosePort() ;
+        FusekiServer server = FusekiServer.create()
+            .port(port)
+            .add("/ds0", dsg)
+            .enableStats(true)
+            .build() ;
+        server.start() ;
+        // No stats
+        String x = HttpOp.execHttpGetString("http://localhost:"+port+"/$/stats") ;
+        assertNotNull(x) ;
+        server.stop() ;
+    }
+
+    // Context path.
+    @Test public void embedded_07() {
+        DatasetGraph dsg = dataset() ;
+        int port = FusekiLib.choosePort() ;
+        
+        FusekiServer server = FusekiServer.create()
+            .port(port)
+            .contextPath("/ABC")
+            .add("/ds", dsg) 
+            .build() ;
+        server.start() ;
+        try {
+            String x1 = HttpOp.execHttpGetString("http://localhost:"+port+"/ds") ;
+            assertNull(x1) ;
+            String x2 = HttpOp.execHttpGetString("http://localhost:"+port+"/ABC/ds") ;
+            assertNotNull(x2) ;
+        } finally { server.stop() ; }
+    }
+    
+    @Test public void embedded_08() {
+        DatasetGraph dsg = dataset() ;
+        int port = FusekiLib.choosePort() ;
+
+        FusekiServer server = FusekiServer.create()
+            .port(port)
+            .parseConfigFile(DIR+"config.ttl") 
+            .build() ;
+        server.start() ;
+        try {
+            query("http://localhost:"+port+"/FuTest", "SELECT * {}", x->{}) ;
+        } finally { server.stop() ; } 
+    }
+    
+    @Test public void embedded_09() {
+        DatasetGraph dsg = dataset() ;
+        int port = FusekiLib.choosePort() ;
+
+        FusekiServer server = FusekiServer.create()
+            .port(port)
+            .contextPath("/ABC")
+            .parseConfigFile(DIR+"config.ttl") 
+            .build() ;
+        server.start() ;
+        try {
+            try {
+                query("http://localhost:"+port+"/FuTest", "ASK{}", x->{}) ;
+            } catch (HttpException ex) {
+                assertEquals(HttpSC.METHOD_NOT_ALLOWED_405, ex.getResponseCode()) ;
+            }
+
+            query("http://localhost:"+port+"/ABC/FuTest","ASK{}",x->{}) ;
+        } finally { server.stop() ; } 
+    }
+
+    @Test public void embedded_20() {
+        DatasetGraph dsg = dataset() ;
+        int port = FusekiLib.choosePort() ;
+
+        DataService dSrv = new DataService(dsg) ;
+        dSrv.addEndpoint(Operation.Query, "q") ;
+        dSrv.addEndpoint(Operation.GSP_R, "gsp") ;
+        FusekiServer server = FusekiServer.create()
+            .add("/dsrv1", dSrv)
+            .port(port)
+            .build() ;
+        server.start() ;
+        try {
+            query("http://localhost:"+port+"/dsrv1/q","ASK{}",x->{}) ;
+            String x1 = HttpOp.execHttpGetString("http://localhost:"+port+"/dsrv1/gsp") ;
+            assertNotNull(x1) ;
+        } finally { server.stop() ; } 
+    }
+    
+    @Test public void embedded_21() {
+        DatasetGraph dsg = dataset() ;
+        int port = FusekiLib.choosePort() ;
+
+        DataService dSrv = new DataService(dsg) ;
+        dSrv.addEndpoint(Operation.Query, "q") ;
+        dSrv.addEndpoint(Operation.GSP_R, "gsp") ;
+        FusekiServer server = FusekiServer.create()
+            .add("/dsrv1", dSrv)
+            .staticFileBase(DIR)
+            .port(port)
+            .build() ;
+        server.start() ;
+        
+        try {
+            query("http://localhost:"+port+"/dsrv1/q","ASK{}",x->{}) ;
+            String x1 = HttpOp.execHttpGetString("http://localhost:"+port+"/dsrv1/gsp") ;
+            assertNotNull(x1) ;
+            // Static
+            String x2 = HttpOp.execHttpGetString("http://localhost:"+port+"/test.txt");
+            assertNotNull(x2) ;
+        } finally { server.stop() ; } 
+    }
+
+    
+    /** Create an HttpEntity for the graph */  
+    protected static HttpEntity graphToHttpEntity(final Graph graph) {
+        final RDFFormat syntax = RDFFormat.TURTLE_BLOCKS ;
+        ContentProducer producer = new ContentProducer() {
+            @Override
+            public void writeTo(OutputStream out) {
+                RDFDataMgr.write(out, graph, syntax) ;
+            }
+        } ;
+        EntityTemplate entity = new EntityTemplate(producer) ;
+        ContentType ct = syntax.getLang().getContentType() ;
+        entity.setContentType(ct.getContentType()) ;
+        return entity ;
+    }
+
+    /*package*/ static DatasetGraph dataset() {
+        return DatasetGraphFactory.createTxnMem() ;
+    }
+
+    /*package*/ static void query(String URL, String query, Consumer<QueryExecution> body) {
+        try (QueryExecution qExec = QueryExecutionFactory.sparqlService(URL, query) ) {
+            body.accept(qExec);
+        }
+    }
+}