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/07/02 09:47:14 UTC
(jena) 06/08: GH-3288: Validate request parameters; refactor tests
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 35350569b4c1fd432d92e7c92af9597c4400debe
Author: Andy Seaborne <an...@apache.org>
AuthorDate: Fri Jun 13 09:08:02 2025 +0100
GH-3288: Validate request parameters; refactor tests
---
.../fuseki/servlets/TestCrossOriginFilterMock.java | 6 +-
.../org/apache/jena/fuseki/mgt/ActionDatasets.java | 304 +++++++---
.../org/apache/jena/fuseki/mgt/FusekiAdmin.java | 18 +-
.../apache/jena/fuseki/mgt/FusekiServerCtl.java | 7 +-
.../org/apache/jena/fuseki/main/FusekiTestLib.java | 4 +
.../org/apache/jena/fuseki/mod/TS_FusekiMods.java | 18 +-
.../jena/fuseki/mod/admin/FusekiServerPerTest.java | 109 ++++
.../fuseki/mod/admin/FusekiServerPerTestClass.java | 138 +++++
.../apache/jena/fuseki/mod/admin/TSMod_Admin.java} | 18 +-
.../apache/jena/fuseki/mod/admin/TestAdmin.java | 617 ++-------------------
.../mod/admin/TestAdminAddDatasetTemplate.java | 241 ++++++++
.../mod/admin/TestAdminAddDatasetsConfigFile.java | 318 +++++++++++
.../fuseki/mod/admin/TestAdminDatabaseOps.java | 484 ++++++++++++++++
.../fuseki/mod/admin/TestTemplateAddDataset.java | 183 ------
.../testing/Config/config-tdb2b.ttl | 2 +-
.../Config/{config-tdb2b.ttl => config-tdb2c.ttl} | 3 +-
16 files changed, 1600 insertions(+), 870 deletions(-)
diff --git a/jena-fuseki2/jena-fuseki-core/src/test/java/org/apache/jena/fuseki/servlets/TestCrossOriginFilterMock.java b/jena-fuseki2/jena-fuseki-core/src/test/java/org/apache/jena/fuseki/servlets/TestCrossOriginFilterMock.java
index e0d57be7f6..4329902347 100644
--- a/jena-fuseki2/jena-fuseki-core/src/test/java/org/apache/jena/fuseki/servlets/TestCrossOriginFilterMock.java
+++ b/jena-fuseki2/jena-fuseki-core/src/test/java/org/apache/jena/fuseki/servlets/TestCrossOriginFilterMock.java
@@ -23,8 +23,8 @@ import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.Collections;
@@ -73,7 +73,7 @@ public class TestCrossOriginFilterMock {
HttpServletResponse httpServletResponse = mock(HttpServletResponse.class);
FilterChain chain = mock(FilterChain.class);
- @Before
+ @BeforeEach
public void setUpTest() {
when(httpServletRequest.getHeader("Origin")).thenReturn("http://localhost:12335");
when(httpServletRequest.getHeaders("Connection")).thenReturn(Collections.emptyEnumeration());
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
index 0bc1a2c74b..5736235c3f 100644
--- 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
@@ -20,8 +20,6 @@ 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;
@@ -35,25 +33,28 @@ 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.lib.NotImplemented;
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.dboe.base.file.Location;
+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.ctl.ActionContainerItem;
import org.apache.jena.fuseki.ctl.JsonDescription;
import org.apache.jena.fuseki.metrics.MetricsProvider;
-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.server.*;
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.Graph;
import org.apache.jena.graph.Node;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.QueryFactory;
import org.apache.jena.rdf.model.*;
+import org.apache.jena.rdf.model.impl.Util;
import org.apache.jena.riot.*;
import org.apache.jena.riot.system.StreamRDF;
import org.apache.jena.riot.system.StreamRDFLib;
@@ -61,15 +62,17 @@ 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.exec.QueryExec;
+import org.apache.jena.sparql.exec.RowSet;
import org.apache.jena.sparql.util.FmtUtils;
+import org.apache.jena.system.G;
+import org.apache.jena.tdb1.TDB1;
+import org.apache.jena.tdb2.TDB2;
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";
@@ -121,43 +124,55 @@ public class ActionDatasets extends ActionContainerItem {
ServletOps.errorBadRequest("Bad request - Content-Type or both parameters dbName and dbType required");
boolean succeeded = false;
- String systemFileCopy = null;
+ // Used in clear-up.
String configFile = null;
+ String systemFileCopy = null;
+ FusekiServerCtl serverCtl = FusekiServerCtl.get(action.getServletContext());
DatasetDescriptionMap registry = new DatasetDescriptionMap();
- synchronized (FusekiAdmin.systemLock) {
+ synchronized (serverCtl.getServerlock()) {
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);
+ // Get the request input.
+ Model modelFromRequest = ModelFactory.createDefaultModel();
+ StreamRDF dest = StreamRDFLib.graph(modelFromRequest.getGraph());
- // ----
- // Keep a persistent copy immediately. This is not used for
- // anything other than being "for the record".
- systemFileCopy = FusekiServerCtl.dirSystemFileArea.resolve(uuid.toString()).toString();
- try ( OutputStream outCopy = IO.openOutputFile(systemFileCopy) ) {
- RDFDataMgr.write(outCopy, descriptionModel, Lang.TURTLE);
+ boolean templatedRequest = false;
+
+ try {
+ if ( hasParams || WebContent.isHtmlForm(ct) ) {
+ assemblerFromForm(action, dest);
+ templatedRequest = true;
+ // dbName, dbType
+ } else if ( WebContent.isMultiPartForm(ct) ) {
+ // Cannot be enabled.
+ ServletOps.errorBadRequest("Service configuration from a multipart upload not supported");
+ //assemblerFromUpload(action, dest);
+ } else {
+ if ( ! FusekiAdmin.allowConfigFiles() )
+ ServletOps.errorBadRequest("Service configuration from an upload file not supported");
+ assemblerFromBody(action, dest);
+ }
+ } catch (RiotException ex) {
+ ActionLib.consumeBody(action);
+ action.log.warn(format("[%d] Failed to read configuration: %s", action.id, ex.getMessage()));
+ ServletOps.errorBadRequest("Failed to read configuration");
}
// ----
// Add the dataset and graph wiring for assemblers
Model model = ModelFactory.createDefaultModel();
- model.add(descriptionModel);
+ model.add(modelFromRequest);
model = AssemblerUtils.prepareForAssembler(model);
// ----
// Process configuration.
-
// Returns the "service fu:name NAME" statement
Statement stmt = findService(model);
+ if ( stmt == null ) {
+ action.log.warn(format("[%d] No service name", action.id));
+ ServletOps.errorBadRequest(format("No service name"));
+ }
Resource subject = stmt.getSubject();
Literal object = stmt.getObject().asLiteral();
@@ -165,39 +180,85 @@ public class ActionDatasets extends ActionContainerItem {
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.
+ final String datasetPath;
+ {
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));
+ // Various explicit check for better error messages.
+
+ if ( datasetName.isEmpty() ) {
+ action.log.warn(format("[%d] Empty dataset name", action.id));
+ ServletOps.errorBadRequest("Empty dataset name");
+ }
+ if ( StringUtils.isBlank(datasetName) ) {
+ action.log.warn(format("[%d] Whitespace dataset name: '%s'", action.id, datasetName));
+ ServletOps.errorBadRequest(format("Whitespace dataset name: '%s'", datasetName));
+ }
+ if ( datasetName.contains(" ") ) {
+ action.log.warn(format("[%d] Bad dataset name (contains spaces) '%s'", action.id, datasetName));
+ ServletOps.errorBadRequest(format("Bad dataset name (contains spaces) '%s'", datasetName));
+ }
+ if ( datasetName.equals("/") ) {
+ action.log.warn(format("[%d] Bad dataset name '%s'", action.id, datasetName));
+ ServletOps.errorBadRequest(format("Bad dataset name '%s'", datasetName));
+ }
+
+ // The service names must be a valid URI path
+ try {
+ ValidString validServiceName = Validators.serviceName(datasetName);
+ } catch (FusekiConfigException ex) {
+ action.log.warn(format("[%d] Invalid service name: '%s'", action.id, datasetName));
+ ServletOps.error(HttpSC.BAD_REQUEST_400, format("Invalid service name: '%s'", datasetName));
+ }
+
+ // Canonical - starts with "/",does not end in "/"
datasetPath = DataAccessPoint.canonical(datasetName);
- // ---- Check whether it already exists
- if ( action.getDataAccessPointRegistry().isRegistered(datasetPath) )
- ServletOps.error(HttpSC.CONFLICT_409, "Name already registered "+datasetPath);
+
+ // For this operation, check additionally that the path does not go outside the expected file area.
+ // This imposes the path component-only rule and does not allow ".."
+ if ( ! isValidServiceName(datasetPath) ) {
+ action.log.warn(format("[%d] Database service name not acceptable: '%s'", action.id, datasetName));
+ ServletOps.error(HttpSC.BAD_REQUEST_400, format("Database service name not acceptable: '%s'", datasetName));
+ }
+ }
+
+ // ---- Check whether it already exists
+ if ( action.getDataAccessPointRegistry().isRegistered(datasetPath) ) {
+ action.log.warn(format("[%d] Name already registered '%s'", action.id, datasetPath));
+ ServletOps.error(HttpSC.CONFLICT_409, format("Name already registered '%s'", datasetPath));
}
+ // -- Validate any TDB locations.
+ // If this is a templated request, there is no need to do this
+ // because the location is "datasetPath" which has been checked.
+ if ( ! templatedRequest ) {
+ List<String> tdbLocations = tdbLocations(action, model.getGraph());
+ for(String tdbLocation : tdbLocations ) {
+ if ( ! isValidTDBLocation(tdbLocation) ) {
+ action.log.warn(format("[%d] TDB database location not acceptable: '%s'", action.id, tdbLocation));
+ ServletOps.error(HttpSC.BAD_REQUEST_400, format("TDB database location not acceptable: '%s'", tdbLocation));
+ }
+ }
+ }
+
+ // ----
+ // Keep a persistent copy with a globally unique name.
+ // This is not used for anything other than being "for the record".
+ systemFileCopy = FusekiServerCtl.dirSystemFileArea.resolve(uuid.toString()).toString();
+ RDFWriter.source(model).lang(Lang.TURTLE).output(systemFileCopy);
+
+ // ----
action.log.info(format("[%d] Create database : name = %s", action.id, datasetPath));
- configFile = FusekiServerCtl.generateConfigurationFilename(datasetPath);
List<String> existing = FusekiServerCtl.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);
- }
+ configFile = FusekiServerCtl.generateConfigurationFilename(datasetPath);
- // Need to be in Resource space at this point.
+ // ---- Build the service
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);
@@ -205,10 +266,18 @@ public class ActionDatasets extends ActionContainerItem {
return null;
}
dataAccessPoint.getDataService().setEndpointProcessors(action.getOperationRegistry());
- dataAccessPoint.getDataService().goActive();
+
+ // Write to configuration directory.
+ RDFWriter.source(model).lang(Lang.TURTLE).output(configFile);
+
if ( ! datasetPath.equals(dataAccessPoint.getName()) )
FmtLog.warn(action.log, "Inconsistent names: datasetPath = %s; DataAccessPoint name = %s", datasetPath, dataAccessPoint);
+
+ dataAccessPoint.getDataService().goActive();
succeeded = true;
+
+ // At this point, a server restarting will find the new service.
+ // This next line makes it dispatchable in this running server.
action.getDataAccessPointRegistry().register(dataAccessPoint);
// Add to metrics
@@ -218,8 +287,8 @@ public class ActionDatasets extends ActionContainerItem {
action.setResponseContentType(WebContent.contentTypeTextPlain);
ServletOps.success(action);
- } catch (IOException ex) { IO.exception(ex); }
- finally {
+ } finally {
+ // Clear-up on failure.
if ( ! succeeded ) {
if ( systemFileCopy != null ) FileOps.deleteSilent(systemFileCopy);
if ( configFile != null ) FileOps.deleteSilent(configFile);
@@ -229,6 +298,41 @@ public class ActionDatasets extends ActionContainerItem {
}
}
+ /**
+ * Check whether a service name is acceptable.
+ * A service name is used as a filesystem path component,
+ * except it may have a leading "/"., to store the database and the configuration.
+ * <p>
+ * The canonical name for a service (see {@link DataAccessPoint#canonical})
+ * starts with a "/" and this will be added if necessary.
+ */
+ private boolean isValidServiceName(String datasetPath) {
+ // Leading "/" is OK , nowhere else is.
+ int idx = datasetPath.indexOf('/', 1);
+ if ( idx > 0 )
+ return false;
+ // No slash, except maybe at the start so a meaningful use of .. can only be at the start.
+ if ( datasetPath.startsWith("/.."))
+ return false;
+ // Character restrictions done by Validators.serviceName
+ return true;
+ }
+
+ // This works for TDB1 as well.
+ private boolean isValidTDBLocation(String tdbLocation) {
+ Location location = Location.create(tdbLocation);
+ if ( location.isMem() )
+ return true;
+ // No ".."
+ if (tdbLocation.startsWith("..") || tdbLocation.contains("/..") ) {
+ // That test was too strict.
+ List<String> components = FileOps.pathComponents(tdbLocation);
+ if ( components.contains("..") )
+ return false;
+ }
+ return true;
+ }
+
/** 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)
@@ -261,8 +365,11 @@ public class ActionDatasets extends ActionContainerItem {
stmt = stmt3;
}
+ if ( stmt == null )
+ return null;
+
if ( ! stmt.getObject().isLiteral() )
- ServletOps.errorBadRequest("Found "+FmtUtils.stringForRDFNode(stmt.getObject())+" : Service names are strings, then used to build the external URI");
+ ServletOps.errorBadRequest("Found "+FmtUtils.stringForRDFNode(stmt.getObject())+" : Service names are strings, which are then used to build the external URI");
return stmt;
}
@@ -305,13 +412,10 @@ public class ActionDatasets extends ActionContainerItem {
ServletOps.errorNotFound("No such dataset registered: "+name);
boolean succeeded = false;
+ FusekiServerCtl serverCtl = FusekiServerCtl.get(action.getServletContext());
- synchronized(FusekiAdmin.systemLock ) {
+ synchronized(serverCtl.getServerlock()) {
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 )
@@ -319,7 +423,8 @@ public class ActionDatasets extends ActionContainerItem {
// Get a reference before removing.
DataService dataService = ref.getDataService();
- // ---- Make it invisible in this running server.
+
+ // Remove from the registry - operation dispatch will not find it any more.
action.getDataAccessPointRegistry().remove(name);
// Find the configuration.
@@ -327,7 +432,7 @@ public class ActionDatasets extends ActionContainerItem {
List<String> configurationFiles = FusekiServerCtl.existingConfigurationFile(filename);
if ( configurationFiles.isEmpty() ) {
- // ---- Unmanaged
+ // -- 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;
@@ -343,23 +448,22 @@ public class ActionDatasets extends ActionContainerItem {
return;
}
- // ---- Remove managed database.
+ // -- 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.
+ // Delete the database for real only if it is in the server
+ // "run/databases" area. Don't delete databases that reside
+ // elsewhere. We have already deleted the configuration file, so the
+ // databases will not be associated with the server anymore.
@SuppressWarnings("removal")
boolean isTDB1 = org.apache.jena.tdb1.sys.TDBInternal.isTDB1(dataService.getDataset());
boolean isTDB2 = org.apache.jena.tdb2.sys.TDBInternal.isTDB2(dataService.getDataset());
- // This occasionally fails in tests due to outstanding transactions.
try {
dataService.shutdown();
} catch (JenaException ex) {
@@ -368,8 +472,9 @@ public class ActionDatasets extends ActionContainerItem {
// 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.
+ // in predictable, unshared locations on disk.
// There may not be any database files, the in-memory case.
+ // (TDB supports an in-memory mode.)
Path pDatabase = FusekiServerCtl.dirDatabases.resolve(filename);
if ( Files.exists(pDatabase)) {
try {
@@ -406,18 +511,16 @@ public class ActionDatasets extends ActionContainerItem {
}
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");
+ // Test for null, empty or only whitespace.
+ if ( StringUtils.isBlank(dbType) || StringUtils.isBlank(dbName) ) {
+ action.log.warn(format("[%d] Both parameters 'dbName' and 'dbType' required and not be blank", action.id));
+ 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);
+ params.put(Template.NAME, dbName);
FusekiServerCtl serverCtl = FusekiServerCtl.get(action.getServletContext());
if ( serverCtl != null )
@@ -427,8 +530,7 @@ public class ActionDatasets extends ActionContainerItem {
// No return.
}
- //action.log.info(format("[%d] Create database : name = %s, type = %s", action.id, dbName, dbType ));
-
+ // -- Get the template
String template = dbTypeToTemplate.get(dbType.toLowerCase(Locale.ROOT));
if ( template == null ) {
List<String> keys = new ArrayList<>(dbTypeToTemplate.keySet());
@@ -441,7 +543,8 @@ public class ActionDatasets extends ActionContainerItem {
}
private static void assemblerFromUpload(HttpAction action, StreamRDF dest) {
- DataUploader.incomingData(action, dest);
+ throw new NotImplemented();
+ //DataUploader.incomingData(action, dest);
}
// ---- Auxiliary functions
@@ -476,6 +579,49 @@ public class ActionDatasets extends ActionContainerItem {
return;
}
dest.prefix("root", base+"#");
- ActionLib.parseOrError(action, dest, lang, base);
+ ActionLib.parse(action, dest, lang, base);
+ }
+
+ // ---- POST
+
+ private static final String NL = "\n";
+
+ @SuppressWarnings("removal")
+ private static final String queryStringLocations =
+ "PREFIX tdb1: <"+TDB1.namespace+">"+NL+
+ "PREFIX tdb2: <"+TDB2.namespace+">"+NL+
+ """
+ SELECT * {
+ ?x ( tdb2:location | tdb1:location) ?location
+ }
+ """ ;
+
+ private static final Query queryLocations = QueryFactory.create(queryStringLocations);
+
+ private static List<String> tdbLocations(HttpAction action, Graph configGraph) {
+ try ( QueryExec exec = QueryExec.graph(configGraph).query(queryLocations).build() ) {
+ RowSet results = exec.select();
+ List<String> locations = new ArrayList<>();
+ results.forEach(b->{
+ Node loc = b.get("location");
+ String location;
+ if ( loc.isURI() )
+ location = loc.getURI();
+ else if ( Util.isSimpleString(loc) )
+ location = G.asString(loc);
+ else {
+ //action.log.warn(format("[%d] Database location is not a string nor a URI", action.id));
+ // No return
+ ServletOps.errorBadRequest("TDB database location is not a string");
+ location = null;
+ }
+ locations.add(location);
+ });
+ return locations;
+ } catch (Exception ex) {
+ // No return
+ ServletOps.errorBadRequest("TDB database location can not be deterined");
+ return null;
+ }
}
}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiAdmin.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiAdmin.java
index 2481c4f699..9da6e68bf3 100644
--- a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiAdmin.java
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiAdmin.java
@@ -19,5 +19,21 @@
package org.apache.jena.fuseki.mgt;
public class FusekiAdmin {
- public final static Object systemLock = new Object();
+ /**
+ * Control whether to allow creating new dataservices by uploading a config file.
+ * See {@link ActionDatasets}.
+ *
+ */
+ public static final String allowConfigFileProperty = "fuseki:allowAddByConfigFile";
+
+ /**
+ * Return whether to allow service configuration files to be uploaded as a file.
+ * See {@link ActionDatasets}.
+ */
+ public static boolean allowConfigFiles() {
+ String value = System.getProperty(allowConfigFileProperty);
+ if ( value != null )
+ return "true".equals(value);
+ return false;
+ }
}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiServerCtl.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiServerCtl.java
index a255b9b20c..9cc1a4fd43 100644
--- a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiServerCtl.java
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiServerCtl.java
@@ -427,10 +427,12 @@ public class FusekiServerCtl {
}
/** Return the filenames of all matching files in the configuration directory (absolute paths returned ). */
- public static List<String> existingConfigurationFile(String baseFilename) {
+ public static List<String> existingConfigurationFile(String serviceName) {
+ String filename = DataAccessPoint.isCanonical(serviceName) ? serviceName.substring(1) : serviceName;
try {
List<String> paths = new ArrayList<>();
- try (DirectoryStream<Path> stream = Files.newDirectoryStream(FusekiServerCtl.dirConfiguration, baseFilename+".*") ) {
+ // This ".* is a file glob pattern, not a regular expression - it looks for file extensions.
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(FusekiServerCtl.dirConfiguration, filename+".*") ) {
stream.forEach((p)-> paths.add(FusekiServerCtl.dirConfiguration.resolve(p).toString() ));
}
return paths;
@@ -438,5 +440,4 @@ public class FusekiServerCtl {
throw new InternalErrorException("Failed to read configuration directory "+FusekiServerCtl.dirConfiguration);
}
}
-
}
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/FusekiTestLib.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/FusekiTestLib.java
index c57f85fa27..83bae8f9d6 100644
--- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/FusekiTestLib.java
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/FusekiTestLib.java
@@ -43,6 +43,10 @@ public class FusekiTestLib {
expectFail(runnable, HttpSC.Code.NOT_FOUND);
}
+ public static void expect409(Runnable runnable) {
+ expectFail(runnable, HttpSC.Code.CONFLICT);
+ }
+
public static void expectFail(Runnable runnable, Code code) {
if ( code == null || ( 200 <= code.getCode() && code.getCode() < 300 ) ) {
runnable.run();
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/TS_FusekiMods.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/TS_FusekiMods.java
index 982da56e2f..b8d8ddf482 100644
--- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/TS_FusekiMods.java
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/TS_FusekiMods.java
@@ -18,27 +18,25 @@
package org.apache.jena.fuseki.mod;
-import org.junit.platform.suite.api.SelectClasses;
-import org.junit.platform.suite.api.Suite;
-
-import org.apache.jena.fuseki.mod.admin.TestAdmin;
-import org.apache.jena.fuseki.mod.admin.TestFusekiReload;
-import org.apache.jena.fuseki.mod.admin.TestTemplateAddDataset;
+import org.apache.jena.fuseki.mod.admin.TSMod_Admin;
import org.apache.jena.fuseki.mod.metrics.TestModPrometheus;
import org.apache.jena.fuseki.mod.shiro.TestModShiro;
+import org.junit.platform.suite.api.SelectClasses;
+import org.junit.platform.suite.api.Suite;
@Suite
@SelectClasses({
// Admin
- TestAdmin.class,
- TestFusekiReload.class,
- TestTemplateAddDataset.class,
+ TSMod_Admin.class,
// UI
// Prometheus
TestModPrometheus.class,
- // Apache Shiro
+
+ // Shiro
TestModShiro.class,
+
+ // Whole server
TestFusekiServer.class
})
public class TS_FusekiMods {
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/FusekiServerPerTest.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/FusekiServerPerTest.java
new file mode 100644
index 0000000000..740f2d6756
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/FusekiServerPerTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.fail;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import org.apache.jena.atlas.lib.FileOps;
+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.sys.FusekiModules;
+import org.apache.jena.fuseki.mgt.FusekiServerCtl;
+import org.apache.jena.fuseki.system.FusekiLogging;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Framework for running tests on a Fuseki server, with a fresh server for each test.
+ */
+public class FusekiServerPerTest {
+
+ @BeforeAll public static void logging() {
+ FusekiLogging.setLogging();
+ }
+
+ protected FusekiServerPerTest() {}
+
+ // One server per test.
+
+ protected void withServer(Consumer<FusekiServer> action) {
+ withServer(null, action);
+ }
+
+ protected void withServer(String configFile, Consumer<FusekiServer> action) {
+ FusekiModules modules = modulesSetup();
+ DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+ FusekiServer.Builder builder = FusekiServer.create().port(0);
+
+ if ( modules != null )
+ builder.fusekiModules(modules);
+
+ if ( configFile != null )
+ builder.parseConfigFile(configFile);
+
+ customizerServer(builder);
+
+ FusekiServer testServer = builder.start();
+ try {
+ action.accept(testServer);
+ } finally {
+ testServer.stop();
+ FusekiServerCtl.clearUpSystemState();
+ }
+ }
+
+ protected void customizerServer(FusekiServer.Builder builder) {}
+
+ protected FusekiModules modulesSetup() { return null; }
+
+ @BeforeEach public void cleanStart() {
+ System.setProperty("FUSEKI_BASE", "target/run");
+ FileOps.clearAll("target/run");
+ }
+
+ @BeforeAll public static void setLogging() {
+ LogCtl.setLevel(Fuseki.backupLogName, "ERROR");
+ LogCtl.setLevel(Fuseki.compactLogName,"ERROR");
+ Awaitility.setDefaultPollDelay(20,TimeUnit.MILLISECONDS);
+ Awaitility.setDefaultPollInterval(50,TimeUnit.MILLISECONDS);
+ }
+
+ @AfterAll public static void unsetLogging() {
+ LogCtl.setLevel(Fuseki.backupLogName, "WARN");
+ LogCtl.setLevel(Fuseki.compactLogName,"WARN");
+ }
+
+ /** Expect two strings to be non-null and be {@link String#equalsIgnoreCase} */
+ protected static void assertEqualsContectType(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);
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/FusekiServerPerTestClass.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/FusekiServerPerTestClass.java
new file mode 100644
index 0000000000..5f1cbc35c7
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/FusekiServerPerTestClass.java
@@ -0,0 +1,138 @@
+/*
+ * 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.fail;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import org.apache.jena.atlas.lib.FileOps;
+import org.apache.jena.atlas.logging.LogCtl;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.ctl.ActionSleep;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.sys.FusekiModules;
+import org.apache.jena.fuseki.mgt.FusekiServerCtl;
+import org.apache.jena.fuseki.mod.FusekiServerRunner;
+import org.apache.jena.fuseki.server.DataAccessPointRegistry;
+import org.apache.jena.fuseki.system.FusekiLogging;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+
+/**
+ * Framework for running tests on a Fuseki server, with a single server for all tests.
+ */
+public class FusekiServerPerTestClass {
+
+ private static String serverURL = null;
+ private static FusekiServer server = null;
+
+ @BeforeAll public static void logging() {
+ FusekiLogging.setLogging();
+ }
+
+ @BeforeAll public static void startServer() {
+ System.setProperty("FUSEKI_BASE", serverArea());
+ FileOps.clearAll(serverArea());
+
+ server = createServerForTest();
+ serverURL = server.serverURL();
+ }
+
+ protected static String serverArea() {
+ return "target/run";
+ }
+
+ protected static DataAccessPointRegistry serverRegistry() {
+ return server.getDataAccessPointRegistry();
+ }
+
+ @AfterAll public static void stopServer() {
+ if ( server != null )
+ server.stop();
+ serverURL = null;
+ FusekiServerCtl.clearUpSystemState();
+ }
+
+ @BeforeAll public static void setLogging() {
+ LogCtl.setLevel(Fuseki.backupLogName, "ERROR");
+ LogCtl.setLevel(Fuseki.compactLogName,"ERROR");
+ Awaitility.setDefaultPollDelay(20,TimeUnit.MILLISECONDS);
+ Awaitility.setDefaultPollInterval(50,TimeUnit.MILLISECONDS);
+ }
+
+ @AfterAll public static void unsetLogging() {
+ LogCtl.setLevel(Fuseki.backupLogName, "WARN");
+ LogCtl.setLevel(Fuseki.compactLogName,"WARN");
+ }
+
+ // For the one-per-class setup, include the usual modules for jena-fuseki-server.
+ private static FusekiModules modulesSetup() {
+ return FusekiServerRunner.serverModules();
+ }
+
+ private static FusekiServer createServerForTest() {
+ FusekiModules modules = modulesSetup();
+ DatasetGraph dsg = DatasetGraphFactory.createTxnMem();
+ FusekiServer testServer = FusekiServer.create()
+ .fusekiModules(modules)
+ .port(0)
+ // Add a database.
+ .add(datasetName(), dsg)
+ // Action used for testing.
+ .addServlet("/$/sleep/*", new ActionSleep())
+ .build()
+ .start();
+ return testServer;
+ }
+
+ protected static String urlRoot() {
+ return serverURL;
+ }
+
+ protected static String adminURL() {
+ return serverURL + "$/";
+ }
+
+ protected static String datasetName() {
+ return "dataset";
+ }
+
+ protected FusekiServerPerTestClass() {}
+
+ // One server per test.
+
+ protected void withServer(Consumer<FusekiServer> action) {
+ action.accept(server);
+ }
+
+ /** Expect two strings to be non-null and be {@link String#equalsIgnoreCase} */
+ protected static void assertEqualsContectType(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);
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiAdmin.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TSMod_Admin.java
similarity index 69%
copy from jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiAdmin.java
copy to jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TSMod_Admin.java
index 2481c4f699..8c75e55674 100644
--- a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/mgt/FusekiAdmin.java
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TSMod_Admin.java
@@ -16,8 +16,18 @@
* limitations under the License.
*/
-package org.apache.jena.fuseki.mgt;
+package org.apache.jena.fuseki.mod.admin;
-public class FusekiAdmin {
- public final static Object systemLock = new Object();
-}
+import org.junit.platform.suite.api.SelectClasses;
+import org.junit.platform.suite.api.Suite;
+
+@Suite
+@SelectClasses({
+ TestAdmin.class,
+ TestAdminDatabaseOps.class,
+ TestAdminAddDatasetsConfigFile.class,
+ TestAdminAddDatasetTemplate.class,
+ TestFusekiReload.class,
+})
+
+public class TSMod_Admin {}
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
index 9586c33c58..bed933ade3 100644
--- 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
@@ -18,140 +18,38 @@
package org.apache.jena.fuseki.mod.admin;
-import static org.apache.jena.fuseki.mgt.ServerMgtConst.*;
+import static org.apache.jena.fuseki.mgt.ServerMgtConst.opServer;
import static org.apache.jena.fuseki.server.ServerConst.opPing;
-import static org.apache.jena.fuseki.server.ServerConst.opStats;
-import static org.apache.jena.http.HttpOp.*;
+import static org.apache.jena.http.HttpOp.httpGet;
+import static org.apache.jena.http.HttpOp.httpGetJson;
+import static org.apache.jena.http.HttpOp.httpPost;
+import static org.apache.jena.http.HttpOp.httpPostRtnJSON;
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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
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.BeforeAll;
-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.FusekiServerCtl;
import org.apache.jena.fuseki.mgt.ServerMgtConst;
import org.apache.jena.fuseki.server.ServerConst;
-import org.apache.jena.fuseki.system.FusekiLogging;
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;
+import org.junit.jupiter.api.Test;
/**
- * Tests of the admin functionality using a pre-configured dataset
- * {@link TestTemplateAddDataset}.
+ * Tests of the admin functionality using a pre-configured dataset.
+ * This class does not test adding and deleting of datasets.
*/
-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;
-
- @BeforeAll public static void logging() {
- FusekiLogging.setLogging();
- }
-
- @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;
- FusekiServerCtl.clearUpSystemState();
- }
-
- 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");
- }
+public class TestAdmin extends FusekiServerPerTestClass {
// --- Ping
@@ -178,286 +76,13 @@ public class TestAdmin {
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 again.
- try {
- httpPost(urlRoot()+"$/"+opDatasets,
- WebContent.contentTypeTurtle+"; charset="+WebContent.charsetUTF8,
- BodyPublishers.ofFile(f));
- } catch (HttpException ex) {
- 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);
+ String x = execSleepTask(1);
}
@Test public void sleep_2() {
try {
- String x = execSleepTask(null, -1);
+ String x = execSleepTask(-1);
fail("Sleep call unexpectedly succeed");
} catch (HttpException ex) {
assertEquals(400, ex.getStatusCode());
@@ -466,7 +91,7 @@ public class TestAdmin {
@Test public void sleep_3() {
try {
- String x = execSleepTask(null, 20*1000+1);
+ String x = execSleepTask(20*1000+1);
fail("Sleep call unexpectedly succeed");
} catch (HttpException ex) {
assertEquals(400, ex.getStatusCode());
@@ -476,7 +101,7 @@ public class TestAdmin {
// Async task testing
@Test public void task_1() {
- String x = execSleepTask(null, 10);
+ String x = execSleepTask(10);
assertNotNull(x);
Integer.parseInt(x);
}
@@ -495,7 +120,7 @@ public class TestAdmin {
@Test public void task_3() {
// Timing dependent.
// Create a "long" running task so we can find it.
- String x = execSleepTask(null, 100);
+ String x = execSleepTask(100);
checkTask(x);
checkInTasks(x);
assertNotNull(x);
@@ -505,7 +130,7 @@ public class TestAdmin {
@Test public void task_4() {
// Timing dependent.
// Create a "short" running task
- String x = execSleepTask(null, 1);
+ String x = execSleepTask(1);
// Check exists in the list of all tasks (should be "finished")
checkInTasks(x);
String url = urlRoot()+"$/tasks/"+x;
@@ -527,31 +152,31 @@ public class TestAdmin {
@Test public void task_5() {
// Short running task - still in info API call.
- String x = execSleepTask(null, 1);
+ String x = execSleepTask(1);
checkInTasks(x);
}
@Test public void task_6() {
- String x1 = execSleepTask(null, 1000);
- String x2 = execSleepTask(null, 1000);
+ String x1 = execSleepTask(1000);
+ String x2 = execSleepTask(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);
+ String x1 = execSleepTask(1000);
+ String x2 = execSleepTask(1000);
+ String x3 = execSleepTask(1000);
+ String x4 = execSleepTask(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);
+ String x5 = execSleepTask(4000);
+ String x6 = execSleepTask(4000);
+ String x7 = execSleepTask(4000);
+ String x8 = execSleepTask(10);
fail("Managed to add a 5th test");
} catch (HttpException ex) {
assertEquals(HttpSC.BAD_REQUEST_400, ex.getStatusCode());
@@ -561,77 +186,8 @@ public class TestAdmin {
}
}
- /** 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) {
+ private String execSleepTask(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;
@@ -727,114 +283,5 @@ public class TestAdmin {
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/TestAdminAddDatasetTemplate.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestAdminAddDatasetTemplate.java
new file mode 100644
index 0000000000..a566dd9995
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestAdminAddDatasetTemplate.java
@@ -0,0 +1,241 @@
+/*
+ * 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.opDatasets;
+import static org.apache.jena.http.HttpOp.httpPost;
+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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.apache.jena.atlas.io.IOX;
+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.base.Sys;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.main.FusekiTestLib;
+import org.apache.jena.fuseki.mgt.FusekiServerCtl;
+import org.apache.jena.http.HttpOp;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.rdfconnection.RDFConnection;
+import org.apache.jena.riot.WebContent;
+import org.apache.jena.sparql.exec.http.Params;
+import org.apache.jena.web.HttpSC;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests of the admin functionality on an empty server and using the template mechanism.
+ * See also {@link TestAdmin}.
+ */
+public class TestAdminAddDatasetTemplate extends FusekiServerPerTestClass {
+
+ @BeforeAll public static void loggingAdmin() {
+ LogCtl.setLevel(Fuseki.adminLogName, "ERROR");
+ }
+
+ @AfterAll public static void resetLoggingAdmin() {
+ LogCtl.setLevel(Fuseki.adminLogName, "ERROR");
+ }
+
+ @Test public void add_dataset_01() {
+ testAddDataset("db_1");
+ }
+
+ @Test public void add_dataset_02() {
+ testAddDataset( "/db_2");
+ }
+
+ @Test public void add_dataset_bad_01() {
+ String dbName = "db_bad_01";
+ testAddDataset(dbName);
+ // This should fail.
+ FusekiTestLib.expect409(()->testAddDataset(dbName));
+ }
+
+ @Test public void add_dataset_bad_02() {
+ badAddDataserverRequest("bad_2 illegal");
+ }
+
+ @Test public void add_dataset_bad_03() {
+ badAddDataserverRequest("bad_3/path");
+ }
+
+ @Test public void add_dataset_bad_04() {
+ badAddDataserverRequest("");
+ }
+
+ @Test public void add_dataset_bad_05() {
+ badAddDataserverRequest(" ");
+ }
+
+ @Test public void add_dataset_bad_06() {
+ badAddDataserverRequest("bad_6_AB CD");
+ }
+
+ @Test public void add_dataset_bad_07() {
+ badAddDataserverRequest("..");
+ }
+
+ @Test public void add_dataset_bad_08() {
+ badAddDataserverRequest("/..");
+ }
+
+ @Test public void add_dataset_bad_09() {
+ badAddDataserverRequest("/../elsewhere");
+ }
+
+ @Test public void add_dataset_bad_10() {
+ badAddDataserverRequest("//bad_10");
+ }
+
+ //@Test
+ public void noOverwriteExistingConfigFile() {
+ withServer(server->{
+ try {
+ var workingDir = Paths.get(serverArea()).toAbsolutePath();
+ var path = workingDir.resolve("configuration/test-ds0-empty.ttl");
+ var dbConfig = path.toFile();
+ dbConfig.createNewFile();
+ try {
+ // refresh the file system so that the file exists
+ dbConfig = path.toFile();
+ assertTrue (dbConfig.exists());
+ assertEquals(0, dbConfig.length());
+
+ // Try to override the file with a new configuration.
+ String ct = WebContent.contentTypeHTMLForm;
+ String body = "dbName=test-ds0-empty&dbType=mem";
+ HttpException ex = assertThrows(org.apache.jena.atlas.web.HttpException.class,
+ ()-> httpPost(server.serverURL() + "$/" + opDatasets, ct, body));
+ assertEquals(HttpSC.CONFLICT_409, ex.getStatusCode());
+ // refresh the file system
+ dbConfig = path.toFile();
+ assertTrue(dbConfig.exists());
+ assertEquals(0, dbConfig.length(), "File should be still empty");
+ }
+ finally {
+ // Clean up the file.
+ if (Files.exists(path)) {
+ Files.delete(path);
+ }
+ }
+ } catch (IOException ex) { throw IOX.exception(ex); }
+ });
+ }
+
+ // add-delete
+
+ @Test public void add_delete_mem_1() {
+ testAddDeleteAdd("db_add_delete_1", "mem", false, false);
+ }
+
+ @Test public void add_delete_tdb_1() {
+ if ( Sys.isWindows )
+ return;
+ testAddDeleteAdd("db_add_delete_tdb_1", "tdb2", false, true);
+ }
+
+ @Test public void add_delete_tdb_2() {
+ if ( Sys.isWindows )
+ return;
+ String dbName = "db_add_delete_tdb_2";
+ testAddDeleteAdd(dbName, "tdb2", false, true);
+ }
+
+ // Attempt to add a in-memory dataset. Used to test the name checking.
+ private void testAddDataset(String dbName) {
+ withServer(server->{
+ String datasetURL = server.datasetURL(dbName);
+ Params params = Params.create().add("dbName", dbName).add("dbType", "mem");
+ // Use the template
+ HttpOp.httpPostForm(adminURL()+"datasets", params);
+ assertTrue(exists(datasetURL));
+ });
+ }
+
+ private void testAddDeleteAdd(String dbName, String dbType, boolean alreadyExists, boolean hasFiles) {
+ withServer(server->{
+ 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 = FusekiServerCtl.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 void badAddDataserverRequest(String dbName) {
+ FusekiTestLib.expect400(()->testAddDataset(dbName));
+ }
+
+ 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/admin/TestAdminAddDatasetsConfigFile.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestAdminAddDatasetsConfigFile.java
new file mode 100644
index 0000000000..9096ccee93
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestAdminAddDatasetsConfigFile.java
@@ -0,0 +1,318 @@
+/*
+ * 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.opDatasets;
+import static org.apache.jena.http.HttpOp.httpDelete;
+import static org.apache.jena.http.HttpOp.httpGet;
+import static org.apache.jena.http.HttpOp.httpPost;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+
+import java.io.FileNotFoundException;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.logging.LogCtl;
+import org.apache.jena.atlas.web.TypedInputStream;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.FusekiTestLib;
+import org.apache.jena.fuseki.main.sys.FusekiModules;
+import org.apache.jena.fuseki.mgt.FusekiAdmin;
+import org.apache.jena.fuseki.test.HttpTest;
+import org.apache.jena.riot.WebContent;
+import org.apache.jena.web.HttpSC;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests of the admin functionality adding and deleting datasets dynamically.
+ * See also {@link TestAdminAddDatasetTemplate}.
+ */
+public class TestAdminAddDatasetsConfigFile extends FusekiServerPerTest {
+ // Name of the dataset in the assembler file.
+ static String dsTest = "test-ds1";
+ static String dsTestInf = "test-ds4";
+
+ static final String dsTestTdb2a = "test-tdb2a";
+ static final String dsTestTdb2b = "test-tdb2b";
+ static final String dsTestTdb2c = "test-tdb2c";
+ static String fileBase = "testing/Config/";
+
+ // Exactly the module under test
+ @Override
+ protected FusekiModules modulesSetup() {
+ return FusekiModules.create(FMod_Admin.create());
+ }
+
+ @BeforeAll public static void loggingAdmin() {
+ LogCtl.setLevel(Fuseki.adminLogName, "ERROR");
+ }
+
+ @AfterAll public static void resetLoggingAdmin() {
+ LogCtl.setLevel(Fuseki.adminLogName, "ERROR");
+ }
+
+ protected void withServerFileEnabled(Consumer<FusekiServer> action) {
+ System.setProperty(FusekiAdmin.allowConfigFileProperty, "true");
+ try {
+ super.withServer(null, action);
+ } finally {
+ System.getProperties().remove(FusekiAdmin.allowConfigFileProperty);
+ }
+ }
+
+ @Test public void add_block_dataset_1() {
+ // Blocked by default.
+ withServer(server -> {
+ FusekiTestLib.expect400(()->addTestDatasetByFile(server, "config-ds-plain-1.ttl"));
+ });
+ }
+
+ @Test public void add_unblocked_dataset_1() {
+ // Blocked by default.
+ withServerFileEnabled(server -> {
+ addTestDatasetByFile(server, "config-ds-plain-1.ttl");
+ });
+ }
+
+ // Try to add twice
+ @Test public void add_add_dataset_1() {
+ withServerFileEnabled(server -> {
+ checkNotThere(server, dsTest);
+
+ addTestDatasetByFile(server, "config-ds-plain-1.ttl");
+ checkExists(server, dsTest);
+
+ // Second try should fail.
+ FusekiTestLib.expect409(()->addTestDatasetByFile(server, "config-ds-plain-1.ttl"));
+
+ // Check still exists.
+ checkExists(server, dsTest);
+ // Delete-able.
+ deleteDataset(server, dsTest);
+ checkNotThere(server, dsTest);
+ });
+ }
+
+ @Test public void add_delete_dataset_1() {
+ withServerFileEnabled(server -> {
+
+ checkNotThere(server, dsTest);
+ checkNotThere(server, dsTestInf);
+ addTestDatasetByFile(server, "config-ds-inf.ttl");
+ checkNotThere(server, dsTest);
+ checkExists(server, dsTestInf);
+
+ deleteDataset(server, dsTestInf);
+
+ checkNotThere(server, dsTestInf);
+ addTestDatasetByFile(server, "config-ds-inf.ttl");
+ checkExists(server, dsTestInf);
+ deleteDataset(server, dsTestInf);
+ });
+ }
+
+ @Test public void add_delete_dataset_2() {
+ withServerFileEnabled(server -> {
+ // New style operations : cause two fuseki:names
+ addTestDatasetByFile(server, "config-ds-plain-2.ttl");
+ checkExists(server, "test-ds2");
+ });
+ }
+
+ @Test public void add_delete_dataset_TDB_1() {
+ withServerFileEnabled(server -> {
+
+ String testDB = dsTestTdb2a;
+ assumeNotWindows();
+
+ checkNotThere(server, testDB);
+
+ addTestDatasetTDB2(server, testDB);
+
+ // Check exists.
+ checkExists(server, testDB);
+
+ // Remove it.
+ deleteDataset(server, testDB);
+ checkNotThere(server, testDB);
+ });
+ }
+
+ @Test public void add_delete_dataset_TDB_2() {
+ withServerFileEnabled(server -> {
+ // This has location "--mem--"
+ String testDB = dsTestTdb2b;
+ checkNotThere(server, testDB);
+ addTestDatasetTDB2(server, testDB);
+ // Check exists.
+ checkExists(server, testDB);
+ // Remove it.
+ deleteDataset(server, testDB);
+ checkNotThere(server, testDB);
+ });
+ }
+
+ @Test public void add_dataset_error_1() {
+ withServerFileEnabled(server -> {
+ HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
+ ()-> addTestDatasetByFile(server, "config-ds-bad-name-1.ttl"));
+ });
+ }
+
+ @Test public void add_dataset_error_2() {
+ withServerFileEnabled(server -> {
+ HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
+ ()-> addTestDatasetByFile(server, "config-ds-bad-name-2.ttl"));
+ });
+ }
+
+ @Test public void add_dataset_error_3() {
+ withServerFileEnabled(server -> {
+ HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
+ ()-> addTestDatasetByFile(server, "config-ds-bad-name-3.ttl"));
+ });
+ }
+
+ @Test public void add_dataset_error_4() {
+ withServerFileEnabled(server -> {
+ HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
+ ()-> addTestDatasetByFile(server, "config-ds-bad-name-4.ttl"));
+ });
+ }
+
+ @Test public void add_dataset_error_5() {
+ withServerFileEnabled(server -> {
+ HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
+ ()-> addTestDatasetBodyPublisher(server, BodyPublishers.noBody()));
+ });
+ }
+
+ @Test public void add_dataset_error_6() {
+ withServerFileEnabled(server -> {
+ HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
+ ()-> addTestDatasetBodyPublisher(server, BodyPublishers.ofString("")));
+ });
+ }
+
+ @Test public void add_dataset_error_7() {
+ withServerFileEnabled(server -> {
+ String level = LogCtl.getLevel(Fuseki.adminLog);
+ LogCtl.setLevel(Fuseki.adminLog, "FATAL");
+ HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
+ ()-> addTestDatasetBodyPublisher(server, BodyPublishers.ofString("JUNK")));
+ LogCtl.setLevel(Fuseki.adminLog, level);
+ });
+ }
+
+ @Test public void add_dataset_error_8() {
+ withServerFileEnabled(server -> {
+ String level = LogCtl.getLevel(Fuseki.adminLog);
+ LogCtl.setLevel(Fuseki.adminLog, "FATAL");
+ HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400,
+ ()-> addTestDatasetTDB2(server, dsTestTdb2c));
+ LogCtl.setLevel(Fuseki.adminLog, level);
+ });
+ }
+
+ @Test public void delete_dataset_1() {
+ withServerFileEnabled(server -> {
+ String name = "NoSuchDataset";
+ HttpTest.expect404( ()-> httpDelete(server.serverURL()+"$/"+opDatasets+"/"+name) );
+ });
+ }
+
+ // ---- Backup
+
+ private void assumeNotWindows() {
+ assumeFalse(SystemUtils.IS_OS_WINDOWS, "Test may be unstable on Windows due to inability to delete memory-mapped files");
+ }
+
+ private void deleteDataset(FusekiServer server, String name) {
+ httpDelete(server.serverURL()+"$/"+opDatasets+"/"+name);
+ }
+
+ private void addTestDatasetWithName(FusekiServer server, String dsName) {
+ addTestDatasetWithName(server, dsName, "mem");
+ }
+
+ private void addTestDatasetWithName(FusekiServer server, String dsName, String dbType) {
+ String URL = server.serverURL()+"$/"+opDatasets+"?dbName="+dsName+"&dbType="+dbType;
+ String ct = WebContent.contentTypeTurtle;
+ httpPost(URL);
+ }
+
+ private void addTestDatasetTDB2(FusekiServer server, String DBname) {
+ Objects.nonNull(DBname);
+ switch(DBname) {
+ case dsTestTdb2a-> addTestDatasetByFile(server, "config-tdb2a.ttl");
+ case dsTestTdb2b-> addTestDatasetByFile(server, "config-tdb2b.ttl");
+ case dsTestTdb2c-> addTestDatasetByFile(server, "config-tdb2c.ttl");
+ default->throw new IllegalArgumentException("No configuration for "+DBname);
+ }
+ }
+
+ private void addTestDatasetByFile(FusekiServer server, String filename) {
+ try {
+ Path f = Path.of(fileBase+filename);
+ BodyPublisher body = BodyPublishers.ofFile(f);
+ addTestDatasetBodyPublisher(server, body);
+ } catch (FileNotFoundException e) {
+ IO.exception(e);
+ }
+ }
+
+ private void addTestDatasetBodyPublisher(FusekiServer server, BodyPublisher body) {
+ String ct = WebContent.contentTypeTurtle;
+ httpPost(server.serverURL()+"$/"+opDatasets, ct, body);
+ }
+
+ private void askPing(FusekiServer server, String name) {
+ if ( name.startsWith("/") )
+ name = name.substring(1);
+ try ( TypedInputStream in = httpGet(server.serverURL()+name+"/sparql?query=ASK%7B%7D") ) {
+ IO.skipToEnd(in);
+ }
+ }
+
+ private void adminPing(FusekiServer server, String name) {
+ try ( TypedInputStream in = httpGet(server.serverURL()+"$/"+opDatasets+"/"+name) ) {
+ IO.skipToEnd(in);
+ }
+ }
+
+ private void checkExists(FusekiServer server, String name) {
+ adminPing(server, name);
+ askPing(server, name);
+ }
+
+ private void checkNotThere(FusekiServer server, String name) {
+ String n = (name.startsWith("/")) ? name.substring(1) : name;
+ // Check gone exists.
+ HttpTest.expect404(()-> adminPing(server, n));
+ HttpTest.expect404(() -> askPing(server, n));
+ }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestAdminDatabaseOps.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestAdminDatabaseOps.java
new file mode 100644
index 0000000000..20f7a7e6e4
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestAdminDatabaseOps.java
@@ -0,0 +1,484 @@
+/*
+ * 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.opBackup;
+import static org.apache.jena.fuseki.mgt.ServerMgtConst.opCompact;
+import static org.apache.jena.fuseki.mgt.ServerMgtConst.opDatasets;
+import static org.apache.jena.fuseki.mgt.ServerMgtConst.opListBackups;
+import static org.apache.jena.fuseki.server.ServerConst.opStats;
+import static org.apache.jena.http.HttpOp.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+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.Lib;
+import org.apache.jena.atlas.web.TypedInputStream;
+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.system.FusekiLogging;
+import org.apache.jena.fuseki.test.HttpTest;
+import org.apache.jena.riot.WebContent;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests of the admin functionality adding and deleting datasets dynamically.
+ * See also {@link TestAdminAddDatasetTemplate}.
+ */
+public class TestAdminDatabaseOps extends FusekiServerPerTest {
+ // Name of the dataset in the assembler file.
+ private static String dsTest = "test-ds1";
+
+ @BeforeAll public static void logging() {
+ FusekiLogging.setLogging();
+ }
+
+ @Override
+ protected FusekiModules modulesSetup() {
+ return FusekiModules.create(FMod_Admin.create());
+ }
+
+ @Override
+ protected void customizerServer(FusekiServer.Builder builder) {
+ builder.add(datasetName(), DatasetGraphFactory.createTxnMem());
+ }
+
+ private String datasetName() {
+ return "dsg";
+ }
+
+ // ---- Backup
+
+ @Test public void create_backup_1() {
+ withServer(server -> {
+ String id = null;
+ try {
+ JsonValue v = httpPostRtnJSON(server.serverURL() + "$/" + opBackup + "/" + datasetName());
+ id = v.getAsObject().getString("taskId");
+ } finally {
+ waitForTasksToFinish(server, 1000, 10, 20000);
+ }
+ assertNotNull(id);
+ checkInTasks(server, id);
+
+ // Check a backup was created
+ try ( TypedInputStream in = httpGet(server.serverURL()+"$/"+opListBackups) ) {
+ assertEqualsContectType(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(server, 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() {
+ withServer(server -> {
+ HttpTest.expect400(()->{
+ JsonValue v = httpPostRtnJSON(server.serverURL() + "$/" + opBackup + "/noSuchDataset");
+ });
+ });
+ }
+
+ @Test public void list_backups_1() {
+ withServer(server -> {
+ try ( TypedInputStream in = httpGet(server.serverURL()+"$/"+opListBackups) ) {
+ assertEqualsContectType(WebContent.contentTypeJSON, in.getContentType());
+ JsonValue v = JSON.parseAny(in);
+ assertNotNull(v.getAsObject().get("backups"));
+ }
+ });
+ }
+
+ // ---- Compact
+
+ @Test public void compact_01() {
+ withServer(server -> {
+ assumeNotWindows();
+
+ String testDB = "dsg-tdb2";
+ try {
+ checkNotThere(server, testDB);
+ addTestDatasetTDB2(server, testDB);
+ checkExists(server, testDB);
+
+ String id = null;
+ try {
+ JsonValue v = httpPostRtnJSON(server.serverURL() + "$/" + opCompact + "/" + testDB);
+ id = v.getAsObject().getString(JsonConstCtl.taskId);
+ } finally {
+ waitForTasksToFinish(server, 1000, 500, 20_000);
+ }
+ assertNotNull(id);
+ checkInTasks(server, id);
+
+ JsonValue task = getTask(server, 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(server, testDB);
+ }
+ });
+ }
+
+ @Test public void compact_02() {
+ withServer(server -> {
+ HttpTest.expect400(()->{
+ JsonValue v = httpPostRtnJSON(server.serverURL() + "$/" + 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() {
+ withServer(server -> {
+ JsonValue v = execGetJSON(server.serverURL()+"$/"+opStats);
+ checkJsonStatsAll(v);
+ });
+ }
+
+ @Test public void stats_2() {
+ withServer(server -> {
+ addTestDatasetWithName(server, dsTest);
+ JsonValue v = execGetJSON(server.serverURL()+"$/"+opStats+"/"+dsTest);
+ checkJsonStatsAll(v);
+ deleteDataset(server, dsTest);
+ });
+ }
+
+ @Test public void stats_3() {
+ withServer(server -> {
+ addTestDatasetWithName(server, dsTest);
+ HttpTest.expect404(()-> execGetJSON(server.serverURL()+"$/"+opStats+"/DoesNotExist"));
+ deleteDataset(server, dsTest);
+ });
+ }
+
+ @Test public void stats_4() {
+ withServer(server -> {
+ JsonValue v = execPostJSON(server.serverURL()+"$/"+opStats);
+ checkJsonStatsAll(v);
+ });
+ }
+
+ @Test public void stats_5() {
+ withServer(server -> {
+ addTestDatasetWithName(server, dsTest);
+ JsonValue v = execPostJSON(server.serverURL()+"$/"+opStats+"/"+dsTest);
+ checkJsonStatsAll(v);
+ deleteDataset(server, dsTest);
+ });
+ }
+
+ // --- List all datasets
+
+ @Test public void list_datasets_1() {
+ withServer(server->{
+ try ( TypedInputStream in = httpGet(urlRoot(server)+"$/"+opDatasets); ) {
+ IO.skipToEnd(in);
+ }
+ });
+ }
+
+ @Test public void list_datasets_2() {
+ withServer(server->{
+ try ( TypedInputStream in = httpGet(urlRoot(server)+"$/"+opDatasets) ) {
+ assertEqualsContentType(WebContent.contentTypeJSON, in.getContentType());
+ JsonValue v = JSON.parseAny(in);
+ assertNotNull(v.getAsObject().get("datasets"));
+ checkJsonDatasetsAll(v);
+ }
+ });
+ }
+
+ // Specific dataset
+ @Test public void list_datasets_3() {
+ withServer( server->checkExists(server, datasetName()) );
+ }
+
+ // Specific dataset
+ @Test public void list_datasets_4() {
+ withServer( server->{
+ HttpTest.expect404( () -> getDatasetDescription(server, "does-not-exist") );
+ });
+ }
+
+ // Specific dataset
+ @Test public void list_datasets_5() {
+ withServer( server->{
+ JsonValue v = getDatasetDescription(server, datasetName());
+ checkJsonDatasetsOne(v.getAsObject());
+ });
+ }
+
+ private String urlRoot(FusekiServer server) {
+ return server.serverURL();
+ }
+
+ private void deleteDataset(FusekiServer server, String name) {
+ httpDelete(server.serverURL()+"$/"+opDatasets+"/"+name);
+ }
+
+ private JsonValue getTask(FusekiServer server, String taskId) {
+ String url = server.serverURL()+"$/tasks/"+taskId;
+ return httpGetJson(url);
+ }
+
+ private JsonValue getDatasetDescription(FusekiServer server, String dsName) {
+ if ( dsName.startsWith("/") )
+ dsName = dsName.substring(1);
+ try (TypedInputStream in = httpGet(server.serverURL() + "$/" + opDatasets + "/" + dsName)) {
+ assertEqualsContectType(WebContent.contentTypeJSON, in.getContentType());
+ JsonValue v = JSON.parse(in);
+ return v;
+ }
+ }
+
+ private void addTestDatasetWithName(FusekiServer server, String dsName) {
+ addTestDatasetWithName(server, dsName, "mem");
+ }
+
+ private void addTestDatasetWithName(FusekiServer server, String dsName, String dbType) {
+ String URL = server.serverURL()+"$/"+opDatasets+"?dbName="+dsName+"&dbType="+dbType;
+ String ct = WebContent.contentTypeTurtle;
+ httpPost(URL);
+ }
+
+ private void addTestDatasetTDB2(FusekiServer server, String DBname) {
+ Objects.nonNull(DBname);
+ addTestDatasetWithName(server, DBname, "TDB2");
+ }
+
+ private void checkTask(FusekiServer server, 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(FusekiServer server, String x) {
+ String url = server.serverURL()+"$/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(server, obj);
+ if ( obj.getString("taskId").equals(x) ) {
+ found++;
+ }
+ }
+ assertEquals(1, found, "Occurrence of taskId count");
+ }
+
+ private List<String> runningTasks(FusekiServer server, String... x) {
+ String url = server.serverURL()+"$/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(server, 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(FusekiServer server, 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(server);
+ if ( x.isEmpty() )
+ return true;
+ Lib.sleep(pollInterval);
+ now = System.currentTimeMillis();
+ }
+ return false;
+ }
+
+ private boolean isRunning(FusekiServer server, JsonObject taskObj) {
+ checkTask(server, taskObj);
+ return taskObj.hasKey("started") && ! taskObj.hasKey("finished");
+ }
+
+ private void askPing(FusekiServer server, String name) {
+ if ( name.startsWith("/") )
+ name = name.substring(1);
+ try ( TypedInputStream in = httpGet(server.serverURL()+name+"/sparql?query=ASK%7B%7D") ) {
+ IO.skipToEnd(in);
+ }
+ }
+
+ private void adminPing(FusekiServer server, String name) {
+ try ( TypedInputStream in = httpGet(server.serverURL()+"$/"+opDatasets+"/"+name) ) {
+ IO.skipToEnd(in);
+ }
+ }
+
+ private void checkExists(FusekiServer server, String name) {
+ adminPing(server, name);
+ askPing(server, name);
+ }
+
+ private void checkNotThere(FusekiServer server, String name) {
+ String n = (name.startsWith("/")) ? name.substring(1) : name;
+ // Check gone exists.
+ HttpTest.expect404(()-> adminPing(server, n));
+ HttpTest.expect404(() -> askPing(server, n));
+ }
+
+ 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) ) {
+ assertEqualsContectType(WebContent.contentTypeJSON, in.getContentType());
+ return JSON.parse(in);
+ }
+ }
+
+ private static void checkJsonDatasetsAll(JsonValue v) {
+ assertNotNull(v.getAsObject().get("datasets"));
+ JsonArray a = v.getAsObject().get("datasets").getAsArray();
+ for ( JsonValue v2 : a )
+ checkJsonDatasetsOne(v2);
+ }
+
+ private static 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 JsonValue execPostJSON(String url) {
+ try ( TypedInputStream in = httpPostStream(url, null, null, null) ) {
+ assertEqualsContectType(WebContent.contentTypeJSON, in.getContentType());
+ return JSON.parse(in);
+ }
+ }
+
+ /** Expect two string to be non-null and be {@link String#equalsIgnoreCase} */
+ private static void assertEqualsContentType(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);
+ }
+}
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
deleted file mode 100644
index c69e2b28fe..0000000000
--- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/mod/admin/TestTemplateAddDataset.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * 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.FusekiServerCtl;
-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.
- FusekiServerCtl.clearUpSystemState();
- }
-
- 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 = FusekiServerCtl.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/testing/Config/config-tdb2b.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2b.ttl
index 572927c331..56a8a9a0d3 100644
--- a/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2b.ttl
+++ b/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2b.ttl
@@ -15,4 +15,4 @@ PREFIX tdb2: <http://jena.apache.org/2016/tdb#>
fuseki:dataset <#dataset> .
<#dataset> rdf:type tdb2:DatasetTDB2 ;
- tdb2:location "target/tdb2b" .
+ tdb2:location "--mem--" .
diff --git a/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2b.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2c.ttl
similarity index 93%
copy from jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2b.ttl
copy to jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2c.ttl
index 572927c331..359edd43de 100644
--- a/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2b.ttl
+++ b/jena-fuseki2/jena-fuseki-main/testing/Config/config-tdb2c.ttl
@@ -15,4 +15,5 @@ PREFIX tdb2: <http://jena.apache.org/2016/tdb#>
fuseki:dataset <#dataset> .
<#dataset> rdf:type tdb2:DatasetTDB2 ;
- tdb2:location "target/tdb2b" .
+ # Bad.
+ tdb2:location "../tdb2c" .