Merge pull request #867 from afs/n3-writer

JENA-1997: Remove old Turtle/N3 writer
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/async/AsyncPool.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/async/AsyncPool.java
index 0315900..aaf2bb6 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/async/AsyncPool.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/async/AsyncPool.java
@@ -65,7 +65,13 @@
             Callable<Object> c = ()->{
                 try { task.run(); }
                 catch (Throwable th) {
+                    // Generally speaking tasks should provide more specific log messages when they fail but this
+                    // handles the case of tasks not doing that
                     Fuseki.serverLog.error(format("Exception in task %s execution", taskId), th);
+                    
+                    // Need to throw the error upwards so that the AsyncTask, which is itself a Callable, can use
+                    // this to set the success flag correctly
+                    throw th;
                 }
                 return null;
             };
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/async/AsyncTask.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/async/AsyncTask.java
index 56cc408..71b4810 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/async/AsyncTask.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/async/AsyncTask.java
@@ -46,7 +46,9 @@
     private final String taskId;
 
     private long requestId;
-
+    
+    private Boolean success = null;
+    
     /*package*/ AsyncTask(Callable<Object> callable,
                           AsyncPool pool,
                           String taskId,
@@ -95,10 +97,15 @@
     public Object call() {
         try {
             start();
-            return callable.call();
+            Object result = callable.call();
+            this.success = true;
+            return result;
         }
-        catch (Exception ex) {
-            log.error("Async task threw an expection", ex);
+        catch (Throwable ex) {
+            // NB - Since the only place that constructs an AsyncTask is AsyncPool.submit() and that is already
+            // set up to handle uncaught exceptions and throw them onwards all we need to do here is set the
+            // success flag to false
+            this.success = false;
             return null;
         }
         finally {
@@ -114,5 +121,9 @@
     public String getFinishPoint() {
         return finishPoint;
     }
+    
+    public Boolean wasSuccessful() {
+        return this.success;
+    }
 }
 
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/ActionSleep.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/ActionSleep.java
index eae26d2..ab47f03 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/ActionSleep.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/ActionSleep.java
@@ -102,6 +102,9 @@
                 log.info(format("[Task %d] << Sleep finish", actionId));
             } catch (Exception ex) {
                 log.info(format("[Task %d] **** Exception", actionId), ex);
+                // Must also throw the error upwards so that the async task tracking infrastucture can set the
+                // success flag correctly
+                throw ex;
             }
         }
     }
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/ActionTasks.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/ActionTasks.java
index df60bf9..b674294 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/ActionTasks.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/ActionTasks.java
@@ -80,7 +80,6 @@
 
             for ( AsyncPool pool : pools ) {
                 for ( AsyncTask aTask : pool.tasks() ) {
-                    //builder.value(aTask.getTaskId());
                     descOneTask(builder, aTask);
                 }
             }
@@ -116,6 +115,8 @@
             builder.key(JsonConstCtl.started).value(aTask.getStartPoint());
         if ( aTask.getFinishPoint() != null )
             builder.key(JsonConstCtl.finished).value(aTask.getFinishPoint());
+        if ( aTask.wasSuccessful() != null )
+            builder.key(JsonConstCtl.success).value(aTask.wasSuccessful());
         builder.finishObject("SingleTask");
     }
 }
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/JsonConstCtl.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/JsonConstCtl.java
index 6f439d6..db67b9b 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/JsonConstCtl.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/JsonConstCtl.java
@@ -25,5 +25,6 @@
     public static final String task             = "task";
     public static final String finished         = "finished";
     public static final String started          = "started";
+    public static final String success          = "success";
 
 }
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiServer.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiServer.java
index 12b1ecd..051757c 100644
--- a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiServer.java
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiServer.java
@@ -1018,6 +1018,7 @@
                 addServlet(context, "/$/stats/*", new ActionStats());
             if ( withMetrics )
                 addServlet(context, "/$/metrics", new ActionMetrics());
+            // TODO Should we support registering other functionality e.g. /$/backup/* and /$/compact/*
 
             servlets.forEach(p-> addServlet(context, p.getLeft(), p.getRight()));
             filters.forEach (p-> addFilter(context, p.getLeft(), p.getRight()));
diff --git a/jena-fuseki2/jena-fuseki-webapp/pom.xml b/jena-fuseki2/jena-fuseki-webapp/pom.xml
index 8744ccf..52693b5 100644
--- a/jena-fuseki2/jena-fuseki-webapp/pom.xml
+++ b/jena-fuseki2/jena-fuseki-webapp/pom.xml
@@ -56,7 +56,7 @@
       <artifactId>jena-text</artifactId>
       <version>3.17.0-SNAPSHOT</version>
     </dependency>
-
+    
     <dependency>
       <groupId>org.eclipse.jetty</groupId>
       <artifactId>jetty-webapp</artifactId>
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ActionBackup.java b/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ActionBackup.java
index aa153af..bb669d4b 100644
--- a/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ActionBackup.java
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ActionBackup.java
@@ -61,8 +61,11 @@
                 log.info(format("[%d] >>>> Start backup %s -> %s", actionId, datasetName, backupFilename));
                 Backup.backup(transactional, dataset, backupFilename);
                 log.info(format("[%d] <<<< Finish backup %s -> %s", actionId, datasetName, backupFilename));
