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 03c5265910aa3a27907bf54f6b4aaae3409afa4f Author: Andy Seaborne <[email protected]> AuthorDate: Fri Jun 13 09:17:14 2025 +0100 GH-3288: Update jena-fuseki-webapp --- .../org/apache/jena/fuseki/mgt/ActionDatasets.java | 510 ++++++++++++++------- .../apache/jena/fuseki/webapp/FusekiWebapp.java | 50 +- .../org/apache/jena/fuseki/TS_FusekiWebapp.java | 2 + .../org/apache/jena/fuseki/TestWebappAdmin.java | 381 +-------------- ...va => TestWebappAdminAddDeleteDatasetFile.java} | 359 +++++---------- .../TestWebappAdminAddDeleteDatasetTemplate.java | 268 +++++++++++ .../jena-fuseki-webapp/testing/config-tdb2c.ttl | 19 + 7 files changed, 798 insertions(+), 791 deletions(-) diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ActionDatasets.java b/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ActionDatasets.java index 09f4796e06..38164418a9 100644 --- a/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ActionDatasets.java +++ b/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/mgt/ActionDatasets.java @@ -19,17 +19,11 @@ package org.apache.jena.fuseki.mgt; import static java.lang.String.format; -import static org.apache.jena.atlas.lib.Lib.lowercase; -import java.io.IOException; -import java.io.OutputStream; import java.io.StringReader; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; @@ -39,51 +33,58 @@ 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.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.metrics.MetricsProvider; +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.fuseki.webapp.FusekiWebapp; +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; +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"; - private static final String tDatabaseTDB = "tdb"; - private static final String tDatabaseTDB1 = "tdb1"; + private static final String tDatabaseTDB1 = "tdb"; private static final String tDatabaseTDB2 = "tdb2"; private static final String tDatabaseMem = "mem"; - // Sync lock. - private static final Object lock = new Object(); - public ActionDatasets() { super(); } @Override - public void validate(HttpAction action) {} + public void validate(HttpAction action) { } // ---- GET : return details of dataset or datasets. @Override @@ -111,10 +112,10 @@ public class ActionDatasets extends ActionContainerItem { // ---- POST + /** Create dataset */ @Override protected JsonValue execPostContainer(HttpAction action) { UUID uuid = UUID.randomUUID(); - DatasetDescriptionMap registry = new DatasetDescriptionMap(); ContentType ct = ActionLib.getContentType(action); @@ -123,42 +124,55 @@ public class ActionDatasets extends ActionContainerItem { if ( ct == null && ! hasParams ) ServletOps.errorBadRequest("Bad request - Content-Type or both parameters dbName and dbType required"); - boolean committed = false; - // Also acts as a concurrency lock - synchronized(lock) { - String systemFileCopy = null; - String configFile = null; + boolean succeeded = false; + // Used in clear-up. + String configFile = null; + String systemFileCopy = null; - try { - // Where to build the templated service/database. - Model modelData = ModelFactory.createDefaultModel(); - StreamRDF dest = StreamRDFLib.graph(modelData.getGraph()); - - if ( hasParams || WebContent.isHtmlForm(ct) ) - assemblerFromForm(action, dest); - else if ( WebContent.isMultiPartForm(ct) ) - assemblerFromUpload(action, dest); - else - assemblerFromBody(action, dest); + DatasetDescriptionMap registry = new DatasetDescriptionMap(); - // ---- - // Keep a persistent copy immediately. This is not used for - // anything other than being "for the record". - systemFileCopy = FusekiWebapp.dirSystemFileArea.resolve(uuid.toString()).toString(); - try ( OutputStream outCopy = IO.openOutputFile(systemFileCopy) ) { - RDFDataMgr.write(outCopy, modelData, Lang.TURTLE); + synchronized (FusekiWebapp.systemLock) { + try { + // Get the request input. + Model modelFromRequest = ModelFactory.createDefaultModel(); + StreamRDF dest = StreamRDFLib.graph(modelFromRequest.getGraph()); + + 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 ( ! FusekiWebapp.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(modelData); - // Add dataset and model declarations. + 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(); @@ -166,39 +180,87 @@ 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) ) - // And abort. - 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 ) { + // -- Validate any TDB locations. + // If this is a templated request, there is no need to do this because the location is "datasetPath" + 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 = FusekiWebapp.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 = FusekiWebapp.generateConfigurationFilename(datasetPath); List<String> existing = FusekiWebapp.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, modelData, Lang.TURTLE); - } + configFile = FusekiWebapp.generateConfigurationFilename(datasetPath); + // ---- 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); @@ -206,27 +268,72 @@ 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); - action.setResponseContentType(WebContent.contentTypeTextPlain); - ServletOps.success(action); - committed = true; + // Add to metrics + MetricsProvider metricProvider = action.getMetricsProvider(); + if ( metricProvider != null ) + action.getMetricsProvider().addDataAccessPointMetrics(dataAccessPoint); - } catch (IOException ex) { IO.exception(ex); } - finally { - if ( ! committed ) { + action.setResponseContentType(WebContent.contentTypeTextPlain); + ServletOps.success(action); + } finally { + // Clear-up on failure. + if ( ! succeeded ) { if ( systemFileCopy != null ) FileOps.deleteSilent(systemFileCopy); if ( configFile != null ) FileOps.deleteSilent(configFile); - } - } + return null; } - return null; + } + + /** + * 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; + String locationString = location.getDirectoryPath(); + // No ".." + if (locationString.startsWith("..") || locationString.contains("/..") ) { + // That test was too strict. + List<String> components = FileOps.pathComponents(locationString); + if ( components.contains("..") ) + return false; + } + return true; } /** Find the service resource. There must be only one in the configuration. */ @@ -234,7 +341,6 @@ public class ActionDatasets extends ActionContainerItem { // Try to find by unique pServiceName (max backwards compatibility) // then try to find by rdf:type fuseki:Service. - // JENA-1794 Statement stmt = getOne(model, null, pServiceName, null); // null means 0 or many, not one. @@ -262,16 +368,17 @@ 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; } @Override protected JsonValue execPostItem(HttpAction action) { - // This used to be the state change function -- active/inactive. - // Leave the core of the operation - tests used it to ping for a database. String name = getItemDatasetName(action); if ( name == null ) name = "''"; @@ -282,6 +389,15 @@ public class ActionDatasets extends ActionContainerItem { if ( dap == null ) ServletOps.errorNotFound("Not found: dataset "+name); + + DataService dSrv = dap.getDataService(); + if ( dSrv == null ) + // If not set explicitly, take from DataAccessPoint + dSrv = action.getDataAccessPoint().getDataService(); + + String s = action.getRequestParameter("state"); + if ( s == null || s.isEmpty() ) + ServletOps.errorBadRequest("No state change given"); return null; } @@ -298,81 +414,90 @@ public class ActionDatasets extends ActionContainerItem { if ( ! action.getDataAccessPointRegistry().isRegistered(name) ) ServletOps.errorNotFound("No such dataset registered: "+name); - boolean committed = false; - synchronized(lock) { - // Redo check inside transaction. - DataAccessPoint ref = action.getDataAccessPointRegistry().get(name); - if ( ref == null ) - ServletOps.errorNotFound("No such dataset registered: "+name); - - // Get a reference before removing. - DataService dataService = ref.getDataService(); - // ---- Make it invisible in this running server. - action.getDataAccessPointRegistry().remove(name); - - // Find the configuration. - String filename = name.startsWith("/") ? name.substring(1) : name; - List<String> configurationFiles = FusekiWebapp.existingConfigurationFile(filename); - - if ( configurationFiles.isEmpty() ) { - // ---- Unmanaged - action.log.warn(format("[%d] Can't delete database configuration - not a managed database; dataset=%s", action.id, name)); + boolean succeeded = false; + + synchronized(FusekiWebapp.systemLock) { + try { + // Redo check inside transaction. + DataAccessPoint ref = action.getDataAccessPointRegistry().get(name); + if ( ref == null ) + ServletOps.errorNotFound("No such dataset registered: "+name); + + // Get a reference before removing. + DataService dataService = ref.getDataService(); + + // Remove from the registry - operation dispatch will not find it any more. + action.getDataAccessPointRegistry().remove(name); + + // Find the configuration. + List<String> configurationFiles = FusekiWebapp.existingConfigurationFile(name); + + if ( configurationFiles.isEmpty() ) { + // -- Unmanaged + action.log.warn(format("[%d] Can't delete database configuration - not a managed database", action.id, name)); // ServletOps.errorOccurred(format("Can't delete database - not a managed configuration", name)); - committed = true; - ServletOps.success(action); - return; - } + succeeded = true; + ServletOps.success(action); + return; + } - if ( configurationFiles.size() > 1 ) { - // -- This should not happen. - action.log.warn(format("[%d] There are %d configuration files, not one.", action.id, configurationFiles.size())); - ServletOps.errorOccurred(format("There are %d configuration files, not one. Delete not performed; manual clean up of the filesystem needed.", - configurationFiles.size())); - return; - } + if ( configurationFiles.size() > 1 ) { + // -- This should not happen. + action.log.warn(format("[%d] There are %d configuration files, not one.", action.id, configurationFiles.size())); + ServletOps.errorOccurred(format("There are %d configuration files, not one. Delete not performed; manual clean up of the filesystem needed.", + configurationFiles.size())); + return; + } - // ---- Remove managed database. - String cfgPathname = configurationFiles.get(0); + // -- Remove managed database. + String cfgPathname = configurationFiles.get(0); - // Delete configuration file. - // Once deleted, server restart will not have the database. - FileOps.deleteSilent(cfgPathname); + // 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()); + @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. - // Unclear what's holding the transaction (maybe another test clearing up slowly). - try { - dataService.shutdown(); - } catch (/*DBOE*/ Exception ex) { } - // JENA-1481: Really delete files. - if ( ( isTDB1 || isTDB2 ) ) { - // Delete databases created by the UI, or the admin operation, which are - // in predictable, unshared location on disk. - // There may not be any database files, the in-memory case. - Path pDatabase = FusekiWebapp.dirDatabases.resolve(filename); - if ( Files.exists(pDatabase)) { - try { - if ( Files.isSymbolicLink(pDatabase)) { - action.log.info(format("[%d] Database is a symbolic link, not removing files %s", action.id, pDatabase)); - } else { - IO.deleteAll(pDatabase); - action.log.info(format("[%d] Deleted database files %s", action.id, pDatabase)); + try { + dataService.shutdown(); + } catch (JenaException ex) { + return; + } + // JENA-1481: Really delete files. + if ( ( isTDB1 || isTDB2 ) ) { + // Delete databases created by the UI, or the admin operation, which are + // in predictable, unshared locations on disk. + // There may not be any database files, the in-memory case. + // (TDB supports an in-memory mode.) + String filename = name.startsWith("/") ? name.substring(1) : name; + Path pDatabase = FusekiWebapp.dirDatabases.resolve(filename); + if ( Files.exists(pDatabase)) { + try { + if ( Files.isSymbolicLink(pDatabase)) { + action.log.info(format("[%d] Database is a symbolic link, not removing files", action.id, pDatabase)); + } else { + IO.deleteAll(pDatabase); + action.log.info(format("[%d] Deleted database files %s", action.id, pDatabase)); + } + } catch (RuntimeIOException ex) { + action.log.error(format("[%d] Error while deleting database files %s: %s", action.id, pDatabase, ex.getMessage()), ex); + // But we have managed to remove it from the running server, and removed its configuration, so declare victory. } - } catch (RuntimeIOException ex) { - action.log.error(format("[%d] Error while deleting database files %s: %s", action.id, pDatabase, ex.getMessage()), ex); - // But we have managed to remove it from the running server, and removed its configuration, so declare victory. } } + + succeeded = true; + ServletOps.success(action); + } finally { + // No clearup needed } - ServletOps.success(action); } } @@ -380,42 +505,56 @@ public class ActionDatasets extends ActionContainerItem { bodyAsGraph(action, dest); } - private static Map<String, String> dbTypeToTemplate = Map.of( - // Default TDB - tDatabaseTDB, Template.templateTDB2_FN, - // Specific TDB - tDatabaseTDB1, Template.templateTDB1_FN, - tDatabaseTDB2, Template.templateTDB2_FN, - // Transactional, in-memory - tDatabaseMem, Template.templateTIM_MemFN); + private static Map<String, String> dbTypeToTemplate = new HashMap<>(); + static { + dbTypeToTemplate.put(tDatabaseTDB1, Template.templateTDB1_FN); + dbTypeToTemplate.put(tDatabaseTDB2, Template.templateTDB2_FN); + dbTypeToTemplate.put(tDatabaseMem, Template.templateTIM_MemFN); + } private static void assemblerFromForm(HttpAction action, StreamRDF dest) { - String x = action.getRequestQueryString(); String dbType = action.getRequestParameter(paramDatasetType); String dbName = action.getRequestParameter(paramDatasetName); - if ( StringUtils.isBlank(dbType) || StringUtils.isBlank(dbName) ) - ServletOps.errorBadRequest("Received HTML form. Both parameters 'dbName' and 'dbType' required"); + // 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); FusekiWebapp.addGlobals(params); - //action.log.info(format("[%d] Create database : name = %s, type = %s", action.id, dbName, dbType )); - - String template = dbTypeToTemplate.get(lowercase(dbType)); - if ( template == null ) - ServletOps.errorBadRequest(format("dbType can be only '%s' ('%s','%s') or '%s'", tDatabaseTDB, tDatabaseTDB1, tDatabaseTDB2, tDatabaseMem)); + String template = dbTypeToTemplate.get(dbType.toLowerCase(Locale.ROOT)); + if ( template == null ) { + List<String> keys = new ArrayList<>(dbTypeToTemplate.keySet()); + Collections.sort(keys); + ServletOps.errorBadRequest(format("dbType can be only one of %s", keys)); + } - String syntax = TemplateFunctions.templateFile(template, params, Lang.TTL); - RDFParser.create().source(new StringReader(syntax)).base("http://base/").lang(Lang.TTL).parse(dest); + String instance = TemplateFunctions.templateFile(template, params, Lang.TTL); + RDFParser.create().source(new StringReader(instance)).base("http://base/").lang(Lang.TTL).parse(dest); } private static void assemblerFromUpload(HttpAction action, StreamRDF dest) { - DataUploader.incomingData(action, dest); + throw new NotImplemented(); + //DataUploader.incomingData(action, dest); + } + + // ---- Auxiliary functions + + private static Quad getOne(DatasetGraph dsg, Node g, Node s, Node p, Node o) { + Iterator<Quad> iter = dsg.findNG(g, s, p, o); + if ( ! iter.hasNext() ) + return null; + Quad q = iter.next(); + if ( iter.hasNext() ) + return null; + return q; } private static Statement getOne(Model m, Resource s, Property p, RDFNode o) { @@ -438,6 +577,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-webapp/src/main/java/org/apache/jena/fuseki/webapp/FusekiWebapp.java b/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/webapp/FusekiWebapp.java index 63184fa6f9..b68c555a55 100644 --- a/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/webapp/FusekiWebapp.java +++ b/jena-fuseki2/jena-fuseki-webapp/src/main/java/org/apache/jena/fuseki/webapp/FusekiWebapp.java @@ -18,8 +18,6 @@ package org.apache.jena.fuseki.webapp; -import static java.lang.String.format; - import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -39,12 +37,12 @@ import org.apache.jena.fuseki.Fuseki; import org.apache.jena.fuseki.FusekiConfigException; import org.apache.jena.fuseki.build.FusekiConfig; import org.apache.jena.fuseki.cmd.FusekiArgs; +import org.apache.jena.fuseki.mgt.ActionDatasets; import org.apache.jena.fuseki.mgt.Template; import org.apache.jena.fuseki.mgt.TemplateFunctions; import org.apache.jena.fuseki.server.DataAccessPoint; import org.apache.jena.fuseki.server.DataAccessPointRegistry; import org.apache.jena.fuseki.server.DataService; -import org.apache.jena.fuseki.servlets.HttpAction; import org.apache.jena.fuseki.servlets.ServletOps; import org.apache.jena.fuseki.system.FusekiCore; import org.apache.jena.graph.Graph; @@ -115,6 +113,27 @@ public class FusekiWebapp // Marks the end of successful initialization. /*package*/static boolean serverInitialized = false; + // Run-time lock for operations that change the server configuration (e..g adding and deleting data services) + public static final 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; + } + public /*package*/ synchronized static void formatBaseArea() { if ( initialized ) return; @@ -407,25 +426,6 @@ public class FusekiWebapp return path; } - /** - * Dataset set name to configuration file name. Return a configuration file name - - * existing one or ".ttl" form if new - */ - public static String datasetNameToConfigurationFile(HttpAction action, String dsName) { - List<String> existing = existingConfigurationFile(dsName); - if ( ! existing.isEmpty() ) { - if ( existing.size() > 1 ) { - action.log.warn(format("[%d] Multiple existing configuration files for %s : %s", - action.id, dsName, existing)); - ServletOps.errorBadRequest("Multiple existing configuration files for "+dsName); - return null; - } - return existing.get(0).toString(); - } - - return generateConfigurationFilename(dsName); - } - /** New configuration file name - absolute filename */ public static String generateConfigurationFilename(String dsName) { String filename = dsName; @@ -437,10 +437,12 @@ public class FusekiWebapp } /** 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(FusekiWebapp.dirConfiguration, baseFilename+".*") ) { + // This ".* is a file glob pattern, not a regular expression - it looks for file extensions. + try (DirectoryStream<Path> stream = Files.newDirectoryStream(FusekiWebapp.dirConfiguration, filename+".*") ) { stream.forEach((p)-> paths.add(FusekiWebapp.dirConfiguration.resolve(p).toString() )); } return paths; diff --git a/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TS_FusekiWebapp.java b/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TS_FusekiWebapp.java index cb517c964c..14733687de 100644 --- a/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TS_FusekiWebapp.java +++ b/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TS_FusekiWebapp.java @@ -33,6 +33,8 @@ import org.junit.runners.Suite; , TestWebappAuthUpdate_JDK.class , TestWebappFileUpload.class , TestWebappAdmin.class + , TestWebappAdminAddDeleteDatasetTemplate.class + , TestWebappAdminAddDeleteDatasetFile.class , TestWebappAdminAPI.class , TestWebappServerReadOnly.class , TestWebappMetrics.class diff --git a/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdmin.java b/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdmin.java index 74c0baf0a2..a6c25fb468 100644 --- a/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdmin.java +++ b/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdmin.java @@ -18,24 +18,25 @@ package org.apache.jena.fuseki; -import static org.apache.jena.fuseki.mgt.ServerMgtConst.*; +import static org.apache.jena.fuseki.mgt.ServerMgtConst.opBackup; +import static org.apache.jena.fuseki.mgt.ServerMgtConst.opDatasets; +import static org.apache.jena.fuseki.mgt.ServerMgtConst.opListBackups; +import static org.apache.jena.fuseki.mgt.ServerMgtConst.opServer; import static org.apache.jena.fuseki.server.ServerConst.opPing; -import static org.apache.jena.fuseki.server.ServerConst.opStats; -import static org.apache.jena.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.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.http.HttpRequest.BodyPublisher; -import java.net.http.HttpRequest.BodyPublishers; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.concurrent.TimeUnit; -import org.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; @@ -53,22 +54,15 @@ import org.apache.jena.fuseki.test.HttpTest; import org.apache.jena.riot.WebContent; import org.apache.jena.web.HttpSC; import org.awaitility.Awaitility; -import org.junit.*; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; /** Tests of the admin functionality */ public class TestWebappAdmin extends AbstractFusekiWebappTest { // 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/"; @Before public void setLogging() { @@ -141,152 +135,6 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { 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(ServerCtl.urlRoot()+"$/"+opDatasets, - WebContent.contentTypeTurtle+"; charset="+WebContent.charsetUTF8, - BodyPublishers.ofFile(f)); - } - // Check exists. - checkExists(dsTest); - try { - } catch (HttpException ex) { - httpPost(ServerCtl.urlRoot()+"$/"+opDatasets, - WebContent.contentTypeTurtle+"; charset="+WebContent.charsetUTF8, - BodyPublishers.ofFile(f)); - assertEquals(HttpSC.CONFLICT_409, ex.getStatusCode()); - } - } catch (IOException ex) { IO.exception(ex); return; } - - // Check exists. - checkExists(dsTest); - deleteDataset(dsTest); - } - - @Test public void add_delete_dataset_3() { - checkNotThere(dsTest); - addTestDataset(); - checkExists(dsTest); - deleteDataset(dsTest); - checkNotThere(dsTest); - addTestDataset(); - checkExists(dsTest); - deleteDataset(dsTest); - } - - @Test public void add_delete_dataset_4() { - checkNotThere(dsTest); - checkNotThere(dsTestInf); - addTestDatasetInf(); - checkNotThere(dsTest); - checkExists(dsTestInf); - - deleteDataset(dsTestInf); - checkNotThere(dsTestInf); - addTestDatasetInf(); - checkExists(dsTestInf); - deleteDataset(dsTestInf); - } - - @Test public void add_delete_dataset_5() { - // New style operations : cause two fuseki:names - addTestDataset(fileBase+"config-ds-plain-2.ttl"); - checkExists("test-ds2"); - } - - @Test public void add_delete_dataset_6() { - String testDB = dsTestTdb2a; - assumeNotWindows(); - - checkNotThere(testDB); - - addTestDatasetTDB2(testDB); - - // Check exists. - checkExists(testDB); - - // Remove it. - deleteDataset(testDB); - checkNotThere(testDB); - } - - @Test public void add_error_1() { - HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400, - ()-> addTestDataset(fileBase+"config-ds-bad-name-1.ttl")); - } - - @Test public void add_error_2() { - HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400, - ()-> addTestDataset(fileBase+"config-ds-bad-name-2.ttl")); - } - - @Test public void add_error_3() { - HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400, - ()-> addTestDataset(fileBase+"config-ds-bad-name-3.ttl")); - } - - @Test public void add_error_4() { - HttpTest.execWithHttpException(HttpSC.BAD_REQUEST_400, - ()-> addTestDataset(fileBase+"config-ds-bad-name-4.ttl")); - } - - @Test public void delete_dataset_1() { - String name = "NoSuchDataset"; - HttpTest.expect404( ()-> httpDelete(ServerCtl.urlRoot()+"$/"+opDatasets+"/"+name) ); - } - -// // ---- Active/Offline. -// -// @Test public void state_1() { -// // Add one -// addTestDataset(); -// try { -// checkExists(dsTest); -// -// httpPost(ServerCtl.urlRoot()+"$/"+opDatasets+"/"+dsTest+"?state=offline"); -// -// checkExistsNotActive(dsTest); -// -// httpPost(ServerCtl.urlRoot()+"$/"+opDatasets+"/"+dsTest+"?state=active"); -// -// checkExists(dsTest); -// } finally { -// deleteDataset(dsTest); -// } -// } -// -// @Test public void state_2() { -// addTestDataset(); -// httpPost(ServerCtl.urlRoot()+"$/"+opDatasets+"/"+dsTest+"?state=offline"); -// deleteDataset(dsTest); -// checkNotThere(dsTest); -// } -// -// @Test public void state_3() { -// addTestDataset(); -// HttpTest.expect404(()->httpPost(ServerCtl.urlRoot()+"$/"+opDatasets+"/DoesNotExist?state=offline")); -// deleteDataset(dsTest); -// } - // ---- Backup @Test public void create_backup_1() { @@ -330,91 +178,6 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { } } - // ---- Compact - - @Test public void compact_01() { - assumeNotWindows(); - - String testDB = dsTestTdb2b; - try { - checkNotThere(testDB); - addTestDatasetTDB2(testDB); - checkExists(testDB); - - String id = null; - try { - JsonValue v = httpPostRtnJSON(ServerCtl.urlRoot() + "$/" + opCompact + "/" + testDB); - id = v.getAsObject().getString(JsonConstCtl.taskId); - } finally { - waitForTasksToFinish(1000, 500, 20_000); - } - Assert.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. - Assert.assertNotNull("Task value", task); - JsonObject obj = task.getAsObject(); - Assert.assertNotNull("Task.getAsObject()", obj); - // Provoke code to get a stacktrace. - obj.getBoolean(JsonConstCtl.success); - // ---- - // The assertion we really wanted to check. - // Check task success - Assert.assertTrue("Expected task to be marked as successful", task.getAsObject().getBoolean(JsonConstCtl.success)); - } finally { - deleteDataset(testDB); - } - } - - @Test public void compact_02() { - HttpTest.expect400(()->{ - JsonValue v = httpPostRtnJSON(ServerCtl.urlRoot() + "$/" + opCompact + "/noSuchDataset"); - }); - } - - private void assumeNotWindows() { - if (SystemUtils.IS_OS_WINDOWS) - throw new AssumptionViolatedException("Test may be unstable on Windows due to inability to delete memory-mapped files"); - } - - // ---- Server - - // ---- Stats - - @Test public void stats_1() { - JsonValue v = execGetJSON(ServerCtl.urlRoot()+"$/"+opStats); - checkJsonStatsAll(v); - } - - @Test public void stats_2() { - addTestDataset(); - JsonValue v = execGetJSON(ServerCtl.urlRoot()+"$/"+opStats+ServerCtl.datasetPath()); - checkJsonStatsAll(v); - deleteDataset(dsTest); - } - - @Test public void stats_3() { - addTestDataset(); - HttpTest.expect404(()-> execGetJSON(ServerCtl.urlRoot()+"$/"+opStats+"/DoesNotExist")); - deleteDataset(dsTest); - } - - @Test public void stats_4() { - JsonValue v = execPostJSON(ServerCtl.urlRoot()+"$/"+opStats); - checkJsonStatsAll(v); - } - - @Test public void stats_5() { - addTestDataset(); - JsonValue v = execPostJSON(ServerCtl.urlRoot()+"$/"+opStats+ServerCtl.datasetPath()); - checkJsonStatsAll(v); - deleteDataset(dsTest); - } - @Test public void sleep_1() { String x = execSleepTask(null, 1); } @@ -542,44 +305,6 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { } } - // -- Add - - private static void addTestDataset() { - addTestDataset(fileBase+"config-ds-plain-1.ttl"); - } - - private static void addTestDatasetInf() { - addTestDataset(fileBase+"config-ds-inf.ttl"); - } - - private static 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 static void addTestDataset(String filename) { - try { - Path f = Path.of(filename); - BodyPublisher body = BodyPublishers.ofFile(f); - String ct = WebContent.contentTypeTurtle; - httpPost(ServerCtl.urlRoot()+"$/"+opDatasets, ct, body); - } catch (FileNotFoundException e) { - IO.exception(e); - } - } - - private static void deleteDataset(String name) { - httpDelete(ServerCtl.urlRoot()+"$/"+opDatasets+"/"+name); - } - private static String execSleepTask(String name, int millis) { String url = ServerCtl.urlRoot()+"$/sleep"; if ( name != null ) { @@ -703,22 +428,6 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { askPing(name); } - private static 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 static void checkNotThere(String name) { - String n = (name.startsWith("/")) ? name.substring(1) : name; - // Check gone exists. - HttpTest.expect404(()-> adminPing(n) ); - HttpTest.expect404(() -> askPing(n) ); - } - private static void checkJsonDatasetsAll(JsonValue v) { assertNotNull(v.getAsObject().get("datasets")); JsonArray a = v.getAsObject().get("datasets").getAsArray(); @@ -734,63 +443,5 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { assertNotNull(obj.get("ds.state")); assertTrue(obj.get("ds.services").isArray()); } - - private static 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 static 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 static void checkJsonStatsCounters(JsonValue v) { - JsonObject obj = v.getAsObject(); - assertTrue(obj.hasKey("Requests")); - assertTrue(obj.hasKey("RequestsGood")); - assertTrue(obj.hasKey("RequestsBad")); - } - - private static JsonValue execGetJSON(String url) { - try ( TypedInputStream in = httpGet(url) ) { - AssertExtra.assertEqualsIgnoreCase(WebContent.contentTypeJSON, in.getContentType()); - return JSON.parse(in); - } - } - - private static JsonValue execPostJSON(String url) { - try ( TypedInputStream in = httpPostStream(url, null, null, null) ) { - AssertExtra.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-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdmin.java b/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdminAddDeleteDatasetFile.java similarity index 71% copy from jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdmin.java copy to jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdminAddDeleteDatasetFile.java index 74c0baf0a2..a566e4e3a1 100644 --- a/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdmin.java +++ b/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdminAddDeleteDatasetFile.java @@ -18,18 +18,24 @@ package org.apache.jena.fuseki; -import static org.apache.jena.fuseki.mgt.ServerMgtConst.*; -import static org.apache.jena.fuseki.server.ServerConst.opPing; +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.awaitility.Awaitility.await; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import java.io.FileNotFoundException; import java.io.IOException; import java.net.http.HttpRequest.BodyPublisher; import java.net.http.HttpRequest.BodyPublishers; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -47,16 +53,21 @@ 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.ctl.JsonConstCtl; -import org.apache.jena.fuseki.mgt.ServerMgtConst; -import org.apache.jena.fuseki.server.ServerConst; import org.apache.jena.fuseki.test.HttpTest; +import org.apache.jena.fuseki.webapp.FusekiWebapp; +import org.apache.jena.query.QueryExecution; +import org.apache.jena.rdfconnection.RDFConnection; import org.apache.jena.riot.WebContent; import org.apache.jena.web.HttpSC; import org.awaitility.Awaitility; -import org.junit.*; +import org.junit.After; +import org.junit.Assert; +import org.junit.AssumptionViolatedException; +import org.junit.Before; +import org.junit.jupiter.api.Test; /** Tests of the admin functionality */ -public class TestWebappAdmin extends AbstractFusekiWebappTest { +public class TestWebappAdminAddDeleteDatasetFile extends AbstractFusekiWebappTest { // Name of the dataset in the assembler file. static String dsTest = "test-ds1"; @@ -74,6 +85,7 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { @Before public void setLogging() { LogCtl.setLevel(Fuseki.backupLogName, "ERROR"); LogCtl.setLevel(Fuseki.compactLogName,"ERROR"); + LogCtl.setLevel(Fuseki.adminLogName,"ERROR"); Awaitility.setDefaultPollDelay(20,TimeUnit.MILLISECONDS); Awaitility.setDefaultPollInterval(50,TimeUnit.MILLISECONDS); } @@ -81,31 +93,16 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { @After public void unsetLogging() { LogCtl.setLevel(Fuseki.backupLogName, "WARN"); LogCtl.setLevel(Fuseki.compactLogName,"WARN"); + LogCtl.setLevel(Fuseki.adminLogName,"WARN"); } - // --- Ping - - @Test public void ping_1() { - httpGet(ServerCtl.urlRoot()+"$/"+opPing); - } - - @Test public void ping_2() { - httpPost(ServerCtl.urlRoot()+"$/"+opPing); - } - - // --- Server status - - @Test public void server_1() { - JsonValue jv = httpGetJson(ServerCtl.urlRoot()+"$/"+opServer); - JsonObject obj = jv.getAsObject(); - // Now optional : assertTrue(obj.hasKey(JsonConst.admin)); - assertTrue(obj.hasKey(ServerConst.datasets)); - assertTrue(obj.hasKey(ServerMgtConst.uptime)); - assertTrue(obj.hasKey(ServerMgtConst.startDT)); - } - - @Test public void server_2() { - httpPost(ServerCtl.urlRoot()+"$/"+opServer); + private static void withFileEnabled(Runnable action) { + System.setProperty(FusekiWebapp.allowConfigFileProperty, "true"); + try { + action.run(); + } finally { + System.getProperties().remove(FusekiWebapp.allowConfigFileProperty); + } } // --- List all datasets @@ -141,6 +138,12 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { checkJsonDatasetsOne(v.getAsObject()); } + @Test public void add_dataset_blocked() { + // Do without enabling upload of configuration files - expected to fail + HttpException ex = assertThrows(HttpException.class, ()->addTestDatasetPerform(fileBase+"config-ds-plain-1.ttl")); + assertEquals(ex.getStatusCode(), HttpSC.BAD_REQUEST_400); + } + // Specific dataset @Test public void add_delete_dataset_1() { checkNotThere(dsTest); @@ -159,24 +162,25 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { @Test public void add_delete_dataset_2() { checkNotThere(dsTest); - try { - Path f = Path.of(fileBase+"config-ds-plain-1.ttl"); - { - httpPost(ServerCtl.urlRoot()+"$/"+opDatasets, - WebContent.contentTypeTurtle+"; charset="+WebContent.charsetUTF8, - BodyPublishers.ofFile(f)); - } - // Check exists. - checkExists(dsTest); + withFileEnabled(()->{ try { - } catch (HttpException ex) { - httpPost(ServerCtl.urlRoot()+"$/"+opDatasets, - WebContent.contentTypeTurtle+"; charset="+WebContent.charsetUTF8, - BodyPublishers.ofFile(f)); - assertEquals(HttpSC.CONFLICT_409, ex.getStatusCode()); - } - } catch (IOException ex) { IO.exception(ex); return; } - + Path f = Path.of(fileBase+"config-ds-plain-1.ttl"); + { + httpPost(ServerCtl.urlRoot()+"$/"+opDatasets, + WebContent.contentTypeTurtle+"; charset="+WebContent.charsetUTF8, + BodyPublishers.ofFile(f)); + } + // Check exists. + checkExists(dsTest); + try { + } catch (HttpException ex) { + httpPost(ServerCtl.urlRoot()+"$/"+opDatasets, + WebContent.contentTypeTurtle+"; charset="+WebContent.charsetUTF8, + BodyPublishers.ofFile(f)); + assertEquals(HttpSC.CONFLICT_409, ex.getStatusCode()); + } + } catch (IOException ex) { IO.exception(ex); return; } + }); // Check exists. checkExists(dsTest); deleteDataset(dsTest); @@ -229,6 +233,34 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { checkNotThere(testDB); } + @Test public void add_delete_dataset_TDB_1() { + String testDB = dsTestTdb2a; + assumeNotWindows(); + + checkNotThere(testDB); + + addTestDatasetTDB2(testDB); + + // Check exists. + checkExists(testDB); + + // Remove it. + deleteDataset(testDB); + checkNotThere(testDB); + } + + @Test public void add_delete_dataset_TDB_2() { + // This has location "--mem--" + String testDB = dsTestTdb2b; + 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")); @@ -249,44 +281,41 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { ()-> addTestDataset(fileBase+"config-ds-bad-name-4.ttl")); } + @Test public void noOverwriteExistingConfigFile() throws IOException { + var workingDir = Paths.get("").toAbsolutePath(); + var path = workingDir.resolve(TS_FusekiWebapp.FusekiTestBase+"/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(ServerCtl.urlRoot()+"$/" + opDatasets, ct, body)); + assertEquals(HttpSC.CONFLICT_409, ex.getStatusCode()); + // refresh the file system + dbConfig = path.toFile(); + assertTrue(dbConfig.exists()); + assertEquals("File should be still empty", 0, dbConfig.length()); + } + finally { + // Clean up the file. + if (Files.exists(path)) { + Files.delete(path); + } + } + } + @Test public void delete_dataset_1() { String name = "NoSuchDataset"; HttpTest.expect404( ()-> httpDelete(ServerCtl.urlRoot()+"$/"+opDatasets+"/"+name) ); } -// // ---- Active/Offline. -// -// @Test public void state_1() { -// // Add one -// addTestDataset(); -// try { -// checkExists(dsTest); -// -// httpPost(ServerCtl.urlRoot()+"$/"+opDatasets+"/"+dsTest+"?state=offline"); -// -// checkExistsNotActive(dsTest); -// -// httpPost(ServerCtl.urlRoot()+"$/"+opDatasets+"/"+dsTest+"?state=active"); -// -// checkExists(dsTest); -// } finally { -// deleteDataset(dsTest); -// } -// } -// -// @Test public void state_2() { -// addTestDataset(); -// httpPost(ServerCtl.urlRoot()+"$/"+opDatasets+"/"+dsTest+"?state=offline"); -// deleteDataset(dsTest); -// checkNotThere(dsTest); -// } -// -// @Test public void state_3() { -// addTestDataset(); -// HttpTest.expect404(()->httpPost(ServerCtl.urlRoot()+"$/"+opDatasets+"/DoesNotExist?state=offline")); -// deleteDataset(dsTest); -// } - // ---- Backup @Test public void create_backup_1() { @@ -381,10 +410,6 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { throw new AssumptionViolatedException("Test may be unstable on Windows due to inability to delete memory-mapped files"); } - // ---- Server - - // ---- Stats - @Test public void stats_1() { JsonValue v = execGetJSON(ServerCtl.urlRoot()+"$/"+opStats); checkJsonStatsAll(v); @@ -415,116 +440,8 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { deleteDataset(dsTest); } - @Test public void sleep_1() { - String x = execSleepTask(null, 1); - } - - @Test public void sleep_2() { - try { - String x = execSleepTask(null, -1); - fail("Sleep call unexpectedly succeed"); - } catch (HttpException ex) { - assertEquals(400, ex.getStatusCode()); - } - } - - @Test public void sleep_3() { - try { - String x = execSleepTask(null, 20*1000+1); - fail("Sleep call unexpectedly succeed"); - } catch (HttpException ex) { - assertEquals(400, ex.getStatusCode()); - } - } - // Async task testing - @Test public void task_1() { - String x = execSleepTask(null, 10); - assertNotNull(x); - Integer.parseInt(x); - } - - @Test public void task_2() { - String x = "NoSuchTask"; - String url = ServerCtl.urlRoot()+"$/tasks/"+x; - HttpTest.expect404(()->httpGetJson(url) ); - try { - checkInTasks(x); - fail("No failure!"); - } catch (AssertionError ex) {} - } - - - @Test public void task_3() { - // Timing dependent. - // Create a "long" running task so we can find it. - String x = execSleepTask(null, 100); - checkTask(x); - checkInTasks(x); - assertNotNull(x); - Integer.parseInt(x); - } - - @Test public void task_4() { - // Timing dependent. - // Create a "short" running task - String x = execSleepTask(null, 1); - // Check exists in the list of all tasks (should be "finished") - checkInTasks(x); - String url = ServerCtl.urlRoot()+"$/tasks/"+x; - - boolean finished = false; - for ( int i = 0; i < 10; i++ ) { - if ( i != 0 ) - Lib.sleep(25); - JsonValue v = httpGetJson(url); - checkTask(v); - if ( v.getAsObject().hasKey("finished") ) { - finished = true; - break; - } - } - if ( ! finished ) - fail("Task has not finished"); - } - - @Test public void task_5() { - // Short running task - still in info API call. - String x = execSleepTask(null, 1); - checkInTasks(x); - } - - @Test public void task_6() { - String x1 = execSleepTask(null, 1000); - String x2 = execSleepTask(null, 1000); - await().timeout(500,TimeUnit.MILLISECONDS).until(() -> runningTasks().size() > 1); - await().timeout(2000, TimeUnit.MILLISECONDS).until(() -> runningTasks().isEmpty()); - } - - @Test public void task_7() { - try { - String x1 = execSleepTask(null, 1000); - String x2 = execSleepTask(null, 1000); - String x3 = execSleepTask(null, 1000); - String x4 = execSleepTask(null, 1000); - try { - // Try to make test more stable on a loaded CI server. - // Unloaded the first sleep will fail but due to slowness/burstiness - // some tasks above may have completed. - String x5 = execSleepTask(null, 4000); - String x6 = execSleepTask(null, 4000); - String x7 = execSleepTask(null, 4000); - String x8 = execSleepTask(null, 10); - fail("Managed to add a 5th test"); - } catch (HttpException ex) { - assertEquals(HttpSC.BAD_REQUEST_400, ex.getStatusCode()); - } - } finally { - waitForTasksToFinish(1000, 250, 4000); - } - } - private void assertEqualsIgnoreCase(String contenttypejson, String contentType) {} private static JsonValue getTask(String taskId) { @@ -566,6 +483,12 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { } private static void addTestDataset(String filename) { + withFileEnabled(()->{ + addTestDatasetPerform(filename); + }); + } + + private static void addTestDatasetPerform(String filename) { try { Path f = Path.of(filename); BodyPublisher body = BodyPublishers.ofFile(f); @@ -580,25 +503,6 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { httpDelete(ServerCtl.urlRoot()+"$/"+opDatasets+"/"+name); } - private static String execSleepTask(String name, int millis) { - String url = ServerCtl.urlRoot()+"$/sleep"; - if ( name != null ) { - if ( name.startsWith("/") ) - name = name.substring(1); - url = url + "/"+name; - } - - JsonValue v = httpPostRtnJSON(url+"?interval="+millis); - String id = v.getAsObject().getString("taskId"); - return id; - } - - private static void checkTask(String x) { - String url = ServerCtl.urlRoot()+"$/tasks/"+x; - JsonValue v = httpGetJson(url); - checkTask(v); - } - private static void checkTask(JsonValue v) { assertNotNull(v); assertTrue(v.isObject()); @@ -703,15 +607,6 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { askPing(name); } - private static 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 static void checkNotThere(String name) { String n = (name.startsWith("/")) ? name.substring(1) : name; // Check gone exists. @@ -776,21 +671,9 @@ public class TestWebappAdmin extends AbstractFusekiWebappTest { } } - /* - 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}* - */ + 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-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdminAddDeleteDatasetTemplate.java b/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdminAddDeleteDatasetTemplate.java new file mode 100644 index 0000000000..8f4015a280 --- /dev/null +++ b/jena-fuseki2/jena-fuseki-webapp/src/test/java/org/apache/jena/fuseki/TestWebappAdminAddDeleteDatasetTemplate.java @@ -0,0 +1,268 @@ +/* + * 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; + +import static org.apache.jena.fuseki.mgt.ServerMgtConst.opDatasets; +import static org.apache.jena.http.HttpOp.httpPost; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.FileNotFoundException; +import java.net.http.HttpRequest.BodyPublisher; +import java.net.http.HttpRequest.BodyPublishers; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; + +import org.apache.jena.atlas.io.IO; +import org.apache.jena.atlas.logging.LogCtl; +import org.apache.jena.atlas.web.HttpException; +import org.apache.jena.atlas.web.TypedInputStream; +import org.apache.jena.base.Sys; +import org.apache.jena.fuseki.webapp.FusekiWebapp; +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.apache.jena.web.HttpSC.Code; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** Tests of the admin functionality */ +public class TestWebappAdminAddDeleteDatasetTemplate extends AbstractFusekiWebappTest { + + // 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/"; + + @Before public void setLogging() { + LogCtl.setLevel(Fuseki.backupLogName, "ERROR"); + LogCtl.setLevel(Fuseki.compactLogName,"ERROR"); + LogCtl.setLevel(Fuseki.adminLogName,"ERROR"); + Awaitility.setDefaultPollDelay(20,TimeUnit.MILLISECONDS); + Awaitility.setDefaultPollInterval(50,TimeUnit.MILLISECONDS); + } + + @After public void unsetLogging() { + LogCtl.setLevel(Fuseki.backupLogName, "WARN"); + LogCtl.setLevel(Fuseki.compactLogName,"WARN"); + LogCtl.setLevel(Fuseki.adminLogName,"WARN"); + } + + @Test public void add_dataset_01() { + testAddDataset("db_1"); + } + + @Test public void add_dataset_02() { + testAddDataset( "/db_2"); + } + + // Do as a file - which is blocked. + @Test public void add_dataset_99() { + expect400(()->addTestDataset(fileBase+"config-ds-plain-1.ttl")); + } + + private static void addTestDataset(String filename) { + try { + Path f = Path.of(filename); + BodyPublisher body = BodyPublishers.ofFile(f); + String ct = WebContent.contentTypeTurtle; + httpPost(ServerCtl.urlRoot()+"$/"+opDatasets, ct, body); + } catch (FileNotFoundException e) { + IO.exception(e); + } + } + + @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"); + } + + // 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) { + Params params = Params.create().add("dbName", dbName).add("dbType", "mem"); + // Use the template + String actionURL = ServerCtl.urlRoot()+"$/datasets"; + HttpOp.httpPostForm(actionURL, params); + String datasetURL = dbName.startsWith("/") + ? ServerCtl.urlRoot()+(dbName.substring(1)) + : ServerCtl.urlRoot()+dbName; + assertTrue(exists(datasetURL)); + } + + private void testAddDeleteAdd(String dbName, String dbType, boolean alreadyExists, boolean hasFiles) { + String datasetURL = ServerCtl.urlRoot()+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(ServerCtl.urlRoot()+"$/datasets", params); + + RDFConnection conn = RDFConnection.connect(ServerCtl.urlRoot()+dbName); + conn.update("INSERT DATA { <x:s> <x:p> 123 }"); + int x1 = count(conn); + assertEquals(1, x1); + + Path pathDB = FusekiWebapp.dirDatabases.resolve(dbName); + + if ( hasFiles ) + assertTrue(Files.exists(pathDB)); + + HttpOp.httpDelete(ServerCtl.urlRoot()+"$/datasets/"+dbName); + + assertFalse(exists(datasetURL)); + + //if ( hasFiles ) + assertFalse(Files.exists(pathDB)); + + // Recreate : no contents. + HttpOp.httpPostForm(ServerCtl.urlRoot()+"$/datasets", params); + assertTrue(exists(datasetURL)); + int x2 = count(conn); + assertEquals(0, x2); + if ( hasFiles ) + assertTrue(Files.exists(pathDB)); + } + + private void badAddDataserverRequest(String dbName) { + 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(); + } + } + + // -- From fusekiTestLib + + public static void expect400(Runnable runnable) { + expectFail(runnable, HttpSC.Code.BAD_REQUEST); + } + + public static void expect401(Runnable runnable) { + expectFail(runnable, HttpSC.Code.UNAUTHORIZED); + } + + public static void expect403(Runnable runnable) { + expectFail(runnable, HttpSC.Code.FORBIDDEN); + } + + public static void expect404(Runnable runnable) { + 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(); + return; + } + try { + runnable.run(); + fail("Failed: Got no exception: Expected HttpException "+code.getCode()); + } catch (HttpException ex) { + if ( ex.getStatusCode() == code.getCode() ) + return; + throw ex; + } + } +} diff --git a/jena-fuseki2/jena-fuseki-webapp/testing/config-tdb2c.ttl b/jena-fuseki2/jena-fuseki-webapp/testing/config-tdb2c.ttl new file mode 100644 index 0000000000..d71cf281a2 --- /dev/null +++ b/jena-fuseki2/jena-fuseki-webapp/testing/config-tdb2c.ttl @@ -0,0 +1,19 @@ +## Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 + +PREFIX : <#> +PREFIX fuseki: <http://jena.apache.org/fuseki#> +PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> + +PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> +PREFIX ja: <http://jena.hpl.hp.com/2005/11/Assembler#> +PREFIX tdb2: <http://jena.apache.org/2016/tdb#> + +<#service1> rdf:type fuseki:Service ; + fuseki:name "test-tdb2b" ; + fuseki:endpoint [ fuseki:name "sparql" ; + fuseki:operation fuseki:query ] ; + fuseki:dataset <#dataset> . + +<#dataset> rdf:type tdb2:DatasetTDB2 ; + # Bad. + tdb2:location "../tdb2c" .
