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 2025/01/01 11:10:59 UTC
(jena) 01/08: GH-2902: Fuseki Server Modules
This is an automated email from the ASF dual-hosted git repository.
andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git
commit f663f5161d24cb55c6bec8043bca6384bf1c7800
Author: Andy Seaborne <an...@apache.org>
AuthorDate: Sun Dec 15 14:45:44 2024 +0000
GH-2902: Fuseki Server Modules
---
.../main/java/org/apache/jena/atlas/net/Host.java | 23 -
.../org/apache/jena/fuseki/server/Counter.java | 19 +-
jena-fuseki2/jena-fuseki-main/pom.xml | 53 +-
.../jena/fuseki/authz/AuthorizationFilter403.java | 58 ++
.../org/apache/jena/fuseki/authz/DenyFilter.java} | 22 +-
.../apache/jena/fuseki/authz/LocalhostFilter.java | 71 ++
.../jena/fuseki/main/cmds/FusekiServerCmd.java | 54 ++
.../org/apache/jena/fuseki/mgt/ActionBackup.java | 77 ++
.../apache/jena/fuseki/mgt/ActionBackupList.java | 94 +++
.../org/apache/jena/fuseki/mgt/ActionCompact.java} | 22 +-
.../org/apache/jena/fuseki/mgt/ActionDatasets.java | 499 ++++++++++++
.../org/apache/jena/fuseki/mgt/ActionLogs.java | 55 ++
.../org/apache/jena/fuseki/mgt/ActionReload.java | 66 ++
.../java/org/apache/jena/fuseki/mgt/Backup.java | 122 +++
.../org/apache/jena/fuseki/mgt/FusekiAdmin.java} | 24 +-
.../java/org/apache/jena/fuseki/mgt/FusekiApp.java | 425 +++++++++++
.../org/apache/jena/fuseki/mgt/ServerMgtConst.java | 41 +
.../java/org/apache/jena/fuseki/mgt/Template.java | 68 ++
.../apache/jena/fuseki/mgt/TemplateFunctions.java | 86 +++
.../fuseki/mod/access/FMod_GraphAccessCtl.java | 46 ++
.../jena/fuseki/mod/admin/ActionServerStatus.java | 99 +++
.../jena/fuseki/mod/admin/ArgModuleAdmin.java | 69 ++
.../apache/jena/fuseki/mod/admin/FMod_Admin.java | 178 +++++
.../jena/fuseki/mod/admin/LocalhostOnly.java | 94 +++
.../apache/jena/fuseki/mod/blank/FMod_BLANK.java | 43 ++
.../org/apache/jena/fuseki/mod/package-info.java | 2 +
.../jena/fuseki/mod/prometheus/ActionMetrics.java | 49 ++
.../fuseki/mod/prometheus/FMod_Prometheus.java | 63 ++
.../mod/prometheus/PrometheusMetricsProvider.java | 59 ++
.../apache/jena/fuseki/mod/shiro/FMod_Shiro.java | 188 +++++
.../jena/fuseki/mod/shiro/FusekiShiroLib.java | 62 ++
.../mod/shiro/ShiroEnvironmentLoaderListener.java | 77 ++
.../org/apache/jena/fuseki/mod/ui/ActionStats.java | 165 ++++
.../apache/jena/fuseki/mod/ui/ActionStatsTxt.java | 189 +++++
.../org/apache/jena/fuseki/mod/ui/FMod_UI.java | 185 +++++
.../org/apache/jena/fuseki/server/config.ttl | 0
.../org/apache/jena/fuseki/server/shiro.ini | 38 +
.../apache/jena/fuseki/server/templates/config-mem | 29 +
.../apache/jena/fuseki/server/templates/config-tdb | 32 +
.../jena/fuseki/server/templates/config-tdb-dir | 30 +
.../jena/fuseki/server/templates/config-tdb-mem | 30 +
.../jena/fuseki/server/templates/config-tdb2 | 32 +
.../jena/fuseki/server/templates/config-tdb2-dir | 31 +
.../jena/fuseki/server/templates/config-tdb2-mem | 30 +
.../org/apache/jena/fuseki/TC_FusekiServer.java} | 31 +-
.../org/apache/jena/fuseki/mod/TC_FusekiMods.java} | 35 +-
.../apache/jena/fuseki/mod/TestFusekiServer.java} | 30 +-
.../jena/fuseki/mod/admin/TS_FusekiServerApp.java} | 29 +-
.../apache/jena/fuseki/mod/admin/TestAdmin.java | 836 +++++++++++++++++++++
.../jena/fuseki/mod/admin/TestFusekiReload.java | 169 +++++
.../fuseki/mod/admin/TestTemplateAddDataset.java | 183 +++++
.../jena/fuseki/mod/metrics/TestModPrometheus.java | 77 ++
.../apache/jena/fuseki/mod/shiro/TestModShiro.java | 220 ++++++
.../src/test/resources/log4j2-test.properties | 15 +-
.../testing/Config/config-ds-bad-name-1.ttl | 15 +
.../testing/Config/config-ds-bad-name-2.ttl | 15 +
.../testing/Config/config-ds-bad-name-3.ttl | 15 +
.../testing/Config/config-ds-bad-name-4.ttl | 15 +
.../testing/Config/config-ds-inf.ttl | 30 +
.../testing/Config/config-ds-plain-1.ttl | 17 +
.../testing/Config/config-ds-plain-2.ttl | 18 +
.../testing/Config/config-tdb2a.ttl | 18 +
.../testing/Config/config-tdb2b.ttl | 18 +
.../testing/Shiro/shiro_localhost.ini | 13 +
.../testing/Shiro/shiro_userpassword.ini | 18 +
65 files changed, 5329 insertions(+), 187 deletions(-)
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/net/Host.java b/jena-base/src/main/java/org/apache/jena/atlas/net/Host.java
index 1028472c30..12449dbd71 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/net/Host.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/net/Host.java
@@ -128,27 +128,4 @@ public class Host {
throw unknownHostException;
}
}
-
- // @formatter:off
-// public static void main(String ... arg) throws UnknownHostException {
-// try {
-// for (Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements();) {
-// NetworkInterface iface = ifaces.nextElement();
-// // Iterate all IP addresses assigned to each card...
-// for (Enumeration<InetAddress> inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements();) {
-// InetAddress inetAddr = inetAddrs.nextElement();
-// System.out.println("IP Address : '" +inetAddr.getHostAddress()+"'");
-// }
-// }
-// System.out.println();
-//
-// InetAddress inetAddr = getLocalHostLANAddress();
-// //InetAddress localhost = InetAddress.getLocalHost();
-// System.out.println("System IP Address : '" +inetAddr.getHostAddress()+"'");
-// } catch (Exception ex) {
-// ex.printStackTrace();
-// System.exit(0);
-// }
-// }
- // @formatter:on
}
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
index 71c6c31d0e..7889b3ce32 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
@@ -18,22 +18,21 @@
package org.apache.jena.fuseki.server;
-import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.LongAdder;
-/** A statistics counter */
-public class Counter
-{
- private AtomicLong counter = new AtomicLong(0);
+/** A statistics counter. The value is "eventual consistent" */
+public class Counter {
+ // Not for synchronization
+ private LongAdder counter = new LongAdder();
- public Counter() {}
+ public Counter() {}
- public void inc() { counter.incrementAndGet(); }
- public void dec() { counter.decrementAndGet(); }
- public long value() { return counter.get(); }
+ public void inc() { counter.increment(); }
+ public void dec() { counter.decrement(); }
+ public long value() { return counter.sum(); }
@Override
public String toString() {
return counter.toString();
}
}
-
diff --git a/jena-fuseki2/jena-fuseki-main/pom.xml b/jena-fuseki2/jena-fuseki-main/pom.xml
index e620084863..b72e861327 100644
--- a/jena-fuseki2/jena-fuseki-main/pom.xml
+++ b/jena-fuseki2/jena-fuseki-main/pom.xml
@@ -83,6 +83,19 @@
<artifactId>jetty-xml</artifactId>
</dependency>
+ <!-- Apache Shiro -->
+ <dependency>
+ <groupId>org.apache.shiro</groupId>
+ <artifactId>shiro-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.shiro</groupId>
+ <artifactId>shiro-web</artifactId>
+ <classifier>jakarta</classifier>
+ </dependency>
+
+ <!-- Testing -->
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
@@ -96,41 +109,39 @@
</dependency>
<dependency>
- <groupId>org.junit.platform</groupId>
- <artifactId>junit-platform-suite</artifactId>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
-
+
<dependency>
- <groupId>org.apache.logging.log4j</groupId>
- <artifactId>log4j-slf4j2-impl</artifactId>
- <optional>true</optional>
- <scope>compile</scope>
+ <groupId>org.junit.platform</groupId>
+ <artifactId>junit-platform-suite</artifactId>
+ <scope>test</scope>
</dependency>
- <!-- Include Shiro - for the example in src/test/java/ -->
-
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-core</artifactId>
- <classifier>jakarta</classifier>
+<!--
+ Only needed if the Junit5 artifacts don't pick the right runner
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>${ver.junit}</version>
<scope>test</scope>
- <optional>true</optional>
</dependency>
+-->
+
<dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-config-core</artifactId>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
<scope>test</scope>
- <optional>true</optional>
</dependency>
<dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-web</artifactId>
- <classifier>jakarta</classifier>
- <scope>test</scope>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-slf4j2-impl</artifactId>
<optional>true</optional>
+ <scope>compile</scope>
</dependency>
<!-- Testing for custom functions only -->
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/authz/AuthorizationFilter403.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/authz/AuthorizationFilter403.java
new file mode 100644
index 0000000000..715331d1f3
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/authz/AuthorizationFilter403.java
@@ -0,0 +1,58 @@
+/**
+ * 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.authz;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.jena.web.HttpSC;
+import org.apache.shiro.web.filter.authz.AuthorizationFilter;
+import org.apache.shiro.web.util.WebUtils;
+
+/** Specialise AuthorizationFilter to yield HTTP 403 on access denied */
+public abstract class AuthorizationFilter403 extends AuthorizationFilter
+{
+ private String message;
+
+ protected AuthorizationFilter403(String text) { setMessage(text); }
+ protected AuthorizationFilter403() { this(null); }
+
+ /** Set the message used in HTTP 403 responses */
+ public void setMessage(String msg) { message = msg; }
+
+ public String getMessage() { return message; }
+
+ @Override
+ protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
+ HttpServletResponse httpResponse;
+ try { httpResponse = WebUtils.toHttp(response); }
+ catch (ClassCastException ex) {
+ // Not a HTTP Servlet operation
+ return super.onAccessDenied(request, response);
+ }
+ if ( message == null )
+ httpResponse.sendError(HttpSC.FORBIDDEN_403);
+ else
+ httpResponse.sendError(HttpSC.FORBIDDEN_403, message);
+ return false; // No further processing.
+ }
+}
+
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/authz/DenyFilter.java
similarity index 65%
copy from jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
copy to jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/authz/DenyFilter.java
index 71c6c31d0e..9e7a5d8e42 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/authz/DenyFilter.java
@@ -16,24 +16,18 @@
* limitations under the License.
*/
-package org.apache.jena.fuseki.server;
+package org.apache.jena.fuseki.authz;
-import java.util.concurrent.atomic.AtomicLong;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
-/** A statistics counter */
-public class Counter
-{
- private AtomicLong counter = new AtomicLong(0);
+/** An authorization filter that always denies access and sends back HTTP 403 */
+public class DenyFilter extends AuthorizationFilter403 {
- public Counter() {}
-
- public void inc() { counter.incrementAndGet(); }
- public void dec() { counter.decrementAndGet(); }
- public long value() { return counter.get(); }
+ public DenyFilter() { super("Access denied"); }
@Override
- public String toString() {
- return counter.toString();
+ protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
+ return false;
}
}
-
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/authz/LocalhostFilter.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/authz/LocalhostFilter.java
new file mode 100644
index 0000000000..f029b56fff
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/authz/LocalhostFilter.java
@@ -0,0 +1,71 @@
+/**
+ * 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.authz;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Set;
+
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import org.apache.shiro.web.filter.authz.PortFilter;
+
+/**
+ * A Filter that can allow or deny access based on whether the
+ * the host that sent the request is the loopback address (AKA localhost).
+ * Use of the external IP address of the local machine does not permit access,
+ * only the loopback interface is authorized.
+ * Responds with HTTP 403 on any denied request.
+ *
+ * Example:
+ * <pre>
+ * [main]
+ * localhost=org.apache.jena.fuseki.authz.LocalhostFilter
+ * ...
+ * [urls]
+ * /LocalFilesForLocalPeople/** = localhost
+ * </pre>
+ * @see PortFilter
+ */
+
+public class LocalhostFilter extends AuthorizationFilter403 {
+
+ private static final String message = "Access denied : only localhost access allowed";
+
+ public LocalhostFilter() { super(message); }
+
+ private static String LOCALHOST_IpV6_a = "[0:0:0:0:0:0:0:1]";
+ private static String LOCALHOST_IpV6_b = "0:0:0:0:0:0:0:1";
+ // This is what appears in the Chrome developer tools client-side.
+ // "[0:0:0:0:0:0:0:1]" by the time it arrives here, It is not clear which
+ // software component is responsible for that.
+ // To be safe we add "[::1]".
+ private static String LOCALHOST_IpV6_c = "[::1]";
+ private static String LOCALHOST_IpV4 = "127.0.0.1"; // Strictly, 127.*.*.*
+
+ private static final Collection<String> localhosts = Set.copyOf(
+ Arrays.asList(LOCALHOST_IpV4, LOCALHOST_IpV6_a, LOCALHOST_IpV6_b, LOCALHOST_IpV6_c));
+
+ @Override
+ protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
+ return localhosts.contains(request.getRemoteAddr());
+ }
+}
+
+
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiServerCmd.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiServerCmd.java
new file mode 100644
index 0000000000..2c61c0ed09
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiServerCmd.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.fuseki.main.cmds;
+
+import org.apache.jena.fuseki.run.FusekiModServer;
+import org.apache.jena.fuseki.system.FusekiLogging;
+
+/** Fuseki command that runs a Fuseki server with the admin UI.
+ * <p>
+ * Use {@code --conf=} for multiple datasets and specific service names.
+ * <p>
+ * The command line dataset setup only supports a single dataset.
+ */
+
+public class FusekiServerCmd {
+ // This class wraps FusekiMain so that it can take control of logging setup.
+ // This class does not depend via inheritance on any Jena code
+ // and does not trigger Jena initialization.
+ // FusekiLogging runs before any Jena code can trigger logging setup.
+ //
+ // Inheritance causes initialization in the super class first, before class
+ // initialization code in this class.
+
+ static { FusekiLogging.setLogging(); }
+
+ /**
+ * Build and run, a server based on command line syntax. This operation does not
+ * return. See {@link FusekiMain#build} to build a server using command line
+ * syntax but not start it.
+ */
+ static public void main(String... args) {
+ // Fix up args
+ // --empty
+ // --modules=true
+ FusekiModServer.runAsync(args).join();
+ }
+}
+
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionBackup.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionBackup.java
new file mode 100644
index 0000000000..758af27d8a
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionBackup.java
@@ -0,0 +1,77 @@
+/**
+ * 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.Fuseki;
+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.slf4j.Logger;
+
+public class ActionBackup extends ActionAsyncTask
+{
+ public ActionBackup() { super("Backup"); }
+
+ @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] Backup dataset %s", action.id, name));
+ // ** Error changing in TaskBase
+ BackupTask task = new BackupTask(action);
+ if ( task.dataset == null ) {
+ ServletOps.errorBadRequest("Dataset not found");
+ return null;
+ }
+ return task;
+ }
+
+ static class BackupTask extends TaskBase {
+ static private Logger log = Fuseki.backupLog;
+
+ public BackupTask(HttpAction action) {
+ super(action);
+ }
+
+ @Override
+ public void run() {
+ try {
+ String backupFilename = Backup.chooseFileName(datasetName);
+ 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 (Throwable ex) {
+ log.warn(format("[%d] **** Exception in backup", actionId), ex);
+ // Pass on - the async task tracking infrastructure will record this.
+ throw ex;
+ }
+ }
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionBackupList.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionBackupList.java
new file mode 100644
index 0000000000..1e52a65b69
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionBackupList.java
@@ -0,0 +1,94 @@
+/**
+ * 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 java.io.File;
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.jena.atlas.json.JsonBuilder;
+import org.apache.jena.atlas.json.JsonValue;
+import org.apache.jena.fuseki.ctl.ActionCtl;
+import org.apache.jena.fuseki.servlets.HttpAction;
+import org.apache.jena.fuseki.servlets.ServletOps;
+
+/**
+ * A JSON API to list all the backups in the backup directory
+ */
+public class ActionBackupList extends ActionCtl {
+
+ @Override
+ public void execGet(HttpAction action) {
+ executeLifecycle(action);
+ }
+
+ @Override
+ public void execPost(HttpAction action) {
+ executeLifecycle(action);
+ }
+
+ @Override
+ public void validate(HttpAction action) {}
+
+ @Override
+ public void execute(HttpAction action) {
+ JsonValue result = description(action);
+ ServletOps.setNoCache(action);
+ ServletOps.sendJsonReponse(action, result);
+ }
+
+ private static DirectoryStream.Filter<Path> filterVisibleFiles = (entry) -> {
+ File f = entry.toFile();
+ return f.isFile() && !f.isHidden();
+ };
+
+ private JsonValue description(HttpAction action) {
+ if ( ! Files.isDirectory(FusekiApp.dirBackups) )
+ ServletOps.errorOccurred(format("[%d] Backup area '%s' is not a directory", action.id, FusekiApp.dirBackups));
+
+ List<Path> paths = new ArrayList<>();
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(FusekiApp.dirBackups, filterVisibleFiles)) {
+ stream.forEach(paths::add);
+ } catch (IOException ex) {
+ action.log.error(format("[%d] Backup file list :: IOException :: %s", action.id, ex.getMessage()));
+ ServletOps.errorOccurred(ex);
+ }
+
+ List<String> fileNames = paths.stream().map((p)->p.getFileName().toString()).sorted().collect(Collectors.toList());
+
+ JsonBuilder builder = new JsonBuilder();
+ builder.startObject("top");
+ builder.key("backups");
+
+ builder.startArray();
+ fileNames.forEach(builder::value);
+ builder.finishArray();
+
+ builder.finishObject("top");
+ return builder.build();
+
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionCompact.java
similarity index 63%
copy from jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
copy to jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionCompact.java
index 71c6c31d0e..3639db5267 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionCompact.java
@@ -1,4 +1,4 @@
-/**
+/*
* 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
@@ -16,24 +16,8 @@
* limitations under the License.
*/
-package org.apache.jena.fuseki.server;
+package org.apache.jena.fuseki.mgt;
-import java.util.concurrent.atomic.AtomicLong;
+public class ActionCompact {
-/** A statistics counter */
-public class Counter
-{
- private AtomicLong counter = new AtomicLong(0);
-
- public Counter() {}
-
- public void inc() { counter.incrementAndGet(); }
- public void dec() { counter.decrementAndGet(); }
- public long value() { return counter.get(); }
-
- @Override
- public String toString() {
- return counter.toString();
- }
}
-
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionDatasets.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionDatasets.java
new file mode 100644
index 0000000000..7630b8c6f0
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionDatasets.java
@@ -0,0 +1,499 @@
+/**
+ * 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 java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jena.atlas.RuntimeIOException;
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.json.JsonBuilder;
+import org.apache.jena.atlas.json.JsonValue;
+import org.apache.jena.atlas.lib.FileOps;
+import org.apache.jena.atlas.lib.InternalErrorException;
+import org.apache.jena.atlas.logging.FmtLog;
+import org.apache.jena.atlas.web.ContentType;
+import org.apache.jena.datatypes.xsd.XSDDatatype;
+import org.apache.jena.fuseki.build.DatasetDescriptionMap;
+import org.apache.jena.fuseki.build.FusekiConfig;
+import org.apache.jena.fuseki.ctl.ActionContainerItem;
+import org.apache.jena.fuseki.ctl.JsonDescription;
+import org.apache.jena.fuseki.server.DataAccessPoint;
+import org.apache.jena.fuseki.server.DataService;
+import org.apache.jena.fuseki.server.FusekiVocab;
+import org.apache.jena.fuseki.server.ServerConst;
+import org.apache.jena.fuseki.servlets.ActionLib;
+import org.apache.jena.fuseki.servlets.HttpAction;
+import org.apache.jena.fuseki.servlets.ServletOps;
+import org.apache.jena.fuseki.system.DataUploader;
+import org.apache.jena.fuseki.system.FusekiNetLib;
+import org.apache.jena.graph.Node;
+import org.apache.jena.rdf.model.*;
+import org.apache.jena.riot.*;
+import org.apache.jena.riot.system.StreamRDF;
+import org.apache.jena.riot.system.StreamRDFLib;
+import org.apache.jena.shared.JenaException;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.Quad;
+import org.apache.jena.sparql.core.assembler.AssemblerUtils;
+import org.apache.jena.sparql.util.FmtUtils;
+import org.apache.jena.vocabulary.RDF;
+import org.apache.jena.web.HttpSC;
+
+public class ActionDatasets extends ActionContainerItem {
+
+
+ static private Property pServiceName = FusekiVocab.pServiceName;
+ //static private Property pStatus = FusekiVocab.pStatus;
+
+ private static final String paramDatasetName = "dbName";
+ private static final String paramDatasetType = "dbType";
+ private static final String tDatabaseTDB = "tdb";
+ private static final String tDatabaseTDB2 = "tdb2";
+ private static final String tDatabaseMem = "mem";
+
+ public ActionDatasets() { super(); }
+
+ @Override
+ public void validate(HttpAction action) { }
+
+ // ---- GET : return details of dataset or datasets.
+ @Override
+ protected JsonValue execGetContainer(HttpAction action) {
+ action.log.info(format("[%d] GET datasets", action.id));
+ JsonBuilder builder = new JsonBuilder();
+ builder.startObject("D");
+ builder.key(ServerConst.datasets);
+ JsonDescription.arrayDatasets(builder, action.getDataAccessPointRegistry());
+ builder.finishObject("D");
+ return builder.build();
+ }
+
+ @Override
+ protected JsonValue execGetItem(HttpAction action) {
+ String item = getItemDatasetName(action);
+ action.log.info(format("[%d] GET dataset %s", action.id, item));
+ JsonBuilder builder = new JsonBuilder();
+ DataAccessPoint dsDesc = getItemDataAccessPoint(action, item);
+ if ( dsDesc == null )
+ ServletOps.errorNotFound("Not found: dataset "+item);
+ JsonDescription.describe(builder, dsDesc);
+ return builder.build();
+ }
+
+ // ---- POST
+
+ @Override
+ protected JsonValue execPostContainer(HttpAction action) {
+ UUID uuid = UUID.randomUUID();
+
+ ContentType ct = ActionLib.getContentType(action);
+
+ boolean hasParams = action.getRequestParameterNames().hasMoreElements();
+
+ if ( ct == null && ! hasParams )
+ ServletOps.errorBadRequest("Bad request - Content-Type or both parameters dbName and dbType required");
+
+ boolean succeeded = false;
+ String systemFileCopy = null;
+ String configFile = null;
+
+ DatasetDescriptionMap registry = new DatasetDescriptionMap();
+
+ synchronized (FusekiAdmin.systemLock) {
+ try {
+ // Where to build the templated service/database.
+ Model descriptionModel = ModelFactory.createDefaultModel();
+ StreamRDF dest = StreamRDFLib.graph(descriptionModel.getGraph());
+
+ if ( hasParams || WebContent.isHtmlForm(ct) )
+ assemblerFromForm(action, dest);
+ else if ( WebContent.isMultiPartForm(ct) )
+ assemblerFromUpload(action, dest);
+ else
+ assemblerFromBody(action, dest);
+
+ // ----
+ // Keep a persistent copy immediately. This is not used for
+ // anything other than being "for the record".
+ systemFileCopy = FusekiApp.dirSystemFileArea.resolve(uuid.toString()).toString();
+ try ( OutputStream outCopy = IO.openOutputFile(systemFileCopy) ) {
+ RDFDataMgr.write(outCopy, descriptionModel, Lang.TURTLE);
+ }
+
+ // ----
+ // Add the dataset and graph wiring for assemblers
+ Model model = ModelFactory.createDefaultModel();
+ model.add(descriptionModel);
+ // See AssemblerUtils.readAssemblerFile(String filename)
+ AssemblerUtils.addRegistered(model);
+
+ // ----
+ // Process configuration.
+
+ // Returns the "service fu:name NAME" statement
+ Statement stmt = findService(model);
+
+ Resource subject = stmt.getSubject();
+ Literal object = stmt.getObject().asLiteral();
+
+ if ( object.getDatatype() != null && ! object.getDatatype().equals(XSDDatatype.XSDstring) )
+ action.log.warn(format("[%d] Service name '%s' is not a string", action.id, FmtUtils.stringForRDFNode(object)));
+
+ String datasetPath;
+ { // Check the name provided.
+ String datasetName = object.getLexicalForm();
+ // This duplicates the code FusekiBuilder.buildDataAccessPoint to give better error messages and HTTP status code."
+
+ // ---- Check and canonicalize name.
+ if ( datasetName.isEmpty() )
+ ServletOps.error(HttpSC.BAD_REQUEST_400, "Empty dataset name");
+ if ( StringUtils.isBlank(datasetName) )
+ ServletOps.error(HttpSC.BAD_REQUEST_400, format("Whitespace dataset name: '%s'", datasetName));
+ if ( datasetName.contains(" ") )
+ ServletOps.error(HttpSC.BAD_REQUEST_400, format("Bad dataset name (contains spaces) '%s'",datasetName));
+ if ( datasetName.equals("/") )
+ ServletOps.error(HttpSC.BAD_REQUEST_400, format("Bad dataset name '%s'",datasetName));
+ datasetPath = DataAccessPoint.canonical(datasetName);
+ // ---- Check whether it already exists
+ if ( action.getDataAccessPointRegistry().isRegistered(datasetPath) )
+ ServletOps.error(HttpSC.CONFLICT_409, "Name already registered "+datasetPath);
+ }
+
+ action.log.info(format("[%d] Create database : name = %s", action.id, datasetPath));
+
+ configFile = FusekiApp.generateConfigurationFilename(datasetPath);
+ List<String> existing = FusekiApp.existingConfigurationFile(datasetPath);
+ if ( ! existing.isEmpty() )
+ ServletOps.error(HttpSC.CONFLICT_409, "Configuration file for '"+datasetPath+"' already exists");
+
+ // Write to configuration directory.
+ try ( OutputStream outCopy = IO.openOutputFile(configFile) ) {
+ RDFDataMgr.write(outCopy, descriptionModel, Lang.TURTLE);
+ }
+
+ // Need to be in Resource space at this point.
+ DataAccessPoint dataAccessPoint = FusekiConfig.buildDataAccessPoint(subject.getModel().getGraph(), subject.asNode(), registry);
+ if ( dataAccessPoint == null ) {
+ FmtLog.error(action.log, "Failed to build DataAccessPoint: datasetPath = %s; DataAccessPoint name = %s", datasetPath, dataAccessPoint);
+ ServletOps.errorBadRequest("Failed to build DataAccessPoint");
+ return null;
+ }
+ dataAccessPoint.getDataService().setEndpointProcessors(action.getOperationRegistry());
+ dataAccessPoint.getDataService().goActive();
+ if ( ! datasetPath.equals(dataAccessPoint.getName()) )
+ FmtLog.warn(action.log, "Inconsistent names: datasetPath = %s; DataAccessPoint name = %s", datasetPath, dataAccessPoint);
+ succeeded = true;
+
+ action.getDataAccessPointRegistry().register(dataAccessPoint);
+ action.setResponseContentType(WebContent.contentTypeTextPlain);
+ ServletOps.success(action);
+ } catch (IOException ex) { IO.exception(ex); }
+ finally {
+ if ( ! succeeded ) {
+ if ( systemFileCopy != null ) FileOps.deleteSilent(systemFileCopy);
+ if ( configFile != null ) FileOps.deleteSilent(configFile);
+ }
+ }
+ return null;
+ }
+ }
+
+ /** Find the service resource. There must be only one in the configuration. */
+ private Statement findService(Model model) {
+ // Try to find by unique pServiceName (max backwards compatibility)
+ // then try to find by rdf:type fuseki:Service.
+
+ Statement stmt = getOne(model, null, pServiceName, null);
+ // null means 0 or many, not one.
+
+ if ( stmt == null ) {
+ // This calculates { ?x rdf:type fu:Service ; ?x fu:name ?name }
+ // One and only one service.
+ Statement stmt2 = getOne(model, null, RDF.type, FusekiVocab.fusekiService);
+ if ( stmt2 == null ) {
+ int count = model.listStatements(null, RDF.type, FusekiVocab.fusekiService).toList().size();
+ if ( count == 0 )
+ ServletOps.errorBadRequest("No triple rdf:type fuseki:Service found");
+ else
+ ServletOps.errorBadRequest("Multiple Fuseki service descriptions");
+ }
+ Statement stmt3 = getOne(model, stmt2.getSubject(), pServiceName, null);
+ if ( stmt3 == null ) {
+ StmtIterator sIter = model.listStatements(stmt2.getSubject(), pServiceName, (RDFNode)null );
+ if ( ! sIter.hasNext() )
+ ServletOps.errorBadRequest("No name given in description of Fuseki service");
+ sIter.next();
+ if ( sIter.hasNext() )
+ ServletOps.errorBadRequest("Multiple names given in description of Fuseki service");
+ throw new InternalErrorException("Inconsistent: getOne didn't fail the second time");
+ }
+ stmt = stmt3;
+ }
+
+ if ( ! stmt.getObject().isLiteral() )
+ ServletOps.errorBadRequest("Found "+FmtUtils.stringForRDFNode(stmt.getObject())+" : Service names are strings, then used to build the external URI");
+
+ return stmt;
+ }
+
+ @Override
+ protected JsonValue execPostItem(HttpAction action) {
+ String name = getItemDatasetName(action);
+ if ( name == null )
+ name = "''";
+ action.log.info(format("[%d] POST dataset %s", action.id, name));
+
+ // Not in the action - this not an ActionService.
+ DataAccessPoint dap = getItemDataAccessPoint(action, name);
+
+ if ( dap == null )
+ ServletOps.errorNotFound("Not found: dataset "+name);
+
+ DataService dSrv = dap.getDataService();
+ if ( dSrv == null )
+ // If not set explicitly, take from DataAccessPoint
+ dSrv = action.getDataAccessPoint().getDataService();
+
+ String s = action.getRequestParameter("state");
+ if ( s == null || s.isEmpty() )
+ ServletOps.errorBadRequest("No state change given");
+ return null;
+ }
+
+ // ---- DELETE
+
+ @Override
+ protected void execDeleteItem(HttpAction action) {
+ // Does not exist?
+ String name = getItemDatasetName(action);
+ if ( name == null )
+ name = "";
+ action.log.info(format("[%d] DELETE dataset=%s", action.id, name));
+
+ if ( ! action.getDataAccessPointRegistry().isRegistered(name) )
+ ServletOps.errorNotFound("No such dataset registered: "+name);
+
+ boolean succeeded = false;
+
+ synchronized(FusekiAdmin.systemLock ) {
+ try {
+ // Here, go offline.
+ // Need to reference count operations when they drop to zero
+ // or a timer goes off, we delete the dataset.
+
+ // Redo check inside transaction.
+ DataAccessPoint ref = action.getDataAccessPointRegistry().get(name);
+ if ( ref == null )
+ ServletOps.errorNotFound("No such dataset registered: "+name);
+
+ // Get a reference before removing.
+ DataService dataService = ref.getDataService();
+ // ---- Make it invisible in this running server.
+ action.getDataAccessPointRegistry().remove(name);
+
+ // Find the configuration.
+ String filename = name.startsWith("/") ? name.substring(1) : name;
+ List<String> configurationFiles = FusekiApp.existingConfigurationFile(filename);
+
+ if ( configurationFiles.isEmpty() ) {
+ // ---- Unmanaged
+ action.log.warn(format("[%d] Can't delete database configuration - not a managed database", action.id, name));
+// ServletOps.errorOccurred(format("Can't delete database - not a managed configuration", name));
+ succeeded = true;
+ ServletOps.success(action);
+ return;
+ }
+
+ if ( configurationFiles.size() > 1 ) {
+ // -- This should not happen.
+ action.log.warn(format("[%d] There are %d configuration files, not one.", action.id, configurationFiles.size()));
+ ServletOps.errorOccurred(format("There are %d configuration files, not one. Delete not performed; manual clean up of the filesystem needed.",
+ configurationFiles.size()));
+ return;
+ }
+
+ // ---- Remove managed database.
+ String cfgPathname = configurationFiles.get(0);
+
+ // Delete configuration file.
+ // Once deleted, server restart will not have the database.
+ FileOps.deleteSilent(cfgPathname);
+
+ // Delete the database for real only when it is in the server "run/databases"
+ // area. Don't delete databases that reside elsewhere. We do delete the
+ // configuration file, so the databases will not be associated with the server
+ // anymore.
+
+ boolean isTDB1 = org.apache.jena.tdb1.sys.TDBInternal.isTDB1(dataService.getDataset());
+ boolean isTDB2 = org.apache.jena.tdb2.sys.TDBInternal.isTDB2(dataService.getDataset());
+
+ // TODO This occasionally fails in tests due to outstanding transactions.
+ try {
+ dataService.shutdown();
+ } catch (JenaException ex) {
+ return;
+ }
+ // JENA-1481: Really delete files.
+ if ( ( isTDB1 || isTDB2 ) ) {
+ // Delete databases created by the UI, or the admin operation, which are
+ // in predictable, unshared location on disk.
+ // There may not be any database files, the in-memory case.
+ Path pDatabase = FusekiApp.dirDatabases.resolve(filename);
+ if ( Files.exists(pDatabase)) {
+ try {
+ if ( Files.isSymbolicLink(pDatabase)) {
+ action.log.info(format("[%d] Database is a symbolic link, not removing files", action.id, pDatabase));
+ } else {
+ IO.deleteAll(pDatabase);
+ action.log.info(format("[%d] Deleted database files %s", action.id, pDatabase));
+ }
+ } catch (RuntimeIOException ex) {
+ action.log.error(format("[%d] Error while deleting database files %s: %s", action.id, pDatabase, ex.getMessage()), ex);
+ // But we have managed to remove it from the running server, and removed its configuration, so declare victory.
+ }
+ }
+ }
+
+ succeeded = true;
+ ServletOps.success(action);
+ } finally {
+ // No clearup needed
+ }
+ }
+ }
+
+ private static void assemblerFromBody(HttpAction action, StreamRDF dest) {
+ bodyAsGraph(action, dest);
+ }
+
+ private static Map<String, String> dbTypeToTemplate = new HashMap<>();
+ static {
+ dbTypeToTemplate.put(tDatabaseTDB, Template.templateTDB1_FN);
+ dbTypeToTemplate.put(tDatabaseTDB2, Template.templateTDB2_FN);
+ dbTypeToTemplate.put(tDatabaseMem, Template.templateTIM_MemFN);
+ }
+
+ private static void assemblerFromForm(HttpAction action, StreamRDF dest) {
+ String x = action.getRequestQueryString();
+ String dbType = action.getRequestParameter(paramDatasetType);
+ String dbName = action.getRequestParameter(paramDatasetName);
+ if ( StringUtils.isBlank(dbType) || StringUtils.isBlank(dbName) )
+ ServletOps.errorBadRequest("Received HTML form. Both parameters 'dbName' and 'dbType' required");
+
+ Map<String, String> params = new HashMap<>();
+
+ if ( dbName.startsWith("/") )
+ params.put(Template.NAME, dbName.substring(1));
+ else
+ params.put(Template.NAME, dbName);
+ FusekiApp.addGlobals(params);
+
+ //action.log.info(format("[%d] Create database : name = %s, type = %s", action.id, dbName, dbType ));
+
+ String template = dbTypeToTemplate.get(dbType.toLowerCase(Locale.ROOT));
+ if ( template == null ) {
+ List<String> keys = new ArrayList<>(dbTypeToTemplate.keySet());
+ Collections.sort(keys);
+ ServletOps.errorBadRequest(format("dbType can be only one of %s", keys));
+ }
+
+ String instance = TemplateFunctions.templateFile(template, params, Lang.TTL);
+ RDFParser.create().source(new StringReader(instance)).base("http://base/").lang(Lang.TTL).parse(dest);
+ }
+
+ private static void assemblerFromUpload(HttpAction action, StreamRDF dest) {
+ DataUploader.incomingData(action, dest);
+ }
+
+ // [ADMIN]
+// // Persistent state change.
+// private static void setDatasetState(String name, Resource newState) {
+// boolean committed = false;
+// system.begin(ReadWrite.WRITE);
+// try {
+// String dbName = name;
+// if ( dbName.startsWith("/") )
+// dbName = dbName.substring(1);
+//
+// String update = StrUtils.strjoinNL
+// (PREFIXES,
+// "DELETE { GRAPH ?g { ?s fu:status ?state } }",
+// "INSERT { GRAPH ?g { ?s fu:status "+FmtUtils.stringForRDFNode(newState)+" } }",
+// "WHERE {",
+// " GRAPH ?g { ?s fu:name '"+dbName+"'; ",
+// " fu:status ?state .",
+// " }",
+// "}"
+// );
+// UpdateRequest req = UpdateFactory.create(update);
+// UpdateAction.execute(req, system);
+// system.commit();
+// committed = true;
+// } finally {
+// if ( ! committed ) system.abort();
+// system.end();
+// }
+// }
+
+ // ---- Auxiliary functions
+
+ private static Quad getOne(DatasetGraph dsg, Node g, Node s, Node p, Node o) {
+ Iterator<Quad> iter = dsg.findNG(g, s, p, o);
+ if ( ! iter.hasNext() )
+ return null;
+ Quad q = iter.next();
+ if ( iter.hasNext() )
+ return null;
+ return q;
+ }
+
+ private static Statement getOne(Model m, Resource s, Property p, RDFNode o) {
+ StmtIterator iter = m.listStatements(s, p, o);
+ if ( ! iter.hasNext() )
+ return null;
+ Statement stmt = iter.next();
+ if ( iter.hasNext() )
+ return null;
+ return stmt;
+ }
+
+ // TODO Merge with Upload.incomingData
+
+ private static void bodyAsGraph(HttpAction action, StreamRDF dest) {
+ HttpServletRequest request = action.getRequest();
+ String base = ActionLib.wholeRequestURL(request);
+ ContentType ct = FusekiNetLib.getContentType(request);
+ Lang lang = RDFLanguages.contentTypeToLang(ct.getContentTypeStr());
+ if ( lang == null ) {
+ ServletOps.errorBadRequest("Unknown content type for triples: " + ct);
+ return;
+ }
+ dest.prefix("root", base+"#");
+ ActionLib.parseOrError(action, dest, lang, base);
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionLogs.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionLogs.java
new file mode 100644
index 0000000000..3b91abf0c4
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionLogs.java
@@ -0,0 +1,55 @@
+/**
+ * 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 org.apache.jena.riot.WebContent.charsetUTF8;
+import static org.apache.jena.riot.WebContent.contentTypeTextPlain;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletOutputStream;
+import org.apache.jena.fuseki.ctl.ActionCtl;
+import org.apache.jena.fuseki.servlets.HttpAction;
+import org.apache.jena.fuseki.servlets.ServletOps;
+
+public class ActionLogs extends ActionCtl
+{
+ public ActionLogs() { super(); }
+
+ @Override
+ public void validate(HttpAction action) {}
+
+ @Override
+ public void execGet(HttpAction action) {
+ executeLifecycle(action);
+ }
+
+ @Override
+ public void execute(HttpAction action) {
+ try {
+ ServletOutputStream out = action.getResponseOutputStream();
+ action.setResponseContentType(contentTypeTextPlain);
+ action.setResponseCharacterEncoding(charsetUTF8);
+ out.println("Not implemented yet");
+ out.println();
+ out.flush();
+ ServletOps.success(action);
+ } catch (IOException ex) { ServletOps.errorOccurred(ex); }
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionReload.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionReload.java
new file mode 100644
index 0000000000..b85fddbbd9
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ActionReload.java
@@ -0,0 +1,66 @@
+/*
+ * 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 org.apache.jena.atlas.logging.FmtLog;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.ctl.ActionCtl;
+import org.apache.jena.fuseki.main.FusekiLib;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.servlets.HttpAction;
+import org.apache.jena.fuseki.servlets.ServletOps;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.riot.RDFParser;
+import org.apache.jena.riot.web.HttpNames;
+
+/**
+ * Administration action to reload the server's dataset configuration.
+ * <p>
+ * This is done by reading the configuration file which may have changed since server startup.
+ * <p>
+ * If the server does not have a configuration file (e.g. command line or programmatic configuration)
+ */
+public class ActionReload extends ActionCtl {
+
+ @Override
+ public void validate(HttpAction action) {
+ if ( action.getRequestMethod() != HttpNames.METHOD_POST ) {
+ ServletOps.errorMethodNotAllowed(action.getRequestMethod());
+ }
+ }
+
+ @Override
+ public void execute(HttpAction action) {
+ FusekiServer server = FusekiServer.get(action.getRequest().getServletContext());
+ if ( server == null ) {
+ ServletOps.errorOccurred("Failed to find the server for this action");
+ return;
+ }
+
+ String configFilename = server.getConfigFilename();
+ if ( configFilename == null ) {
+ FmtLog.warn(Fuseki.serverLog, "[%d] Server does not have an associated configuration file", action.id);
+ ServletOps.errorBadRequest("Server does not have an associated configuration file");
+ return;
+ }
+ Model model = RDFParser.source(configFilename).toModel();
+ FmtLog.info(Fuseki.serverLog, "[%d] Reload configuration", action.id);
+ FusekiLib.reload(server, model);
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/Backup.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/Backup.java
new file mode 100644
index 0000000000..70ea5f5156
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/Backup.java
@@ -0,0 +1,122 @@
+/**
+ * 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 java.io.BufferedOutputStream;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.zip.GZIPOutputStream;
+
+import org.apache.jena.atlas.io.IOX;
+import org.apache.jena.atlas.lib.DateTimeUtils;
+import org.apache.jena.atlas.logging.FmtLog;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.FusekiException;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.Transactional;
+import org.apache.jena.sparql.core.TransactionalNull;
+import org.apache.jena.system.Txn;
+
+/** Perform a backup */
+public class Backup
+{
+ public static String chooseFileName(String dsName) {
+ // Without the "/" - i.e. a relative name.
+ String ds = dsName;
+ if ( ds.startsWith("/") )
+ ds = ds.substring(1);
+ if ( ds.contains("/") ) {
+ Fuseki.adminLog.warn("Dataset name: weird format: "+dsName);
+ // Some kind of fixup
+ ds = ds.replace("/", "_");
+ }
+
+ String timestamp = DateTimeUtils.nowAsString("yyyy-MM-dd_HH-mm-ss");
+ String filename = ds + "_" + timestamp;
+ filename = FusekiApp.dirBackups.resolve(filename).toString();
+ return filename;
+ }
+
+ // Record of all backups so we don't attempt to backup the
+ // same dataset multiple times at the same time.
+ private static Set<DatasetGraph> activeBackups = Collections.newSetFromMap(new ConcurrentHashMap<>());
+
+ /**
+ * Perform a backup.
+ * <p>
+ * A backup is a dump of the dataset in compressed N-Quads, done inside a transaction.
+ */
+ public static void backup(Transactional transactional, DatasetGraph dsg, String backupfile) {
+ if ( transactional == null )
+ transactional = new TransactionalNull();
+ Txn.executeRead(transactional, ()->backup(dsg, backupfile));
+ }
+
+ // This seems to achieve about the same as "gzip -6"
+ // It's not too expensive in elapsed time but it's not
+ // zero cost. GZip, large buffer.
+ private static final boolean USE_GZIP = true;
+
+ /**
+ * Perform a backup.
+ *
+ * @see #backup(Transactional, DatasetGraph, String)
+ */
+
+ private static void backup(DatasetGraph dsg, String backupfile) {
+ if (dsg == null) {
+ throw new FusekiException("No dataset provided to backup");
+ }
+
+ // Per backup source lock.
+ synchronized(activeBackups) {
+ // Atomically check-and-set
+ if ( activeBackups.contains(dsg) )
+ FmtLog.warn(Fuseki.serverLog, "Backup already in progress");
+ activeBackups.add(dsg);
+ }
+
+ if ( !backupfile.endsWith(".nq") )
+ backupfile = backupfile + ".nq";
+
+ if ( USE_GZIP )
+ backupfile = backupfile + ".gz";
+
+ try {
+ IOX.safeWrite(Path.of(backupfile), outfile -> {
+ OutputStream out = outfile;
+ if ( USE_GZIP )
+ out = new GZIPOutputStream(outfile, 8 * 1024);
+ try (OutputStream out2 = new BufferedOutputStream(out)) {
+ RDFDataMgr.write(out2, dsg, Lang.NQUADS);
+ }
+ });
+ } finally {
+ // Remove lock.
+ synchronized(activeBackups) {
+ activeBackups.remove(dsg);
+ }
+ }
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiAdmin.java
similarity index 63%
copy from jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
copy to jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiAdmin.java
index 71c6c31d0e..2481c4f699 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiAdmin.java
@@ -1,4 +1,4 @@
-/**
+/*
* 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
@@ -16,24 +16,8 @@
* limitations under the License.
*/
-package org.apache.jena.fuseki.server;
+package org.apache.jena.fuseki.mgt;
-import java.util.concurrent.atomic.AtomicLong;
-
-/** A statistics counter */
-public class Counter
-{
- private AtomicLong counter = new AtomicLong(0);
-
- public Counter() {}
-
- public void inc() { counter.incrementAndGet(); }
- public void dec() { counter.decrementAndGet(); }
- public long value() { return counter.get(); }
-
- @Override
- public String toString() {
- return counter.toString();
- }
+public class FusekiAdmin {
+ public final static Object systemLock = new Object();
}
-
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiApp.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiApp.java
new file mode 100644
index 0000000000..d6cd0dc570
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiApp.java
@@ -0,0 +1,425 @@
+/*
+ * 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 static org.apache.jena.atlas.lib.Lib.getenv;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+
+import org.apache.jena.atlas.RuntimeIOException;
+import org.apache.jena.atlas.io.IOX;
+import org.apache.jena.atlas.lib.FileOps;
+import org.apache.jena.atlas.lib.InternalErrorException;
+import org.apache.jena.atlas.lib.Lib;
+import org.apache.jena.atlas.logging.FmtLog;
+import org.apache.jena.cmd.CmdException;
+import org.apache.jena.dboe.sys.Names;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.FusekiConfigException;
+import org.apache.jena.fuseki.build.DatasetDescriptionMap;
+import org.apache.jena.fuseki.build.FusekiConfig;
+import org.apache.jena.fuseki.server.DataAccessPoint;
+import org.apache.jena.fuseki.server.DataService;
+import org.apache.jena.fuseki.server.FusekiVocabG;
+import org.apache.jena.fuseki.servlets.HttpAction;
+import org.apache.jena.fuseki.servlets.ServletOps;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.rdf.model.*;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFLanguages;
+import org.apache.jena.riot.RDFParser;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.assembler.AssemblerUtils;
+import org.apache.jena.system.G;
+
+public class FusekiApp {
+ // Relative names of directories in the FUSEKI_BASE area.
+ public static final String databasesLocationBase = "databases";
+ // Place to put Lucene text and spatial indexes.
+ //private static final String databaseIndexesDir = "indexes";
+
+ public static final String backupDirNameBase = "backups";
+ public static final String configDirNameBase = "configuration";
+ public static final String logsNameBase = "logs";
+ public static final String systemFileAreaBase = "system_files";
+ public static final String templatesNameBase = "templates";
+ public static final String DFT_SHIRO_INI = "shiro.ini";
+ public static final String DFT_CONFIG = "config.ttl";
+
+ private static int BaseFusekiAutoModuleLevel = 500;
+ public static int levelFModAdmin = BaseFusekiAutoModuleLevel;
+ public static int levelFModUI = BaseFusekiAutoModuleLevel+10;
+ public static int levelFModShiro = BaseFusekiAutoModuleLevel+20;
+
+
+ /** Directory for TDB databases - this is known to the assembler templates */
+ public static Path dirDatabases = null;
+
+ /** Directory for writing backups */
+ public static Path dirBackups = null;
+
+ /** Directory for assembler files */
+ public static Path dirConfiguration = null;
+
+ /** Directory for assembler files */
+ public static Path dirLogs = null;
+
+// /** Directory for system database */
+// public static Path dirSystemDatabase = null;
+
+ /** Directory for files uploaded (e.g upload assembler descriptions); not data uploads. */
+ public static Path dirSystemFileArea = null;
+
+ /** Directory for assembler files */
+ public static Path dirTemplates = null;
+
+ private static boolean initialized = false;
+ // Marks the end of successful initialization.
+ /*package*/static boolean serverInitialized = false;
+
+
+
+// /**
+// * Root of the Fuseki installation for fixed files.
+// * This may be null (e.g. running inside a web application container)
+// */
+// public static Path FUSEKI_HOME = null;
+
+ /**
+ * Root of the varying files in this deployment. Often $PWD/run.
+ * This must be writable.
+ */
+ public static Path FUSEKI_BASE = set_FUSEKI_BASE();
+
+ public static String envFusekiBase = "FUSEKI_BASE";
+ public static String envFusekiShiro = "FUSEKI_SHIRO";
+
+ private static Path set_FUSEKI_BASE() {
+ // Does not guarantee existence
+ Path setting = null;
+ if ( FUSEKI_BASE == null )
+ setting = calc_FUSEKI_BASE();
+ setting = setting.toAbsolutePath();
+ return setting;
+ }
+
+ private static Path calc_FUSEKI_BASE() {
+ String valueFusekiBase = getenv("FUSEKI_BASE");
+ if ( valueFusekiBase == null )
+ valueFusekiBase = dftFusekiBase;
+ return Path.of(valueFusekiBase);
+ }
+
+ // Default - "run" in the current directory.
+ public static final String dftFusekiBase = "run";
+
+ static void setEnvironment() {
+ if ( FUSEKI_BASE == null )
+ FUSEKI_BASE = set_FUSEKI_BASE();
+
+ FmtLog.info(Fuseki.configLog, "FUSEKI_BASE=%s", FUSEKI_BASE);
+ if ( ! Files.exists(FUSEKI_BASE) ) {
+ try {
+ Files.createDirectories(FUSEKI_BASE);
+ } catch (IOException e) {
+ throw new FusekiConfigException("Failed to create FUSEKI_BASE: "+FUSEKI_BASE);
+ }
+ }
+ // Further checks in ensureBaseArea
+ }
+
+ public static Path setup() {
+ // Command line arguments "--base" ...
+ setEnvironment();
+ // Format the BASE area.
+ FusekiApp.ensureBaseArea(FUSEKI_BASE);
+ return FUSEKI_BASE;
+ }
+
+ /**
+ * Create directories if found to be missing.
+ */
+ public static void ensureBaseArea(Path FUSEKI_BASE) {
+ if ( Files.exists(FUSEKI_BASE) ) {
+ if ( ! Files.isDirectory(FUSEKI_BASE) )
+ throw new FusekiConfigException("FUSEKI_BASE is not a directory: "+FUSEKI_BASE);
+ if ( ! Files.isWritable(FUSEKI_BASE) )
+ throw new FusekiConfigException("FUSEKI_BASE is not writable: "+FUSEKI_BASE);
+ } else {
+ ensureDir(FUSEKI_BASE);
+ }
+
+ // Ensure FUSEKI_BASE has the assumed directories.
+ dirTemplates = writeableDirectory(FUSEKI_BASE, templatesNameBase);
+ dirDatabases = writeableDirectory(FUSEKI_BASE, databasesLocationBase);
+ dirBackups = writeableDirectory(FUSEKI_BASE, backupDirNameBase);
+ dirConfiguration = writeableDirectory(FUSEKI_BASE, configDirNameBase);
+ dirLogs = writeableDirectory(FUSEKI_BASE, logsNameBase);
+ dirSystemFileArea = writeableDirectory(FUSEKI_BASE, systemFileAreaBase);
+
+ // ---- Initialize with files.
+
+// // Copy missing files into FUSEKI_BASE
+ // Interacts with FMod_Shiro.
+ if ( Lib.getenv(FusekiApp.envFusekiShiro) == null ) {
+ copyFileIfMissing(null, DFT_SHIRO_INI, FUSEKI_BASE);
+ System.setProperty(FusekiApp.envFusekiShiro, FUSEKI_BASE.resolve(DFT_SHIRO_INI).toString());
+ }
+
+ copyFileIfMissing(null, DFT_CONFIG, FUSEKI_BASE);
+ for ( String n : Template.templateNames ) {
+ copyFileIfMissing(null, n, FUSEKI_BASE);
+ }
+
+ serverInitialized = true;
+ }
+
+ /** Copy a file from src to dst under name fn.
+ * If src is null, try as a classpath resource
+ * @param src Source directory, or null meaning use java resource.
+ * @param fn File name, a relative path.
+ * @param dst Destination directory.
+ *
+ */
+ private static void copyFileIfMissing(Path src, String fn, Path dst) {
+ // fn may be a path.
+ Path dstFile = dst.resolve(fn);
+ if ( Files.exists(dstFile) )
+ return;
+ if ( src != null ) {
+ Path srcFile = src.resolve(fn);
+ if ( ! Files.exists(dstFile) )
+ throw new FusekiConfigException("File not found: "+srcFile);
+ try {
+ IOX.safeWrite(dstFile, output->Files.copy(srcFile, output));
+ } catch (RuntimeIOException e) {
+ throw new FusekiConfigException("Failed to copy file "+srcFile+" to "+dstFile, e);
+ }
+ } else {
+ copyFileFromResource(fn, dstFile);
+ }
+ }
+
+ private static void copyFileFromResource(String fn, Path dstFile) {
+ try {
+ // Get from the file from area "org/apache/jena/fuseki/server"
+ String absName = "org/apache/jena/fuseki/server/"+fn;
+ InputStream input = FusekiApp.class
+ // Else prepends classname as path
+ .getClassLoader()
+ .getResourceAsStream(absName);
+
+ if ( input == null )
+ throw new FusekiConfigException("Failed to find resource '"+absName+"'");
+ IOX.safeWrite(dstFile, (output)-> input.transferTo(output));
+ }
+ catch (RuntimeException e) {
+ throw new FusekiConfigException("Failed to copy "+fn+" to "+dstFile, e);
+ }
+ }
+
+ private static List<DataAccessPoint> processServerConfigFile(String configFilename) {
+ if ( ! FileOps.exists(configFilename) ) {
+ Fuseki.configLog.warn("Configuration file '" + configFilename+"' does not exist");
+ return Collections.emptyList();
+ }
+ //Fuseki.configLog.info("Configuration file: " + configFilename);
+ Model model = AssemblerUtils.readAssemblerFile(configFilename);
+ if ( model.size() == 0 )
+ return Collections.emptyList();
+ List<DataAccessPoint> x = FusekiConfig.processServerConfiguration(model, Fuseki.getContext());
+ return x;
+ }
+
+ private static DataAccessPoint configFromTemplate(String templateFile, String datasetPath,
+ boolean allowUpdate, Map<String, String> params) {
+ // ---- Setup
+ if ( params == null ) {
+ params = new HashMap<>();
+ params.put(Template.NAME, datasetPath);
+ } else {
+ if ( ! params.containsKey(Template.NAME) ) {
+ Fuseki.configLog.warn("No NAME found in template parameters (added)");
+ params.put(Template.NAME, datasetPath);
+ }
+ }
+ //-- Logging
+ Fuseki.configLog.info("Template file: " + templateFile);
+ String dir = params.get(Template.DIR);
+ if ( dir != null ) {
+ if ( ! Objects.equals(dir, Names.memName) && !FileOps.exists(dir) )
+ throw new CmdException("Directory not found: " + dir);
+ }
+ //-- Logging
+
+ datasetPath = DataAccessPoint.canonical(datasetPath);
+
+ // DRY -- ActionDatasets (and others?)
+ addGlobals(params);
+
+ String str = TemplateFunctions.templateFile(templateFile, params, Lang.TTL);
+ Lang lang = RDFLanguages.filenameToLang(str, Lang.TTL);
+
+ Graph configuration = RDFParser.fromString(str, lang).toGraph();
+ List<Node> x = G.listPO(configuration, FusekiVocabG.pServiceName, null);
+ if ( x.isEmpty() )
+ ServletOps.errorBadRequest("No name given in description of Fuseki service");
+ if ( x.size() > 1 )
+ ServletOps.errorBadRequest("Multiple names given in description of Fuseki service");
+ Node fusekiService = x.get(0);
+ DatasetDescriptionMap registry = new DatasetDescriptionMap();
+ DataAccessPoint dap = FusekiConfig.buildDataAccessPoint(configuration, fusekiService, registry);
+ return dap;
+ }
+
+ public static void addGlobals(Map<String, String> params) {
+ if ( params == null ) {
+ Fuseki.configLog.warn("FusekiApp.addGlobals : params is null", new Throwable());
+ return;
+ }
+
+ if ( ! params.containsKey("FUSEKI_BASE") )
+ params.put("FUSEKI_BASE", pathStringOrElse(FUSEKI_BASE, "unset"));
+// if ( ! params.containsKey("FUSEKI_HOME") )
+// params.put("FUSEKI_HOME", pathStringOrElse(FusekiAppEnv.FUSEKI_HOME, "unset"));
+ }
+
+ private static String pathStringOrElse(Path path, String dft) {
+ if ( path == null )
+ return dft;
+ return path.toString();
+ }
+
+ // DRY -- ActionDatasets (and others?)
+ private static Statement getOne(Model m, Resource s, Property p, RDFNode o) {
+ StmtIterator iter = m.listStatements(s, p, o);
+ if ( ! iter.hasNext() )
+ return null;
+ Statement stmt = iter.next();
+ if ( iter.hasNext() )
+ return null;
+ return stmt;
+ }
+
+ private static DataAccessPoint datasetDefaultConfiguration(String name, DatasetGraph dsg, boolean allowUpdate) {
+ name = DataAccessPoint.canonical(name);
+ DataService ds = FusekiConfig.buildDataServiceStd(dsg, allowUpdate);
+ DataAccessPoint dap = new DataAccessPoint(name, ds);
+ return dap;
+ }
+
+ // ---- Helpers
+
+ /** Ensure a directory exists, creating it if necessary.
+ */
+ private static void ensureDir(Path directory) {
+ File dir = directory.toFile();
+ if ( ! dir.exists() ) {
+ boolean b = dir.mkdirs();
+ if ( ! b )
+ throw new FusekiConfigException("Failed to create directory: "+directory);
+ }
+ else if ( ! dir.isDirectory())
+ throw new FusekiConfigException("Not a directory: "+directory);
+ }
+
+ private static void mustExist(Path directory) {
+ File dir = directory.toFile();
+ if ( ! dir.exists() )
+ throw new FusekiConfigException("Does not exist: "+directory);
+ if ( ! dir.isDirectory())
+ throw new FusekiConfigException("Not a directory: "+directory);
+ }
+
+ private static boolean emptyDir(Path dir) {
+ return dir.toFile().list().length <= 2;
+ }
+
+ private static boolean exists(Path directory) {
+ File dir = directory.toFile();
+ return dir.exists();
+ }
+
+ private static Path writeableDirectory(Path root , String relName ) {
+ Path p = makePath(root, relName);
+ ensureDir(p);
+ if ( ! Files.isWritable(p) )
+ throw new FusekiConfigException("Not writable: "+p);
+ return p;
+ }
+
+ private static Path makePath(Path root , String relName ) {
+ Path path = root.resolve(relName);
+ // Must exist
+// try { path = path.toRealPath(); }
+// catch (IOException e) { IO.exception(e); }
+ return path;
+ }
+
+ /**
+ * Dataset set name to configuration file name. Return a configuration file name -
+ * existing one or ".ttl" form if new
+ */
+ public static String datasetNameToConfigurationFile(HttpAction action, String dsName) {
+ List<String> existing = existingConfigurationFile(dsName);
+ if ( ! existing.isEmpty() ) {
+ if ( existing.size() > 1 ) {
+ action.log.warn(format("[%d] Multiple existing configuration files for %s : %s",
+ action.id, dsName, existing));
+ ServletOps.errorBadRequest("Multiple existing configuration files for "+dsName);
+ return null;
+ }
+ return existing.get(0).toString();
+ }
+
+ return generateConfigurationFilename(dsName);
+ }
+
+ /** New configuration file name - absolute filename */
+ public static String generateConfigurationFilename(String dsName) {
+ String filename = dsName;
+ // Without "/"
+ if ( filename.startsWith("/"))
+ filename = filename.substring(1);
+ Path p = FusekiApp.dirConfiguration.resolve(filename+".ttl");
+ return p.toString();
+ }
+
+ /** Return the filenames of all matching files in the configuration directory (absolute paths returned ). */
+ public static List<String> existingConfigurationFile(String baseFilename) {
+ try {
+ List<String> paths = new ArrayList<>();
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(FusekiApp.dirConfiguration, baseFilename+".*") ) {
+ stream.forEach((p)-> paths.add(FusekiApp.dirConfiguration.resolve(p).toString() ));
+ }
+ return paths;
+ } catch (IOException ex) {
+ throw new InternalErrorException("Failed to read configuration directory "+FusekiApp.dirConfiguration);
+ }
+ }
+
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ServerMgtConst.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ServerMgtConst.java
new file mode 100644
index 0000000000..056b161470
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/ServerMgtConst.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+/**
+ * Various constants used in the management API functions and JSON responses in the
+ * webapp/full server.
+ */
+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";
+
+ public static final String uptime = "uptime";
+ public static final String startDT = "startDateTime";
+// public static final String server = "server";
+// public static final String port = "port";
+ public static final String hostname = "hostname";
+ public static final String admin = "admin";
+ public static final String version = "version";
+ public static final String built = "built";
+ public static final String services = "services";
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/Template.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/Template.java
new file mode 100644
index 0000000000..2ef63652b8
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/Template.java
@@ -0,0 +1,68 @@
+/**
+ * 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 java.nio.file.Path;
+
+
+public class Template
+{
+ public static Path getPath(String templateName) {
+ return FusekiApp.FUSEKI_BASE.resolve(templateName);
+ }
+
+ public static final String templateDir = "templates";
+
+ // These are used by the command line start up.
+ public static final String templateServiceFN = templateDir+"/config-service"; // Dummy used by dataset-less service.
+
+ // TDB1 - for backwards compatibility, the files are called "tdb"
+ public static final String templateTDB1_FN = templateDir+"/config-tdb";
+ public static final String templateTDB1_MemFN = templateDir+"/config-tdb-mem";
+ public static final String templateTDB1_DirFN = templateDir+"/config-tdb-dir";
+ public static final String templateTDB1_DirReadFN = templateDir+"/config-tdb-dir-read-only";
+
+ public static final String templateTDB2_FN = templateDir+"/config-tdb2";
+ public static final String templateTDB2_MemFN = templateDir+"/config-tdb2-mem";
+ public static final String templateTDB2_DirFN = templateDir+"/config-tdb2-dir";
+ public static final String templateTDB2_DirReadFN = templateDir+"/config-tdb2-dir-read-only";
+
+
+ public static final String templateTIM_MemFN = templateDir+"/config-mem";
+
+ // Template may be in a resources area of a jar file so you can't do a directory listing.
+ public static final String[] templateNames = {
+ templateTIM_MemFN,
+
+ templateTDB1_FN ,
+ templateTDB1_MemFN ,
+ templateTDB1_DirFN ,
+ //templateTDB1_DirReadFN,
+
+ templateTDB2_FN ,
+ templateTDB2_MemFN ,
+ templateTDB2_DirFN ,
+ //templateTDB2_DirReadFN
+ };
+
+ public static final String NAME = "NAME";
+ public static final String DATA = "DATA";
+ public static final String DIR = "DIR";
+}
+
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/TemplateFunctions.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/TemplateFunctions.java
new file mode 100644
index 0000000000..009cc655ae
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/TemplateFunctions.java
@@ -0,0 +1,86 @@
+/**
+ * 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 java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.util.FileUtils;
+
+public class TemplateFunctions
+{
+ /** Read in a template from a file, substitute for {NAME} and return the string. */
+ public static String templateFile(String templateName, Map<String, String> params, Lang lang) {
+ String templateFilename = Template.getPath(templateName).toString();
+ String template;
+ try { template = FileUtils.readWholeFileAsUTF8(templateFilename); }
+ catch (IOException ex) {
+ Fuseki.serverLog.error("File not found: "+templateFilename);
+ IO.exception(ex); return null;
+ }
+ return templateString(template, params, lang);
+ }
+
+ /** Read a template file, substitute for {NAME} and return the model. */
+ public static String templateResource(String resourceName, Map<String, String> params, Lang lang) {
+ String template;
+ try {
+ InputStream in = TemplateFunctions.class.getClassLoader().getResourceAsStream(resourceName);
+ if ( in == null )
+ Fuseki.serverLog.error("Resource not found: "+resourceName);
+ template = FileUtils.readWholeFileAsUTF8(in);
+ }
+ catch (IOException ex) {
+ Fuseki.serverLog.error("Error reading resource: "+resourceName);
+ IO.exception(ex); return null;
+ }
+ return templateString(template, params, lang);
+ }
+
+ /** Create a template from a String */
+ public static String templateString(String template, Map<String, String> params, Lang lang) {
+ for ( Entry<String, String> e : params.entrySet() ) {
+ // Literal string replacement.
+ // If using .replaceAll, need to use Match.quoteReplacement on the value.
+ String x = e.getValue();
+ String k = "{"+e.getKey()+"}";
+
+ if ( lang != null ) {
+ if ( Lang.TTL.equals(lang) ||
+ Lang.TRIG.equals(lang) ||
+ Lang.NT.equals(lang) ||
+ Lang.NQ.equals(lang) ||
+ Lang.JSONLD.equals(lang) ||
+ Lang.RDFJSON.equals(lang)
+ ) {
+ // Make safe for a RDF language ""-string - especially MS Windows \ path separators.
+ x = x.replace("\\", "\\\\");
+ x = x.replace("\"", "\\\"");
+ }
+ }
+ template = template.replace(k, x);
+ }
+ return template;
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/access/FMod_GraphAccessCtl.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/access/FMod_GraphAccessCtl.java
new file mode 100644
index 0000000000..fc6e74236a
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/access/FMod_GraphAccessCtl.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.fuseki.mod.access;
+
+import org.apache.jena.fuseki.access.DataAccessCtl;
+import org.apache.jena.fuseki.access.VocabSecurity;
+import org.apache.jena.fuseki.main.FusekiLib;
+import org.apache.jena.fuseki.main.sys.FusekiModule;
+import org.apache.jena.fuseki.server.DataAccessPoint;
+import org.apache.jena.rdf.model.Model;
+
+public class FMod_GraphAccessCtl implements FusekiModule {
+
+ public FMod_GraphAccessCtl() {
+ VocabSecurity.init();
+ }
+
+ @Override
+ public String name() {
+ return "GraphAccessCtl";
+ }
+
+ @Override
+ public void configDataAccessPoint(DataAccessPoint dap, Model configModel) {
+ if ( DataAccessCtl.isAccessControlled(dap.getDataService().getDataset()) ) {
+ dap.getDataService().forEachEndpoint(ep->
+ FusekiLib.modifyForAccessCtl(ep, DataAccessCtl.requestUserServlet));
+ }
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/admin/ActionServerStatus.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/admin/ActionServerStatus.java
new file mode 100644
index 0000000000..d4589ede33
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/admin/ActionServerStatus.java
@@ -0,0 +1,99 @@
+/**
+ * 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.mod.admin;
+
+import static org.apache.jena.riot.WebContent.charsetUTF8;
+import static org.apache.jena.riot.WebContent.contentTypeJSON;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.jena.atlas.json.JSON;
+import org.apache.jena.atlas.json.JsonBuilder;
+import org.apache.jena.atlas.json.JsonValue;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.ctl.ActionCtl;
+import org.apache.jena.fuseki.ctl.JsonDescription;
+import org.apache.jena.fuseki.mgt.ServerMgtConst;
+import org.apache.jena.fuseki.server.DataAccessPointRegistry;
+import org.apache.jena.fuseki.server.ServerConst;
+import org.apache.jena.fuseki.servlets.HttpAction;
+import org.apache.jena.fuseki.servlets.ServletOps;
+
+/** Description of datasets for a server */
+public class ActionServerStatus extends ActionCtl
+{
+ public ActionServerStatus() { super(); }
+
+ @Override
+ public void validate(HttpAction action) {}
+
+ @Override
+ public void execGet(HttpAction action) {
+ executeLifecycle(action);
+ }
+
+ @Override
+ public void execPost(HttpAction action) {
+ executeLifecycle(action);
+ }
+
+ @Override
+ public void execute(HttpAction action) {
+ try {
+ description(action);
+ ServletOps.success(action);
+ } catch (IOException e) {
+ ServletOps.errorOccurred(e);
+ }
+ }
+
+ private void description(HttpAction action) throws IOException {
+ OutputStream out = action.getResponseOutputStream();
+ action.setResponseContentType(contentTypeJSON);
+ action.setResponseCharacterEncoding(charsetUTF8);
+
+ JsonBuilder builder = new JsonBuilder();
+ builder.startObject();
+ describeServer(builder, action.getRequestLocalPort());
+ describeDatasets(builder, action.getDataAccessPointRegistry());
+ builder.finishObject();
+
+ JsonValue v = builder.build();
+ JSON.write(out, v);
+ out.write('\n');
+ out.flush();
+ }
+
+ private void describeServer(JsonBuilder builder, int requestPort) {
+ String versionStr = Fuseki.VERSION;
+ builder
+ .pair(ServerMgtConst.version, versionStr)
+ .pair(ServerMgtConst.startDT, Fuseki.serverStartedAt())
+ .pair(ServerMgtConst.uptime, Fuseki.serverUptimeSeconds())
+ ;
+ }
+
+ private void describeDatasets(JsonBuilder builder, DataAccessPointRegistry registry) {
+ builder.key(ServerConst.datasets);
+ JsonDescription.arrayDatasets(builder, registry);
+ }
+
+}
+
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/admin/ArgModuleAdmin.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/admin/ArgModuleAdmin.java
new file mode 100644
index 0000000000..6cb3ed0b4e
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/admin/ArgModuleAdmin.java
@@ -0,0 +1,69 @@
+/*
+ * 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.mod.admin;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.jena.cmd.ArgDecl;
+import org.apache.jena.cmd.ArgModuleGeneral;
+import org.apache.jena.cmd.CmdArgModule;
+import org.apache.jena.cmd.CmdGeneral;
+import org.apache.jena.fuseki.FusekiConfigException;
+import org.apache.jena.fuseki.mgt.FusekiApp;
+
+public class ArgModuleAdmin implements ArgModuleGeneral {
+ // Add a static of "extra command"
+
+ private ArgDecl argAdmin = new ArgDecl(true, "admin");
+ private ArgDecl argAdminArea = new ArgDecl(true, "adminArea", "adminBase");
+
+ public ArgModuleAdmin() { }
+
+ @Override
+ public void processArgs(CmdArgModule cmdLine) {
+ System.out.println("ArgModuleAdmin");
+ String admin = cmdLine.getValue(argAdmin);
+ if ( admin == null ) {
+ return;
+ }
+
+ if ( admin.equals("localhost") ) {}
+ else {
+ String pwFile = admin;
+ }
+
+ String dirStr = cmdLine.getValue(argAdminArea);
+ Path directory = Path.of(dirStr);
+
+ if ( ! Files.isDirectory(directory) )
+ throw new FusekiConfigException("Not a directory: "+dirStr);
+
+ if ( ! Files.isWritable(directory) )
+ throw new FusekiConfigException("Not writable: "+dirStr);
+
+ FusekiApp.FUSEKI_BASE = directory;
+ }
+
+ @Override
+ public void registerWith(CmdGeneral cmdLine) {
+ cmdLine.add(argAdmin, "--admin=[UserPasswordFile|localhost]", "Enable the admin module");
+ cmdLine.add(argAdminArea, "--run=DIR", "Admin state directory");
+ }
+}
\ No newline at end of file
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/admin/FMod_Admin.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/admin/FMod_Admin.java
new file mode 100644
index 0000000000..09b0fcffc6
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/admin/FMod_Admin.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.fuseki.mod.admin;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.jena.atlas.logging.FmtLog;
+import org.apache.jena.cmd.ArgDecl;
+import org.apache.jena.cmd.ArgModuleGeneral;
+import org.apache.jena.cmd.CmdArgModule;
+import org.apache.jena.cmd.CmdGeneral;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.FusekiConfigException;
+import org.apache.jena.fuseki.build.FusekiConfig;
+import org.apache.jena.fuseki.ctl.ActionCtl;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.cmds.ServerArgs;
+import org.apache.jena.fuseki.main.sys.FusekiModule;
+import org.apache.jena.fuseki.mgt.ActionBackup;
+import org.apache.jena.fuseki.mgt.ActionBackupList;
+import org.apache.jena.fuseki.mgt.ActionDatasets;
+import org.apache.jena.fuseki.mgt.FusekiApp;
+import org.apache.jena.fuseki.server.DataAccessPoint;
+import org.apache.jena.rdf.model.Model;
+import org.slf4j.Logger;
+
+public class FMod_Admin implements FusekiModule {
+
+ private static FusekiModule singleton = new FMod_Admin();
+ // ----
+
+// @Override
+// public void start() {}
+//
+// @Override
+// public int level() {
+// return FusekiApp.levelFModAdmin;
+// }
+
+ @Override
+ public String name() {
+ return "FMod Admin";
+ }
+
+ public static FMod_Admin create() {
+ return new FMod_Admin();
+ }
+
+ public FMod_Admin() {}
+
+ private static Logger LOG = Fuseki.configLog;
+
+ private ArgDecl argAdmin = new ArgDecl(true, "admin");
+ private ArgDecl argAdminArea = new ArgDecl(true, "adminArea", "adminBase");
+
+ @Override
+ public void serverArgsModify(CmdGeneral fusekiCmd, ServerArgs serverArgs) {
+
+ fusekiCmd.getUsage().startCategory("Admin");
+
+ ArgModuleGeneral argModule = new ArgModuleGeneral() {
+ @Override
+ public void registerWith(CmdGeneral cmdLine) {
+ cmdLine.add(argAdmin, "--admin", "Enable server admin with user:password");
+ cmdLine.add(argAdminArea,"--adminRun", "Directory for server configuration");
+ }
+ @Override
+ public void processArgs(CmdArgModule cmdLine) {}
+ };
+ argModule.registerWith(fusekiCmd);
+ }
+
+ @Override
+ public void serverArgsPrepare(CmdGeneral fusekiCmd, ServerArgs serverArgs) {
+ String admin = fusekiCmd.getValue(argAdmin);
+ if ( admin == null ) {
+ return;
+ }
+
+ Path directory = null;
+ String dirStr = fusekiCmd.getValue(argAdminArea);
+ if ( dirStr != null )
+ directory = Path.of(dirStr);
+
+ if ( admin.equals("localhost") ) {}
+ else {
+ String pwFile = admin;
+ }
+
+ if ( directory != null ) {
+ if ( ! Files.isDirectory(directory) )
+ throw new FusekiConfigException("Not a directory: "+dirStr);
+
+ if ( ! Files.isWritable(directory) )
+ throw new FusekiConfigException("Not writable: "+dirStr);
+ }
+ FusekiApp.FUSEKI_BASE = directory;
+ }
+
+// @Override
+// public void serverArgsBuilder(FusekiServer.Builder serverBuilder, Model configModel) {
+// }
+
+ // ----
+
+ @Override
+ public void prepare(FusekiServer.Builder builder, Set<String> datasetNames, Model configModel) {
+ // Unpack
+ Path path = FusekiApp.setup();
+
+ FmtLog.info(LOG, "Fuseki Admin: %s", path);
+
+ // Shiro.
+ Path shiroIni = path.resolve(FusekiApp.DFT_SHIRO_INI);
+ if ( Files.exists(shiroIni) ) {
+ System.setProperty(FusekiApp.envFusekiShiro, shiroIni.toString());
+ } else {
+ FmtLog.info(LOG, "No shiro.ini: dir=%s", path);
+ }
+
+ String configDir = FusekiApp.dirConfiguration.toString();
+ List<DataAccessPoint> directoryDatabases = FusekiConfig.readConfigurationDirectory(configDir);
+
+ if ( directoryDatabases.isEmpty() )
+ FmtLog.info(LOG, "No databases: dir=%s", configDir);
+ else {
+ directoryDatabases.forEach(dap -> FmtLog.info(Fuseki.configLog, "Database: %s", dap.getName()));
+ }
+
+ directoryDatabases.forEach(db -> {
+ String dbName = db.getName();
+ if ( datasetNames.contains(dbName) ) {
+ FmtLog.warn(LOG, "Database '%s' already added to the Fuseki server builder", dbName);
+ // ?? builder.remove(dbName);
+ }
+ builder.add(dbName, db.getDataService());
+ // ** builder.add(DataAccessPoint);
+ });
+
+ // Modify the server to include the admin operations.
+ // Security is performed by FMod_Shiro.
+ ActionCtl actionBackup = new ActionBackup();
+ builder
+ .addServlet("/$/datasets/*", new ActionDatasets())
+ .addServlet("/$/server", new ActionServerStatus())
+ .addServlet("/$/backup/*", actionBackup)
+ .addServlet("/$/backups/*", actionBackup)
+ .addServlet("/$/backups-list", new ActionBackupList())
+
+ // Enables the task subsystem and is also called by enableCompact
+ .enableTasks(true)
+
+ // Can also be enabled by FMod_UI
+ .enableStats(true)
+ .enablePing(true)
+ .enableCompact(true)
+ ;
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/admin/LocalhostOnly.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/admin/LocalhostOnly.java
new file mode 100644
index 0000000000..9429b60650
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/admin/LocalhostOnly.java
@@ -0,0 +1,94 @@
+/**
+ * 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.mod.admin;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.web.HttpSC;
+import org.slf4j.Logger;
+
+/**
+ * Responds with HTTP 403 on any denied request.
+ */
+public class LocalhostOnly implements Filter {
+
+ private static String LOCALHOST_IpV6_a = "[0:0:0:0:0:0:0:1]";
+ private static String LOCALHOST_IpV6_b = "0:0:0:0:0:0:0:1";
+ // This is what appears in the Chrome developer tools client-side.
+ // "[0:0:0:0:0:0:0:1]" by the time it arrives here, it is not clear which
+ // software component is responsible for that.
+ // To be safe we add "[::1]".
+ private static String LOCALHOST_IpV6_c = "[::1]";
+ private static String LOCALHOST_IpV4 = "127.0.0.1"; // Strictly, 127.*.*.*
+
+ private static final Collection<String> localhosts = new HashSet<>(
+ Arrays.asList(LOCALHOST_IpV4, LOCALHOST_IpV6_a, LOCALHOST_IpV6_b, LOCALHOST_IpV6_c));
+
+ private static Logger log = Fuseki.serverLog;
+ private static final String message = "Access denied : only localhost access allowed";
+
+ public LocalhostOnly() { }
+
+ // "permit" and "deny" lists
+ private List<String> secured = Arrays.asList("/$/backup", "/$/compact", "/$/datasets");
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ try {
+ HttpServletRequest req = (HttpServletRequest)request;
+ HttpServletResponse resp = (HttpServletResponse)response;
+ /**
+ * Check request from "localhost, else 403.
+ */
+ boolean accept = checkRequest(req, resp);
+ if ( ! accept ) {
+ // Log
+ resp.sendError(HttpSC.FORBIDDEN_403);
+ return;
+ }
+ } catch (Throwable ex) {
+ log.info("Filter: unexpected exception: "+ex.getMessage(),ex);
+ }
+ // Continue.
+ chain.doFilter(request, response);
+ }
+
+ public boolean checkRequest(HttpServletRequest req, HttpServletResponse resp) {
+ String uri = req.getRequestURI();
+ for ( String s : secured ) {
+ if ( uri.startsWith(s) ) {
+ if ( ! checkLocalhost(req) )
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static boolean checkLocalhost(HttpServletRequest request) {
+ return localhosts.contains(request.getRemoteAddr()) ;
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/blank/FMod_BLANK.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/blank/FMod_BLANK.java
new file mode 100644
index 0000000000..4b56d2d7b1
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/blank/FMod_BLANK.java
@@ -0,0 +1,43 @@
+/*
+ * 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.mod.blank;
+
+import org.apache.jena.fuseki.main.sys.FusekiAutoModule;
+
+/**
+ * Template.
+ *
+ * src/main/resources/META-INF/services/org.apache.jena.fuseki.main.sys.FusekiAutoModule
+ */
+public class FMod_BLANK implements FusekiAutoModule {
+ @Override
+ public String name() { return "BLANK"; }
+
+// @Override public void start() { }
+// @Override public void prepare(FusekiServer.Builder serverBuilder, Set<String> datasetNames, Model configModel) { }
+// @Override public void configured(DataAccessPointRegistry dapRegistry, Model configModel) {
+// dapRegistry.accessPoints().forEach(accessPoint->configDataAccessPoint(accessPoint, configModel));
+// }
+// @Override public void configDataAccessPoint(DataAccessPoint dap, Model configModel) {}
+// @Override public void server(FusekiServer server) { }
+// @Override public void serverBeforeStarting(FusekiServer server) { }
+// @Override public void serverAfterStarting(FusekiServer server) { }
+// @Override public void serverStopped(FusekiServer server) { }
+// @Override public void stop() {}
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/package-info.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/package-info.java
new file mode 100644
index 0000000000..7345d58a49
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/package-info.java
@@ -0,0 +1,2 @@
+package org.apache.jena.fuseki.mod;
+
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/prometheus/ActionMetrics.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/prometheus/ActionMetrics.java
new file mode 100644
index 0000000000..92164563a6
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/prometheus/ActionMetrics.java
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jena.fuseki.mod.prometheus;
+
+import org.apache.jena.fuseki.ctl.ActionCtl;
+import org.apache.jena.fuseki.metrics.MetricsProviderRegistry;
+import org.apache.jena.fuseki.servlets.ActionLib;
+import org.apache.jena.fuseki.servlets.HttpAction;
+import org.apache.jena.fuseki.servlets.ServletOps;
+
+public class ActionMetrics extends ActionCtl {
+
+ public ActionMetrics() { super(); }
+
+ @Override
+ public void execGet(HttpAction action) {
+ super.executeLifecycle(action);
+ }
+
+ @Override
+ public void execOptions(HttpAction action) {
+ ActionLib.doOptionsGet(action);
+ ServletOps.success(action);
+ }
+
+ @Override
+ public void validate(HttpAction action) {}
+
+ @Override
+ public void execute(HttpAction action) {
+ MetricsProviderRegistry.get().scrape( action );
+ ServletOps.success(action);
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/prometheus/FMod_Prometheus.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/prometheus/FMod_Prometheus.java
new file mode 100644
index 0000000000..0d51e0bd4f
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/prometheus/FMod_Prometheus.java
@@ -0,0 +1,63 @@
+/*
+ * 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.mod.prometheus;
+
+import java.util.Set;
+
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.sys.FusekiModule;
+import org.apache.jena.fuseki.metrics.MetricsProviderRegistry;
+import org.apache.jena.rdf.model.Model;
+
+/**
+ * Prometheus Metrics.
+ *
+ * PrometheusMetricsProvider
+ */
+public class FMod_Prometheus implements FusekiModule {
+
+ private static FusekiModule singleton = new FMod_Prometheus();
+ public static FusekiModule get() {
+ return singleton;
+ }
+
+ public FMod_Prometheus() {}
+
+// @Override
+// public int level() {
+// return 5000;
+// }
+//
+// @Override public void start() {
+// Fuseki.configLog.info("FMod Prometheus Metrics");
+// MetricsProviderRegistry.set(new PrometheusMetricsProvider());
+// }
+
+ @Override
+ public String name() { return "FMod Prometheus Metrics"; }
+
+ @Override public void prepare(FusekiServer.Builder serverBuilder, Set<String> datasetNames, Model configModel) {
+ //MetricsProviderRegistry.set(new PrometheusMetricsProvider());
+ serverBuilder.addServlet("/$/metrics", new ActionMetrics());
+ }
+
+ @Override public void server(FusekiServer server) {
+ MetricsProviderRegistry.dataAccessPointMetrics(server.getDataAccessPointRegistry());
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/prometheus/PrometheusMetricsProvider.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/prometheus/PrometheusMetricsProvider.java
new file mode 100644
index 0000000000..74e13fadc1
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/prometheus/PrometheusMetricsProvider.java
@@ -0,0 +1,59 @@
+/**
+ * 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.mod.prometheus;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.prometheusmetrics.PrometheusConfig;
+import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
+import jakarta.servlet.ServletOutputStream;
+import org.apache.jena.fuseki.metrics.FusekiMetrics;
+import org.apache.jena.fuseki.metrics.MetricsProvider;
+import org.apache.jena.fuseki.servlets.HttpAction;
+import org.apache.jena.fuseki.servlets.ServletOps;
+import org.apache.jena.riot.WebContent;
+
+/**
+ */
+public class PrometheusMetricsProvider implements MetricsProvider {
+
+ private PrometheusMeterRegistry meterRegistry;
+
+ public PrometheusMetricsProvider() {
+ meterRegistry = new PrometheusMeterRegistry( PrometheusConfig.DEFAULT );
+ meterRegistry.config().commonTags( "application", "fuseki" );
+ FusekiMetrics.registerMetrics(meterRegistry);
+ }
+
+ @Override
+ public MeterRegistry getMeterRegistry() {
+ return meterRegistry;
+ }
+
+ @Override
+ public void scrape(HttpAction action) {
+ try (ServletOutputStream out = action.getResponseOutputStream()) {
+ ServletOps.success(action);
+ action.setResponseContentType( WebContent.contentTypeTextPlain );
+ action.setResponseCharacterEncoding( WebContent.charsetUTF8 );
+
+ out.write( meterRegistry.scrape().getBytes() );
+ } catch (Throwable t) {
+ ServletOps.errorOccurred( t );
+ }
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/shiro/FMod_Shiro.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/shiro/FMod_Shiro.java
new file mode 100644
index 0000000000..e70a44d58d
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/shiro/FMod_Shiro.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.mod.shiro;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+
+import jakarta.servlet.Filter;
+import org.apache.jena.atlas.io.IOX;
+import org.apache.jena.atlas.lib.IRILib;
+import org.apache.jena.atlas.lib.Lib;
+import org.apache.jena.cmd.ArgDecl;
+import org.apache.jena.cmd.CmdException;
+import org.apache.jena.cmd.CmdGeneral;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.FusekiConfigException;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.cmds.ServerArgs;
+import org.apache.jena.fuseki.main.sys.FusekiModule;
+import org.apache.jena.fuseki.mgt.FusekiApp;
+import org.apache.jena.rdf.model.Model;
+import org.apache.shiro.web.servlet.ShiroFilter;
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee10.servlet.SessionHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Fuseki Module for Apache Shiro.
+ * <p>
+ * TODO
+ * Configuration
+ */
+public class FMod_Shiro implements FusekiModule {
+
+// @Override
+// public void start() {
+// Fuseki.serverLog.info("FMod Shiro");
+// }
+ //
+// @Override
+// public int level() {
+// return FusekiApp.levelFModShiro;
+// }
+
+ @Override
+ public String name() {
+ return "FMod Shiro";
+ }
+
+ public static FMod_Shiro create() {
+ return new FMod_Shiro();
+ }
+
+ // Assumes the whole system is "Shiro".
+ // No setup?
+
+ public static final Logger shiroConfigLog = LoggerFactory.getLogger(Fuseki.PATH + ".Shiro");
+
+ private static List<String> defaultIniFileLocations = List.of("file:shiro.ini", "file:/etc/fuseki/shiro.ini");
+ private static List<String> iniFileLocations = null;
+
+ private static ArgDecl argShiroIni = new ArgDecl(true, "shiro", "shiro-ini");
+
+ // XXX Should be a per build variable.
+ private String shiroFile = null;
+
+ public FMod_Shiro() {
+ this(null);
+ }
+
+ public FMod_Shiro(String shiroFile) {
+ this.shiroFile = shiroFile;
+ }
+
+ // ---- If used from the command line
+ @Override
+ public void serverArgsModify(CmdGeneral fusekiCmd, ServerArgs serverArgs) {
+ fusekiCmd.add(argShiroIni);
+ }
+
+ @Override
+ public void serverArgsPrepare(CmdGeneral fusekiCmd, ServerArgs serverArgs) {
+ if ( fusekiCmd.contains(argShiroIni) ) {
+ shiroFile = fusekiCmd.getValue(argShiroIni);
+ Path path = Path.of(shiroFile);
+ IOX.checkReadableFile(path, CmdException::new);
+ }
+ }
+
+ // The filter is added in prepare().
+ // This allows other Fuseki modules, such as FMod_Admin, to setup shiro.ini.
+ // FMod_Admin unpacks a default one to $FUSEKI_BASE/shiro.ini (usually "run/shiro.ini")
+
+// @Override
+// public void serverArgsBuilder(FusekiServer.Builder serverBuilder, Model configModel) {
+// //Add filter.
+// }
+
+ /**
+ * Determine the Shiro configuration file.
+ * This applies whether command line arguments used for programmatic setup.
+ */
+ @Override
+ public void prepare(FusekiServer.Builder serverBuilder, Set<String> datasetNames, Model configModel) {
+ if ( shiroFile == null ) {
+ // Environment variable: FUSEKI_SHIRO
+ shiroFile = Lib.getenv(FusekiApp.envFusekiShiro);
+ }
+
+ if ( shiroFile == null ) {
+ return;
+ }
+
+ if ( shiroFile != null ) {
+ IOX.checkReadableFile(shiroFile, FusekiConfigException::new);
+ Filter filter = new FusekiShiroFilter(shiroFile);
+ // This is a "before" filter.
+ serverBuilder.addFilter("/*", filter);
+ }
+
+ // Clear.
+ shiroFile = null;
+ }
+
+ /**
+ * FusekiShiroFilter, includes Shiro initialization. Fuseki is a
+ * not a webapp so it needs to trigger off servlet initialization.
+ */
+ private static class FusekiShiroFilter extends ShiroFilter {
+
+ private final String shiroInitializationFile;
+
+ FusekiShiroFilter(String filename) {
+ shiroInitializationFile = IRILib.filenameToIRI(filename);
+ }
+
+ @Override
+ public void init() throws Exception {
+ // Intercept Shiro initialization.
+ List<String> locations = List.of();
+ if ( shiroInitializationFile != null ) {
+ locations = List.of(shiroInitializationFile);
+ }
+ FusekiShiroLib.shiroEnvironment(getServletContext(), locations);
+ super.init();
+ }
+ }
+
+ @Override
+ public void serverBeforeStarting(FusekiServer server) {
+ // Shiro requires a session handler.
+ // This needs the Jetty server to have been created.
+ org.eclipse.jetty.server.Server jettyServer = server.getJettyServer();
+ try {
+ ServletContextHandler servletContextHandler = (ServletContextHandler)(jettyServer.getHandler());
+ if ( servletContextHandler.getSessionHandler() == null ) {
+ SessionHandler sessionsHandler = new SessionHandler();
+ servletContextHandler.setHandler(sessionsHandler);
+ }
+ } catch (RuntimeException ex) {
+ shiroConfigLog.error("Failed to set a session manager - server aborted");
+ throw ex;
+ }
+ }
+
+ @Override
+ public void serverAfterStarting(FusekiServer server) {}
+
+// @Override public void serverStopped(FusekiServer server) { }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/shiro/FusekiShiroLib.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/shiro/FusekiShiroLib.java
new file mode 100644
index 0000000000..c963771efc
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/shiro/FusekiShiroLib.java
@@ -0,0 +1,62 @@
+/*
+ * 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.mod.shiro;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import jakarta.servlet.ServletContext;
+import org.apache.jena.fuseki.FusekiConfigException;
+import org.apache.jena.rfc3986.URIScheme;
+import org.apache.shiro.lang.io.ResourceUtils;
+import org.apache.shiro.web.env.EnvironmentLoaderListener;
+
+/*package*/ class FusekiShiroLib {
+ static void shiroEnvironment(ServletContext servletContext, List<String> possibleShiroIniFiles) {
+ // Shiro environment initialization, done here because we don't have webapp listeners.
+ EnvironmentLoaderListener shiroListener = new ShiroEnvironmentLoaderListener(possibleShiroIniFiles);
+ try {
+ shiroListener.initEnvironment(servletContext);
+ } catch (org.apache.shiro.config.ConfigurationException ex) {
+ ShiroEnvironmentLoaderListener.shiroConfigLog.error("Failed to initialize Shiro: "+ex.getMessage());
+ throw new FusekiConfigException(ex.getMessage());
+ }
+ }
+
+ private static String fileSchemePrefix = URIScheme.FILE.getPrefix();
+
+ /** Look for a Shiro ini file, returning the first found, or return null */
+ static String huntForShiroIni(List<String> locations) {
+ for ( String loc : locations ) {
+ // If file:, look for that file.
+ if ( loc.startsWith(fileSchemePrefix) ) {
+ Path p = Path.of(loc.substring(fileSchemePrefix.length()));
+ if ( Files.exists(p) )
+ return loc;
+ // Ignore.
+ continue;
+ }
+ // No scheme. May be a classpath resource.
+ if ( ResourceUtils.resourceExists(loc) )
+ return loc;
+ }
+ return null;
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/shiro/ShiroEnvironmentLoaderListener.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/shiro/ShiroEnvironmentLoaderListener.java
new file mode 100644
index 0000000000..c99f47aee7
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/shiro/ShiroEnvironmentLoaderListener.java
@@ -0,0 +1,77 @@
+/*
+ * 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.mod.shiro;
+
+import static java.lang.String.format;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.shiro.web.env.EnvironmentLoaderListener;
+import org.apache.shiro.web.env.ResourceBasedWebEnvironment;
+import org.apache.shiro.web.env.WebEnvironment;
+import org.slf4j.Logger;
+
+/**
+ * A Shiro {@link EnvironmentLoaderListener} that supports multiple possible
+ * locations for a {@code shiro.ini} file. It will return the first found in a list
+ * of possible file names.
+ */
+class ShiroEnvironmentLoaderListener extends EnvironmentLoaderListener{
+
+ public static final Logger shiroConfigLog = FMod_Shiro.shiroConfigLog;
+
+ private List<String> locations;
+
+ /*package*/ ShiroEnvironmentLoaderListener(List<String> locations) {
+ this.locations = locations;
+ }
+
+ /**
+ * When given multiple locations for the shiro.ini file, and
+ * if a {@link ResourceBasedWebEnvironment}, check the list of configuration
+ * locations, testing whether the name identified an existing resource.
+ * For the first resource name found to exist, reset the {@link ResourceBasedWebEnvironment}
+ * to name that resource alone so the normal Shiro initialization executes.
+ */
+ @Override
+ protected void customizeEnvironment(WebEnvironment environment) {
+ if ( locations == null )
+ return;
+
+ // Look for shiro.ini
+ if ( environment instanceof ResourceBasedWebEnvironment ) {
+ ResourceBasedWebEnvironment env = (ResourceBasedWebEnvironment)environment;
+ String[] configLocations = env.getConfigLocations();
+ if ( configLocations != null && configLocations.length > 0 ) {
+ // Set some other way.
+ shiroConfigLog.info(format("Shiro file resource %s", Arrays.asList(configLocations)));
+ return;
+ }
+ String loc = FusekiShiroLib.huntForShiroIni(locations);
+ if ( loc == null ) {
+ shiroConfigLog.info(format("No Shiro file found (tried: %s)", locations));
+ return;
+ }
+ shiroConfigLog.info("Shiro INI: "+loc);
+ String[] configLocationsHere = new String[] {loc};
+ env.setConfigLocations(configLocationsHere);
+ }
+ }
+}
\ No newline at end of file
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/ui/ActionStats.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/ui/ActionStats.java
new file mode 100644
index 0000000000..51953d1568
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/ui/ActionStats.java
@@ -0,0 +1,165 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.fuseki.mod.ui;
+
+import static java.lang.String.format;
+
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jena.atlas.json.JsonBuilder;
+import org.apache.jena.atlas.json.JsonObject;
+import org.apache.jena.atlas.json.JsonValue;
+import org.apache.jena.fuseki.ctl.ActionContainerItem;
+import org.apache.jena.fuseki.server.*;
+import org.apache.jena.fuseki.servlets.HttpAction;
+import org.apache.jena.fuseki.servlets.ServletOps;
+
+/**
+ * JSON output for stats, than JSON {@link ActionStats}.
+ * Separate because content negotiation for known JSON may
+ * be a simple "Accept: {@literal *}/*}".
+ * <p>
+ * <pre>
+ * /$/serviceurl -- all datatsets
+ * /$/serviceurl/name -- one dataset
+ * </pre>
+ *
+ */
+public class ActionStats extends ActionContainerItem
+{
+ // For endpoint with "" as name.
+ private static String emptyNameKeyPrefix = "_";
+
+ public ActionStats() { super(); }
+
+ @Override
+ public void validate(HttpAction action) {}
+
+ @Override
+ protected JsonValue execPostContainer(HttpAction action) {
+ return execCommonContainer(action);
+ }
+
+ @Override
+ protected JsonValue execPostItem(HttpAction action) {
+ return execCommonItem(action);
+ }
+
+ @Override
+ protected JsonValue execGetContainer(HttpAction action) {
+ return execCommonContainer(action);
+ }
+
+ @Override
+ protected JsonValue execGetItem(HttpAction action) {
+ return execCommonItem(action);
+ }
+
+ // This does not consult the system database for dormant etc.
+ protected JsonValue execCommonContainer(HttpAction action) {
+ if ( action.verbose )
+ action.log.info(format("[%d] GET stats all", action.id));
+ return generateStats(action.getDataAccessPointRegistry());
+ }
+
+ public static JsonObject generateStats(DataAccessPointRegistry registry) {
+ JsonBuilder builder = new JsonBuilder();
+ builder.startObject("top");
+ builder.key(ServerConst.datasets);
+ builder.startObject("datasets");
+ registry.forEach((name, access)->statsDataset(builder, access));
+ builder.finishObject("datasets");
+ builder.finishObject("top");
+ return builder.build().getAsObject();
+ }
+
+ protected JsonValue execCommonItem(HttpAction action) {
+ String datasetPath = getItemDatasetName(action);
+ if ( action.verbose )
+ action.log.info(format("[%d] GET stats dataset %s", action.id, datasetPath));
+
+ JsonBuilder builder = new JsonBuilder();
+ DataAccessPoint dap = getItemDataAccessPoint(action, datasetPath);
+ if ( dap == null )
+ ServletOps.errorNotFound(datasetPath);
+ builder.startObject("TOP");
+
+ builder.key(ServerConst.datasets);
+ builder.startObject("datasets");
+ statsDataset(builder, datasetPath, action.getDataAccessPointRegistry());
+ builder.finishObject("datasets");
+
+ builder.finishObject("TOP");
+ return builder.build();
+ }
+
+ public static JsonObject generateStats(DataAccessPoint access) {
+ JsonBuilder builder = new JsonBuilder();
+ statsDataset(builder, access);
+ return builder.build().getAsObject();
+ }
+
+ private void statsDataset(JsonBuilder builder, String name, DataAccessPointRegistry registry) {
+ DataAccessPoint access = registry.get(name);
+ statsDataset(builder, access);
+ }
+
+ private static void statsDataset(JsonBuilder builder, DataAccessPoint access) {
+ // Object started
+ builder.key(access.getName());
+ DataService dSrv = access.getDataService();
+ builder.startObject("counters");
+
+ builder.key(CounterName.Requests.getName()).value(dSrv.getCounters().value(CounterName.Requests));
+ builder.key(CounterName.RequestsGood.getName()).value(dSrv.getCounters().value(CounterName.RequestsGood));
+ builder.key(CounterName.RequestsBad.getName()).value(dSrv.getCounters().value(CounterName.RequestsBad));
+
+ builder.key(ServerConst.endpoints).startObject("endpoints");
+ int unique = 0;
+ for ( Operation operName : dSrv.getOperations() ) {
+ List<Endpoint> endpoints = access.getDataService().getEndpoints(operName);
+ for ( Endpoint endpoint : endpoints ) {
+ String k = endpoint.getName();
+ if ( StringUtils.isEmpty(k) )
+ k = emptyNameKeyPrefix+(++unique);
+ // Endpoint names are unique for a given service.
+ builder.key(k);
+ builder.startObject();
+
+ operationCounters(builder, endpoint);
+ builder.key(ServerConst.operation).value(operName.getJsonName());
+ builder.key(ServerConst.description).value(operName.getDescription());
+
+ builder.finishObject();
+ }
+ }
+ builder.finishObject("endpoints");
+ builder.finishObject("counters");
+ }
+
+ private static void operationCounters(JsonBuilder builder, Endpoint operation) {
+ for (CounterName cn : operation.getCounters().counters()) {
+ Counter c = operation.getCounters().get(cn);
+ builder.key(cn.getName()).value(c.value());
+ }
+ }
+}
+
+
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/ui/ActionStatsTxt.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/ui/ActionStatsTxt.java
new file mode 100644
index 0000000000..7b2bbe7ab6
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/ui/ActionStatsTxt.java
@@ -0,0 +1,189 @@
+/**
+ * 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.mod.ui;
+
+import static java.lang.String.format;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.jena.atlas.json.JsonValue;
+import org.apache.jena.fuseki.ctl.ActionContainerItem;
+import org.apache.jena.fuseki.server.*;
+import org.apache.jena.fuseki.servlets.HttpAction;
+import org.apache.jena.fuseki.servlets.ServletOps;
+import org.apache.jena.riot.WebContent;
+
+/**
+ * Text output for stats, than JSON {@link ActionStats}.
+ * Separate because content negotiation for known JSON may
+ * be a simple "Accept: {@literal *}/*}".
+ * <p>
+ * <pre>
+ * /$/serviceurl -- all datatsets
+ * /$/serviceurl/name -- one dataset
+ * </pre>
+ *
+ */
+public class ActionStatsTxt extends ActionContainerItem {
+
+ public ActionStatsTxt() { super(); }
+
+ @Override
+ public void validate(HttpAction action) {}
+
+ // Stats as plain text.
+ // Not simple conneg because JSON can be wanted but the HRRP is "Accept */*"
+ // Probably replace with a "simple" view.
+
+ // All datasets
+ @Override
+ protected JsonValue execGetContainer(HttpAction action) {
+ execContainer(action);
+ return null;
+ }
+
+ // One dataset
+ @Override
+ protected JsonValue execGetItem(HttpAction action) {
+ execItem(action);
+ return null;
+ }
+
+ // POST is GET (but know to be fresh)
+ @Override
+ protected JsonValue execPostContainer(HttpAction action) {
+ execContainer(action);
+ return null;
+ }
+
+ @Override
+ protected JsonValue execPostItem(HttpAction action) {
+ execItem(action);
+ return null;
+ }
+
+ private void execContainer(HttpAction action) {
+ if ( action.verbose )
+ action.log.info(format("[%d] GET stats text", action.id));
+ try {
+ statsTxt(action.getResponse(), action.getDataAccessPointRegistry());
+ } catch (IOException ex) {
+ action.log.warn(format("[%d] GET stats text: IO error: %s", action.id, ex.getMessage()));
+ }
+ }
+
+ private void execItem(HttpAction action) {
+ String name = getItemDatasetName(action);
+ if ( name == null )
+ ServletOps.errorBadRequest("No dataset name");
+ DataAccessPoint desc = action.getDataAccessPointRegistry().get(name);
+ if ( desc == null )
+ ServletOps.errorBadRequest("No such dataset: "+name);
+ try {
+ HttpServletResponse resp = action.getResponse();
+ ServletOutputStream out = resp.getOutputStream();
+ resp.setContentType(WebContent.contentTypeTextPlain);
+ resp.setCharacterEncoding(WebContent.charsetUTF8);
+ statsTxt(out, desc);
+ } catch (IOException ex) {
+ action.log.warn(format("[%d] GET stats text: IO error: %s", action.id, ex.getMessage()));
+ }
+ }
+
+ private void statsTxt(HttpServletResponse resp, DataAccessPointRegistry registry) throws IOException {
+ ServletOutputStream out = resp.getOutputStream();
+ resp.setContentType(WebContent.contentTypeTextPlain);
+ resp.setCharacterEncoding(WebContent.charsetUTF8);
+
+ Iterator<String> iter = registry.keys().iterator();
+ while (iter.hasNext()) {
+ String ds = iter.next();
+ DataAccessPoint desc = registry.get(ds);
+ statsTxt(out, desc);
+ if ( iter.hasNext() )
+ out.println();
+ }
+ out.flush();
+ }
+
+ private void statsTxt(ServletOutputStream out, DataAccessPoint desc) throws IOException {
+ DataService dSrv = desc.getDataService();
+ out.println("Dataset: " + desc.getName());
+ out.println(" Requests = " + dSrv.getCounters().value(CounterName.Requests));
+ out.println(" Good = " + dSrv.getCounters().value(CounterName.RequestsGood));
+ out.println(" Bad = " + dSrv.getCounters().value(CounterName.RequestsBad));
+
+ if ( desc.getDataService().hasOperation(Operation.Query)) {
+ out.println(" SPARQL Query:");
+ out.println(" Request = " + counter(dSrv, Operation.Query, CounterName.Requests));
+ out.println(" Good = " + counter(dSrv, Operation.Query, CounterName.RequestsGood));
+ out.println(" Bad requests = " + counter(dSrv, Operation.Query, CounterName.RequestsBad));
+ out.println(" Timeouts = " + counter(dSrv, Operation.Query, CounterName.QueryTimeouts));
+ out.println(" Bad exec = " + counter(dSrv, Operation.Query, CounterName.QueryExecErrors));
+ //out.println(" IO Errors = " + counter(dSrv, Operation.Query, CounterName.QueryIOErrors));
+ }
+
+ if ( desc.getDataService().hasOperation(Operation.Update)) {
+ out.println(" SPARQL Update:");
+ out.println(" Request = " + counter(dSrv, Operation.Update, CounterName.Requests));
+ out.println(" Good = " + counter(dSrv, Operation.Update, CounterName.RequestsGood));
+ out.println(" Bad requests = " + counter(dSrv, Operation.Update, CounterName.RequestsBad));
+ out.println(" Bad exec = " + counter(dSrv, Operation.Update, CounterName.UpdateExecErrors));
+ }
+
+ if ( desc.getDataService().hasOperation(Operation.Upload)) {
+ out.println(" Upload:");
+ out.println(" Requests = " + counter(dSrv, Operation.Upload, CounterName.Requests));
+ out.println(" Good = " + counter(dSrv, Operation.Upload, CounterName.RequestsGood));
+ out.println(" Bad = " + counter(dSrv, Operation.Upload, CounterName.RequestsBad));
+ }
+
+ if ( desc.getDataService().hasOperation(Operation.GSP_R) || desc.getDataService().hasOperation(Operation.GSP_RW) ) {
+ out.println(" SPARQL Graph Store Protocol:");
+ out.println(" GETs = " + gspValue(dSrv, CounterName.HTTPget) + " (good=" + gspValue(dSrv, CounterName.HTTPgetGood)
+ + "/bad=" + gspValue(dSrv, CounterName.HTTPgetBad) + ")");
+ if ( desc.getDataService().hasOperation(Operation.GSP_RW) ) {
+ out.println(" PUTs = " + gspValue(dSrv, CounterName.HTTPput) + " (good=" + gspValue(dSrv, CounterName.HTTPputGood)
+ + "/bad=" + gspValue(dSrv, CounterName.HTTPputBad) + ")");
+ out.println(" POSTs = " + gspValue(dSrv, CounterName.HTTPpost) + " (good=" + gspValue(dSrv, CounterName.HTTPpostGood)
+ + "/bad=" + gspValue(dSrv, CounterName.HTTPpostBad) + ")");
+ out.println(" PATCHs = " + gspValue(dSrv, CounterName.HTTPpatch) + " (good=" + gspValue(dSrv, CounterName.HTTPpatchGood)
+ + "/bad=" + gspValue(dSrv, CounterName.HTTPpatchBad) + ")");
+ out.println(" DELETEs = " + gspValue(dSrv, CounterName.HTTPdelete) + " (good=" + gspValue(dSrv, CounterName.HTTPdeleteGood)
+ + "/bad=" + gspValue(dSrv, CounterName.HTTPdeleteBad) + ")");
+ }
+ out.println(" HEADs = " + gspValue(dSrv, CounterName.HTTPhead) + " (good=" + gspValue(dSrv, CounterName.HTTPheadGood)
+ + "/bad=" + gspValue(dSrv, CounterName.HTTPheadBad) + ")");
+ }
+ }
+
+ private long counter(DataService dSrv, Operation operation, CounterName cName) {
+ return 0;
+ }
+
+ private long gspValue(DataService dSrv, CounterName cn) {
+ return counter(dSrv, Operation.GSP_RW, cn) +
+ counter(dSrv, Operation.GSP_R, cn);
+ }
+}
+
+
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/ui/FMod_UI.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/ui/FMod_UI.java
new file mode 100644
index 0000000000..a46ec343aa
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mod/ui/FMod_UI.java
@@ -0,0 +1,185 @@
+/*
+ * 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.mod.ui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.jena.atlas.io.IOX;
+import org.apache.jena.atlas.logging.FmtLog;
+import org.apache.jena.cmd.ArgDecl;
+import org.apache.jena.cmd.CmdException;
+import org.apache.jena.cmd.CmdGeneral;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.FusekiConfigException;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.cmds.ServerArgs;
+import org.apache.jena.fuseki.main.sys.FusekiModule;
+import org.apache.jena.fuseki.mgt.FusekiApp;
+import org.apache.jena.fuseki.validation.DataValidator;
+import org.apache.jena.fuseki.validation.IRIValidator;
+import org.apache.jena.fuseki.validation.QueryValidator;
+import org.apache.jena.fuseki.validation.UpdateValidator;
+import org.apache.jena.rdf.model.Model;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.slf4j.Logger;
+
+public class FMod_UI implements FusekiModule {
+
+ // Only one module needed - it is stateless.
+ private static FusekiModule singleton = new FMod_UI();
+ public static FusekiModule get() {
+ return singleton;
+ }
+
+ public FMod_UI() {}
+
+ private static Logger LOG = Fuseki.configLog;
+
+// // After FMod_admin
+// @Override
+// public int level() {
+// return FusekiApp.levelFModUI;
+// }
+
+ private static ArgDecl argUIFiles = new ArgDecl(true, "ui");
+ private String uiAppLocation = null;
+ /** Java resource name used to find the UI files. */
+ private static String resourceNameUI = "webapp";
+ /** Directory name of the root of UI files with {@code FUSEKI_BASE} */
+ private static String directoryNameUI = "webapp";
+
+ @Override
+ public String name() {
+ return "FMod UI";
+ }
+
+ // ---- If used from the command line
+ @Override
+ public void serverArgsModify(CmdGeneral fusekiCmd, ServerArgs serverArgs) {
+ fusekiCmd.add(argUIFiles);
+ }
+
+ @Override
+ public void serverArgsPrepare(CmdGeneral fusekiCmd, ServerArgs serverArgs) {
+ if ( fusekiCmd.contains(argUIFiles) ) {
+ uiAppLocation = fusekiCmd.getValue(argUIFiles);
+ IOX.checkReadableDirectory(uiAppLocation, CmdException::new);
+ }
+ }
+
+ @Override
+ public void prepare(FusekiServer.Builder builder, Set<String> datasetNames, Model configModel) {
+ if ( builder.staticFileBase() != null ) {
+ FmtLog.warn(LOG, "Static content location has already been set: %s", builder.staticFileBase());
+ return;
+ }
+
+ if ( uiAppLocation == null ) {
+ uiAppLocation = findFusekiApp();
+ if ( uiAppLocation == null ) {
+ LOG.warn("No Static content location has been found");
+ return;
+ }
+ } else {
+ FmtLog.info(LOG, "UI file area = %s", uiAppLocation);
+ }
+
+ builder.staticFileBase(uiAppLocation) // Set the UI files area.
+ .addServlet("/$/validate/query", new QueryValidator())
+ .addServlet("/$/validate/update", new UpdateValidator())
+ .addServlet("/$/validate/iri", new IRIValidator())
+ .addServlet("/$/validate/data", new DataValidator())
+ .enableStats(true);
+ // LOG.info("Fuseki UI loaded");
+ }
+
+ /**
+ * Locate the UI files.
+ * <ol>
+ * <li>Command line name of a directory</li>
+ * <li>{@code $FUSEKI_BASE/webapp}</li>
+ * <li>Classpath java resource {@code webapp}</li>
+ * <ol>
+ */
+ private String findFusekiApp() {
+ // 1:: Command line setting.
+ if ( uiAppLocation != null )
+ return uiAppLocation;
+
+ // 2:: $FUSEKI_BASE/webapp
+ // If the FUSEKI_BASE does not exists, it is created later in FMod_admin.prepare
+ // and does not include Fuseki app.
+ String x = fromPath(FusekiApp.FUSEKI_BASE, directoryNameUI);
+ if ( x != null ) {
+ LOG.info("Fuseki UI - path resource: "+x);
+ return x;
+ }
+
+ // 3:: From a jar.
+ // Format jar:file:///.../jena-fuseki-ui-VERSION.jar!/webapp/"
+ String r = fromClasspath(resourceNameUI);
+ if ( r != null ) {
+ // Simplify name.
+ String displayName = loggingName(r);
+ FmtLog.info(LOG, "UI Base = %s", displayName);
+ return r;
+ }
+ // Bad!
+ return null;
+ }
+
+ // Look for "$resourceName" on the classpath.
+ private static String fromClasspath(String resourceName) {
+ // Jetty 12.0.15 => warning "Leaked mount"
+ // Logger : "org.eclipse.jetty.util.resource.ResourceFactory"
+ //ResourceFactory resourceFactory = ResourceFactory.root();
+
+ ResourceFactory resourceFactory = ResourceFactory.closeable();
+ Resource resource = resourceFactory.newClassLoaderResource(resourceName);
+ if ( resource != null )
+ return resource.getURI().toString();
+ return null;
+ }
+
+ // Look for "$path/$resourceName"
+ private static String fromPath(Path path, String resourceName) {
+ if ( path != null ) {
+ Path path2 = path.resolve(resourceName);
+ if ( Files.exists(path2) ) {
+ IOX.checkReadableDirectory(path2, FusekiConfigException::new);
+ return path2.toAbsolutePath().toString();
+ }
+ }
+ return null;
+ }
+
+ private static Pattern regex = Pattern.compile("([^/]*)!");
+
+ private String loggingName(String r) {
+ Matcher matcher = regex.matcher(r);
+ if ( ! matcher.find() )
+ return r;
+ return matcher.group(1);
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/config.ttl b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/config.ttl
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/shiro.ini b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/shiro.ini
new file mode 100644
index 0000000000..72fa8942eb
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/shiro.ini
@@ -0,0 +1,38 @@
+# Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0
+
+[main]
+# Development
+ssl.enabled = false
+
+plainMatcher=org.apache.shiro.authc.credential.SimpleCredentialsMatcher
+localhostFilter=org.apache.jena.fuseki.authz.LocalhostFilter
+
+[users]
+# Implicitly adds "iniRealm = org.apache.shiro.realm.text.IniRealm"
+## admin=pw
+user1=passwd1
+
+[roles]
+
+[urls]
+## Control functions open to anyone
+/$/status = anon
+/$/server = anon
+/$/ping = anon
+/$/metrics = anon
+
+## and the rest are restricted to localhost.
+/$/** = localhostFilter
+
+
+## If you want simple, basic authentication user/password
+## on the operations,
+## 1 - set a better password in [users] above.
+## 2 - comment out the "/$/** = localhost" line and use:
+##/$/** = authcBasic,user[admin]
+
+## or to allow any access.
+##/$/** = anon
+
+# Everything else
+/**=anon
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-mem b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-mem
new file mode 100644
index 0000000000..6f42e594b4
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-mem
@@ -0,0 +1,29 @@
+# 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 tdb: <http://jena.hpl.hp.com/2008/tdb#> .
+@prefix ja: <http://jena.hpl.hp.com/2005/11/Assembler#> .
+
+## ---------------------------------------------------------------
+## Updatable in-memory dataset.
+
+<#service1> rdf:type fuseki:Service ;
+ # URI of the dataset -- http://host:port/{NAME}
+ fuseki:name "{NAME}" ;
+ fuseki:serviceQuery "" ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:serviceQuery "query" ;
+ fuseki:serviceUpdate "" ;
+ fuseki:serviceUpdate "update" ;
+ fuseki:serviceUpload "upload" ;
+ fuseki:serviceReadWriteGraphStore "data" ;
+ fuseki:serviceReadGraphStore "get" ;
+ fuseki:dataset <#dataset> ;
+ .
+
+# Transactional, in-memory dataset. Initially empty.
+<#dataset> rdf:type ja:DatasetTxnMem .
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb
new file mode 100644
index 0000000000..9b185884e2
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb
@@ -0,0 +1,32 @@
+# 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 tdb: <http://jena.hpl.hp.com/2008/tdb#> .
+@prefix ja: <http://jena.hpl.hp.com/2005/11/Assembler#> .
+
+## ---------------------------------------------------------------
+## Updatable TDB dataset with all services enabled.
+
+<#service_tdb_all> rdf:type fuseki:Service ;
+ rdfs:label "TDB {NAME}" ;
+ fuseki:name "{NAME}" ;
+ fuseki:serviceQuery "" ;
+ fuseki:serviceQuery "query" ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:serviceUpdate "" ;
+ fuseki:serviceUpdate "update" ;
+ fuseki:serviceUpload "upload" ;
+ fuseki:serviceReadWriteGraphStore "data" ;
+ # A separate read-only graph store endpoint:
+ fuseki:serviceReadGraphStore "get" ;
+ fuseki:dataset <#tdb_dataset_readwrite> ;
+ .
+
+<#tdb_dataset_readwrite> rdf:type tdb:DatasetTDB ;
+ tdb:location "{FUSEKI_BASE}/databases/{NAME}" ;
+ ##ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "3000" ] ;
+ ##tdb:unionDefaultGraph true ;
+ .
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb-dir b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb-dir
new file mode 100644
index 0000000000..f22de072b5
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb-dir
@@ -0,0 +1,30 @@
+# 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 tdb: <http://jena.hpl.hp.com/2008/tdb#> .
+@prefix ja: <http://jena.hpl.hp.com/2005/11/Assembler#> .
+
+## ---------------------------------------------------------------
+## Updatable TDB dataset with all services enabled.
+
+<#service_tdb_all> rdf:type fuseki:Service ;
+ rdfs:label "TDB {NAME}" ;
+ fuseki:name "{NAME}" ;
+ fuseki:serviceQuery "" ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:serviceQuery "query" ;
+ fuseki:serviceUpdate "" ;
+ fuseki:serviceUpdate "update" ;
+ fuseki:serviceUpload "upload" ;
+ fuseki:serviceReadWriteGraphStore "data" ;
+ fuseki:serviceReadGraphStore "get" ;
+ fuseki:dataset <#tdb_dataset_readwrite> ;
+ .
+
+<#tdb_dataset_readwrite> rdf:type tdb:DatasetTDB ;
+ tdb:location "{DIR}" ;
+ ##tdb:unionDefaultGraph true ;
+ .
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb-mem b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb-mem
new file mode 100644
index 0000000000..6b9c65653b
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb-mem
@@ -0,0 +1,30 @@
+# 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 tdb: <http://jena.hpl.hp.com/2008/tdb#> .
+@prefix ja: <http://jena.hpl.hp.com/2005/11/Assembler#> .
+
+## ---------------------------------------------------------------
+## Updatable TDB dataset im-memory with all services enabled.
+
+<#service_tdb_all> rdf:type fuseki:Service ;
+ rdfs:label "TDB {NAME}" ;
+ fuseki:name "{NAME}" ;
+ fuseki:serviceQuery "" ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:serviceQuery "query" ;
+ fuseki:serviceUpdate "" ;
+ fuseki:serviceUpdate "update" ;
+ fuseki:serviceUpload "upload" ;
+ fuseki:serviceReadWriteGraphStore "data" ;
+ fuseki:serviceReadGraphStore "get" ;
+ fuseki:dataset <#tdb_dataset_readwrite> ;
+ .
+
+<#tdb_dataset_readwrite> rdf:type tdb:DatasetTDB ;
+ tdb:location "--mem--" ;
+ ## tdb:unionDefaultGraph true ;
+ .
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb2 b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb2
new file mode 100644
index 0000000000..3fa7c16608
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb2
@@ -0,0 +1,32 @@
+# 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 tdb2: <http://jena.apache.org/2016/tdb#>
+PREFIX ja: <http://jena.hpl.hp.com/2005/11/Assembler#>
+
+## ---------------------------------------------------------------
+## Updatable TDB dataset with all services enabled.
+
+<#service_tdb_all> rdf:type fuseki:Service ;
+ rdfs:label "TDB2 {NAME}" ;
+ fuseki:name "{NAME}" ;
+ fuseki:serviceQuery "" ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:serviceQuery "query" ;
+ fuseki:serviceUpdate "" ;
+ fuseki:serviceUpdate "update" ;
+ fuseki:serviceUpload "upload" ;
+ fuseki:serviceReadWriteGraphStore "data" ;
+ fuseki:serviceReadGraphStore "get" ;
+ fuseki:dataset <#tdb_dataset_readwrite> ;
+
+ .
+
+<#tdb_dataset_readwrite> rdf:type tdb2:DatasetTDB2 ;
+ tdb2:location "{FUSEKI_BASE}/databases/{NAME}" ;
+ ##ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "3000" ] ;
+ ##tdb2:unionDefaultGraph true ;
+ .
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb2-dir b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb2-dir
new file mode 100644
index 0000000000..3312b9348c
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb2-dir
@@ -0,0 +1,31 @@
+# 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 tdb2: <http://jena.apache.org/2016/tdb#>
+PREFIX ja: <http://jena.hpl.hp.com/2005/11/Assembler#>
+
+## ---------------------------------------------------------------
+## Updatable TDB2 dataset with all services enabled.
+
+<#service_tdb_all> rdf:type fuseki:Service ;
+ rdfs:label "TDB2 {NAME}" ;
+ fuseki:name "{NAME}" ;
+ fuseki:serviceQuery "" ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:serviceQuery "query" ;
+ fuseki:serviceUpdate "" ;
+ fuseki:serviceUpdate "update" ;
+ fuseki:serviceUpload "upload" ;
+ fuseki:serviceReadWriteGraphStore "data" ;
+ fuseki:serviceReadGraphStore "get" ;
+ fuseki:dataset <#tdb_dataset_readwrite> ;
+
+ .
+
+<#tdb_dataset_readwrite> rdf:type tdb2:DatasetTDB2 ;
+ tdb2:location "{DIR}" ;
+ ##tdb2:unionDefaultGraph true ;
+ .
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb2-mem b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb2-mem
new file mode 100644
index 0000000000..de362d09d8
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/resources/org/apache/jena/fuseki/server/templates/config-tdb2-mem
@@ -0,0 +1,30 @@
+# 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 tdb2: <http://jena.apache.org/2016/tdb#>
+PREFIX ja: <http://jena.hpl.hp.com/2005/11/Assembler#>
+
+## ---------------------------------------------------------------
+## Updatable TDB2 dataset im-memory with all services enabled.
+
+<#service_tdb_all> rdf:type fuseki:Service ;
+ rdfs:label "TDB2 {NAME}" ;
+ fuseki:name "{NAME}" ;
+ fuseki:serviceQuery "" ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:serviceQuery "query" ;
+ fuseki:serviceUpdate "" ;
+ fuseki:serviceUpdate "update" ;
+ fuseki:serviceUpload "upload" ;
+ fuseki:serviceReadWriteGraphStore "data" ;
+ fuseki:serviceReadGraphStore "get" ;
+ fuseki:dataset <#tdb_dataset_readwrite> ;
+ .
+
+<#tdb_dataset_readwrite> rdf:type tdb2:DatasetTDB2 ;
+ tdb2:location "--mem--" ;
+ ## tdb2:unionDefaultGraph true ;
+ .
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/TC_FusekiServer.java
similarity index 63%
copy from jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
copy to jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/TC_FusekiServer.java
index 71c6c31d0e..fd30ecb946 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/TC_FusekiServer.java
@@ -1,4 +1,4 @@
-/**
+/*
* 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
@@ -16,24 +16,17 @@
* limitations under the License.
*/
-package org.apache.jena.fuseki.server;
+package org.apache.jena.fuseki;
-import java.util.concurrent.atomic.AtomicLong;
+import org.junit.platform.suite.api.SelectClasses;
+import org.junit.platform.suite.api.Suite;
-/** A statistics counter */
-public class Counter
-{
- private AtomicLong counter = new AtomicLong(0);
-
- public Counter() {}
-
- public void inc() { counter.incrementAndGet(); }
- public void dec() { counter.decrementAndGet(); }
- public long value() { return counter.get(); }
-
- @Override
- public String toString() {
- return counter.toString();
- }
-}
+import org.apache.jena.fuseki.main.TC_FusekiMain;
+import org.apache.jena.fuseki.mod.TC_FusekiMods;
+@Suite
+@SelectClasses({
+ TC_FusekiMain.class,
+ TC_FusekiMods.class
+})
+public class TC_FusekiServer {}
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/TC_FusekiMods.java
similarity index 60%
copy from jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
copy to jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/TC_FusekiMods.java
index 71c6c31d0e..d2fafda62f 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/TC_FusekiMods.java
@@ -1,4 +1,4 @@
-/**
+/*
* 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
@@ -16,24 +16,25 @@
* limitations under the License.
*/
-package org.apache.jena.fuseki.server;
+package org.apache.jena.fuseki.mod;
-import java.util.concurrent.atomic.AtomicLong;
+import org.junit.platform.suite.api.SelectClasses;
+import org.junit.platform.suite.api.Suite;
-/** A statistics counter */
-public class Counter
-{
- private AtomicLong counter = new AtomicLong(0);
+import org.apache.jena.fuseki.mod.admin.TS_FusekiServerApp;
+import org.apache.jena.fuseki.mod.metrics.TestModPrometheus;
+import org.apache.jena.fuseki.mod.shiro.TestModShiro;
- public Counter() {}
+@Suite
+@SelectClasses({
+ TS_FusekiServerApp.class,
+ // UI
- public void inc() { counter.incrementAndGet(); }
- public void dec() { counter.decrementAndGet(); }
- public long value() { return counter.get(); }
-
- @Override
- public String toString() {
- return counter.toString();
- }
+ // Prometheus
+ TestModPrometheus.class,
+ // Apache Shiro
+ TestModShiro.class
+})
+public class TC_FusekiMods {
+ public TC_FusekiMods() {}
}
-
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/TestFusekiServer.java
similarity index 59%
copy from jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
copy to jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/TestFusekiServer.java
index 71c6c31d0e..e0a6b2392d 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/TestFusekiServer.java
@@ -1,4 +1,4 @@
-/**
+/*
* 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
@@ -16,24 +16,24 @@
* limitations under the License.
*/
-package org.apache.jena.fuseki.server;
+package org.apache.jena.fuseki.mod;
-import java.util.concurrent.atomic.AtomicLong;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
-/** A statistics counter */
-public class Counter
-{
- private AtomicLong counter = new AtomicLong(0);
+import org.junit.jupiter.api.Test;
- public Counter() {}
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.run.FusekiModServer;
- public void inc() { counter.incrementAndGet(); }
- public void dec() { counter.decrementAndGet(); }
- public long value() { return counter.get(); }
+/**
+ * Test for the whole Fuseki server, not components.
+ */
+public class TestFusekiServer {
+ @Test public void run() {
+ // Setup
+ FusekiServer server = FusekiModServer.runAsync("--port=0", "--empty");
+ int port = server.getPort();
+ assertNotEquals(0, port, "Port is zero after async start");
- @Override
- public String toString() {
- return counter.toString();
}
}
-
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TS_FusekiServerApp.java
similarity index 63%
copy from jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
copy to jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TS_FusekiServerApp.java
index 71c6c31d0e..b36360b8cb 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Counter.java
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TS_FusekiServerApp.java
@@ -1,4 +1,4 @@
-/**
+/*
* 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
@@ -16,24 +16,17 @@
* limitations under the License.
*/
-package org.apache.jena.fuseki.server;
+package org.apache.jena.fuseki.mod.admin;
-import java.util.concurrent.atomic.AtomicLong;
+import org.junit.platform.suite.api.SelectClasses;
+import org.junit.platform.suite.api.Suite;
-/** A statistics counter */
-public class Counter
-{
- private AtomicLong counter = new AtomicLong(0);
+@Suite
+@SelectClasses({
+ TestAdmin.class,
+ TestFusekiReload.class,
+ TestTemplateAddDataset.class,
+})
+public class TS_FusekiServerApp {
- public Counter() {}
-
- public void inc() { counter.incrementAndGet(); }
- public void dec() { counter.decrementAndGet(); }
- public long value() { return counter.get(); }
-
- @Override
- public String toString() {
- return counter.toString();
- }
}
-
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestAdmin.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestAdmin.java
new file mode 100644
index 0000000000..df0f6bafdd
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestAdmin.java
@@ -0,0 +1,836 @@
+/**
+ * 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.mod.admin;
+
+import static org.apache.jena.fuseki.mgt.ServerMgtConst.*;
+import static org.apache.jena.fuseki.server.ServerConst.opPing;
+import static org.apache.jena.fuseki.server.ServerConst.opStats;
+import static org.apache.jena.http.HttpOp.*;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.json.JSON;
+import org.apache.jena.atlas.json.JsonArray;
+import org.apache.jena.atlas.json.JsonObject;
+import org.apache.jena.atlas.json.JsonValue;
+import org.apache.jena.atlas.lib.FileOps;
+import org.apache.jena.atlas.lib.Lib;
+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.fuseki.ctl.ActionSleep;
+import org.apache.jena.fuseki.ctl.JsonConstCtl;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.sys.FusekiModules;
+import org.apache.jena.fuseki.mgt.FusekiApp;
+import org.apache.jena.fuseki.mgt.ServerMgtConst;
+import org.apache.jena.fuseki.server.ServerConst;
+import org.apache.jena.fuseki.test.HttpTest;
+import org.apache.jena.riot.WebContent;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.web.HttpSC;
+import org.awaitility.Awaitility;
+
+/**
+ * Tests of the admin functionality using a pre-configured dataset
+ * {@link TestTemplateAddDataset}.
+ */
+public class TestAdmin {
+
+ // Name of the dataset in the assembler file.
+ static String dsTest = "test-ds1";
+ static String dsTestInf = "test-ds4";
+
+ // There are two Fuseki-TDB2 tests: add_delete_dataset_6() and compact_01().
+ //
+ // On certain build systems (GH action/Linux under load, ASF Jenkins sometimes),
+ // add_delete_dataset_6 fails (transactions active), or compact_01 (gets a 404),
+ // if the two databases are the same.
+ static String dsTestTdb2a = "test-tdb2a";
+ static String dsTestTdb2b = "test-tdb2b";
+ static String fileBase = "testing/Config/";
+
+ private String serverURL = null;
+ private FusekiServer server = null;
+
+ @BeforeEach public void startServer() {
+ System.setProperty("FUSEKI_BASE", "target/run");
+ FileOps.clearAll("target/run");
+
+ server = createServerForTest();
+ serverURL = server.serverURL();
+ //String adminURL = server.serverURL()+"$";
+ //AuthEnv.get().registerUsernamePassword(adminURL, "admin","pw");
+ }
+
+ // Exactly the module under test
+ private static FusekiModules moduleSetup() {
+ return FusekiModules.create(FMod_Admin.create());
+ }
+
+ private FusekiServer createServerForTest() {
+ FusekiModules modules = moduleSetup();
+ DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+ FusekiServer testServer = FusekiServer.create()
+ .fusekiModules(modules)
+ .port(0)
+ .add(datasetName(), dsg)
+ .addServlet("/$/sleep/*", new ActionSleep())
+ .build()
+ .start();
+ return testServer;
+ }
+
+ @AfterEach public void stopServer() {
+ if ( server != null )
+ server.stop();
+ serverURL = null;
+ // Clearup FMod_Shiro.
+ System.getProperties().remove(FusekiApp.envFusekiShiro);
+ }
+
+ protected String urlRoot() {
+ return serverURL;
+ }
+
+ protected String datasetName() {
+ return "dataset";
+ }
+
+ protected String datasetPath() {
+ return "/"+datasetName();
+ }
+
+ @BeforeEach public void setLogging() {
+ LogCtl.setLevel(Fuseki.backupLogName, "ERROR");
+ LogCtl.setLevel(Fuseki.compactLogName,"ERROR");
+ Awaitility.setDefaultPollDelay(20,TimeUnit.MILLISECONDS);
+ Awaitility.setDefaultPollInterval(50,TimeUnit.MILLISECONDS);
+ }
+
+ @AfterEach public void unsetLogging() {
+ LogCtl.setLevel(Fuseki.backupLogName, "WARN");
+ LogCtl.setLevel(Fuseki.compactLogName,"WARN");
+ }
+
+ // --- Ping
+
+ @Test public void ping_1() {
+ httpGet(urlRoot()+"$/"+opPing);
+ }
+
+ @Test public void ping_2() {
+ httpPost(urlRoot()+"$/"+opPing);
+ }
+
+ // --- Server status
+
+ @Test public void server_1() {
+ JsonValue jv = httpGetJson(urlRoot()+"$/"+opServer);
+ JsonObject obj = jv.getAsObject();
+ // Now optional : assertTrue(obj.hasKey(JsonConst.admin));
+ assertTrue(obj.hasKey(ServerConst.datasets));
+ assertTrue(obj.hasKey(ServerMgtConst.uptime));
+ assertTrue(obj.hasKey(ServerMgtConst.startDT));
+ }
+
+ @Test public void server_2() {
+ httpPost(urlRoot()+"$/"+opServer);
+ }
+
+ // --- List all datasets
+
+ @Test public void list_datasets_1() {
+ try ( TypedInputStream in = httpGet(urlRoot()+"$/"+opDatasets); ) {
+ IO.skipToEnd(in);
+ }
+ }
+
+ @Test public void list_datasets_2() {
+ try ( TypedInputStream in = httpGet(urlRoot()+"$/"+opDatasets) ) {
+ assertEqualsIgnoreCase(WebContent.contentTypeJSON, in.getContentType());
+ JsonValue v = JSON.parseAny(in);
+ assertNotNull(v.getAsObject().get("datasets"));
+ checkJsonDatasetsAll(v);
+ }
+ }
+
+ // Specific dataset
+ @Test public void list_datasets_3() {
+ checkExists(datasetName());
+ }
+
+ // Specific dataset
+ @Test public void list_datasets_4() {
+ HttpTest.expect404( () -> getDatasetDescription("does-not-exist") );
+ }
+
+ // Specific dataset
+ @Test public void list_datasets_5() {
+ JsonValue v = getDatasetDescription(datasetName());
+ checkJsonDatasetsOne(v.getAsObject());
+ }
+
+ // Specific dataset
+ @Test public void add_delete_dataset_1() {
+ checkNotThere(dsTest);
+
+ addTestDataset();
+
+ // Check exists.
+ checkExists(dsTest);
+
+ // Remove it.
+ deleteDataset(dsTest);
+ checkNotThere(dsTest);
+ }
+
+ // Try to add twice
+ @Test public void add_delete_dataset_2() {
+ checkNotThere(dsTest);
+
+ try {
+ Path f = Path.of(fileBase+"config-ds-plain-1.ttl");
+ {
+ httpPost(urlRoot()+"$/"+opDatasets,
+ WebContent.contentTypeTurtle+"; charset="+WebContent.charsetUTF8,
+ BodyPublishers.ofFile(f));
+ }
+ // Check exists.
+ checkExists(dsTest);
+ try {
+ } catch (HttpException ex) {
+ httpPost(urlRoot()+"$/"+opDatasets,
+ WebContent.contentTypeTurtle+"; charset="+WebContent.charsetUTF8,
+ BodyPublishers.ofFile(f));
+ assertEquals(HttpSC.CONFLICT_409, ex.getStatusCode());
+ }
+ } catch (IOException ex) { IO.exception(ex); return; }
+
+ // Check exists.
+ checkExists(dsTest);
+ deleteDataset(dsTest);
+ }
+
+ @Test public void add_delete_dataset_3() {
+ checkNotThere(dsTest);
+ addTestDataset();
+ checkExists(dsTest);
+ deleteDataset(dsTest);
+ checkNotThere(dsTest);
+ addTestDataset();
+ checkExists(dsTest);
+ deleteDataset(dsTest);
+ }
+
+ @Test public void add_delete_dataset_4() {
+ checkNotThere(dsTest);
+ checkNotThere(dsTestInf);
+ addTestDatasetInf();
+ checkNotThere(dsTest);
+ checkExists(dsTestInf);
+
+ deleteDataset(dsTestInf);
+ checkNotThere(dsTestInf);
+ addTestDatasetInf();
+ checkExists(dsTestInf);
+ deleteDataset(dsTestInf);
+ }
+
+ @Test public void add_delete_dataset_5() {
+ // New style operations : cause two fuseki:names
+ addTestDataset(fileBase+"config-ds-plain-2.ttl");
+ checkExists("test-ds2");
+ }
+
+ @Test public void add_delete_dataset_6() {
+ String testDB = dsTestTdb2a;
+ assumeNotWindows();
+
+ checkNotThere(testDB);
+
+ addTestDatasetTDB2(testDB);
+
+ // Check exists.
+ checkExists(testDB);
+
+ // Remove it.
+ deleteDataset(testDB);
+ checkNotThere(testDB);
+ }
+
+ @Test public void add_error_1() {
+ HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
+ ()-> addTestDataset(fileBase+"config-ds-bad-name-1.ttl"));
+ }
+
+ @Test public void add_error_2() {
+ HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
+ ()-> addTestDataset(fileBase+"config-ds-bad-name-2.ttl"));
+ }
+
+ @Test public void add_error_3() {
+ HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
+ ()-> addTestDataset(fileBase+"config-ds-bad-name-3.ttl"));
+ }
+
+ @Test public void add_error_4() {
+ HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
+ ()-> addTestDataset(fileBase+"config-ds-bad-name-4.ttl"));
+ }
+
+ @Test public void delete_dataset_1() {
+ String name = "NoSuchDataset";
+ HttpTest.expect404( ()-> httpDelete(urlRoot()+"$/"+opDatasets+"/"+name) );
+ }
+
+ // ---- Backup
+
+ @Test public void create_backup_1() {
+ String id = null;
+ try {
+ JsonValue v = httpPostRtnJSON(urlRoot() + "$/" + opBackup + "/" + datasetName());
+ id = v.getAsObject().getString("taskId");
+ } finally {
+ waitForTasksToFinish(1000, 10, 20000);
+ }
+ assertNotNull(id);
+ checkInTasks(id);
+
+ // Check a backup was created
+ try ( TypedInputStream in = httpGet(urlRoot()+"$/"+opListBackups) ) {
+ assertEqualsIgnoreCase(WebContent.contentTypeJSON, in.getContentType());
+ JsonValue v = JSON.parseAny(in);
+ assertNotNull(v.getAsObject().get("backups"));
+ JsonArray a = v.getAsObject().get("backups").getAsArray();
+ assertEquals(1, a.size());
+ }
+
+ JsonValue task = getTask(id);
+ assertNotNull(id);
+ // Expect task success
+ assertTrue(task.getAsObject().getBoolean(JsonConstCtl.success), "Expected task to be marked as successful");
+ }
+
+ @Test
+ public void create_backup_2() {
+ HttpTest.expect400(()->{
+ JsonValue v = httpPostRtnJSON(urlRoot() + "$/" + opBackup + "/noSuchDataset");
+ });
+ }
+
+ @Test public void list_backups_1() {
+ try ( TypedInputStream in = httpGet(urlRoot()+"$/"+opListBackups) ) {
+ assertEqualsIgnoreCase(WebContent.contentTypeJSON, in.getContentType());
+ JsonValue v = JSON.parseAny(in);
+ assertNotNull(v.getAsObject().get("backups"));
+ }
+ }
+
+ // ---- Compact
+
+ @Test public void compact_01() {
+ assumeNotWindows();
+
+ String testDB = dsTestTdb2b;
+ try {
+ checkNotThere(testDB);
+ addTestDatasetTDB2(testDB);
+ checkExists(testDB);
+
+ String id = null;
+ try {
+ JsonValue v = httpPostRtnJSON(urlRoot() + "$/" + opCompact + "/" + testDB);
+ id = v.getAsObject().getString(JsonConstCtl.taskId);
+ } finally {
+ waitForTasksToFinish(1000, 500, 20_000);
+ }
+ assertNotNull(id);
+ checkInTasks(id);
+
+ JsonValue task = getTask(id);
+ // ----
+ // The result assertion is throwing NPE occasionally on some heavily loaded CI servers.
+ // This may be because of server or test code encountering a very long wait.
+ // These next statements check the assumed structure of the return.
+ assertNotNull(task, "Task value");
+ JsonObject obj = task.getAsObject();
+ assertNotNull(obj, "Task.getAsObject()");
+ // Provoke code to get a stacktrace.
+ obj.getBoolean(JsonConstCtl.success);
+ // ----
+ // The assertion we really wanted to check.
+ // Check task success
+ assertTrue(task.getAsObject().getBoolean(JsonConstCtl.success),
+ "Expected task to be marked as successful");
+ } finally {
+ deleteDataset(testDB);
+ }
+ }
+
+ @Test public void compact_02() {
+ HttpTest.expect400(()->{
+ JsonValue v = httpPostRtnJSON(urlRoot() + "$/" + opCompact + "/noSuchDataset");
+ });
+ }
+
+ private void assumeNotWindows() {
+ assumeFalse(SystemUtils.IS_OS_WINDOWS, "Test may be unstable on Windows due to inability to delete memory-mapped files");
+ }
+
+ // ---- Server
+
+ // ---- Stats
+
+ @Test public void stats_1() {
+ JsonValue v = execGetJSON(urlRoot()+"$/"+opStats);
+ checkJsonStatsAll(v);
+ }
+
+ @Test public void stats_2() {
+ addTestDataset();
+ JsonValue v = execGetJSON(urlRoot()+"$/"+opStats+datasetPath());
+ checkJsonStatsAll(v);
+ deleteDataset(dsTest);
+ }
+
+ @Test public void stats_3() {
+ addTestDataset();
+ HttpTest.expect404(()-> execGetJSON(urlRoot()+"$/"+opStats+"/DoesNotExist"));
+ deleteDataset(dsTest);
+ }
+
+ @Test public void stats_4() {
+ JsonValue v = execPostJSON(urlRoot()+"$/"+opStats);
+ checkJsonStatsAll(v);
+ }
+
+ @Test public void stats_5() {
+ addTestDataset();
+ JsonValue v = execPostJSON(urlRoot()+"$/"+opStats+datasetPath());
+ checkJsonStatsAll(v);
+ deleteDataset(dsTest);
+ }
+
+ @Test public void sleep_1() {
+ String x = execSleepTask(null, 1);
+ }
+
+ @Test public void sleep_2() {
+ try {
+ String x = execSleepTask(null, -1);
+ fail("Sleep call unexpectedly succeed");
+ } catch (HttpException ex) {
+ assertEquals(400, ex.getStatusCode());
+ }
+ }
+
+ @Test public void sleep_3() {
+ try {
+ String x = execSleepTask(null, 20*1000+1);
+ fail("Sleep call unexpectedly succeed");
+ } catch (HttpException ex) {
+ assertEquals(400, ex.getStatusCode());
+ }
+ }
+
+ // Async task testing
+
+ @Test public void task_1() {
+ String x = execSleepTask(null, 10);
+ assertNotNull(x);
+ Integer.parseInt(x);
+ }
+
+ @Test public void task_2() {
+ String x = "NoSuchTask";
+ String url = urlRoot()+"$/tasks/"+x;
+ HttpTest.expect404(()->httpGetJson(url) );
+ try {
+ checkInTasks(x);
+ fail("No failure!");
+ } catch (AssertionError ex) {}
+ }
+
+
+ @Test public void task_3() {
+ // Timing dependent.
+ // Create a "long" running task so we can find it.
+ String x = execSleepTask(null, 100);
+ checkTask(x);
+ checkInTasks(x);
+ assertNotNull(x);
+ Integer.parseInt(x);
+ }
+
+ @Test public void task_4() {
+ // Timing dependent.
+ // Create a "short" running task
+ String x = execSleepTask(null, 1);
+ // Check exists in the list of all tasks (should be "finished")
+ checkInTasks(x);
+ String url = urlRoot()+"$/tasks/"+x;
+
+ boolean finished = false;
+ for ( int i = 0; i < 10; i++ ) {
+ if ( i != 0 )
+ Lib.sleep(25);
+ JsonValue v = httpGetJson(url);
+ checkTask(v);
+ if ( v.getAsObject().hasKey("finished") ) {
+ finished = true;
+ break;
+ }
+ }
+ if ( ! finished )
+ fail("Task has not finished");
+ }
+
+ @Test public void task_5() {
+ // Short running task - still in info API call.
+ String x = execSleepTask(null, 1);
+ checkInTasks(x);
+ }
+
+ @Test public void task_6() {
+ String x1 = execSleepTask(null, 1000);
+ String x2 = execSleepTask(null, 1000);
+ await().timeout(500,TimeUnit.MILLISECONDS).until(() -> runningTasks().size() > 1);
+ await().timeout(2000, TimeUnit.MILLISECONDS).until(() -> runningTasks().isEmpty());
+ }
+
+ @Test public void task_7() {
+ try {
+ String x1 = execSleepTask(null, 1000);
+ String x2 = execSleepTask(null, 1000);
+ String x3 = execSleepTask(null, 1000);
+ String x4 = execSleepTask(null, 1000);
+ try {
+ // Try to make test more stable on a loaded CI server.
+ // Unloaded the first sleep will fail but due to slowness/burstiness
+ // some tasks above may have completed.
+ String x5 = execSleepTask(null, 4000);
+ String x6 = execSleepTask(null, 4000);
+ String x7 = execSleepTask(null, 4000);
+ String x8 = execSleepTask(null, 10);
+ fail("Managed to add a 5th test");
+ } catch (HttpException ex) {
+ assertEquals(HttpSC.BAD_REQUEST_400, ex.getStatusCode());
+ }
+ } finally {
+ waitForTasksToFinish(1000, 250, 4000);
+ }
+ }
+
+ /** Expect two string to be non-null and be {@link String#equalsIgnoreCase} */
+ private void assertEqualsIgnoreCase(String expected, String actual) {
+ if ( expected == null && actual == null )
+ return;
+ if ( expected == null || actual == null )
+ fail("Expected: "+expected+" Got: "+actual);
+ if ( ! expected.equalsIgnoreCase(actual) )
+ fail("Expected: "+expected+" Got: "+actual);
+ }
+
+ private JsonValue getTask(String taskId) {
+ String url = urlRoot()+"$/tasks/"+taskId;
+ return httpGetJson(url);
+ }
+
+ private JsonValue getDatasetDescription(String dsName) {
+ if ( dsName.startsWith("/") )
+ dsName = dsName.substring(1);
+ try (TypedInputStream in = httpGet(urlRoot() + "$/" + opDatasets + "/" + dsName)) {
+ assertEqualsIgnoreCase(WebContent.contentTypeJSON, in.getContentType());
+ JsonValue v = JSON.parse(in);
+ return v;
+ }
+ }
+
+ // -- Add
+
+ private void addTestDataset() {
+ addTestDataset(fileBase+"config-ds-plain-1.ttl");
+ }
+
+ private void addTestDatasetInf() {
+ addTestDataset(fileBase+"config-ds-inf.ttl");
+ }
+
+ private void addTestDatasetTDB2(String DBname) {
+ Objects.nonNull(DBname);
+ if ( DBname.equals(dsTestTdb2a) ) {
+ addTestDataset(fileBase+"config-tdb2a.ttl");
+ return;
+ }
+ if ( DBname.equals(dsTestTdb2b) ) {
+ addTestDataset(fileBase+"config-tdb2b.ttl");
+ return;
+ }
+ throw new IllegalArgumentException("No configuration for "+DBname);
+ }
+
+ private void addTestDataset(String filename) {
+ try {
+ Path f = Path.of(filename);
+ BodyPublisher body = BodyPublishers.ofFile(f);
+ String ct = WebContent.contentTypeTurtle;
+ httpPost(urlRoot()+"$/"+opDatasets, ct, body);
+ } catch (FileNotFoundException e) {
+ IO.exception(e);
+ }
+ }
+
+ private void deleteDataset(String name) {
+ httpDelete(urlRoot()+"$/"+opDatasets+"/"+name);
+ }
+
+ private String execSleepTask(String name, int millis) {
+ String url = urlRoot()+"$/sleep";
+ if ( name != null ) {
+ if ( name.startsWith("/") )
+ name = name.substring(1);
+ url = url + "/"+name;
+ }
+
+ JsonValue v = httpPostRtnJSON(url+"?interval="+millis);
+ String id = v.getAsObject().getString("taskId");
+ return id;
+ }
+
+ private void checkTask(String x) {
+ String url = urlRoot()+"$/tasks/"+x;
+ JsonValue v = httpGetJson(url);
+ checkTask(v);
+ }
+
+ private void checkTask(JsonValue v) {
+ assertNotNull(v);
+ assertTrue(v.isObject());
+ //System.out.println(v);
+ JsonObject obj = v.getAsObject();
+ try {
+ assertTrue(obj.hasKey("task"));
+ assertTrue(obj.hasKey("taskId"));
+ // Not present until it runs : "started"
+ } catch (AssertionError ex) {
+ System.out.println(obj);
+ throw ex;
+ }
+ }
+
+ private void checkInTasks(String x) {
+ String url = urlRoot()+"$/tasks";
+ JsonValue v = httpGetJson(url);
+ assertTrue(v.isArray());
+ JsonArray array = v.getAsArray();
+ int found = 0;
+ for ( int i = 0; i < array.size(); i++ ) {
+ JsonValue jv = array.get(i);
+ assertTrue(jv.isObject());
+ JsonObject obj = jv.getAsObject();
+ checkTask(obj);
+ if ( obj.getString("taskId").equals(x) ) {
+ found++;
+ }
+ }
+ assertEquals(1, found, "Occurrence of taskId count");
+ }
+
+ private List<String> runningTasks(String... x) {
+ String url = urlRoot()+"$/tasks";
+ JsonValue v = httpGetJson(url);
+ assertTrue(v.isArray());
+ JsonArray array = v.getAsArray();
+ List<String> running = new ArrayList<>();
+ for ( int i = 0; i < array.size(); i++ ) {
+ JsonValue jv = array.get(i);
+ assertTrue(jv.isObject());
+ JsonObject obj = jv.getAsObject();
+ if ( isRunning(obj) )
+ running.add(obj.getString("taskId"));
+ }
+ return running;
+ }
+
+ /**
+ * Wait for tasks to all finish.
+ * Algorithm: wait for {@code pause}, then start polling for upto {@code maxWaitMillis}.
+ * Intervals in milliseconds.
+ * @param pauseMillis
+ * @param pollInterval
+ * @param maxWaitMillis
+ * @return
+ */
+ private boolean waitForTasksToFinish(int pauseMillis, int pollInterval, int maxWaitMillis) {
+ // Wait for them to finish.
+ // Divide into chunks
+ if ( pauseMillis > 0 )
+ Lib.sleep(pauseMillis);
+ long start = System.currentTimeMillis();
+ long endTime = start + maxWaitMillis;
+ final int intervals = maxWaitMillis/pollInterval;
+ long now = start;
+ for (int i = 0 ; i < intervals ; i++ ) {
+ // May have waited (much) longer than the pollInterval : heavily loaded build systems.
+ if ( now-start > maxWaitMillis )
+ break;
+ List<String> x = runningTasks();
+ if ( x.isEmpty() )
+ return true;
+ Lib.sleep(pollInterval);
+ now = System.currentTimeMillis();
+ }
+ return false;
+ }
+
+ private boolean isRunning(JsonObject taskObj) {
+ checkTask(taskObj);
+ return taskObj.hasKey("started") && ! taskObj.hasKey("finished");
+ }
+
+ private void askPing(String name) {
+ if ( name.startsWith("/") )
+ name = name.substring(1);
+ try ( TypedInputStream in = httpGet(urlRoot()+name+"/sparql?query=ASK%7B%7D") ) {
+ IO.skipToEnd(in);
+ }
+ }
+
+ private void adminPing(String name) {
+ try ( TypedInputStream in = httpGet(urlRoot()+"$/"+opDatasets+"/"+name) ) {
+ IO.skipToEnd(in);
+ }
+ }
+
+ private void checkExists(String name) {
+ adminPing(name);
+ askPing(name);
+ }
+
+ private void checkExistsNotActive(String name) {
+ adminPing(name);
+ try { askPing(name);
+ fail("askPing did not cause an Http Exception");
+ } catch ( HttpException ex ) {}
+ JsonValue v = getDatasetDescription(name);
+ assertFalse(v.getAsObject().get("ds.state").getAsBoolean().value());
+ }
+
+ private void checkNotThere(String name) {
+ String n = (name.startsWith("/")) ? name.substring(1) : name;
+ // Check gone exists.
+ HttpTest.expect404(()-> adminPing(n) );
+ HttpTest.expect404(() -> askPing(n) );
+ }
+
+ private void checkJsonDatasetsAll(JsonValue v) {
+ assertNotNull(v.getAsObject().get("datasets"));
+ JsonArray a = v.getAsObject().get("datasets").getAsArray();
+ for ( JsonValue v2 : a )
+ checkJsonDatasetsOne(v2);
+ }
+
+ private void checkJsonDatasetsOne(JsonValue v) {
+ assertTrue(v.isObject());
+ JsonObject obj = v.getAsObject();
+ assertNotNull(obj.get("ds.name"));
+ assertNotNull(obj.get("ds.services"));
+ assertNotNull(obj.get("ds.state"));
+ assertTrue(obj.get("ds.services").isArray());
+ }
+
+ private void checkJsonStatsAll(JsonValue v) {
+ assertNotNull(v.getAsObject().get("datasets"));
+ JsonObject a = v.getAsObject().get("datasets").getAsObject();
+ for ( String dsname : a.keys() ) {
+ JsonValue obj = a.get(dsname).getAsObject();
+ checkJsonStatsOne(obj);
+ }
+ }
+
+ private void checkJsonStatsOne(JsonValue v) {
+ checkJsonStatsCounters(v);
+ JsonObject obj1 = v.getAsObject().get("endpoints").getAsObject();
+ for ( String srvName : obj1.keys() ) {
+ JsonObject obj2 = obj1.get(srvName).getAsObject();
+ assertTrue(obj2.hasKey("description"));
+ assertTrue(obj2.hasKey("operation"));
+ checkJsonStatsCounters(obj2);
+ }
+ }
+
+ private void checkJsonStatsCounters(JsonValue v) {
+ JsonObject obj = v.getAsObject();
+ assertTrue(obj.hasKey("Requests"));
+ assertTrue(obj.hasKey("RequestsGood"));
+ assertTrue(obj.hasKey("RequestsBad"));
+ }
+
+ private JsonValue execGetJSON(String url) {
+ try ( TypedInputStream in = httpGet(url) ) {
+ assertEqualsIgnoreCase(WebContent.contentTypeJSON, in.getContentType());
+ return JSON.parse(in);
+ }
+ }
+
+ private JsonValue execPostJSON(String url) {
+ try ( TypedInputStream in = httpPostStream(url, null, null, null) ) {
+ assertEqualsIgnoreCase(WebContent.contentTypeJSON, in.getContentType());
+ return JSON.parse(in);
+ }
+ }
+
+ /*
+ GET /$/ping
+ POST /$/ping
+ POST /$/datasets/
+ GET /$/datasets/
+ DELETE /$/datasets/*{name}*
+ GET /$/datasets/*{name}*
+ POST /$/datasets/*{name}*?state=offline
+ POST /$/datasets/*{name}*?state=active
+ POST /$/backup/*{name}*
+ POST /$/compact/*{name}*
+ GET /$/server
+ POST /$/server/shutdown
+ GET /$/stats/
+ GET /$/stats/*{name}*
+ */
+}
+
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestFusekiReload.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestFusekiReload.java
new file mode 100644
index 0000000000..ee68e96df1
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestFusekiReload.java
@@ -0,0 +1,169 @@
+/*
+ * 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.mod.admin;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.jena.atlas.io.IOX;
+import org.apache.jena.atlas.logging.LogCtl;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.FusekiTestLib;
+import org.apache.jena.fuseki.mgt.ActionReload;
+import org.apache.jena.http.HttpOp;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.riot.RDFParser;
+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.sparql.exec.QueryExec;
+import org.apache.jena.sparql.exec.RowSetOps;
+import org.apache.jena.sparql.exec.http.QueryExecHTTP;
+
+public class TestFusekiReload {
+
+ private static Path fConfigServer = Path.of("target/config-reload.ttl");
+ private static Path DIR = Path.of("testing/Config/");
+ private static Path fConfig1 = DIR.resolve("reload-config1.ttl");
+ private static Path fConfig2 = DIR.resolve("reload-config2.ttl");
+
+ @Before public void before() {
+ // Initial state
+ copyFile(fConfig1, fConfigServer);
+ }
+
+ @AfterClass public static void after() {
+ try {
+ Files.delete(fConfigServer);
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+
+ @Test public void serverReload_1() {
+ FusekiServer server = server(fConfigServer);
+ try {
+ server.start();
+
+ serverBeforeReload(server);
+
+ // Change configuration file.
+ copyFile(fConfig2, fConfigServer);
+ Model newConfig = RDFParser.source(fConfigServer).toModel();
+
+ // Reload operation on the server
+ HttpOp.httpPost("http://localhost:"+server.getPort()+"/$/reload");
+
+ serverAfterReload(server);
+ }
+ finally { server.stop(); }
+ }
+
+ @Test public void serverReload_2() {
+ FusekiServer server = serverNoConfigFile();
+ try {
+ server.start();
+
+ serverBeforeReload(server);
+
+ // Change configuration file.
+ // Error!
+ copyFile(fConfig2, fConfigServer);
+ Model newConfig = RDFParser.source(fConfigServer).toModel();
+
+ LogCtl.withLevel(Fuseki.serverLog, "ERROR", ()->
+ // Poke server - Operation denied - no configuration file.
+ FusekiTestLib.expect400(()->HttpOp.httpPost("http://localhost:"+server.getPort()+"/$/reload"))
+ );
+
+ // No change
+ serverBeforeReload(server);
+ }
+ finally { server.stop(); }
+ }
+
+ private static void copyFile(Path pathSrc, Path pathDest) {
+ try {
+ Files.copy(pathSrc, pathDest, StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException ex) { throw IOX.exception(ex); }
+ }
+
+ private static void serverBeforeReload(FusekiServer server) {
+ String URL = "http://localhost:"+server.getPort()+"/$/ping";
+ String x = HttpOp.httpGetString(URL);
+ query(server, "/ds", "SELECT * { }", 200);
+ query(server, "/dataset2", "SELECT * { ?s ?p ?o }", 404);
+ query(server, "/zero", "SELECT * { }", 404);
+ query(server, "/codedsg", "SELECT * { }", 200);
+ }
+
+ private static void serverAfterReload(FusekiServer server) {
+ String URL = "http://localhost:"+server.getPort()+"/$/ping";
+ String x = HttpOp.httpGetString(URL);
+ query(server, "/ds", "SELECT * { }", 404);
+ query(server, "/dataset2", "SELECT * { ?s ?p ?o }", 200);
+ query(server, "/zero", "SELECT * { }", 404);
+ // Replaced.
+ query(server, "/codedsg", "SELECT * { }", 404);
+ }
+
+ private static void query(FusekiServer server, String datasetName, String queryString, int expectedStatusCode) {
+ QueryExec qExec = QueryExecHTTP.service(server.datasetURL(datasetName)).query(queryString).build();
+ try {
+ RowSetOps.consume(qExec.select());
+ assertEquals(datasetName, expectedStatusCode, 200);
+ } catch (QueryExceptionHTTP ex) {
+ assertEquals(datasetName, expectedStatusCode, ex.getStatusCode());
+ }
+ }
+
+ private static FusekiServer serverNoConfigFile() {
+ DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+ FusekiServer server = FusekiServer.create().port(0)
+ // .verbose(true)
+ .addServlet("/$/reload", new ActionReload())
+ .add("/ds", dsg)
+ .add("/codedsg", dsg)
+ .build();
+ return server;
+ }
+
+
+ private static FusekiServer server(Path fConfig) {
+ DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+ FusekiServer server = FusekiServer.create().port(0)
+ // .verbose(true)
+ .addServlet("/$/reload", new ActionReload())
+ .parseConfigFile(fConfig)
+ .add("/codedsg", dsg)
+ .build();
+ return server;
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestTemplateAddDataset.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestTemplateAddDataset.java
new file mode 100644
index 0000000000..975876095d
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestTemplateAddDataset.java
@@ -0,0 +1,183 @@
+/**
+ * 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.mod.admin;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.*;
+
+import org.apache.jena.atlas.lib.FileOps;
+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.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.sys.FusekiModules;
+import org.apache.jena.fuseki.mgt.FusekiApp;
+import org.apache.jena.http.HttpOp;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.rdfconnection.RDFConnection;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.exec.http.Params;
+import org.apache.jena.web.HttpSC;
+import org.awaitility.Awaitility;
+
+/**
+ * Tests of the admin functionality on an empty server and using the template mechanism.
+ * See also {@link TestAdmin}.
+ */
+public class TestTemplateAddDataset {
+
+ // One server for all tests
+ private static String serverURL = null;
+ private static FusekiServer server = null;
+
+ @BeforeAll public static void startServer() {
+ System.setProperty("FUSEKI_BASE", "target/run");
+ FileOps.clearAll("target/run");
+
+ server = createServerForTest();
+ serverURL = server.serverURL();
+ }
+
+ // Exactly the module under test
+ private static FusekiModules moduleSetup() {
+ return FusekiModules.create(FMod_Admin.create());
+ }
+
+ private static FusekiServer createServerForTest() {
+ FusekiModules modules = moduleSetup();
+ DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+ FusekiServer testServer = FusekiServer.create()
+ .fusekiModules(modules)
+ .port(0)
+ .build()
+ .start();
+ return testServer;
+ }
+
+ @AfterAll public static void stopServer() {
+ if ( server != null )
+ server.stop();
+ serverURL = null;
+ // Clearup FMod_Shiro.
+ System.getProperties().remove(FusekiApp.envFusekiShiro);
+ }
+
+ protected String urlRoot() {
+ return serverURL;
+ }
+
+ protected String adminURL() {
+ return serverURL+"$/";
+ }
+
+ @BeforeEach public void setLogging() {
+ LogCtl.setLevel(Fuseki.backupLogName, "ERROR");
+ LogCtl.setLevel(Fuseki.compactLogName,"ERROR");
+ Awaitility.setDefaultPollDelay(20,TimeUnit.MILLISECONDS);
+ Awaitility.setDefaultPollInterval(50,TimeUnit.MILLISECONDS);
+ }
+
+ @AfterEach public void unsetLogging() {
+ LogCtl.setLevel(Fuseki.backupLogName, "WARN");
+ LogCtl.setLevel(Fuseki.compactLogName,"WARN");
+ }
+
+ @Order(value = 1)
+ @Test public void add_delete_api_1() throws Exception {
+ if ( org.apache.jena.tdb1.sys.SystemTDB.isWindows )
+ return;
+ testAddDelete("db_mem", "mem", false, false);
+ }
+
+ @Order(value = 2)
+ @Test public void add_delete_api_2() throws Exception {
+ if ( org.apache.jena.tdb1.sys.SystemTDB.isWindows )
+ return;
+ // This should fail.
+ HttpException ex = assertThrows(HttpException.class, ()->testAddDelete("db_mem", "mem", true, false));
+ // 409 conflict - "a request conflicts with the current state of the target resource."
+ // and the target resource is the container "/$/datasets"
+ assertEquals(HttpSC.CONFLICT_409, ex.getStatusCode());
+ }
+
+ private void testAddDelete(String dbName, String dbType, boolean alreadyExists, boolean hasFiles) {
+ String datasetURL = server.datasetURL(dbName);
+ Params params = Params.create().add("dbName", dbName).add("dbType", dbType);
+
+ if ( alreadyExists )
+ assertTrue(exists(datasetURL));
+ else
+ assertFalse(exists(datasetURL));
+
+ // Use the template
+ HttpOp.httpPostForm(adminURL()+"datasets", params);
+
+ RDFConnection conn = RDFConnection.connect(server.datasetURL(dbName));
+ conn.update("INSERT DATA { <x:s> <x:p> 123 }");
+ int x1 = count(conn);
+ assertEquals(1, x1);
+
+ Path pathDB = FusekiApp.dirDatabases.resolve(dbName);
+
+ if ( hasFiles )
+ assertTrue(Files.exists(pathDB));
+
+ HttpOp.httpDelete(adminURL()+"datasets/"+dbName);
+
+ assertFalse(exists(datasetURL));
+
+ //if ( hasFiles )
+ assertFalse(Files.exists(pathDB));
+
+ // Recreate : no contents.
+ HttpOp.httpPostForm(adminURL()+"datasets", params);
+ assertTrue(exists(datasetURL), ()->"false: exists("+datasetURL+")");
+ int x2 = count(conn);
+ assertEquals(0, x2);
+ if ( hasFiles )
+ assertTrue(Files.exists(pathDB));
+ }
+
+ private static boolean exists(String url) {
+ try ( TypedInputStream in = HttpOp.httpGet(url) ) {
+ return true;
+ } catch (HttpException ex) {
+ if ( ex.getStatusCode() == HttpSC.NOT_FOUND_404 )
+ return false;
+ throw ex;
+ }
+ }
+
+ static int count(RDFConnection conn) {
+ try ( QueryExecution qExec = conn.query("SELECT (count(*) AS ?C) { ?s ?p ?o }")) {
+ return qExec.execSelect().next().getLiteral("C").getInt();
+ }
+ }
+}
+
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/metrics/TestModPrometheus.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/metrics/TestModPrometheus.java
new file mode 100644
index 0000000000..ce3be524fb
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/metrics/TestModPrometheus.java
@@ -0,0 +1,77 @@
+/*
+ * 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.mod.metrics;
+
+import static org.apache.jena.http.HttpLib.handleResponseRtnString;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.InputStream;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.sys.FusekiModules;
+import org.apache.jena.fuseki.mod.prometheus.FMod_Prometheus;
+import org.apache.jena.http.HttpEnv;
+import org.apache.jena.http.HttpLib;
+import org.apache.jena.riot.WebContent;
+import org.apache.jena.riot.web.HttpNames;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+
+public class TestModPrometheus {
+
+ private FusekiServer testServer = null;
+
+ @BeforeEach void setupServer() {
+ DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+ FusekiModules fusekiModules = FusekiModules.create(FMod_Prometheus.get());
+ testServer = FusekiServer.create()
+ .add("/ds", dsg)
+ .enableMetrics(false) // N.B. false. Instead, use module to setup.
+ .fusekiModules(fusekiModules)
+ .build();
+ testServer.start();
+ }
+
+ @AfterEach void teardownServer() {
+ if ( testServer != null )
+ testServer.stop();
+ }
+
+ @Test
+ public void can_retrieve_metrics() {
+ String metricsURL = testServer.serverURL()+"$/metrics";
+ HttpRequest request = HttpRequest.newBuilder().uri(HttpLib.toRequestURI(metricsURL)).build();
+ HttpResponse<InputStream> response = HttpLib.executeJDK(HttpEnv.getDftHttpClient(), request, BodyHandlers.ofInputStream());
+ String body = handleResponseRtnString(response);
+
+ String ct = response.headers().firstValue(HttpNames.hContentType).orElse(null);
+ assertNotNull(ct, "No Content-Type");
+ assertTrue(ct.contains(WebContent.contentTypeTextPlain));
+ assertTrue(ct.contains(WebContent.charsetUTF8));
+ assertTrue(body.contains("fuseki_requests_good"));
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/shiro/TestModShiro.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/shiro/TestModShiro.java
new file mode 100644
index 0000000000..dafc636f78
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/shiro/TestModShiro.java
@@ -0,0 +1,220 @@
+/*
+ * 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.mod.shiro;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.net.Authenticator;
+import java.net.http.HttpClient;
+import java.util.regex.Pattern;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.jena.atlas.net.Host;
+import org.apache.jena.atlas.web.HttpException;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.cmds.FusekiMain;
+import org.apache.jena.fuseki.main.sys.FusekiModule;
+import org.apache.jena.fuseki.main.sys.FusekiModules;
+import org.apache.jena.fuseki.mgt.FusekiApp;
+import org.apache.jena.fuseki.system.FusekiLogging;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.http.HttpEnv;
+import org.apache.jena.http.HttpOp;
+import org.apache.jena.http.auth.AuthEnv;
+import org.apache.jena.http.auth.AuthLib;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.exec.http.GSP;
+import org.apache.jena.sparql.exec.http.QueryExecHTTP;
+
+public class TestModShiro {
+ static final String unlocal = Host.getHostAddress();
+ static final String localRE = Pattern.quote("localhost");
+
+ static {
+ FusekiLogging.setLogging();
+ // Incase it is finding file:log4j.properties first.
+// LogCtl.disable(Fuseki.serverLog);
+// LogCtl.disable(Fuseki.actionLog);
+// LogCtl.disable(FMod_Shiro.shiroConfigLog);
+ }
+
+ @BeforeEach void before() {
+ System.getProperties().remove(FusekiApp.envFusekiShiro);
+ AuthEnv.get().clearAuthEnv();
+ }
+
+ @AfterEach void after() {
+ AuthEnv.get().clearAuthEnv();
+ }
+
+ @AfterAll static void afterAll() {
+ System.getProperties().remove(FusekiApp.envFusekiShiro);
+ }
+
+ private String unlocalhost(FusekiServer server, String dataset) {
+ String local = server.datasetURL(dataset);
+ if ( unlocal != null )
+ local = local.replaceFirst(localRE, unlocal);
+ return local;
+ }
+
+ /** Builder for a server with Shiro */
+ private FusekiServer.Builder serverBuilderWithShiro(String filename) {
+ System.getProperties().setProperty(FusekiApp.envFusekiShiro, filename);
+ FusekiModules modules = FusekiModules.create(FMod_Shiro.create());
+ return FusekiServer.create()
+ .port(0)
+ .fusekiModules(modules);
+ }
+
+ @Test public void access_localhost() {
+ DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+ FusekiModules modules = FusekiModules.create(FMod_Shiro.create());
+ FusekiServer server = serverBuilderWithShiro("testing/Shiro/shiro_localhost.ini")
+ .add("/local/ds", dsg)
+ .add("/public/ds", dsg)
+ .build();
+ server.start();
+ String dsPublic = "/public/ds";
+ String dsLocal = "/local/ds";
+ try {
+ attemptByAddr(server, dsPublic);
+ HttpException httpEx = assertThrows(HttpException.class, ()->attemptByAddr(server, dsLocal));
+ assertEquals(403, httpEx.getStatusCode(), "Expected HTTP 403");
+
+ attemptByLocalhost(server, dsLocal);
+ } finally {
+ server.stop();
+ AuthEnv.get().clearAuthEnv();
+ }
+ }
+
+ @Test public void access_userPassword() {
+ String dsname = "/ds";
+ DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+ FusekiServer server = serverBuilderWithShiro("testing/Shiro/shiro_userpassword.ini")
+ .add(dsname, dsg)
+ .enablePing(true)
+ .build();
+ server.start();
+
+ String URL = server.datasetURL(dsname);
+
+ try {
+ // No user-password
+ {
+ HttpException httpEx = assertThrows(HttpException.class, ()->attemptByLocalhost(server, dsname));
+ assertEquals(401, httpEx.getStatusCode(), "Expected HTTP 401");
+ }
+
+ // user-password via authenticator: localhost
+ {
+ Authenticator authenticator = AuthLib.authenticator("user1", "passwd1");
+ HttpClient httpClient = HttpEnv.httpClientBuilder().authenticator(authenticator).build();
+ attemptByLocalhost(server, httpClient, dsname);
+ // and a SPARQL query
+ QueryExecHTTP.service(URL).httpClient(httpClient).query("ASK{}").ask();
+ }
+
+ // user-password via registration
+ {
+ AuthEnv.get().registerUsernamePassword(server.serverURL(), "user1", "passwd1");
+ attemptByLocalhost(server, dsname);
+ AuthEnv.get().unregisterUsernamePassword(server.serverURL());
+ }
+
+ // try the ping (proxy for admin operations)
+ {
+ Authenticator authenticator = AuthLib.authenticator("admin", "pw");
+ HttpClient httpClient = HttpEnv.httpClientBuilder().authenticator(authenticator).build();
+ HttpOp.httpGetString(httpClient, server.serverURL()+"$/ping");
+ AuthEnv.get().unregisterUsernamePassword(server.serverURL());
+ }
+
+ {
+ // Bad password
+ AuthEnv.get().registerUsernamePassword(server.serverURL(), "user1", "passwd2");
+ HttpException httpEx = assertThrows(HttpException.class, ()->attemptByLocalhost(server, dsname));
+ assertEquals(401, httpEx.getStatusCode(), "Expected HTTP 401");
+ AuthEnv.get().unregisterUsernamePassword(server.serverURL());
+ }
+
+ } finally {
+ server.stop();
+ AuthEnv.get().clearAuthEnv();
+ }
+ }
+
+ @Test public void shiroByCommandLine() {
+ String dsname = "/ds";
+ FusekiModule fmod = FMod_Shiro.create();
+ FusekiMain.addCustomiser(fmod);
+
+ // And also a module!
+ FusekiServer server = FusekiMain.builder("--port=0", "--shiro=testing/Shiro/shiro_userpassword.ini", "--mem", dsname)
+ // Must be same instance.
+ .fusekiModules(FusekiModules.create(fmod))
+ .build();
+ server.start();
+ try {
+ // No user-password
+ HttpException httpEx = assertThrows(HttpException.class, ()->attemptByLocalhost(server, dsname));
+ assertEquals(401, httpEx.getStatusCode(), "Expected HTTP 401");
+ } finally { server.stop(); }
+ FusekiMain.resetCustomisers();
+ }
+
+ // ----------------------------
+
+ // TEST **** command line
+
+ private void attemptByAddr(FusekiServer server, String dsname) {
+ attemptByAddr(server, null, dsname);
+ }
+
+ private void attemptByAddr(FusekiServer server, HttpClient httpClient, String dsname) {
+ String URL = server.datasetURL(dsname);
+ String URLip = unlocalhost(server, dsname);
+ attempt(URLip, httpClient);
+ }
+
+ private void attemptByLocalhost(FusekiServer server, String dsname) {
+ attemptByLocalhost(server, null, dsname);
+ }
+
+ private void attemptByLocalhost(FusekiServer server, HttpClient httpClient, String dsname) {
+ String URL = server.datasetURL(dsname);
+ attempt(URL, httpClient);
+ }
+
+ private void attempt(String URL, HttpClient httpClient) {
+ GSP gsp = GSP.service(URL).defaultGraph();
+ if ( httpClient != null )
+ gsp.httpClient(httpClient);
+ Graph g = gsp.GET();
+ assertNotNull(g);
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/resources/log4j2-test.properties b/jena-fuseki2/jena-fuseki-main/src/test/resources/log4j2-test.properties
index ac4c56bbcd..26b8c4b2e2 100644
--- a/jena-fuseki2/jena-fuseki-main/src/test/resources/log4j2-test.properties
+++ b/jena-fuseki2/jena-fuseki-main/src/test/resources/log4j2-test.properties
@@ -25,18 +25,8 @@ logger.arq-exec.level = INFO
logger.fuseki.name = org.apache.jena.fuseki
logger.fuseki.level = WARN
-## Some tests generate request WARNings.
logger.fuseki-fuseki.name = org.apache.jena.fuseki.Fuseki
-logger.fuseki-fuseki.level = ERROR
-
-#logger.fuseki-server.name = org.apache.jena.fuseki.Server
-#logger.fuseki-server.level = INFO
-#
-#logger.fuseki-admin.name = org.apache.jena.fuseki.Admin
-#logger.fuseki-admin.level = INFO
-#
-#logger.fuseki-compact.name = org.apache.jena.fuseki.Compact
-#logger.fuseki-compact.level = WARN
+logger.fuseki-fuseki.level = WARN
logger.fuseki-autoload.name = org.apache.jena.fuseki.main.sys.FusekiAutoModules
logger.fuseki-autoload.level = ERROR
@@ -47,6 +37,9 @@ logger.http.level = INFO
logger.riot.name = org.apache.jena.riot
logger.riot.level = INFO
+logger.riot.name = org.apache.shiro
+logger.riot.level = WARN
+
logger.jetty.name = org.eclipse.jetty
logger.jetty.level = WARN
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-bad-name-1.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-bad-name-1.ttl
new file mode 100644
index 0000000000..05640fd0cf
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-bad-name-1.ttl
@@ -0,0 +1,15 @@
+@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#> .
+
+<#service1> rdf:type fuseki:Service ;
+ # URI of the dataset -- http://host:port/ds
+ fuseki:name "" ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:dataset <#emptyDataset> ;
+ .
+
+<#emptyDataset> rdf:type ja:RDFDataset .
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-bad-name-2.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-bad-name-2.ttl
new file mode 100644
index 0000000000..27df06bcb4
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-bad-name-2.ttl
@@ -0,0 +1,15 @@
+@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#> .
+
+<#service1> rdf:type fuseki:Service ;
+ # URI of the dataset -- http://host:port/ds
+ fuseki:name " " ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:dataset <#emptyDataset> ;
+ .
+
+<#emptyDataset> rdf:type ja:RDFDataset .
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-bad-name-3.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-bad-name-3.ttl
new file mode 100644
index 0000000000..19caef108b
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-bad-name-3.ttl
@@ -0,0 +1,15 @@
+@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#> .
+
+<#service1> rdf:type fuseki:Service ;
+ # URI of the dataset -- http://host:port/ds
+ fuseki:name "ABC DEF" ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:dataset <#emptyDataset> ;
+ .
+
+<#emptyDataset> rdf:type ja:RDFDataset .
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-bad-name-4.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-bad-name-4.ttl
new file mode 100644
index 0000000000..f57840e418
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-bad-name-4.ttl
@@ -0,0 +1,15 @@
+@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#> .
+
+<#service1> rdf:type fuseki:Service ;
+ # URI of the dataset -- http://host:port/ds
+ fuseki:name "/ABC DEF " ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:dataset <#emptyDataset> ;
+ .
+
+<#emptyDataset> rdf:type ja:RDFDataset .
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-inf.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-inf.ttl
new file mode 100644
index 0000000000..b75cf18ce3
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-inf.ttl
@@ -0,0 +1,30 @@
+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#>
+
+<#service2> rdf:type fuseki:Service ;
+ fuseki:name "test-ds4" ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:dataset <#infDataset> ;
+ .
+
+<#infDataset> rdf:type ja:RDFDataset ;
+ ja:defaultGraph <#infGraph> ;
+ .
+
+<#infGraph> a ja:InfModel;
+ ja:reasoner [ja:reasonerURL <http://jena.hpl.hp.com/2003/OWLFBRuleReasoner>] ;
+ ja:baseModel <#tdb_graph> ;
+ .
+
+<#tdb_graph> rdf:type tdb2:GraphTDB ;
+ tdb2:dataset <#tdb_dataset> ;
+ .
+
+<#tdb_dataset> rdf:type tdb2:DatasetTDB ;
+ tdb2:location "--mem--" ;
+ .
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-plain-1.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-plain-1.ttl
new file mode 100644
index 0000000000..f993f350fd
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-plain-1.ttl
@@ -0,0 +1,17 @@
+## 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#>
+
+<#service1> #rdf:type fuseki:Service ;
+ # URI of the dataset -- http://host:port/ds
+ fuseki:name "test-ds1" ;
+ fuseki:serviceQuery "sparql" ;
+ fuseki:dataset <#emptyDataset> ;
+ .
+
+<#emptyDataset> rdf:type ja:RDFDataset .
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-plain-2.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-plain-2.ttl
new file mode 100644
index 0000000000..d4b3fc9022
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/Config/config-ds-plain-2.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#>
+
+<#service1> rdf:type fuseki:Service ;
+ fuseki:name "test-ds2" ;
+ fuseki:endpoint [
+ fuseki:operation fuseki:query ;
+ fuseki:name "sparql"
+ ] ;
+ fuseki:dataset <#emptyDataset> ;
+ .
+
+<#emptyDataset> rdf:type ja:MemoryDataset .
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2a.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2a.ttl
new file mode 100644
index 0000000000..6ad26b9835
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2a.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 ;
+ fuseki:name "test-tdb2a" ;
+ fuseki:endpoint [ fuseki:name "sparql" ;
+ fuseki:operation fuseki:query ] ;
+ fuseki:dataset <#dataset> .
+
+<#dataset> rdf:type tdb2:DatasetTDB2 ;
+ tdb2:location "target/tdb2a" .
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2b.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2b.ttl
new file mode 100644
index 0000000000..572927c331
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2b.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 ;
+ fuseki:name "test-tdb2b" ;
+ fuseki:endpoint [ fuseki:name "sparql" ;
+ fuseki:operation fuseki:query ] ;
+ fuseki:dataset <#dataset> .
+
+<#dataset> rdf:type tdb2:DatasetTDB2 ;
+ tdb2:location "target/tdb2b" .
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Shiro/shiro_localhost.ini b/jena-fuseki2/jena-fuseki-main/testing/Shiro/shiro_localhost.ini
new file mode 100644
index 0000000000..2b9cce0c48
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/Shiro/shiro_localhost.ini
@@ -0,0 +1,13 @@
+# Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0
+
+[main]
+localhostFilter = org.apache.jena.fuseki.authz.LocalhostFilter
+
+[users]
+admin=pw
+
+[roles]
+
+[urls]
+/local/** = localhostFilter
+/public/** = anon
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Shiro/shiro_userpassword.ini b/jena-fuseki2/jena-fuseki-main/testing/Shiro/shiro_userpassword.ini
new file mode 100644
index 0000000000..c0b5729726
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/Shiro/shiro_userpassword.ini
@@ -0,0 +1,18 @@
+# Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0
+
+[main]
+plainMatcher=org.apache.shiro.authc.credential.SimpleCredentialsMatcher
+
+[users]
+admin=pw
+
+[roles]
+
+[users]
+admin=pw
+user1=passwd1
+
+[urls]
+/$/ping = authcBasic,user[admin]
+/ds = authcBasic,user[user1]
+/**=anon