-            } catch (Exception ex) {
+            } catch (Throwable ex) {
                 log.info(format("[%d] **** Exception in backup", actionId), ex);
+                // Must also throw the error upwards so that the async task tracking infrastucture can set the
+                // success flag correctly
+                throw ex;
             }
         }
     }
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ActionCompact.java b/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ActionCompact.java
new file mode 100644
index 0000000..4d8a597
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ActionCompact.java
@@ -0,0 +1,72 @@
+/**
+ * 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.mgt;
+
+import static java.lang.String.format;
+
+import org.apache.jena.fuseki.ctl.ActionAsyncTask;
+import org.apache.jena.fuseki.ctl.TaskBase;
+import org.apache.jena.fuseki.servlets.HttpAction;
+import org.apache.jena.fuseki.servlets.ServletOps;
+import org.apache.jena.tdb2.DatabaseMgr;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ActionCompact extends ActionAsyncTask
+{
+    public ActionCompact() { super("Compact"); }
+
+    @Override
+    public void validate(HttpAction action) {}
+
+    @Override
+    protected Runnable createRunnable(HttpAction action) {
+        String name = getItemName(action);
+        if ( name == null ) {
+            action.log.error("Null for dataset name in item request");
+            ServletOps.errorOccurred("Null for dataset name in item request");
+            return null;
+        }
+
+        action.log.info(format("[%d] Compact dataset %s", action.id, name));
+        return new CompactTask(action);
+    }
+
+    static class CompactTask extends TaskBase {
+        static private Logger log = LoggerFactory.getLogger("Compact");
+
+        public CompactTask(HttpAction action) {
+            super(action);
+        }
+
+        @Override
+        public void run() {
+            try {
+                log.info(format("[%d] >>>> Start compact %s", actionId, datasetName));
+                DatabaseMgr.compact(dataset);
+                log.info(format("[%d] <<<< Finish compact %s", actionId, datasetName));
+            } catch (Throwable ex) {
+                log.info(format("[%d] **** Exception in compact", actionId), ex);
+                // Must also throw the error upwards so that the async task tracking infrastucture can set the
+                // success flag correctly
+                throw ex;
+            }
+        }
+    }
+}
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/Backup.java b/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/Backup.java
index 3ac2457..8d49f3b 100644
--- a/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/Backup.java
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/Backup.java
@@ -27,7 +27,6 @@
 import org.apache.jena.atlas.io.IO;
 import org.apache.jena.atlas.lib.DateTimeUtils;
 import org.apache.jena.atlas.logging.FmtLog;
-import org.apache.jena.atlas.logging.Log;
 import org.apache.jena.fuseki.Fuseki;
 import org.apache.jena.fuseki.FusekiException;
 import org.apache.jena.fuseki.webapp.FusekiWebapp;
@@ -71,10 +70,7 @@
         transactional.begin(ReadWrite.READ);
         try {
             Backup.backup(dsg, backupfile);
-        } catch (Exception ex) {
-            Log.warn(Fuseki.serverLog, "Exception in backup", ex);
-        }
-        finally {
+        } finally {
             transactional.end();
         }
     }
@@ -84,6 +80,10 @@
      * @see #backup(Transactional, DatasetGraph, String)
      */
     private static void backup(DatasetGraph dsg, String backupfile) {
+        if (dsg == null) {
+            throw new FusekiException("No dataset provided to backup");
+        }
+        
         if ( !backupfile.endsWith(".nq") )
             backupfile = backupfile + ".nq";
 
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ServerMgtConst.java b/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ServerMgtConst.java
index dd92290..056b161 100644
--- a/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ServerMgtConst.java
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ServerMgtConst.java
@@ -24,6 +24,8 @@
  */
 public class ServerMgtConst {
     public static final String  opDatasets      = "datasets";
+    public static final String  opBackup        = "backup";
+    public static final String  opCompact       = "compact";
     public static final String  opListBackups   = "backups-list";
     public static final String  opServer        = "server";
 
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/WEB-INF/web.xml b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/WEB-INF/web.xml
index 7af1640..ab64a9a 100644
--- a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/WEB-INF/web.xml
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/WEB-INF/web.xml
@@ -207,6 +207,11 @@
     <servlet-name>BackupListServlet</servlet-name>
     <servlet-class>org.apache.jena.fuseki.mgt.ActionBackupList</servlet-class>
   </servlet>
+  
+  <servlet>
+    <servlet-name>ActionCompact</servlet-name>
+    <servlet-class>org.apache.jena.fuseki.mgt.ActionCompact</servlet-class>
+  </servlet>
 
   <!-- An action that only creates a background task that sleeps. -->
   <servlet>
@@ -233,6 +238,11 @@
     <servlet-name>BackupListServlet</servlet-name>
     <url-pattern>/$/backups-list</url-pattern>
   </servlet-mapping>
+  
+  <servlet-mapping>
+    <servlet-name>ActionCompact</servlet-name>
+    <url-pattern>/$/compact/*</url-pattern>
+  </servlet-mapping>
 
   <!-- Admin controls-->
 
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestAdmin.java b/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestAdmin.java
index 6b07ea0..d7d8866 100644
--- a/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestAdmin.java
+++ b/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestAdmin.java
@@ -18,11 +18,8 @@
 
 package org.apache.jena.fuseki;
 
-import static org.apache.jena.fuseki.mgt.ServerMgtConst.opDatasets;
-import static org.apache.jena.fuseki.mgt.ServerMgtConst.opListBackups;
-import static org.apache.jena.fuseki.mgt.ServerMgtConst.opServer;
-import static org.apache.jena.fuseki.server.ServerConst.opPing;
-import static org.apache.jena.fuseki.server.ServerConst.opStats;
+import static org.apache.jena.fuseki.mgt.ServerMgtConst.*;
+import static org.apache.jena.fuseki.server.ServerConst.*;
 import static org.apache.jena.riot.web.HttpOp.execHttpDelete;
 import static org.apache.jena.riot.web.HttpOp.execHttpGet;
 import static org.apache.jena.riot.web.HttpOp.execHttpPost;
@@ -35,6 +32,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import org.apache.commons.lang3.SystemUtils;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
 import org.apache.http.entity.FileEntity;
@@ -46,6 +44,7 @@
 import org.apache.jena.atlas.lib.Lib;
 import org.apache.jena.atlas.web.HttpException;
 import org.apache.jena.atlas.web.TypedInputStream;
+import org.apache.jena.fuseki.ctl.JsonConstCtl;
 import org.apache.jena.fuseki.mgt.ServerMgtConst;
 import org.apache.jena.fuseki.server.ServerConst;
 import org.apache.jena.fuseki.test.FusekiTest;
@@ -53,15 +52,18 @@
 import org.apache.jena.riot.web.HttpOp;
 import org.apache.jena.riot.web.HttpResponseHandler;
 import org.apache.jena.web.HttpSC;
+import org.junit.Assert;
+import org.junit.AssumptionViolatedException;
 import org.junit.Test;
 
 /** Tests of the admin functionality */
 public class TestAdmin extends AbstractFusekiTest {
 
     // Name of the dataset in the assembler file.
-    static String dsTest    = "test-ds1";
-    static String dsTestInf = "test-ds4";
-    static String fileBase  = "testing/";
+    static String dsTest      = "test-ds1";
+    static String dsTestInf   = "test-ds4";
+    static String dsTestTdb2  = "test-tdb2";
+    static String fileBase    = "testing/";
 
     // --- Ping
 
@@ -187,6 +189,21 @@
         addTestDataset(fileBase+"config-ds-plain-2.ttl");
         checkExists("test-ds2");
     }
+    
+    @Test public void add_delete_dataset_6() {
+        assumeNotWindows();
+        
+        checkNotThere(dsTestTdb2);
+
+        addTestDatasetTdb2();
+
+        // Check exists.
+        checkExists(dsTestTdb2);
+
+        // Remove it.
+        deleteDataset(dsTestTdb2);
+        checkNotThere(dsTestTdb2);
+    }
 
     @Test public void add_error_1() {
         FusekiTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
@@ -247,6 +264,116 @@
     }
 
     // ---- Backup
+    
+    @Test public void create_backup_1() {
+        String id = null;
+        try {
+            JsonResponseHandler x = new JsonResponseHandler();
+            execHttpPost(ServerCtl.urlRoot() + "$/" + opBackup + "/" + ServerCtl.datasetPath(), null, WebContent.contentTypeJSON, x);
+            JsonValue v = x.getJSON();
+            id = v.getAsObject().getString("taskId");
+        } finally {
+            waitForTasksToFinish(1000, 5000);
+        }
+        Assert.assertNotNull(id);
+        checkInTasks(id);
+        
+        // Check a backup was created
+        try ( TypedInputStream in = execHttpGet(ServerCtl.urlRoot()+"$/"+opListBackups) ) {
+            assertEqualsIgnoreCase(WebContent.contentTypeJSON, in.getContentType());
+            JsonValue v = JSON.parseAny(in);
+            assertNotNull(v.getAsObject().get("backups"));
+            JsonArray a = v.getAsObject().get("backups").getAsArray();
+            Assert.assertEquals(1, a.size());
+        }
+        
+        JsonValue task = getTask(id);
+        Assert.assertNotNull(id);
+        // Expect task success
+        Assert.assertTrue("Expected task to be marked as successful", task.getAsObject().getBoolean(JsonConstCtl.success));
+    }
+    
+    @Test
+    public void create_backup_2() {
+        String id = null;
+        try {
+            JsonResponseHandler x = new JsonResponseHandler();
+            execHttpPost(ServerCtl.urlRoot() + "$/" + opBackup + "/noSuchDataset", null, WebContent.contentTypeJSON, x);
+            JsonValue v = x.getJSON();
+            id = v.getAsObject().getString(JsonConstCtl.taskId);
+        } finally {
+            waitForTasksToFinish(1000, 5000);
+        }
+        Assert.assertNotNull(id);
+        checkInTasks(id);
+        
+        JsonValue task = getTask(id);
+        Assert.assertNotNull(task);
+        // Expect task failure
+        Assert.assertFalse("Expected task to be marked as failed", task.getAsObject().getBoolean(JsonConstCtl.success));
+    }
+
+    @Test public void list_backups_1() {
+        try ( TypedInputStream in = execHttpGet(ServerCtl.urlRoot()+"$/"+opListBackups) ) {
+            assertEqualsIgnoreCase(WebContent.contentTypeJSON, in.getContentType());
+            JsonValue v = JSON.parseAny(in);
+            assertNotNull(v.getAsObject().get("backups"));
+        }
+    }
+    
+    // ---- Compact
+    
+    @Test public void compact_01() {
+        assumeNotWindows();
+        try {
+            checkNotThere(dsTestTdb2);
+            addTestDatasetTdb2();
+            checkExists(dsTestTdb2);
+            
+            String id = null;
+            try {
+                JsonResponseHandler x = new JsonResponseHandler();
+                execHttpPost(ServerCtl.urlRoot() + "$/" + opCompact + "/" + dsTestTdb2, null, WebContent.contentTypeJSON, x);
+                JsonValue v = x.getJSON();
+                id = v.getAsObject().getString(JsonConstCtl.taskId);
+            } finally {
+                waitForTasksToFinish(1000, 5000);
+            }
+            Assert.assertNotNull(id);
+            checkInTasks(id);
+            
+            JsonValue task = getTask(id);
+            Assert.assertNotNull(id);
+            // Expect task success
+            Assert.assertTrue("Expected task to be marked as successful", task.getAsObject().getBoolean(JsonConstCtl.success));
+        } finally {
+            deleteDataset(dsTestTdb2);
+        }
+    }
+    
+    @Test public void compact_02() {
+        String id = null;
+        try {
+            JsonResponseHandler x = new JsonResponseHandler();
+            execHttpPost(ServerCtl.urlRoot() + "$/" + opCompact + "/noSuchDataset", null, WebContent.contentTypeJSON, x);
+            JsonValue v = x.getJSON();
+            id = v.getAsObject().getString(JsonConstCtl.taskId);
+        } finally {
+            waitForTasksToFinish(1000, 5000);
+        }
+        Assert.assertNotNull(id);
+        checkInTasks(id);
+        
+        JsonValue task = getTask(id);
+        Assert.assertNotNull(id);
+        // Expect task failure
+        Assert.assertFalse("Expected task to be marked as failed", task.getAsObject().getBoolean(JsonConstCtl.success));
+    }
+
+    private void assumeNotWindows() {
+        if (SystemUtils.IS_OS_WINDOWS)
+            throw new AssumptionViolatedException("Test may be unstable on Windows due to inability to delete memory-mapped files");
+    }
 
     // ---- Server
 
@@ -393,14 +520,6 @@
         }
     }
 
