JENA-1221: Merge commit 'refs/pull/162/head' of github.com:apache/jena

This closes #162.
diff --git a/jena-arq/src/main/java/org/apache/jena/atlas/json/JsonObject.java b/jena-arq/src/main/java/org/apache/jena/atlas/json/JsonObject.java
index b83d1cb..b7e89db 100644
--- a/jena-arq/src/main/java/org/apache/jena/atlas/json/JsonObject.java
+++ b/jena-arq/src/main/java/org/apache/jena/atlas/json/JsonObject.java
@@ -54,11 +54,6 @@
         return map.containsKey(key) ;
     }
     
-//    @Override
-//    public boolean containsValue(Object value) {
-//        return map.containsValue(value) ;
-//    }
-    
     public Set<String> keys() {
         return map.keySet() ;
     }
@@ -71,6 +66,11 @@
         return map.get(key) ;
     }
 
+    /** For walking structures */
+    public JsonObject getObj(String key) {
+        return get(key).getAsObject() ;
+    }
+
     public boolean isEmpty() {
         return map.isEmpty() ;
     }
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/mgt/ActionStats.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/mgt/ActionStats.java
index b44e678..fc95331 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/mgt/ActionStats.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/mgt/ActionStats.java
@@ -30,6 +30,7 @@
 import javax.servlet.http.HttpServletResponse ;
 
 import org.apache.jena.atlas.json.JsonBuilder ;
+import org.apache.jena.atlas.json.JsonObject ;
 import org.apache.jena.atlas.json.JsonValue ;
 import org.apache.jena.fuseki.server.* ;
 import org.apache.jena.fuseki.servlets.HttpAction ;
@@ -44,21 +45,20 @@
     @Override
     protected JsonValue execGetContainer(HttpAction action) { 
         action.log.info(format("[%d] GET stats all", action.id)) ;
-        JsonBuilder builder = new JsonBuilder() ;
-        builder.startObject("top") ;
-        
-        builder.key(JsonConst.datasets) ;
-        builder.startObject("datasets") ;
-        for ( String ds : action.getDataAccessPointRegistry().keys() ) {
-            DataAccessPoint access = action.getDataAccessPointRegistry().get(ds) ;
-            statsDataset(builder, access) ; 
-        }
-        builder.finishObject("datasets") ;
-        
-        builder.finishObject("top") ;
-        return builder.build() ;
+        return generateStats(action.getDataAccessPointRegistry()) ;
     }
 
+    public static JsonObject generateStats(DataAccessPointRegistry registry) {
+        JsonBuilder builder = new JsonBuilder() ;
+        builder.startObject("top") ;
+        builder.key(JsonConst.datasets) ;
+        builder.startObject("datasets") ;
+        registry.forEach((name, access)->statsDataset(builder, access));
+        builder.finishObject("datasets") ;
+        builder.finishObject("top") ;
+        return builder.build().getAsObject() ;
+    }
+    
     @Override
     protected JsonValue execGetItem(HttpAction action) {
         action.log.info(format("[%d] GET stats dataset %s", action.id, action.getDatasetName())) ;
@@ -75,13 +75,19 @@
         builder.finishObject("TOP") ;
         return builder.build() ;
     }
-
+    
+    public static JsonObject generateStats(DataAccessPoint access) {
+        JsonBuilder builder = new JsonBuilder() ;
+        statsDataset(builder, access) ;
+        return builder.build().getAsObject() ;
+    }
+    
     private void statsDataset(JsonBuilder builder, String name, DataAccessPointRegistry registry) {
         DataAccessPoint access = registry.get(name) ;
         statsDataset(builder, access);
     }
     
