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:06 UTC

[55/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/test/java/org/apache/jena/fuseki/main/TestFusekiCustomOperation.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiCustomOperation.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiCustomOperation.java
new file mode 100644
index 0000000..6003320
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiCustomOperation.java
@@ -0,0 +1,188 @@
+/*
+ * 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.*;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.atlas.web.TypedInputStream;
+import org.apache.jena.fuseki.FusekiConfigException;
+import org.apache.jena.fuseki.FusekiLib;
+import org.apache.jena.fuseki.build.FusekiBuilder;
+import org.apache.jena.fuseki.server.DataService;
+import org.apache.jena.fuseki.server.Operation;
+import org.apache.jena.fuseki.servlets.ActionService;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.rdfconnection.RDFConnection;
+import org.apache.jena.rdfconnection.RDFConnectionFactory;
+import org.apache.jena.riot.web.HttpOp;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.junit.Test;
+
+/** Test for adding a new operation */
+public class TestFusekiCustomOperation {
+    private static final Operation newOp = Operation.register("Special", "Custom operation");
+    private static final String contentType = "application/special";
+    private static final String endpointName = "special";
+
+    private final ActionService customHandler = new CustomService();
+    private final int port = FusekiLib.choosePort();
+    private final String url = "http://localhost:"+port;
+
+    @Test
+    public void cfg_dataservice() {
+        // Create a DataService and add the endpoint -> operation association.
+        // This still needs the server to have the operation registered.  
+        DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+        DataService dataService = new DataService(dsg);
+        FusekiBuilder.populateStdServices(dataService, true);
+        FusekiBuilder.addServiceEP(dataService, newOp, endpointName);
+        
+        FusekiServer server = 
+            FusekiServer.create()
+                .port(port)
+                .registerOperation(newOp, contentType, customHandler)
+                .add("/ds", dataService)
+                .build();
+        testServer(server, true, true);
+    }
+        
+    @Test
+    public void cfg_builder_CT() {
+        FusekiServer server = 
+            FusekiServer.create()
+                .port(port)
+                .registerOperation(newOp, contentType, customHandler)
+                .add("/ds", DatasetGraphFactory.createTxnMem(), true)
+                .addOperation("/ds", endpointName, newOp)
+                .build();
+        testServer(server, true, true);
+    }
+
+    @Test
+    public void cfg_builder_noCT() {
+        FusekiServer server = 
+            FusekiServer.create()
+                .port(port)
+                .registerOperation(newOp, null, customHandler)
+                .add("/ds", DatasetGraphFactory.createTxnMem(), true)
+                .addOperation("/ds", endpointName, newOp)
+                .build();
+        testServer(server, true, false);
+    }
+    
+    @Test(expected=FusekiConfigException.class)
+    public void cfg_bad_01() {
+        FusekiServer.create()
+        .port(port)
+        .registerOperation(newOp, null, customHandler)
+        .addOperation("/UNKNOWN", endpointName, newOp);
+        //.build();
+    }
+    
+    @Test(expected=FusekiConfigException.class)
+    public void cfg_bad_02() {
+        FusekiServer.create()
+        .port(port)
+        //.registerOperation(newOp, null, customHandler)
+        .add("/ds", DatasetGraphFactory.createTxnMem(), true)
+        // Unregistered.
+        .addOperation("/ds", endpointName, newOp);
+        //.build();
+    }
+    
+    public void cfg_bad_ct_not_enabkled_here() {
+        FusekiServer server = FusekiServer.create()
+            .port(port)
+            .registerOperation(newOp, "app/special", customHandler)
+            .add("/ds", DatasetGraphFactory.createTxnMem(), true)
+            // Unregistered.
+            .addOperation("/ds", endpointName, newOp)
+            .build();
+        testServer(server, false, false);
+    }
+
+
+    private void testServer(FusekiServer server, boolean withEndpoint, boolean withContentType) {
+        try { 
+            server.start();
+            // Try query (no extension required)
+            try(RDFConnection rconn = RDFConnectionFactory.connect(url+"/ds")) {
+                try(QueryExecution qExec = rconn.query("ASK {}")) {
+                    qExec.execAsk();
+                }
+            }
+
+            if ( withEndpoint ) {
+                // Service endpoint name : GET
+                String s1 = HttpOp.execHttpGetString(url+"/ds/"+endpointName);
+    
+                // Service endpoint name : POST
+                try ( TypedInputStream stream = HttpOp.execHttpPostStream(url+"/ds/"+endpointName, "ignored", "", "text/plain") ) {
+                    String x = IOUtils.toString(stream, StandardCharsets.UTF_8);
+                    assertNotNull(x);
+                } catch (IOException ex) {
+                    IO.exception(ex);
+                }
+            } else {
+                // No endpoint so we expect a 404.
+                try {
+                    // Service endpoint name : GET
+                    HttpOp.execHttpGet(url+"/ds/"+endpointName);
+                    fail("Expected to fail HTTP GET");
+                } catch (HttpException ex) {
+                    assertEquals(404, ex.getResponseCode());   
+                }   
+            }
+             
+            if ( withContentType ) {
+                // Content-type
+                try ( TypedInputStream stream = HttpOp.execHttpPostStream(url+"/ds", contentType, "", "text/plain") ) {
+                    String x = IOUtils.toString(stream, StandardCharsets.UTF_8);
+                    assertNotNull(x);
+                } catch (IOException ex) {
+                    IO.exception(ex);
+                }
+            } else {
+                // No Content-Type
+                try ( TypedInputStream stream = HttpOp.execHttpPostStream(url+"/ds", contentType, "", "text/plain") ) {
+                    fail("Expected to fail HTTP POST using Content-Type");
+                } catch (HttpException ex) {} 
+
+                // Service endpoint name. DELETE -> fails 405
+                try { 
+                    HttpOp.execHttpDelete(url+"/ds/"+endpointName);
+                    throw new IllegalStateException("DELETE succeeded");
+                } catch (HttpException ex) {
+                    assertEquals(405, ex.getResponseCode());   
+                }
+            }
+        } finally {
+            server.stop();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiTestAuth.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiTestAuth.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiTestAuth.java
new file mode 100644
index 0000000..939725f
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiTestAuth.java
@@ -0,0 +1,98 @@
+/*
+ * 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.FusekiTestAuth.assertAuthHttpException;
+
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.HttpClient;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.jena.atlas.logging.LogCtl;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.atlas.web.TypedInputStream;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.riot.web.HttpOp;
+import org.eclipse.jetty.security.SecurityHandler;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestFusekiTestAuth {
+    
+    static {
+        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");
+    }
+    
+    private static String USER      = "user1234";
+    private static String PASSWORD  = "password1234";
+    
+    @BeforeClass
+    public static void ctlBeforeClass() {
+        SecurityHandler sh = FusekiTestAuth.makeSimpleSecurityHandler("/*", USER, PASSWORD);
+        FusekiTestAuth.setupServer(false, sh);
+    }
+
+    @AfterClass
+    public static void ctlAfterClass() {
+        FusekiTestAuth.teardownServer();
+        HttpOp.setDefaultHttpClient(HttpOp.createPoolingHttpClient());
+    }
+
+    @Test(expected=HttpException.class) 
+    public void testServer_auth_no_auth() {
+      // No Auth
+      try ( TypedInputStream in = HttpOp.execHttpGet(FusekiTestAuth.urlDataset(), "*/*") ) {}
+      catch (HttpException ex) { throw assertAuthHttpException(ex); }
+    }
+    
+    @Test public void testServer_auth() {
+        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(FusekiTestAuth.urlDataset(), "*/*", client, null) ) {}
+    }
+    
+    @Test(expected=HttpException.class)
+    public void testServer_auth_bad_user() {
+        BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
+        Credentials credentials = new UsernamePasswordCredentials("USERUSER", PASSWORD);
+        credsProvider.setCredentials(AuthScope.ANY, credentials);
+        HttpClient client = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build();
+        try ( TypedInputStream in = HttpOp.execHttpGet(FusekiTestAuth.urlDataset(), "*/*", client, null) ) {}
+        catch (HttpException ex) { throw assertAuthHttpException(ex); }
+    }
+        
+    @Test(expected=HttpException.class)
+    public void testServer_auth_bad_password() {
+        BasicCredentialsProvider credsProv = new BasicCredentialsProvider();
+        credsProv.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(USER, "WRONG"));
+        HttpClient client = HttpClients.custom().setDefaultCredentialsProvider(credsProv).build();
+        
+        try ( TypedInputStream in = HttpOp.execHttpGet(FusekiTestAuth.urlDataset(), "*/*", client, null) ) {}
+        catch (HttpException ex) { throw assertAuthHttpException(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/TestFusekiTestServer.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiTestServer.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiTestServer.java
new file mode 100644
index 0000000..8637d36
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiTestServer.java
@@ -0,0 +1,60 @@
+/*
+ * 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.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.HttpClient;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.atlas.web.TypedInputStream;
+import org.apache.jena.riot.web.HttpOp;
+import org.apache.jena.web.HttpSC;
+import org.junit.*;
+
+public class TestFusekiTestServer {
+    
+//    @BeforeClass static public void beforeSuiteClass() { ServerCtl.ctlBeforeTestSuite(); } 
+//    @AfterClass  static public void afterSuiteClass()  { ServerCtl.ctlAfterTestSuite(); }
+
+    // This is file is the "suite".
+    
+    @BeforeClass public static void ctlBeforeClass() { FusekiTestServer.ctlBeforeTestSuite(); FusekiTestServer.ctlBeforeClass(); }
+    @AfterClass  public static void ctlAfterClass()  { FusekiTestServer.ctlAfterClass();      FusekiTestServer.ctlAfterTestSuite(); }
+    @Before      public void ctlBeforeTest()         { FusekiTestServer.ctlBeforeTest(); }
+    @After       public void ctlAfterTest()          { FusekiTestServer.ctlAfterTest(); }
+    
+    @Test public void testServer_1() {
+        HttpOp.execHttpGetString(FusekiTestServer.urlDataset());
+    }
+    
+    @Test public void testServer_2() {
+        BasicCredentialsProvider credsProv = new BasicCredentialsProvider();
+        credsProv.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("USER", "PASSWORD"));
+        HttpClient client = HttpClients.custom().setDefaultCredentialsProvider(credsProv).build();
+        
+        // No auth set - should work.
+        try ( TypedInputStream in = HttpOp.execHttpGet(FusekiTestServer.urlDataset(), "*/*") ) {}
+        catch (HttpException ex) {
+            Assert.assertTrue(ex.getResponseCode() == HttpSC.FORBIDDEN_403 || ex.getResponseCode() == HttpSC.UNAUTHORIZED_401 );
+            throw 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/TestMultipleEmbedded.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestMultipleEmbedded.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestMultipleEmbedded.java
new file mode 100644
index 0000000..ecb4e21
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestMultipleEmbedded.java
@@ -0,0 +1,170 @@
+/*
+ * 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.TestEmbeddedFuseki.dataset;
+import static org.apache.jena.fuseki.main.TestEmbeddedFuseki.query;
+import static org.junit.Assert.assertEquals ;
+import static org.junit.Assert.assertTrue ;
+
+import java.io.OutputStream ;
+
+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.fuseki.FusekiException ;
+import org.apache.jena.fuseki.FusekiLib;
+import org.apache.jena.graph.Graph ;
+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.sparql.core.DatasetGraph ;
+import org.apache.jena.sparql.core.Quad ;
+import org.apache.jena.sparql.sse.SSE ;
+import org.apache.jena.system.Txn ;
+import org.junit.Test ;
+
+public class TestMultipleEmbedded {
+    
+    static Quad q1 = SSE.parseQuad("(_ :s :p 1)") ;
+    static Quad q2 = SSE.parseQuad("(_ :s :p 2)") ;
+    
+    // Two servers, same port -> bad.
+    @Test(expected=FusekiException.class)
+    public void multiple_01() {
+        DatasetGraph dsg = dataset() ;
+
+        int port = FusekiLib.choosePort() ;
+        FusekiServer server1 = FusekiServer.create().port(port).add("/ds1", dsg).build() ;
+        // Same port - Bad.
+        FusekiServer server2 = FusekiServer.create().port(port).add("/ds2", dsg).build() ;
+    
+        server1.start();
+        
+        try {
+            server2.start();
+        } catch (FusekiException ex) {
+            assertTrue(ex.getCause() instanceof java.net.BindException ) ;
+            throw ex ;
+        } finally {
+            try { server1.stop() ; } catch (Exception ex) {}
+            try { server2.stop() ; } catch (Exception ex) {}
+        }
+    }
+
+    // Two servers, different ports -> good.
+    @Test
+    public void multiple_02() {
+        DatasetGraph dsg = dataset() ;
+        int port1 = FusekiLib.choosePort() ;
+        FusekiServer server1 = FusekiServer.create().port(port1).add("/ds1", dsg).build() ;
+
+        // Different port - good
+        int port2 = FusekiLib.choosePort() ;
+        FusekiServer server2 = FusekiServer.create().port(port2).add("/ds2", dsg).build() ;
+
+        try {
+            server1.start();
+            server2.start();
+        } finally {
+            try { server1.stop() ; } catch (Exception ex) {}
+            try { server2.stop() ; } catch (Exception ex) {}
+        }
+    }
+    
+    // Two servers, two datasets.
+    @Test
+    public void multiple_03() {
+        DatasetGraph dsg1 = dataset() ;
+        DatasetGraph dsg2 = dataset() ;
+        // Same name.
+        int port1 = FusekiLib.choosePort() ;
+        FusekiServer server1 = FusekiServer.create().port(port1).add("/ds", dsg1).build().start() ;
+        Txn.executeWrite(dsg1, ()->dsg1.add(q1));
+        
+        int port2 = FusekiLib.choosePort() ;
+        FusekiServer server2 = FusekiServer.create().port(port2).add("/ds", dsg2).build().start() ;
+        Txn.executeWrite(dsg2, ()->dsg2.add(q2));
+        
+        query("http://localhost:"+port1+"/ds/", "SELECT * {?s ?p 1}", qExec->{
+            ResultSet rs = qExec.execSelect() ; 
+            int x = ResultSetFormatter.consume(rs) ;
+            assertEquals(1, x) ;
+        }) ;
+        query("http://localhost:"+port2+"/ds/", "SELECT * {?s ?p 1}", qExec->{
+            ResultSet rs = qExec.execSelect() ; 
+            int x = ResultSetFormatter.consume(rs) ;
+            assertEquals(0, x) ;
+        }) ;
+        server1.stop();
+        // server2 still running
+        query("http://localhost:"+port2+"/ds/", "SELECT * {?s ?p 2}", qExec->{
+            ResultSet rs = qExec.execSelect() ; 
+            int x = ResultSetFormatter.consume(rs) ;
+            assertEquals(1, x) ;
+        }) ;
+        server2.stop();
+    }
+    
+    // Two servers, one dataset under two names.
+    @Test
+    public void multiple_04() {
+        DatasetGraph dsg = dataset() ;
+        
+        int port1 = FusekiLib.choosePort() ;
+        FusekiServer server1 = FusekiServer.create().port(port1).add("/ds1", dsg).build().start() ;
+        Txn.executeWrite(dsg, ()->dsg.add(q1));
+        
+        int port2 = FusekiLib.choosePort() ;
+        FusekiServer server2 = FusekiServer.create().port(port2).add("/ds2", dsg).build().start() ;
+        Txn.executeWrite(dsg, ()->dsg.add(q2));
+        
+        query("http://localhost:"+port1+"/ds1", "SELECT * {?s ?p ?o}", qExec->{
+            ResultSet rs = qExec.execSelect() ; 
+            int x = ResultSetFormatter.consume(rs) ;
+            assertEquals(2, x) ;
+        }) ;
+        query("http://localhost:"+port2+"/ds2", "SELECT * {?s ?p ?o}", qExec->{
+            ResultSet rs = qExec.execSelect() ; 
+            int x = ResultSetFormatter.consume(rs) ;
+            assertEquals(2, x) ;
+        }) ;
+
+        server1.stop();
+        server2.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 ;
+    }
+}
+

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/ExtendFuseki_AddService_1.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/ExtendFuseki_AddService_1.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/ExtendFuseki_AddService_1.java
new file mode 100644
index 0000000..bfdcb42
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/ExtendFuseki_AddService_1.java
@@ -0,0 +1,158 @@
+/*
+ * 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.examples;
+
+import java.io.IOException;
+
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.logging.LogCtl;
+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.main.FusekiServer;
+import org.apache.jena.fuseki.server.Operation;
+import org.apache.jena.fuseki.servlets.ActionService;
+import org.apache.jena.riot.web.HttpOp;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.util.FileUtils;
+import org.apache.jena.web.HttpSC;
+
+/**
+ * This example show adding a custom operation to Fuseki.
+ * <p>
+ * There are two ways operations are routed: for a datset {@code /dataset}:
+ * <ol>
+ * <li>By endpoint name: {@code /dataset/endpoint}</li>
+ * <li>By content type, when a POST is made on the {@code /dataset}.</li>
+ * </ol>
+ * The first is the usual way; the second is not a common pattern.
+ * <p>
+ * The second way is in addition to the endpoint; an endpoint is always required to
+ * enabled routing by {@code Content-Type}.
+ * <p>
+ * The process for adding an operation is:
+ * <ul>
+ * <li>Register the operation with the server, with its implmementation.</li>
+ * <li>Add the operation to a datasets.</li>
+ * </ul>
+ * <pre>
+ *   // Register operation.
+ *   Operation myOperation = Operation.register("Special", "Custom operation");
+ *   // An implementation to call
+ *   ActionService customHandler = new SpecialService();
+ *   // Builder pattern ...
+ *   FusekiServer server = 
+ *       FusekiServer.create().port (1122)
+ *          // Register the operation with the server, together with implementation. 
+ *          .registerOperation(myOperation, customHandler)
+ *          // Add a dataset
+ *          .add("/dataset", DatasetGraphFactory.createTxnMem(), true)
+ *          // Add operation by endpoint
+ *          .addOperation("/dataset", "endpoint", myOperation)
+ *          .build();
+ * </pre>
+ * @see SpecialService
+ */
+
+public class ExtendFuseki_AddService_1 {
+    static { LogCtl.setLog4j(); }
+
+    // Endpoint dispatch only.
+    
+    // Choose free port for the example
+    // Normally, this is fixed and published, and fixed in URLs.
+    // To make the example portable, we ask the OS for a free port.
+    static int PORT             = FusekiLib.choosePort();
+    
+    // The server
+    static String SERVER_URL    = "http://localhost:"+PORT+"/";
+    
+    static String DATASET       = "dataset";
+    
+    public static void main(String ...args) {
+        // Create a new operation: operations are really just names (symbols). The code to
+        // run is found by looking up the operation in a per-server table that gives the server-specific
+        // implementation as an ActionService.
+
+        Operation myOperation = Operation.register("Special", "Custom operation");
+        
+        // Service endpoint name.
+        // This can be different for different datasets even in the same server.
+        // c.f. {@code fuseki:serviceQuery}
+        
+        String endpointName = "special";
+
+        // The handled for the new operation.
+        
+        ActionService customHandler = new SpecialService();
+        
+        FusekiServer server = 
+            FusekiServer.create().port(PORT)
+                .verbose(true)
+
+                // Register the new operation, and it's handler, but no Content-Type
+                .registerOperation(myOperation, customHandler)
+                
+                // Add a dataset with the normal, default naming services 
+                // (/sparql, /query, /update, /upload, /data, /get)  
+                .add(DATASET, DatasetGraphFactory.createTxnMem(), true)
+                
+                // Add the custom service, mapping from endpoint to operation for a specific dataset.
+                .addOperation(DATASET, endpointName, myOperation)
+                
+                // And build the server.
+                .build();
+        
+        // Start the server. This does not block this thread.
+        server.start();
+        
+        // Try some operations on the server using the service URL. 
+        String customOperationURL = SERVER_URL + DATASET + "/" + endpointName;
+        
+        try {
+
+            // Service endpoint name : GET
+            String s1 = HttpOp.execHttpGetString(customOperationURL);
+            System.out.print(s1);
+            if ( s1 == null )
+                System.out.println();
+
+            // Service endpoint name : POST
+            try ( TypedInputStream stream = HttpOp.execHttpPostStream(customOperationURL, null, "text/plain") ) {
+                String s2 = FileUtils.readWholeFileAsUTF8(stream);
+                System.out.print(s2);
+                if ( s2 == null )
+                    System.out.println();
+            } catch (IOException ex) { IO.exception(ex); }
+
+            // Service endpoint name. DELETE -> fails 405
+            try { 
+                HttpOp.execHttpDelete(customOperationURL);
+                throw new IllegalStateException("DELETE succeeded");
+            } catch (HttpException ex) {
+                if ( ex.getResponseCode() != HttpSC.METHOD_NOT_ALLOWED_405 )
+                    System.err.println("Unexpected HTTP Response Code: "+ex.getMessage());
+                else
+                    System.out.println("DELETE rejected correctly: "+ex.getMessage());
+            }
+        } finally {
+            server.stop();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/ExtendFuseki_AddService_2.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/ExtendFuseki_AddService_2.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/ExtendFuseki_AddService_2.java
new file mode 100644
index 0000000..295eebc
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/ExtendFuseki_AddService_2.java
@@ -0,0 +1,123 @@
+/*
+ * 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.examples;
+
+import org.apache.jena.atlas.logging.LogCtl;
+import org.apache.jena.fuseki.FusekiLib;
+import org.apache.jena.fuseki.build.FusekiBuilder;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.server.DataService;
+import org.apache.jena.fuseki.server.Operation;
+import org.apache.jena.fuseki.servlets.ActionService;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.query.QueryExecutionFactory;
+import org.apache.jena.query.QueryFactory;
+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.engine.http.QueryExceptionHTTP;
+import org.apache.jena.web.HttpSC;
+
+/**
+ * Create custom endpoint names, dispatch by {@code Content-Type}.
+ * See also {@link ExtendFuseki_AddService_1} for more details.
+ */
+
+public class ExtendFuseki_AddService_2 {
+    static { LogCtl.setLog4j(); }
+
+    // Endpoint dispatch only.
+    static int PORT             = FusekiLib.choosePort();
+    
+    // The server
+    static String SERVER_URL    = "http://localhost:"+PORT+"/";
+    
+    static String DATASET       = "dataset";
+    
+    public static void main(String ...args) {
+        // Register a new operation
+
+        Operation myOperation = Operation.register("Special", "Custom operation");
+        
+        // Service endpoint names.
+        
+        String queryEndpoint = "q";
+        String customEndpoint = "x";
+        
+        // Make a DataService with custom named for endpoints.
+        // In this example, "q" for SPARQL query and "x" for our custom extension and no others.
+        DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+        DataService dataService = new DataService(dsg);
+        // This would add the usual defaults.
+        //FusekiBuilder.populateStdServices(dataService, true);
+        FusekiBuilder.addServiceEP(dataService, myOperation, customEndpoint);
+        FusekiBuilder.addServiceEP(dataService, Operation.Query, queryEndpoint);
+
+        // The handled for the new operation.
+        ActionService customHandler = new SpecialService();
+        
+        FusekiServer server = 
+            FusekiServer.create().port(PORT)
+                .verbose(true)
+                // Register the new operation, and it's handler
+                .registerOperation(myOperation, customHandler)
+
+                // The DataService.
+                .add(DATASET, dataService)
+                
+                // And build the server.
+                .build();
+        
+        server.start();
+        
+        // Try some operations on the server using the service URL. 
+        String customOperationURL = SERVER_URL + DATASET + "/" + customEndpoint;
+        String queryOperationURL = SERVER_URL + DATASET + "/" + queryEndpoint;
+        
+        Query query = QueryFactory.create("ASK{}"); 
+        
+        
+        try {
+            
+            // Try custom name - OK
+            try ( QueryExecution qExec = QueryExecutionFactory.sparqlService(queryOperationURL, query) ) {
+                qExec.execAsk();
+            }
+            
+            // Try default name - 404
+            try ( QueryExecution qExec = QueryExecutionFactory.sparqlService(SERVER_URL + DATASET + "/sparql", query) ) {
+                qExec.execAsk();
+                throw new RuntimeException("Didn't fail");
+            } catch (QueryExceptionHTTP ex) {
+                if ( ex.getResponseCode() != HttpSC.NOT_FOUND_404 ) {
+                    throw new RuntimeException("Not a 404", ex);
+                }
+            }
+            
+            // Service endpoint name : GET
+            String s1 = HttpOp.execHttpGetString(customOperationURL);
+            if ( s1 == null )
+                throw new RuntimeException("Failed: "+customOperationURL);
+            
+        } finally {
+            server.stop();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/ExtendFuseki_AddService_3.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/ExtendFuseki_AddService_3.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/ExtendFuseki_AddService_3.java
new file mode 100644
index 0000000..f0dc08f
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/ExtendFuseki_AddService_3.java
@@ -0,0 +1,107 @@
+/*
+ * 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.examples;
+
+import java.io.IOException;
+
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.logging.LogCtl;
+import org.apache.jena.atlas.web.TypedInputStream;
+import org.apache.jena.fuseki.FusekiLib;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.server.Operation;
+import org.apache.jena.fuseki.servlets.ActionService;
+import org.apache.jena.riot.web.HttpOp;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.util.FileUtils;
+
+/**
+ * This example show adding a custom operation to Fuseki, with dispatch by {@code Content-Type}.
+ * <p>
+ * See {@link ExtendFuseki_AddService_1} fior a geenral description of the routing process.
+ * @see SpecialService
+ */
+
+public class ExtendFuseki_AddService_3 {
+    static { LogCtl.setLog4j(); }
+
+    static int PORT             = FusekiLib.choosePort();
+    
+    // The server
+    static String SERVER_URL    = "http://localhost:"+PORT+"/";
+    
+    static String DATASET       = "dataset";
+    
+    public static void main(String ...args) {
+        // Create a new operation: operations are really just names (symbols). The code to
+        // run is found by looking up the operation in a per-server table that gives the server-specific
+        // implementation as an ActionService.
+
+        Operation myOperation = Operation.register("Special", "Custom operation");
+        
+        // Service endpoint name.
+        // This can be different for different datasets even in the same server.
+        // c.f. {@code fuseki:serviceQuery}
+        
+        String endpointName = "special";
+        String contentType = "application/special";
+
+        // The handled for the new operation.
+        
+        ActionService customHandler = new SpecialService();
+        
+        FusekiServer server = 
+            FusekiServer.create().port(PORT)
+                .verbose(true)
+
+                // Register the new operation, with content type and handler
+                .registerOperation(myOperation, contentType, customHandler)
+                
+                // Add a dataset with the normal, default naming services 
+                // (/sparql, /query, /update, /upload, /data, /get)  
+                .add(DATASET, DatasetGraphFactory.createTxnMem(), true)
+                
+                // Add the custom service, mapping from endpoint to operation for a specific dataset.
+                // Required when when routing via Content-Type. 
+                .addOperation(DATASET, endpointName, myOperation)
+                
+                // And build the server.
+                .build();
+        
+        // Start the server. This does not block this thread.
+        server.start();
+        
+        // Try some operations on the server using the service URL.
+        String datasetURL = SERVER_URL + DATASET;
+        //String customOperationURL = SERVER_URL + DATASET + "/" + endpointName;
+        
+        try {
+
+            // Dataset endpoint name : POST, with Content-type.
+            try ( TypedInputStream stream = HttpOp.execHttpPostStream(datasetURL, contentType, "", "text/plain") ) {
+                String s2 = FileUtils.readWholeFileAsUTF8(stream);
+                System.out.print(s2);
+                if ( s2 == null )
+                    System.out.println();
+            } catch (IOException ex) { IO.exception(ex); }
+        } finally {
+            server.stop();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/SpecialService.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/SpecialService.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/SpecialService.java
new file mode 100644
index 0000000..d7a00cc
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/SpecialService.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.examples;
+
+import java.io.IOException;
+
+import org.apache.jena.atlas.logging.LogCtl;
+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 SpecialService extends ActionREST {
+    static { LogCtl.setLog4j(); }
+    
+    @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.getActionURI());
+    }
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/src/test/resources/log4j.properties
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/resources/log4j.properties b/jena-fuseki2/jena-fuseki-main/src/test/resources/log4j.properties
new file mode 100644
index 0000000..e84e60e
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/resources/log4j.properties
@@ -0,0 +1,40 @@
+# Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0
+
+# Plain output to stdout
+log4j.appender.jena.plainstdout=org.apache.log4j.ConsoleAppender
+log4j.appender.jena.plainstdout.target=System.out
+log4j.appender.jena.plainstdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.jena.plainstdout.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ss}] %-10c{1} %-5p %m%n
+## %d{ISO8601} -- includes "ss,sss"
+## log4j.appender.jena.plainstdout.layout.ConversionPattern=[%d{ISO8601}] %-10c{1} %-5p %m%n
+
+# Unadorned, for the NCSA requests log.
+log4j.appender.fuseki.plain=org.apache.log4j.ConsoleAppender
+log4j.appender.fuseki.plain.target=System.out
+log4j.appender.fuseki.plain.layout=org.apache.log4j.PatternLayout
+log4j.appender.fuseki.plain.layout.ConversionPattern=%m%n
+
+log4j.rootLogger=INFO, jena.plainstdout
+log4j.logger.org.apache.jena=WARN
+log4j.logger.org.apache.jena.fuseki=INFO
+
+# Others
+log4j.logger.org.eclipse.jetty=WARN
+log4j.logger.org.apache.shiro=WARN
+
+# Fuseki System logs.
+log4j.logger.org.apache.jena.fuseki.Server=INFO
+log4j.logger.org.apache.jena.fuseki.Fuseki=INFO
+log4j.logger.org.apache.jena.fuseki.Admin=INFO
+log4j.logger.org.apache.jena.fuseki.Validate=INFO
+log4j.logger.org.apache.jena.fuseki.Config=INFO
+
+# NCSA Request log.
+log4j.additivity.org.apache.jena.fuseki.Request=false
+log4j.logger.org.apache.jena.fuseki.Request=OFF, fuseki.plain
+
+# TDB
+log4j.logger.org.apache.jena.tdb.loader=INFO
+## Parser output
+log4j.additivity.org.apache.jena.riot=false
+log4j.logger.org.apache.jena.riot=INFO, jena.plainstdout

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/testing/FusekiEmbedded/config.ttl
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/testing/FusekiEmbedded/config.ttl b/jena-fuseki2/jena-fuseki-main/testing/FusekiEmbedded/config.ttl
new file mode 100644
index 0000000..5a7f84a
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/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;
+.

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-main/testing/FusekiEmbedded/test.txt
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-main/testing/FusekiEmbedded/test.txt b/jena-fuseki2/jena-fuseki-main/testing/FusekiEmbedded/test.txt
new file mode 100644
index 0000000..1a98ff9
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/FusekiEmbedded/test.txt
@@ -0,0 +1 @@
+Test data.

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/jena-fuseki-server/pom.xml
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-server/pom.xml b/jena-fuseki2/jena-fuseki-server/pom.xml
new file mode 100644
index 0000000..d91bac1
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-server/pom.xml
@@ -0,0 +1,113 @@
+<?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 Server Jar</name>
+  <artifactId>jena-fuseki-server</artifactId>
+
+  <parent>
+    <groupId>org.apache.jena</groupId>
+    <artifactId>jena-fuseki</artifactId>
+    <version>3.9.0-SNAPSHOT</version>
+    <relativePath>../</relativePath>
+  </parent> 
+
+  <packaging>jar</packaging>
+  <description>Fuseki server - combined jar with built-in webserver.</description>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>org.apache.jena</groupId>
+      <artifactId>jena-fuseki-main</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.jena</groupId>
+      <artifactId>jena-fuseki-access</artifactId>
+      <version>3.9.0-SNAPSHOT</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.jena</groupId>
+      <artifactId>jena-cmds</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-log4j12</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>log4j</groupId>
+      <artifactId>log4j</artifactId>
+    </dependency>
+    
+  </dependencies>
+
+  <build>
+    <plugins>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <configuration>
+          <shadedArtifactAttached>false</shadedArtifactAttached>
+          <transformers>
+            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+              <mainClass>org.apache.jena.fuseki.main.cmds.FusekiMainCmd</mainClass>
+            </transformer>
+            <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
+            <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer" />
+            <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer">
+              <addHeader>false</addHeader>
+            </transformer>
+          </transformers>
+          <filters>
+            <filter>
+              <artifact>*:*</artifact>
+              <excludes>
+                <!-- Some jars are signed but shading breaks that.
+                     Don't include signing files.
+                -->
+                <exclude>META-INF/*.SF</exclude>
+                <exclude>META-INF/*.DSA</exclude>
+                <exclude>META-INF/*.RSA</exclude>
+              </excludes>
+            </filter>
+          </filters>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <!--<phase /><!- - Switch off -->
+            <goals>
+              <goal>shade</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+    </plugins>
+
+  </build>
+  
+</project>

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-fuseki2/pom.xml
----------------------------------------------------------------------
diff --git a/jena-fuseki2/pom.xml b/jena-fuseki2/pom.xml
index 5607c06..1f73ecc 100644
--- a/jena-fuseki2/pom.xml
+++ b/jena-fuseki2/pom.xml
@@ -59,14 +59,15 @@
 
   <modules>
     <module>jena-fuseki-core</module>
-    <module>jena-fuseki-embedded</module>
     <module>jena-fuseki-access</module>
 
+    <module>jena-fuseki-main</module>
+    <module>jena-fuseki-server</module>
+
     <module>jena-fuseki-webapp</module>
     <module>jena-fuseki-war</module>
     <module>jena-fuseki-fulljar</module>
 
-    <module>jena-fuseki-basic</module>
     <module>apache-jena-fuseki</module>
   </modules>
   

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-integration-tests/pom.xml
----------------------------------------------------------------------
diff --git a/jena-integration-tests/pom.xml b/jena-integration-tests/pom.xml
index 14289d7..d70ef7d 100644
--- a/jena-integration-tests/pom.xml
+++ b/jena-integration-tests/pom.xml
@@ -115,7 +115,7 @@
 
     <dependency>
       <groupId>org.apache.jena</groupId>
-      <artifactId>jena-fuseki-embedded</artifactId>
+      <artifactId>jena-fuseki-main</artifactId>
       <version>3.9.0-SNAPSHOT</version>
       <scope>test</scope>
     </dependency>

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionFusekiBinary.java
----------------------------------------------------------------------
diff --git a/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionFusekiBinary.java b/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionFusekiBinary.java
index 881b87e..8b7f4dc 100644
--- a/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionFusekiBinary.java
+++ b/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionFusekiBinary.java
@@ -22,7 +22,7 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import org.apache.jena.fuseki.FusekiLib;
-import org.apache.jena.fuseki.embedded.FusekiServer;
+import org.apache.jena.fuseki.main.FusekiServer;
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
 import org.apache.jena.query.QueryExecution;

http://git-wip-us.apache.org/repos/asf/jena/blob/7e6d03af/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionRemote.java
----------------------------------------------------------------------
diff --git a/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionRemote.java b/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionRemote.java
index 5dc87f1..00eb5a0 100644
--- a/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionRemote.java
+++ b/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionRemote.java
@@ -21,7 +21,7 @@ package org.apache.jena.test.rdfconnection;
 import org.apache.jena.atlas.logging.LogCtl ;
 import org.apache.jena.fuseki.Fuseki ;
 import org.apache.jena.fuseki.FusekiLib;
-import org.apache.jena.fuseki.embedded.FusekiServer ;
+import org.apache.jena.fuseki.main.FusekiServer ;
 import org.apache.jena.rdfconnection.AbstractTestRDFConnection;
 import org.apache.jena.rdfconnection.RDFConnection;
 import org.apache.jena.rdfconnection.RDFConnectionFactory;