-    @Test public void list_backups_1() {
-        try ( TypedInputStream in = execHttpGet(ServerCtl.urlRoot()+"$/"+opListBackups) ) {
-            assertEqualsIgnoreCase(WebContent.contentTypeJSON, in.getContentType());
-            JsonValue v = JSON.parseAny(in);
-            assertNotNull(v.getAsObject().get("backups"));
-        }
-    }
-
     private void assertEqualsIgnoreCase(String contenttypejson, String contentType) {}
 
     private static JsonValue getTask(String taskId) {
@@ -425,6 +544,10 @@
     private static void addTestDatasetInf() {
         addTestDataset(fileBase+"config-ds-inf.ttl");
     }
+    
+    private static void addTestDatasetTdb2() {
+        addTestDataset(fileBase+"config-tdb2.ttl");
+    }
 
     private static void addTestDataset(String filename) {
         File f = new File(filename);
@@ -659,6 +782,7 @@
         POST    /$/datasets/*{name}*?state=offline
         POST    /$/datasets/*{name}*?state=active
         POST    /$/backup/*{name}*
+        POST    /$/compact/*{name}*
         GET     /$/server
         POST    /$/server/shutdown
         GET     /$/stats/
diff --git a/jena-fuseki2/jena-fuseki-webapp/testing/config-tdb2.ttl b/jena-fuseki2/jena-fuseki-webapp/testing/config-tdb2.ttl
new file mode 100644
index 0000000..0c35f4c
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/testing/config-tdb2.ttl
@@ -0,0 +1,18 @@
+## Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0
+
+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 tdb2:    <http://jena.apache.org/2016/tdb#>
+
+<#service1> #rdf:type fuseki:Service ;
+    # URI of the dataset -- http://host:port/ds
+    fuseki:name                        "test-tdb2" ; 
+    fuseki:serviceQuery                "sparql" ;
+    fuseki:dataset                     <#dataset> .
+
+<#dataset> rdf:type      tdb2:DatasetTDB2 ;
+    tdb2:location "target/tdb2" .
diff --git a/jena-geosparql/pom.xml b/jena-geosparql/pom.xml
index f0975b0..2ce3d9c 100644
--- a/jena-geosparql/pom.xml
+++ b/jena-geosparql/pom.xml
@@ -41,7 +41,7 @@
     <dependency>
       <groupId>org.apache.sis.non-free</groupId>
       <artifactId>sis-embedded-data</artifactId>
-      <version>0.8</version>
+      <version>1.0</version>
       <scope>test</scope>
     </dependency>
 
@@ -60,7 +60,7 @@
     <dependency>
       <groupId>org.apache.sis.core</groupId>
       <artifactId>sis-referencing</artifactId>
-      <version>0.8</version>
+      <version>1.0</version>
     </dependency>
 
     <dependency>
diff --git a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/datatype/GMLDatatypeTest.java b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/datatype/GMLDatatypeTest.java
index 2678e91..5cfa147 100644
--- a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/datatype/GMLDatatypeTest.java
+++ b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/datatype/GMLDatatypeTest.java
@@ -17,7 +17,12 @@
  */
 package org.apache.jena.geosparql.implementation.datatype;
 
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
 import java.io.IOException;
+
 import org.apache.jena.geosparql.configuration.GeoSPARQLConfig;
 import org.apache.jena.geosparql.implementation.DimensionInfo;
 import org.apache.jena.geosparql.implementation.GeometryWrapper;
@@ -25,22 +30,9 @@
 import org.apache.jena.geosparql.implementation.jts.CustomCoordinateSequence;
 import org.apache.jena.geosparql.implementation.jts.CustomGeometryFactory;
 import org.apache.jena.geosparql.implementation.vocabulary.SRS_URI;
-import static org.hamcrest.CoreMatchers.not;
 import org.jdom2.JDOMException;
-import org.junit.After;
-import org.junit.AfterClass;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.locationtech.jts.geom.Coordinate;
-import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.GeometryFactory;
-import org.locationtech.jts.geom.LineString;
-import org.locationtech.jts.geom.LinearRing;
-import org.locationtech.jts.geom.Point;
-import org.locationtech.jts.geom.Polygon;
+import org.junit.*;
+import org.locationtech.jts.geom.*;
 
 /**
  *
diff --git a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/datatype/WKTDatatypeTest.java b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/datatype/WKTDatatypeTest.java
index 73f6856..fdbc349 100644
--- a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/datatype/WKTDatatypeTest.java
+++ b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/datatype/WKTDatatypeTest.java
@@ -28,7 +28,7 @@
 import org.junit.After;
 import org.junit.AfterClass;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
+import static org.hamcrest.MatcherAssert.assertThat;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
diff --git a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/registry/SRSRegistryTest.java b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/registry/SRSRegistryTest.java
index 7f230ab..0269aa7 100644
--- a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/registry/SRSRegistryTest.java
+++ b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/registry/SRSRegistryTest.java
@@ -87,10 +87,10 @@
                     + "  Scope[\"Horizontal component of 3D system. Used by the GPS satellite navigation system and for NATO military geodetic surveying.\"],\n"
                     + "  Area[\"World.\"],\n"
                     + "  BBox[-90.00, -180.00, 90.00, 180.00],\n"
-                    + "  Id[\"CRS\", 84, Citation[\"OGC:WMS\"], URI[\"urn:ogc:def:crs:OGC:1.3:CRS84\"]]]";
+                    + "  Id[\"CRS\", 84, Citation[\"WMS\"], URI[\"urn:ogc:def:crs:OGC:1.3:CRS84\"]]]";
 
             CoordinateReferenceSystem expResult = CRS.fromWKT(default_CRS_WKT);
-            CoordinateReferenceSystem result = SRSRegistry.getCRS(srsURI);
+            CoordinateReferenceSystem result = SRSRegistry.getCRS(srsURI);          
             assertEquals(expResult.toWKT(), result.toWKT());
         } catch (FactoryException ex) {
 
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/Imports.java b/jena-shacl/src/main/java/org/apache/jena/shacl/Imports.java
new file mode 100644
index 0000000..222221a
--- /dev/null
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/Imports.java
@@ -0,0 +1,152 @@
+/*
+ * 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.shacl;
+
+import static org.apache.jena.atlas.iterator.Iter.iter;
+import static org.apache.jena.sparql.graph.NodeConst.nodeOwlImports;
+import static org.apache.jena.sparql.graph.NodeConst.nodeOwlOntology;
+import static org.apache.jena.sparql.graph.NodeConst.nodeRDFType;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.jena.atlas.lib.Pair;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.GraphUtil;
+import org.apache.jena.graph.Node;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.riot.other.G;
+import org.apache.jena.riot.system.IRIResolver;
+import org.apache.jena.sparql.graph.GraphFactory;
+
+/**
+ * Import processing.
+ * <p>
+ * Imports are triggered by a base (a single triple "? rdf:type owl:Ontology")
+ * and imports (triples "base owl:Imports URI").
+ * <p>
+ * If there are other "? owl:imports ?" triples, they are ignored.
+ */
+public class Imports {
+    private Imports() {}
+
+    /**
+     * Load a graph and process owl:imports to create a new, single graph.
+     */
+    public static Graph loadWithImports(String url) {
+        url = IRIResolver.resolveString(url);
+        Graph graph = RDFDataMgr.loadGraph(url);
+        return withImportsWorker(url, graph);
+    }
+
+    /**
+     * Process and return the owl:imports closure of a graph. The graph is included
+     * in the results. Note that without knowing the URI, the start graph may be read
+     * again if it is named as an import.
+     */
+    public static Graph withImports(Graph graph) {
+        return withImportsWorker(null, graph);
+    }
+
+    /**
+     * Process and return the owl:imports closure of a graph.
+     * The graph is included in the results.
+     */
+    public static Graph withImports(String url, Graph graph) {
+        url = IRIResolver.resolveString(url);
+        return withImportsWorker(url, graph);
+    }
+
+    private static Graph withImportsWorker(String url, Graph graph) {
+        // Partial check for any imports. Are there any imports triples?
+        boolean hasImports = G.contains(graph, null, nodeOwlImports, null);
+        if ( ! hasImports )
+            return graph;
+        // Probably some work to do.
+        // This is "import self", and start the "visited".
+        Graph acc = GraphFactory.createDefaultGraph();
+        GraphUtil.addInto(acc, graph);
+        Set<String> visited = new HashSet<>();
+        if ( url != null )
+            visited.add(url);
+        processImports(visited, graph, acc);
+        return acc;
+    }
+
+    /** Carefully traverse the imports, loading graphs. */
+    private static void processImports(Set<String> visited, Graph graph, Graph acc) {
+        List<Node> imports = imports(graph);
+        for ( Node imported : imports ) {
+            if ( ! imported.isURI() )
+                // Ignore non-URIs.
+                continue;
+            String uri = imported.getURI();
+            if ( ! visited.contains(uri) ) {
+                visited.add(uri);
+                // Read into a temporary graph to isolate errors.
+                try {
+                    Graph g2 = RDFDataMgr.loadGraph(uri);
+                    GraphUtil.addInto(acc, g2);
+                    processImports(visited, g2, acc);
+                } catch (RuntimeException ex) {}
+            }
+        }
+    }
+
+    /** Return the imports for a graph */
+    public static List<Node> imports(Graph graph) {
+        Pair<Node,List<Node>> pair = baseAndImports(graph);
+        return pair.getRight();
+    }
+
+    /**
+     * Locate the base (a single triple ? rdf:type owl:Ontology)
+     * and imports (triples "base owl:Imports URI").
+     * Returns a Pair of (null,EmptyList) for no base.
+     */
+    public static Pair<Node,List<Node>> baseAndImports(Graph graph) {
+        Node base = G.getZeroOrOnePO(graph, nodeRDFType, nodeOwlOntology);
+        if ( base == null )
+            return Pair.create(null, Collections.emptyList());
+        List<Node> imports = allImports(base, graph);
+        return Pair.create(base, imports);
+    }
+
+    /**
+     * Locate the base (a single triple ? rdf:type owl:Ontology).
+     * If none or more than one matching triple, then return null.
+     */
+    public static Node base(Graph graph) {
+        // Filter for URI?
+        return G.getZeroOrOnePO(graph, nodeRDFType, nodeOwlOntology);
+    }
+
+    /**
+     * Locate any imports (triples "base owl:Imports URI").
+     * Base may be a wildcard indicating "any owl:imports".
+     */
+    public static List<Node> allImports(Node base, Graph graph) {
+        List<Node> imports = iter(G.listSP(graph, base, nodeOwlImports)).filter(Node::isURI).collect(Collectors.toList());
+        return imports;
+    }
+}
+
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/Shapes.java b/jena-shacl/src/main/java/org/apache/jena/shacl/Shapes.java
index 12401a0..8e10e70 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/Shapes.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/Shapes.java
@@ -18,19 +18,14 @@
 
 package org.apache.jena.shacl;
 
-import static org.apache.jena.sparql.graph.NodeConst.nodeOwlImports;
-import static org.apache.jena.sparql.graph.NodeConst.nodeOwlOntology;
-import static org.apache.jena.sparql.graph.NodeConst.nodeRDFType;
-
 import java.util.*;
 
 import org.apache.jena.atlas.iterator.Iter;
+import org.apache.jena.atlas.lib.Pair;
 import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
 import org.apache.jena.rdf.model.Model;
 import org.apache.jena.riot.RDFDataMgr;
-import org.apache.jena.riot.other.G;
-import org.apache.jena.riot.other.RDFDataException;
 import org.apache.jena.shacl.engine.Targets;
 import org.apache.jena.shacl.parser.Shape;
 import org.apache.jena.shacl.parser.ShapesParser;
@@ -70,6 +65,16 @@
         return parse(g);
     }
 
+    /** Load the file, parse the graph and return the shapes. */
+    public static Shapes parse(String fileOrURL, boolean withImports) {
+        Graph g = withImports
+            ? Imports.loadWithImports(fileOrURL)
+            : RDFDataMgr.loadGraph(fileOrURL);
+        return parse(g);
+    }
+
+
+    
     /** Parse the graph and return the shapes connected to the targets. */
     public static Shapes parse(Graph graph) {
         return parseAll(graph);
@@ -122,7 +127,7 @@
         return new Shapes(shapesGraph, shapesMap, targets, rootShapes, declShapes);
     }
 
-    public Shapes(Graph shapesGraph, Map<Node, Shape> shapesMap, Targets targets,
+    private Shapes(Graph shapesGraph, Map<Node, Shape> shapesMap, Targets targets,
                   Collection<Shape> rootShapes, Collection<Shape> declShapes) {
         this.shapesGraph = shapesGraph;
         this.targets = targets;
@@ -130,15 +135,10 @@
         this.rootShapes = rootShapes;
         this.declShapes = declShapes;
 
-        Node _shapesBase = null;
-        List<Node> _imports = null;
         // Extract base and imports.
-        try {
-            _shapesBase = G.getOnePO(shapesGraph, nodeRDFType, nodeOwlOntology);
-            _imports = G.listSP(shapesGraph, _shapesBase, nodeOwlImports);
-        } catch (RDFDataException ex) {}
-        this.shapesBase = _shapesBase;
-        this.imports = _imports;
+        Pair<Node,List<Node>> pair = Imports.baseAndImports(shapesGraph);
+        this.shapesBase = pair.getLeft();
+        this.imports = pair.getRight();
     }
 
     public boolean isEmpty() {
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/parser/ShapesParser.java b/jena-shacl/src/main/java/org/apache/jena/shacl/parser/ShapesParser.java
index b4daec9..bb2e331 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/parser/ShapesParser.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/parser/ShapesParser.java
@@ -70,13 +70,13 @@
      * Applications should call functions in {@link Shapes} rather than call the parser directly.
      */
     public static Collection<Shape> parseShapes(Graph shapesGraph, Targets targets, Map<Node, Shape> shapesMap) {
-        // Cycle detection. 
+        // Cycle detection.
         Set<Node> cycles = new HashSet<>();
         return parseShapes(shapesGraph, targets, shapesMap, cycles);
     }
-    
+
     /*package*/ static Collection<Shape> parseShapes(Graph shapesGraph, Targets targets, Map<Node, Shape> shapesMap, Set<Node> cycles) {
-        
+
         Targets rootShapes = targets;
 
         if ( DEBUG )
@@ -173,21 +173,20 @@
 
     // ---- Main parser worker.
     /**
-     *  Parse one shape updating the record of shapes already parsed.
+     *  Parse one shape, updating the record of shapes already parsed.
      *
      * @param shapesMap
      * @param shapesGraph
      * @param shNode
      * @return Shape
      */
-    
+
     public static Shape parseShape(Map<Node, Shape> shapesMap, Graph shapesGraph, Node shNode) {
         Set<Node> traversed = new HashSet<>();
         Shape shape = parseShapeStep(traversed, shapesMap, shapesGraph, shNode);
         return shape;
     }
 
-
 //    /** Parse a specific shape from the Shapes graph */
 //    private static Shape parseShape(Graph shapesGraph, Node shNode) {
 //        // Avoid recursion.
@@ -230,18 +229,18 @@
     | sh:path         |
     -------------------
      */
-    
+
     /** Do nothing placeholder shape. */
-    static Shape unshape(Graph shapesGraph, Node shapeNode) { return 
+    static Shape unshape(Graph shapesGraph, Node shapeNode) { return
             new NodeShape(shapesGraph, shapeNode, false, Severity.Violation,
                           Collections.emptySet(), Collections.emptySet(),
                           Collections.singleton(new JLogConstraint("Cycle")),
                           Collections.emptySet());
     }
-    
+
     /** parse a shape during a parsing process */
     /*package*/ static Shape parseShapeStep(Set<Node> traversed, Map<Node, Shape> parsed, Graph shapesGraph, Node shapeNode) {
-        try { 
+        try {
             // Called by Constraints
             if ( parsed.containsKey(shapeNode) )
                 return parsed.get(shapeNode);
diff --git a/jena-shacl/src/test/files/imports/graph1.ttl b/jena-shacl/src/test/files/imports/graph1.ttl
new file mode 100644
index 0000000..1331124
--- /dev/null
+++ b/jena-shacl/src/test/files/imports/graph1.ttl
@@ -0,0 +1,12 @@
+PREFIX : <http://example/>
+
+PREFIX rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#> 
+PREFIX rdfs:    <http://www.w3.org/2000/01/rdf-schema#>
+PREFIX sh:      <http://www.w3.org/ns/shacl#>
+PREFIX xsd:     <http://www.w3.org/2001/XMLSchema#>
+PREFIX owl:     <http://www.w3.org/2002/07/owl#>
+
+:graph1 rdf:type owl:Ontology ;
+        owl:imports <graph2.ttl> , <graph3.ttl> .
+
+:graph1 :p [] .
diff --git a/jena-shacl/src/test/files/imports/graph2.ttl b/jena-shacl/src/test/files/imports/graph2.ttl
new file mode 100644
index 0000000..2e71c06
--- /dev/null
+++ b/jena-shacl/src/test/files/imports/graph2.ttl
@@ -0,0 +1,13 @@
+PREFIX : <http://example/>
+
+PREFIX rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#> 
+PREFIX rdfs:    <http://www.w3.org/2000/01/rdf-schema#>
+PREFIX sh:      <http://www.w3.org/ns/shacl#>
+PREFIX xsd:     <http://www.w3.org/2001/XMLSchema#>
+PREFIX owl:     <http://www.w3.org/2002/07/owl#>
+
+:graph2 rdf:type owl:Ontology .
+
+:graph2 owl:imports <graph4.ttl> .
+
+:graph2 :p [] .
diff --git a/jena-shacl/src/test/files/imports/graph3.ttl b/jena-shacl/src/test/files/imports/graph3.ttl
new file mode 100644
index 0000000..7ab1c13
--- /dev/null
+++ b/jena-shacl/src/test/files/imports/graph3.ttl
@@ -0,0 +1,16 @@
+PREFIX : <http://example/>
+
+PREFIX rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#> 
+PREFIX rdfs:    <http://www.w3.org/2000/01/rdf-schema#>
+PREFIX sh:      <http://www.w3.org/ns/shacl#>
+PREFIX xsd:     <http://www.w3.org/2001/XMLSchema#>
+PREFIX owl:     <http://www.w3.org/2002/07/owl#>
+
+:graph3 rdf:type owl:Ontology .
+
+# Complicated - if starting at graph1, this is a cycle.
+:graph3 owl:imports <graph1.ttl> .
+
+:graph3 owl:imports <graph4.ttl> , <graph5.ttl> .
+
+:graph3 :p [] .
diff --git a/jena-shacl/src/test/files/imports/graph4.ttl b/jena-shacl/src/test/files/imports/graph4.ttl
new file mode 100644
index 0000000..8e68743
--- /dev/null
+++ b/jena-shacl/src/test/files/imports/graph4.ttl
@@ -0,0 +1,9 @@
+PREFIX : <http://example/>
+
+PREFIX rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#> 
+PREFIX rdfs:    <http://www.w3.org/2000/01/rdf-schema#>
+PREFIX sh:      <http://www.w3.org/ns/shacl#>
+PREFIX xsd:     <http://www.w3.org/2001/XMLSchema#>
+PREFIX owl:     <http://www.w3.org/2002/07/owl#>
+
+:graph4 :p [] .
diff --git a/jena-shacl/src/test/files/imports/graph5.ttl b/jena-shacl/src/test/files/imports/graph5.ttl
new file mode 100644
index 0000000..2f2c7c4
--- /dev/null
+++ b/jena-shacl/src/test/files/imports/graph5.ttl
@@ -0,0 +1,9 @@
+PREFIX : <http://example/>
+
+PREFIX rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#> 
+PREFIX rdfs:    <http://www.w3.org/2000/01/rdf-schema#>
+PREFIX sh:      <http://www.w3.org/ns/shacl#>
+PREFIX xsd:     <http://www.w3.org/2001/XMLSchema#>
+PREFIX owl:     <http://www.w3.org/2002/07/owl#>
+
+:graph5 :p [] .
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java b/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
index 9fa247b..3ced5f8 100644
--- a/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
+++ b/jena-shacl/src/test/java/org/apache/jena/shacl/TC_SHACL.java
@@ -19,6 +19,7 @@
 package org.apache.jena.shacl;
 
 import org.apache.jena.shacl.compact.TS_Compact;
+import org.apache.jena.shacl.tests.TestImports;
 import org.apache.jena.shacl.tests.TestValidationReport;
 import org.apache.jena.shacl.tests.jena_shacl.TS_JenaShacl;
 import org.apache.jena.shacl.tests.std.TS_StdSHACL;
@@ -31,6 +32,7 @@
     , TS_StdSHACL.class
     , TS_JenaShacl.class
     , TS_Compact.class
+    , TestImports.class
 } )
 
 public class TC_SHACL { }
diff --git a/jena-shacl/src/test/java/org/apache/jena/shacl/tests/TestImports.java b/jena-shacl/src/test/java/org/apache/jena/shacl/tests/TestImports.java
new file mode 100644
index 0000000..0d39d16
--- /dev/null
+++ b/jena-shacl/src/test/java/org/apache/jena/shacl/tests/TestImports.java
@@ -0,0 +1,115 @@
+/*
+ * 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.shacl.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.apache.jena.atlas.lib.Pair;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.riot.other.G;
+import org.apache.jena.riot.system.IRIResolver;
+import org.apache.jena.shacl.Imports;
+import org.junit.Test;
+
+public class TestImports {
+    // Work in absolute URIs.
+    private static String FILES = IRIResolver.resolveString("src/test/files/imports");
+    private static Node g1 = NodeFactory.createURI("http://example/graph1");
+    private static Node g2 = NodeFactory.createURI("http://example/graph2");
+    private static Node g3 = NodeFactory.createURI("http://example/graph3");
+    private static Node g4 = NodeFactory.createURI("http://example/graph4");
+    private static Node g5 = NodeFactory.createURI("http://example/graph5");
+
+    private static Node u1 = NodeFactory.createURI(FILES+"/graph1.ttl");
+    private static Node u2 = NodeFactory.createURI(FILES+"/graph2.ttl");
+    private static Node u3 = NodeFactory.createURI(FILES+"/graph3.ttl");
+    private static Node u4 = NodeFactory.createURI(FILES+"/graph4.ttl");
+    private static Node u5 = NodeFactory.createURI(FILES+"/graph5.ttl");
+
+    private static Node predicate = NodeFactory.createURI("http://example/p");
+
+    @Test public void testImports1() {
+        Graph graph = RDFDataMgr.loadGraph(FILES+"/graph1.ttl");
+        Node base = Imports.base(graph);
+        assertEquals(g1, base);
+    }
+
+    @Test public void testImports2() {
+        Graph graph = RDFDataMgr.loadGraph(FILES+"/graph1.ttl");
+        List<Node> imports = Imports.imports(graph);
+        assertEquals(2, imports.size());
+        assertTrue(imports.contains(u2));
+        assertTrue(imports.contains(u3));
+    }
+
+    @Test public void testImports3() {
+        Graph graph = RDFDataMgr.loadGraph(FILES+"/graph1.ttl");
+
+        Pair<Node, List<Node>> pair = Imports.baseAndImports(graph);
+        Node base = pair.getLeft();
+        List<Node> imports = pair.getRight();
+
+        assertEquals(g1, base);
+        assertEquals(2, imports.size());
+        assertTrue(imports.contains(u2));
+        assertTrue(imports.contains(u3));
+    }
+
+    @Test public void testImportsLoading1() {
+        Graph graph = Imports.loadWithImports(FILES+"/graph1.ttl");
+        // Used blank nodes to detect loaded once or multiple times.
+        //RDFDataMgr.write(System.out, graph, Lang.TTL);
+        assertTrue(G.containsOne(graph, g1, predicate, null));
+        assertTrue(G.containsOne(graph, g2, predicate, null));
+        assertTrue(G.containsOne(graph, g3, predicate, null));
+        assertTrue(G.containsOne(graph, g4, predicate, null));
+        assertTrue(G.containsOne(graph, g5, predicate, null));
+    }
+
+    @Test public void testImportsLoading2() {
+        Graph graph1 = RDFDataMgr.loadGraph(FILES+"/graph1.ttl");
+        Graph graph = Imports.withImports(FILES+"/graph1.ttl",graph1);
+        assertTrue(G.containsOne(graph, g1, predicate, null));
+        assertTrue(G.containsOne(graph, g2, predicate, null));
+        assertTrue(G.containsOne(graph, g3, predicate, null));
+        assertTrue(G.containsOne(graph, g4, predicate, null));
+        assertTrue(G.containsOne(graph, g5, predicate, null));
+    }
+
+    @Test public void testImportsLoading3() {
+        Graph graph1 = RDFDataMgr.loadGraph(FILES+"/graph1.ttl");
+        Graph graph = Imports.withImports(graph1);
+        // Will be read again due to not knowing it URI.
+        // Skip test.
+        // assertTrue(G.containsOne(graph, g1, p, null));
+        assertTrue(G.containsOne(graph, g2, predicate, null));
+        assertTrue(G.containsOne(graph, g3, predicate, null));
+        assertTrue(G.containsOne(graph, g4, predicate, null));
+        assertTrue(G.containsOne(graph, g5, predicate, null));
+    }
+
+
+}
+