-    private void statsDataset(JsonBuilder builder, DataAccessPoint access) {
+    private static void statsDataset(JsonBuilder builder, DataAccessPoint access) {
         // Object started
         builder.key(access.getName()) ;
         DataService dSrv = access.getDataService() ;
@@ -91,29 +97,13 @@
         builder.key(CounterName.RequestsGood.name()).value(dSrv.getCounters().value(CounterName.RequestsGood)) ;
         builder.key(CounterName.RequestsBad.name()).value(dSrv.getCounters().value(CounterName.RequestsBad)) ;
         
-        
-        // Build the operation -> endpoint list map.
-        
-//      MultiMap<OperationName, Endpoint> map = MultiMap.createMapList() ;
-//      for ( OperationName operName : dSrv.getOperations() ) {
-//          List<Endpoint> endpoints = access.getDataService().getOperation(operName) ;
-//          for ( Endpoint endpoint : endpoints )
-//              map.put(operName, endpoint) ; 
-//      }
-        
-        
         builder.key(JsonConst.endpoints).startObject("endpoints") ;
         
         for ( OperationName operName : dSrv.getOperations() ) {
             List<Endpoint> endpoints = access.getDataService().getOperation(operName) ;
-//            System.err.println(operName+" : "+endpoints.size()) ;
-//            for ( Endpoint endpoint : endpoints )
-//                System.err.println("  "+endpoint.getEndpoint()) ;
             
             for ( Endpoint endpoint : endpoints ) {
-                
-                // Endpoint names are unique but not services.
-                
+                // Endpoint names are unique for a given service.
                 builder.key(endpoint.getEndpoint()) ;
                 builder.startObject() ;
                 
@@ -126,10 +116,9 @@
         }
         builder.finishObject("endpoints") ;
         builder.finishObject("counters") ;
-
     }
 
-    private void operationCounters(JsonBuilder builder, Endpoint operation) {
+    private static void operationCounters(JsonBuilder builder, Endpoint operation) {
         for (CounterName cn : operation.getCounters().counters()) {
             Counter c = operation.getCounters().get(cn) ;
             builder.key(cn.name()).value(c.value()) ;
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 eee14ea..533b49e 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
@@ -50,18 +50,21 @@
         }) ;
     }
     
-    // To be removed ...
+    // TODO To be removed ...
     private static DataAccessPointRegistry singleton = new DataAccessPointRegistry() ;
+    // Still used by ServerTest and FusekiEmbeddedServer (but nowhere else)
     public static DataAccessPointRegistry get() { return singleton ; }
 
-    private static final String attrNameRegistry = "jena.apache.org/fuseki/dataAccessPointRegistry" ;
+    private static final String attrNameRegistry = "jena-fuseki:dataAccessPointRegistry" ;
     // Policy for the location of the server-wide DataAccessPointRegistry 
     public static DataAccessPointRegistry get(ServletContext cxt) {
         //return (DataAccessPointRegistry)cxt.getAttribute(attrName) ;
         return singleton ;
     }
     
-    public static void set(ServletContext cxt, DataAccessPointRegistry registry) { 
+    public static void set(ServletContext cxt, DataAccessPointRegistry registry) {
+        // Temporary until get() removed completely.
+        singleton = registry ;
         cxt.setAttribute(attrNameRegistry, registry) ;
     }
 }
diff --git a/jena-fuseki2/jena-fuseki-core/src/test/java/org/apache/jena/fuseki/TS_Fuseki.java b/jena-fuseki2/jena-fuseki-core/src/test/java/org/apache/jena/fuseki/TS_Fuseki.java
index e7a09ee..9424eb8 100644
--- a/jena-fuseki2/jena-fuseki-core/src/test/java/org/apache/jena/fuseki/TS_Fuseki.java
+++ b/jena-fuseki2/jena-fuseki-core/src/test/java/org/apache/jena/fuseki/TS_Fuseki.java
@@ -59,7 +59,6 @@
         FusekiLogging.setLogging();
         FusekiEnv.setEnvironment() ;
         
-        // Occasionally log4j.properties gets out of step.
         LogCtl.setLevel("org.apache.shiro",          "WARN") ;
         LogCtl.setLevel("org.eclipse.jetty",         "WARN");
         
diff --git a/jena-fuseki2/jena-fuseki-embedded/pom.xml b/jena-fuseki2/jena-fuseki-embedded/pom.xml
new file mode 100644
index 0000000..72fc04e
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-embedded/pom.xml
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" 
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <name>Apache Jena - Fuseki Embedded Server</name>
+  <artifactId>jena-fuseki-embedded</artifactId>
+
+  <parent>
+    <groupId>org.apache.jena</groupId>
+    <artifactId>jena-fuseki</artifactId>
+    <version>2.4.1-SNAPSHOT</version>
+  </parent> 
+
+  <packaging>jar</packaging>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.jena</groupId>
+      <artifactId>jena-fuseki-core</artifactId>
+      <version>2.4.1-SNAPSHOT</version>
+      <!-- No specific logging - leave to the application -->
+      <exclusions>
+        <exclusion>
+          <groupId>org.slf4j</groupId>
+          <artifactId>slf4j-log4j12</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>log4j</groupId>
+          <artifactId>log4j</artifactId>
+        </exclusion>
+        <!-- -->
+         <exclusion>
+          <groupId>org.apache.jena</groupId>
+          <artifactId>jena-text</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.apache.jena</groupId>
+          <artifactId>jena-spatial</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+
+    <!-- Logging examples -->
+    <!-- java.util.logging.
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-jdk14</artifactId>
+      <version>1.7.21</version>
+    </dependency>
+    -->
+
+    <!-- Log4j
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-log4j12</artifactId>
+      <version>1.7.21</version>
+    </dependency>
+
+    <dependency>
+      <groupId>log4j</groupId>
+      <artifactId>log4j</artifactId>
+      <version>1.2.17</version>
+    </dependency> 
+    -->
+
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>attach-sources</id>
+            <phase>package</phase>
+            <goals>
+              <goal>jar-no-fork</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <includes>
+            <include>**/TS_*.java</include>
+          </includes>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <configuration>
+          <overWriteReleases>false</overWriteReleases>
+          <overWriteIfNewer>true</overWriteIfNewer>
+        </configuration>
+      </plugin>
+
+    </plugins>
+
+  </build>
+
+</project>
diff --git a/jena-fuseki2/jena-fuseki-embedded/src/main/java/org/apache/jena/fuseki/embedded/FusekiEmbeddedServer.java b/jena-fuseki2/jena-fuseki-embedded/src/main/java/org/apache/jena/fuseki/embedded/FusekiEmbeddedServer.java
new file mode 100644
index 0000000..aa206fa
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-embedded/src/main/java/org/apache/jena/fuseki/embedded/FusekiEmbeddedServer.java
@@ -0,0 +1,305 @@
+/*
+ * 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.util.HashMap ;
+import java.util.List ;
+import java.util.Map ;
+
+import javax.servlet.ServletContext ;
+
+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.OperationName ;
+import org.apache.jena.fuseki.servlets.FusekiFilter ;
+import org.apache.jena.query.Dataset ;
+import org.apache.jena.sparql.core.DatasetGraph ;
+import org.eclipse.jetty.server.HttpConnectionFactory ;
+import org.eclipse.jetty.server.Server ;
+import org.eclipse.jetty.server.ServerConnector ;
+import org.eclipse.jetty.servlet.FilterHolder ;
+import org.eclipse.jetty.servlet.ServletContextHandler ;
+
+/**
+ * Embedded Fuseki server. This is a Fuseki server running with a precofigured set of
+ * datasets and services. 
+ * There is no admin UI.
+ * <p>
+ * To create a embedded sever, use {@link FusekiEmbeddedServer} ({@link #make} is a
+ * packaging of a call to {@link FusekiEmbeddedServer} for the case of one dataset,
+ * responding to localhost only).
+ * <p>
+ * The application should call {@link #start()} to actually start the server
+ * (it wil run in the background : see {@link #join}).
+ * <p>Example:
+ * <pre>
+ *      DatasetGraph dsg = ... ;
+ *      FusekiEmbeddedServer server = FusekiEmbeddedServer.create()
+ *          .setPort(1234)
+ *          .add("/ds", dsg)
+ *          .build() ;
+ *       server.start() ;
+ * </pre>
+ * Compact form (use the builder pattern above to get more flexibility):
+ * <pre>
+ *    FusekiEmbeddedServer.make(1234, "/ds", dsg).start() ;
+ * </pre>
+ * 
+ */
+public class FusekiEmbeddedServer {
+    static { 
+        //FusekiEnv.mode = FusekiEnv.INIT.EMBEDDED ;
+        // Stop anything accidently resetting Fuseki server logging. 
+        //FusekiLogging.allowLoggingReset(false) ;
+    }
+    
+    /** Construct a Fuseki server for one dataset.
+     * It only responds to localhost. 
+     * The returned server has not been started  */ 
+    static public FusekiEmbeddedServer make(int port, String name, DatasetGraph dsg) {
+        return create()
+            .setPort(port)
+            .setLoopback(true)
+            .add(name, dsg)
+            .build() ;
+    }
+    
+    public static Builder create() {
+        return new Builder() ;
+    }
+    
+    public final Server server ;
+    private int port ;
+    
+    public FusekiEmbeddedServer(Server server) {
+        this.server = server ;
+        port = ((ServerConnector)server.getConnectors()[0]).getPort() ;
+    }
+    
+    /** 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 void start() { 
+        try { server.start(); }
+        catch (Exception e) { throw new FusekiException(e) ; }
+        if ( port == 0 )
+            port = ((ServerConnector)server.getConnectors()[0]).getLocalPort() ;
+        Fuseki.serverLog.info("Start Fuseki (port="+port+")");
+    }
+
+    /** Stop the server. */
+    public void stop() { 
+        Fuseki.serverLog.info("Stop Fuseki (port="+port+")");
+        try { server.stop(); }
+        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) ; }
+    }
+    
+    /** FusekiEmbeddedServer.Builder */
+    public static class Builder {
+        // Keeping this allows accumulation of data access points for one name.  
+        private Map<String, DataService> map = new HashMap<>() ;
+        private int port = 3330 ;
+        private boolean loopback = false ;
+        private boolean withStats = false ;
+        private String contextPath = "/" ;
+        
+        /* Set the port to run on */ 
+        public Builder setPort(int port) {
+            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 ;
+        }
+        
+        /** Restrict the server to only respoding to the localhost interface. */ 
+        public Builder setLoopback(boolean loopback) {
+            this.loopback = loopback;
+            return this ;
+        }
+
+        /** Add the "/$/stats" servlet that responds with stats about the server,
+         * including counts of all calls made.
+         */ 
+        public Builder enableStats(boolean withStats) {
+            this.withStats = withStats;
+            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()) ;
+        }
+
+        /* 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) ;
+        }
+
+        /* 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) ;
+        }
+            
+        
+        /* Add the dataset with given name and a default set of services. */  
+        public Builder add(String name, DatasetGraph dsg, boolean allowUpdate) {
+            DataService dSrv = FusekiBuilder.buildDataService(dsg, allowUpdate) ; 
+            return add(name, dSrv) ;
+        }
+        
+        /* Add a data service that includes dataset and service names.*/  
+        public Builder add(String name, DataService dataService) {
+            return add$(name, dataService) ; 
+        }
+        
+        /* Add an operation, specifing it's endpoint name.
+         * This adds endpoints to any existing data service already setup by the builder.   
+         */
+        public Builder add(String name, Dataset ds, OperationName opName, String epName) {
+            return add(name, ds.asDatasetGraph(), opName, epName) ; 
+        }
+
+            /* Add an operation, specifing it's endpoint name.
+         * This adds endpoints to any existing data service already setup by the builder.   
+         */
+        public Builder add(String name, DatasetGraph dsg, OperationName opName, String epName) {
+            DataService dSrv = map.get(name) ;
+            if ( dSrv == null ) {
+                dSrv = new DataService(dsg) ;
+                map.put(name, dSrv) ;
+            }
+            dSrv.addEndpoint(opName, epName);
+            return this ; 
+        }
+
+        private Builder add$(String name, DataService dataService) {
+            name = DataAccessPoint.canonical(name) ;
+            if ( map.containsKey(name) )
+                throw new FusekiConfigException("Attempt to add a DataService for a different dataset: "+name) ;
+            map.put(name, dataService) ;
+            
+            // Merge endpoints : too complicated 
+//            DataService dSrv = map.get(name) ;
+//            if ( dSrv != null ) {
+//                DatasetGraph dsg1 = dSrv.getDataset() ;
+//                DatasetGraph dsg2 = dataService.getDataset() ;
+//                if ( dsg1 != dsg2 ) // Object identity
+//                    throw new FusekiConfigException("Attempt to add a DataService for a different dataset: "+name) ;
+//                dSrv.getOperations() ;
+//            } else
+//                map.put(name, dataService) ;
+            return this ;
+        }
+        
+        /** Read and parse a Fuseki services/datasets file.
+         *  <p>
+         *  The application is responsible for ensuring a correct classpath. For example,
+         *  including a dependency on {@code jena-text} if the configuration file
+         *  includes a text index.     
+         */
+        public Builder parseConfigFile(String filename) {
+            List<DataAccessPoint> x = FusekiConfig.readConfigurationFile(filename) ;
+            // Unbundle so that they accumulate.
+            x.forEach(dap-> add(dap.getName(), dap.getDataService())) ;
+            return this ;
+        }
+
+        /** Build a server according to the current description */ 
+        public FusekiEmbeddedServer build() {
+            DataAccessPointRegistry registry = new DataAccessPointRegistry() ;
+            map.forEach((name, dSrv) -> {
+                DataAccessPoint dap = new DataAccessPoint(name, dSrv) ;
+                registry.put(name, dap) ;
+            }) ;
+            ServletContextHandler handler = buildServletContext(contextPath, registry) ;
+            if ( withStats )
+                handler.addServlet(ActionStats.class, "/$/stats") ;
+            DataAccessPointRegistry.set(handler.getServletContext(), registry) ;
+            Server server = jettyServer(port, loopback) ;
+            server.setHandler(handler);
+            return new FusekiEmbeddedServer(server) ;
+        }
+
+        /** Build a ServletContextHandler with the Fuseki router : {@link FusekiFilter} */
+        private static ServletContextHandler buildServletContext(String contextPath, DataAccessPointRegistry registry) {
+            if ( contextPath == null || contextPath.isEmpty() )
+                contextPath = "/" ;
+            else if ( !contextPath.startsWith("/") )
+                contextPath = "/" + contextPath ;
+            ServletContextHandler context = new ServletContextHandler() ;
+            FusekiFilter ff = new FusekiFilter() ;
+            FilterHolder h = new FilterHolder(ff) ;
+            context.setContextPath(contextPath) ;
+            context.addFilter(h, "/*", null) ;
+            context.setDisplayName(Fuseki.servletRequestLogName) ;
+            context.setErrorHandler(new FusekiErrorHandler1()) ;
+            return context ;
+        }
+        
+        /** Jetty server */
+        private static Server jettyServer(int port, boolean loopback) {
+            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(5 * 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) ;
+            server.addConnector(connector);
+            if ( loopback )
+                connector.setHost("localhost");
+            return server ;
+        }
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..c66149d
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TS_EmbeddedFuseki.java
@@ -0,0 +1,49 @@
+/*
+ * 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 org.apache.jena.atlas.logging.LogCtl ;
+import org.apache.jena.fuseki.Fuseki ;
+import org.apache.jena.fuseki.FusekiLogging ;
+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  
+})
+public class TS_EmbeddedFuseki {
+    @BeforeClass public static void setupForFusekiServer() {
+        FusekiLogging.setLogging();
+        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");
+    }
+}
diff --git a/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TestEmbeddedFuseki.java b/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TestEmbeddedFuseki.java
new file mode 100644
index 0000000..cbc0adb
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-embedded/src/test/java/org/apache/jena/fuseki/embedded/TestEmbeddedFuseki.java
@@ -0,0 +1,269 @@
+/*
+ * 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.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.server.DataAccessPointRegistry ;
+import org.apache.jena.fuseki.server.DataService ;
+import org.apache.jena.fuseki.server.OperationName ;
+import org.apache.jena.graph.Graph ;
+import org.apache.jena.query.* ;
+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 public void embedded_01() {
+        DatasetGraph dsg = dataset() ;
+        FusekiEmbeddedServer server = FusekiEmbeddedServer.create().add("/ds", dsg).build() ;
+        assertTrue(DataAccessPointRegistry.get().isRegistered("/ds")) ;
+        server.start() ;
+        query("http://localhost:3330/ds/query", "SELECT * { ?s ?p ?o}", qExec-> {
+            ResultSet rs = qExec.execSelect() ; 
+            assertFalse(rs.hasNext()) ;
+        }) ;
+        server.stop() ;
+    }
+    
+    @Test public void embedded_01a() {
+        Dataset ds = DatasetFactory.createTxnMem() ;
+        FusekiEmbeddedServer server = FusekiEmbeddedServer.create().add("/ds", ds).build() ;
+        assertTrue(DataAccessPointRegistry.get().isRegistered("/ds")) ;
+        server.start() ;
+        query("http://localhost:3330/ds/query", "SELECT * { ?s ?p ?o}", qExec-> {
+            ResultSet rs = qExec.execSelect() ; 
+            assertFalse(rs.hasNext()) ;
+        }) ;
+        server.stop() ;
+    }
+
+    @Test public void embedded_02() {
+        DatasetGraph dsg = dataset() ;
+        FusekiEmbeddedServer server = FusekiEmbeddedServer.make(3330, "/ds2", dsg) ;
+        // But no /ds
+        assertEquals(1,  DataAccessPointRegistry.get().size()) ;
+        assertTrue(DataAccessPointRegistry.get().isRegistered("/ds2")) ;
+        assertFalse(DataAccessPointRegistry.get().isRegistered("/ds")) ;
+        try {
+            server.start() ;
+        } finally { server.stop() ; }
+    }
+    
+    @Test public void embedded_03() {
+        DatasetGraph dsg = dataset() ;
+        FusekiEmbeddedServer server = FusekiEmbeddedServer.create()
+            .setPort(3331)
+            .add("/ds1", dsg) 
+            .build() ;
+        server.start() ;
+        try {
+            // Add while live.
+            Txn.execWrite(dsg,  ()->{
+                Quad q = SSE.parseQuad("(_ :s :p _:b)") ;
+                dsg.add(q); 
+            }) ;
+            query("http://localhost:3331/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.execWrite(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(OperationName.Quads_RW, "");
+        dataService.addEndpoint(OperationName.Query, "");
+        dataService.addEndpoint(OperationName.Update, "");
+        
+        FusekiEmbeddedServer server = FusekiEmbeddedServer.create()
+            .setPort(3332)
+            .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:3332/data", e) ;
+    
+            // Get data out.
+            try ( TypedInputStream in = HttpOp.execHttpGet("http://localhost:3332/data") ) { 
+                Graph g2 = GraphFactory.createDefaultGraph() ;
+                RDFDataMgr.read(g2, in, RDFLanguages.contentTypeToLang(in.getContentType())) ;
+                assertTrue(g.isIsomorphicWith(g2)) ;
+            }
+            // Query.
+            query("http://localhost:3332/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:3332/data").execute(); 
+            // Query again.
+            query("http://localhost:3332/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() ;
+        FusekiEmbeddedServer server = FusekiEmbeddedServer.create()
+            .setPort(3330)
+            .add("/ds0", dsg) 
+            .build() ;
+        server.start() ;
+        try {
+            // No stats
+            String x = HttpOp.execHttpGetString("http://localhost:3330/$/stats") ;
+            assertNull(x) ;  
+        } finally { server.stop() ; }
+    }
+    
+    @Test public void embedded_06() {
+        DatasetGraph dsg = dataset() ;
+        FusekiEmbeddedServer server = FusekiEmbeddedServer.create()
+            .setPort(3330)
+            .add("/ds0", dsg)
+            .enableStats(true)
+            .build() ;
+        server.start() ;
+        // No stats
+        String x = HttpOp.execHttpGetString("http://localhost:3330/$/stats") ;
+        assertNotNull(x) ;
+        server.stop() ;
+    }
+
+    // Context path.
+    @Test public void embedded_07() {
+        DatasetGraph dsg = dataset() ;
+        FusekiEmbeddedServer server = FusekiEmbeddedServer.create()
+            .setPort(3330)
+            .setContextPath("/ABC")
+            .add("/ds", dsg) 
+            .build() ;
+        server.start() ;
+        try {
+            String x1 = HttpOp.execHttpGetString("http://localhost:3330/ds") ;
+            assertNull(x1) ;
+            String x2 = HttpOp.execHttpGetString("http://localhost:3330/ABC/ds") ;
+            assertNotNull(x2) ;
+        } finally { server.stop() ; }
+    }
+    
+    @Test public void embedded_08() {
+        DatasetGraph dsg = dataset() ;
+        FusekiEmbeddedServer server = FusekiEmbeddedServer.create()
+            .setPort(3330)
+            .parseConfigFile(DIR+"config.ttl") 
+            .build() ;
+        server.start() ;
+        try {
+            query("http://localhost:3330/FuTest", "SELECT * {}", x->{}) ;
+        } finally { server.stop() ; } 
+    }
+    
+    @Test public void embedded_09() {
+        DatasetGraph dsg = dataset() ;
+        FusekiEmbeddedServer server = FusekiEmbeddedServer.create()
+            .setPort(3330)
+            .setContextPath("/ABC")
+            .parseConfigFile(DIR+"config.ttl") 
+            .build() ;
+        server.start() ;
+        try {
+            try {
+                query("http://localhost:3330/FuTest", "ASK{}", x->{}) ;
+            } catch (HttpException ex) {
+                assertEquals(HttpSC.METHOD_NOT_ALLOWED_405, ex.getResponseCode()) ;
+            }
+
+            query("http://localhost:3330/ABC/FuTest","ASK{}",x->{}) ;
+        } 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 ;
+    }
+
+    private DatasetGraph dataset() {
+        return DatasetGraphFactory.createTxnMem() ;
+    }
+
+    private static void query(String URL, String query, Consumer<QueryExecution> body) {
+        try (QueryExecution qExec = QueryExecutionFactory.sparqlService(URL, query) ) {
+            body.accept(qExec);
+        }
+    }
+}
diff --git a/jena-fuseki2/jena-fuseki-embedded/testing/FusekiEmbedded/config.ttl b/jena-fuseki2/jena-fuseki-embedded/testing/FusekiEmbedded/config.ttl
new file mode 100644
index 0000000..5a7f84a
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-embedded/testing/FusekiEmbedded/config.ttl
@@ -0,0 +1,18 @@
+@prefix :        <#> .
+@prefix fuseki:  <http://jena.apache.org/fuseki#> .
+@prefix rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+@prefix rdfs:    <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix ja:      <http://jena.hpl.hp.com/2005/11/Assembler#> .
+@prefix tdb:     <http://jena.hpl.hp.com/2008/tdb#> .
+
+<#serviceInMemory> rdf:type fuseki:Service;
+    rdfs:label "test";
+    fuseki:name "FuTest";
+    fuseki:serviceQuery "query";
+    fuseki:serviceUpdate "update";
+    fuseki:serviceUpload "upload" ;
+    fuseki:dataset <#dataset> ;
+.
+
+<#dataset> rdf:type ja:RDFDataset;
+.
diff --git a/jena-fuseki2/pom.xml b/jena-fuseki2/pom.xml
index 11728ee..7b6f126 100644
--- a/jena-fuseki2/pom.xml
+++ b/jena-fuseki2/pom.xml
@@ -70,6 +70,7 @@
 
   <modules>
     <module>jena-fuseki-core</module>
+    <module>jena-fuseki-embedded</module>
     <module>jena-fuseki-war</module>
     <module>jena-fuseki-server</module>
     <module>apache-jena-fuseki</module>