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 <a...@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" .