diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/Fuseki.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/Fuseki.java index a7ffa72ac99..11f3a4a5863 100644 --- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/Fuseki.java +++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/Fuseki.java @@ -40,37 +40,33 @@ public class Fuseki { // General fixed constants. - // See also FusekiServer for the naming on the filesystem /** Path as package name */ - static public String PATH = "org.apache.jena.fuseki"; + static public final String PATH = "org.apache.jena.fuseki"; /** a unique IRI for the Fuseki namespace */ - static public String FusekiIRI = "http://jena.apache.org/Fuseki"; + static public final String FusekiIRI = "http://jena.apache.org/Fuseki"; /** * a unique IRI including the symbol notation for which properties should be * appended */ - static public String FusekiSymbolIRI = "http://jena.apache.org/fuseki#"; + static public final String FusekiSymbolIRI = "http://jena.apache.org/fuseki#"; /** Default location of the pages for the Fuseki UI */ - static public String PagesStatic = "pages"; + static public final String PagesStatic = "pages"; /** Dummy base URi string for parsing SPARQL Query and Update requests */ - static public final String BaseParserSPARQL = "http://server/unset-base/"; + static public final String BaseParserSPARQL = "http://server/unset-base/"; /** Dummy base URi string for parsing SPARQL Query and Update requests */ - static public final String BaseUpload = "http://server/unset-base/"; - - /** Add CORS header */ - static public final boolean CORS_ENABLED = false; + static public final String BaseUpload = "http://server/unset-base/"; /** The name of the Fuseki server.*/ - static public final String NAME = "Apache Jena Fuseki"; + static public final String NAME = "Apache Jena Fuseki"; /** Version of this Fuseki instance */ - static public final String VERSION = Version.versionForClass(Fuseki.class).orElse(""); + static public final String VERSION = Version.versionForClass(Fuseki.class).orElse(""); /** Supporting Graph Store Protocol direct naming. *

@@ -175,6 +171,7 @@ public class Fuseki { public static final String attrNameRegistry = "org.apache.jena.fuseki:DataAccessPointRegistry"; public static final String attrOperationRegistry = "org.apache.jena.fuseki:OperationRegistry"; public static final String attrAuthorizationService = "org.apache.jena.fuseki:AuthorizationService"; + public static final String attrFusekiServer = "org.apache.jena.fuseki:Server"; public static void setVerbose(ServletContext cxt, boolean verbose) { cxt.setAttribute(attrVerbose, Boolean.valueOf(verbose)); diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiConfig.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiConfig.java index cf1e0f16c1f..19e6c1028bd 100644 --- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiConfig.java +++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiConfig.java @@ -285,7 +285,7 @@ public static List servicesAndDatasets(Model model) { NamedDatasetAssembler.sharedDatasetPool.clear(); // ---- Services // Server to services. - ResultSet rs = BuildLib.query("SELECT * { ?s fu:services [ list:member ?service ] }", model, "s", server); + ResultSet rs = BuildLib.query("SELECT ?service { ?s fu:services [ list:member ?service ] }", model, "s", server); List accessPoints = new ArrayList<>(); // If none, look for services by type. diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/ActionStatsText.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/ActionStatsText.java index 0ff5559b4b9..a1e6b4c4a42 100644 --- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/ActionStatsText.java +++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/ctl/ActionStatsText.java @@ -38,12 +38,12 @@ public class ActionStatsText extends ActionCtl @Override public void validate(HttpAction action) { - switch(action.getMethod() ) { + switch(action.getRequestMethod() ) { case HttpNames.METHOD_GET: case HttpNames.METHOD_POST: return; default: - ServletOps.errorMethodNotAllowed(action.getMethod()); + ServletOps.errorMethodNotAllowed(action.getRequestMethod()); } } diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataAccessPointRegistry.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataAccessPointRegistry.java index 14566f0977f..221333b45dc 100644 --- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataAccessPointRegistry.java +++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataAccessPointRegistry.java @@ -29,6 +29,7 @@ import org.apache.jena.fuseki.Fuseki; import org.apache.jena.fuseki.FusekiException; import org.apache.jena.fuseki.metrics.MetricsProviderRegistry; +import org.apache.jena.fuseki.servlets.HttpAction; /** * Registry of (dataset name, {@link DataAccessPoint}). @@ -66,7 +67,7 @@ public void register(DataAccessPoint accessPt) { */ public List accessPoints() { List accessPoints = new ArrayList<>(size()); - // Make a copy for safety. + // Copy for safety. forEach((_name, accessPoint) -> accessPoints.add(accessPoint)); return accessPoints; } @@ -97,7 +98,14 @@ public void print(String string) { }); } - // The server DataAccessPointRegistry is held in the ServletContext for the server. + /** The server DataAccessPointRegistry is held in the ServletContext. + *

+ * Reload may change this object for another one. Therefore, code should obtain the + * DataAccessPointRegistry once per operation. + *

+ * Each request, has a stable {@link HttpAction#getDataAccessPointRegistry()}. + *

Getting the {@link DataAccessPointRegistry} is atomic. + */ public static DataAccessPointRegistry get(ServletContext cxt) { DataAccessPointRegistry registry = (DataAccessPointRegistry)cxt.getAttribute(Fuseki.attrNameRegistry); if ( registry == null ) @@ -105,6 +113,10 @@ public static DataAccessPointRegistry get(ServletContext cxt) { return registry; } + /** + * Set or change the {@link DataAccessPointRegistry}. + * This is atomic. (In Jetty, it is backed by a ConcurrentHashMap). + */ public static void set(ServletContext cxt, DataAccessPointRegistry registry) { cxt.setAttribute(Fuseki.attrNameRegistry, registry); } diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Dispatcher.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Dispatcher.java index a93c3cc3ac3..8ceac84ea15 100644 --- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Dispatcher.java +++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/Dispatcher.java @@ -388,11 +388,11 @@ private static Operation mapGSP(HttpAction action, Operation operation, Endpoint } else if ( GSP_RW.equals(operation) ) { // If asking for GSP_RW, but only GSP_R is available ... // ... if OPTIONS, use GSP_R. - if ( action.getMethod().equals(HttpNames.METHOD_OPTIONS) && epSet.contains(GSP_R) ) + if ( action.getRequestMethod().equals(HttpNames.METHOD_OPTIONS) && epSet.contains(GSP_R) ) return GSP_R; // ... else 405 if ( epSet.contains(GSP_R) ) - ServletOps.errorMethodNotAllowed(action.getMethod()); + ServletOps.errorMethodNotAllowed(action.getRequestMethod()); } } return operation; diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java index 47ec1ac1792..3f218692f9b 100644 --- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java +++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java @@ -401,8 +401,6 @@ public static void setCommonHeadersForOptions(HttpAction action) { } private static void setCommonHeadersForOptions(HttpServletResponse httpResponse) { - if ( Fuseki.CORS_ENABLED ) - httpResponse.setHeader(HttpNames.hAccessControlAllowHeaders, "X-Requested-With, Content-Type, Authorization"); setCommonHeaders(httpResponse); } @@ -411,8 +409,6 @@ public static void setCommonHeaders(HttpAction action) { } private static void setCommonHeaders(HttpServletResponse httpResponse) { - if ( Fuseki.CORS_ENABLED ) - httpResponse.setHeader(HttpNames.hAccessControlAllowOrigin, "*"); if ( Fuseki.outputFusekiServerHeader ) httpResponse.setHeader(HttpNames.hServer, Fuseki.serverHttpName); } diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionProcessor.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionProcessor.java index caad104ec4c..44085722aae 100644 --- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionProcessor.java +++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionProcessor.java @@ -29,7 +29,7 @@ public interface ActionProcessor { * @param action HTTP Action */ public default void process(HttpAction action) { - switch ( action.getMethod() ) { + switch ( action.getRequestMethod() ) { case METHOD_GET -> execGet(action); case METHOD_POST -> execPost(action); case METHOD_PATCH -> execPatch(action); @@ -38,7 +38,7 @@ public default void process(HttpAction action) { case METHOD_HEAD -> execHead(action); case METHOD_OPTIONS-> execOptions(action); case METHOD_TRACE -> execTrace(action); - default -> execAny(action.getMethod(), action); + default -> execAny(action.getRequestMethod(), action); } } diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/BaseActionREST.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/BaseActionREST.java index b8ff4c3cdc2..2797d09337e 100644 --- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/BaseActionREST.java +++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/BaseActionREST.java @@ -46,6 +46,6 @@ public class BaseActionREST extends ActionREST { public void validate(HttpAction action) { } private void notSupported(HttpAction action) { - ServletOps.errorMethodNotAllowed(action.getMethod()+" "+action.getDatasetName()); + ServletOps.errorMethodNotAllowed(action.getRequestMethod()+" "+action.getDatasetName()); } } diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/HttpAction.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/HttpAction.java index dc3bfd89d5e..8e314c7a4ee 100644 --- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/HttpAction.java +++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/HttpAction.java @@ -278,6 +278,7 @@ public void begin(TxnType txnType) { transactional.begin(txnType); activeDSG = dsg; if ( dataService != null ) + // Paired with finishTxn in end() dataService.startTxn(txnType); } @@ -332,7 +333,8 @@ private void endOfAction() { } public void commit() { - dataService.finishTxn(); + // Signalling the end of transaction is done in end(). + //dataService.finishTxn(); transactional.commit(); end(); } @@ -462,6 +464,8 @@ public String toString() { // ---- Request - response abstraction. + /** @deprecated Use {@link #getRequestMethod}. */ + @Deprecated(since="5.1.0", forRemoval=true) public String getMethod() { return request.getMethod(); } public HttpServletRequest getRequest() { return request; } diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServletOps.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServletOps.java index 92612ffe388..c30c6a0ea85 100644 --- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServletOps.java +++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServletOps.java @@ -252,11 +252,11 @@ public static void errorForbidden(String msg) { } public static void error(int statusCode) { - throw new ActionErrorException(statusCode, null, null); + throw actionErrorException(statusCode, null, null); } public static void error(int statusCode, String string) { - throw new ActionErrorException(statusCode, string, null); + throw actionErrorException(statusCode, string, null); } public static void errorOccurred(String message) { @@ -268,11 +268,14 @@ public static void errorOccurred(Throwable ex) { } public static void errorOccurred(String message, Throwable ex) { - if ( message == null ) - System.err.println(); if ( ex instanceof ActionErrorException actionErr ) throw actionErr; - throw new ActionErrorException(HttpSC.INTERNAL_SERVER_ERROR_500, message, ex); + throw actionErrorException(HttpSC.INTERNAL_SERVER_ERROR_500, message, ex); + /* Does not return */ + } + + private static ActionErrorException actionErrorException(int statusCode, String message, Throwable ex) { + return new ActionErrorException(statusCode, message, ex); } public static String formatForLog(String string) { diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/UploadRDF.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/UploadRDF.java index 2e336932edc..0920baaf86a 100644 --- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/UploadRDF.java +++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/UploadRDF.java @@ -54,7 +54,7 @@ public class UploadRDF extends ActionREST { @Override protected void doPatch(HttpAction action) { unsupported(action); } private void unsupported(HttpAction action) { - ServletOps.errorMethodNotAllowed(action.getMethod()); + ServletOps.errorMethodNotAllowed(action.getRequestMethod()); } @Override diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/system/FusekiLogging.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/system/FusekiLogging.java index a94cc415a40..7fd3788eaf5 100644 --- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/system/FusekiLogging.java +++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/system/FusekiLogging.java @@ -34,7 +34,7 @@ /** * FusekiLogging. *

- * This applies to Fuseki run from the command line and embedded. + * This applies to Fuseki run from the command line, as a combined jar and as an embedded server. *

* This does not apply to Fuseki running in Tomcat where it uses the * servlet 3.0 mechanism described in @@ -49,10 +49,9 @@ public class FusekiLogging // Set logging. // 1/ Use system property log4j2.configurationFile if defined. - // 2/ Use file:log4j2.properties if exists + // 2/ Use file:log4j2.properties if exists [Jena extension] // 3/ Use log4j2.properties on the classpath. - // 4/ Use org/apache/jena/fuseki/log4j2.properties on the classpath. - // 5/ Use built in string + // 4/ Use built in string /** * Places for the log4j properties file at (3). diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiLib.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiLib.java index 0b0d152525d..bd7c94becc4 100644 --- a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiLib.java +++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiLib.java @@ -21,20 +21,24 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import jakarta.servlet.ServletContext; +import org.apache.jena.atlas.logging.Log; +import org.apache.jena.fuseki.Fuseki; import org.apache.jena.fuseki.access.AccessCtl_AllowGET; import org.apache.jena.fuseki.access.AccessCtl_Deny; import org.apache.jena.fuseki.access.AccessCtl_GSP_R; import org.apache.jena.fuseki.access.AccessCtl_SPARQL_QueryDataset; -import org.apache.jena.fuseki.server.DataAccessPointRegistry; -import org.apache.jena.fuseki.server.Endpoint; -import org.apache.jena.fuseki.server.Operation; +import org.apache.jena.fuseki.build.FusekiConfig; +import org.apache.jena.fuseki.server.*; import org.apache.jena.fuseki.servlets.ActionService; import org.apache.jena.fuseki.servlets.GSP_RW; import org.apache.jena.fuseki.servlets.HttpAction; +import org.apache.jena.rdf.model.Model; import org.apache.jena.riot.WebContent; /** Actions on and about a {@link FusekiServer} */ @@ -52,6 +56,34 @@ public static Collection names(FusekiServer server) { return names; } + /** + * Process a configuration mode to find the DataServices and reset the server + * {@link DataAccessPointRegistry}. The only server-level setting processed is + * the {@code fuseki:services} list. Other settings are ignored. + */ + public static void reload(FusekiServer server, Model configuration) { + DataAccessPointRegistry newRegistry = new DataAccessPointRegistry(); + OperationRegistry operationRegistry = server.getOperationRegistry(); + + try { + List newDAPs = FusekiConfig.servicesAndDatasets(configuration); + newDAPs.forEach(dap->newRegistry.register(dap)); + FusekiServer.Builder.prepareDataServices(newRegistry, operationRegistry); + // Reload : switch DataAccessPointRegistry + setDataAccessPointRegistry(server, newRegistry); + } catch (RuntimeException ex) { + Log.error(Fuseki.serverLog, "Failed to load a new configuration", ex); + } + } + + public static void setDataAccessPointRegistry(FusekiServer server, DataAccessPointRegistry newRegistry) { + Objects.requireNonNull(server, "server"); + Objects.requireNonNull(newRegistry, "newRegistry"); + ServletContext cxt = server.getServletContext(); + // This is atomic (in Jetty, it is backed by a ConcurrentHashMap). + DataAccessPointRegistry.set(cxt, newRegistry); + } + /** * Return a {@code FusekiServer.Builder} setup for data access control. */ @@ -82,7 +114,6 @@ public static FusekiServer.Builder fusekiBuilderAccessCtl(FusekiServer.Builder b public static void modifyForAccessCtl(DataAccessPointRegistry dapRegistry, Function determineUser) { dapRegistry.forEach((name, dap) -> { dap.getDataService().forEachEndpoint(ep->{ - Operation op = ep.getOperation(); modifyForAccessCtl(ep, determineUser); }); }); diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiServer.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiServer.java index 8e84dbdd366..d4fdf91c39e 100644 --- a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiServer.java +++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiServer.java @@ -39,6 +39,8 @@ import org.apache.jena.atlas.lib.IRILib; import org.apache.jena.atlas.lib.Pair; import org.apache.jena.atlas.lib.Registry; +import org.apache.jena.atlas.logging.FmtLog; +import org.apache.jena.atlas.logging.Log; import org.apache.jena.atlas.web.AuthScheme; import org.apache.jena.fuseki.Fuseki; import org.apache.jena.fuseki.FusekiConfigException; @@ -144,8 +146,20 @@ public static Builder create() { * dispatches to an operation, and an operation maps to an implementation. This is a * specialised operation - normal use is the operation {@link #create()}. */ - public static Builder create(OperationRegistry serviceDispatchRegistry) { - return new Builder(serviceDispatchRegistry); + public static Builder create(OperationRegistry operationRegistry) { + return new Builder(operationRegistry); + } + + /** + * Return the {@code FusekiServer} associated with a {@link ServletContext}. + *

+ * This will return null if the {@link ServletContext} is not from a FusekiServer. + */ + public static FusekiServer get(ServletContext cxt) { + FusekiServer server = (FusekiServer)cxt.getAttribute(Fuseki.attrFusekiServer); + if ( server == null ) + Log.warn(Fuseki.serverLog, "No FusekiServer resgiterd in ServletContext"); + return server; } /** @@ -163,18 +177,24 @@ public static Builder create(OperationRegistry serviceDispatchRegistry) { private int httpsPort; private final String staticContentDir; private final ServletContext servletContext; + private final String configFilename; private final FusekiModules modules; private FusekiServer(int httpPort, int httpsPort, Server server, String staticContentDir, FusekiModules modules, + String configFilename, ServletContext fusekiServletContext) { this.server = Objects.requireNonNull(server); this.httpPort = httpPort; this.httpsPort = httpsPort; this.staticContentDir = staticContentDir; this.servletContext = Objects.requireNonNull(fusekiServletContext); + this.configFilename = configFilename; this.modules = Objects.requireNonNull(modules); + + // So servlets can find the server. + servletContext.setAttribute(Fuseki.attrFusekiServer, this); } /** @@ -302,6 +322,14 @@ public FusekiModules getModules() { return modules; } + /** + * Return the filename of the configuration file. + * Returns null if configuration was by APIs and there is no such file was used. + */ + public String getConfigFilename() { + return configFilename; + } + /** * Start the server - the server continues to run after this call returns. * To synchronise with the server stopping, call {@link #join}. @@ -411,6 +439,9 @@ public static class Builder { private boolean withTasks = false; private String jettyServerConfig = null; + + // Record calls to parseConfigFile. + private String configFileName = null; private Model configModel = null; private Map corsInitParams = null; @@ -767,12 +798,34 @@ public Builder add(String name, DataService dataService) { /** * Configure using a Fuseki services/datasets assembler file. *

- * The application is responsible for ensuring a correct classpath. For example, - * including a dependency on {@code jena-text} if the configuration file includes - * a text index. + * The configuration file is processed in this call so that subsequent + * builder operations can refer to data services created by this call. + *

+ * The calling application is responsible for ensuring a correct classpath. + * For example, including a dependency on {@code jena-text} if the + * configuration file includes a text index. + */ + public Builder parseConfigFile(Path filename) { + requireNonNull(filename, "filename"); + parseConfigFile(filename.toString()); + return this; + } + + /** + * Configure using a Fuseki services/datasets assembler file. + *

+ * The configuration file is processed in this call so that subsequent + * builder operations can refer to data services created by this call. + *

+ * The calling application is responsible for ensuring a correct classpath. + * For example, including a dependency on {@code jena-text} if the + * configuration file includes a text index. */ public Builder parseConfigFile(String filename) { requireNonNull(filename, "filename"); + if ( configFileName != null ) + FmtLog.warn(Fuseki.configLog, "Multiple configuration files"); + this.configFileName = filename; Model model = AssemblerUtils.readAssemblerFile(filename); parseConfig(model); return this; @@ -832,6 +885,8 @@ public Builder jettyServerConfig(String filename) { requireNonNull(filename, "filename"); if ( ! FileOps.exists(filename) ) throw new FusekiConfigException("File no found: "+filename); + if ( this.jettyServerConfig != null ) + FmtLog.warn(Fuseki.configLog, "Jetty configuration file already provied - replacing with new setting"); this.jettyServerConfig = filename; return this; } @@ -1307,15 +1362,20 @@ public FusekiServer build() { // Prepare the DataAccessPointRegistry. // Put it in the servlet context. // This would be the reload operation. - applyDatabaseSetup(handler, dapRegistry, operationReg); + applyDatabaseSetup(handler.getServletContext(), dapRegistry, operationReg); // Must be after the DataAccessPointRegistry is in the servlet context. if ( hasFusekiSecurityHandler ) applyAccessControl(handler, dapRegistry); if ( jettyServerConfig != null ) { + // Jetty server configuration provided. Server server = jettyServer(handler, jettyServerConfig); - return new FusekiServer(-1, -1, server, staticContentDir, modules, handler.getServletContext()); + return new FusekiServer(-1, -1, server, + staticContentDir, + modules, + configFileName, + handler.getServletContext()); } Server server; @@ -1340,7 +1400,11 @@ public FusekiServer build() { if ( networkLoopback ) applyLocalhost(server); - FusekiServer fusekiServer = new FusekiServer(httpPort, httpsPort, server, staticContentDir, modules, handler.getServletContext()); + FusekiServer fusekiServer = new FusekiServer(httpPort, httpsPort, server, + staticContentDir, + modules, + configFileName, + handler.getServletContext()); FusekiModuleStep.server(fusekiServer); return fusekiServer; } finally { @@ -1383,7 +1447,21 @@ private ServletContextHandler buildFusekiServerContext() { return handler; } - private static void prepareDataServices(DataAccessPointRegistry dapRegistry, OperationRegistry operationReg) { + /** + * Setup up a {@link ServlectContext} by setting attributes for + * {@link DataAccessPointRegistry} and {@link OperationRegistry}. + */ + private static void applyDatabaseSetup(ServletContext servletContext, + DataAccessPointRegistry dapRegistry, + OperationRegistry operationReg) { + // Final wiring up of DataAccessPointRegistry + prepareDataServices(dapRegistry, operationReg); + + OperationRegistry.set(servletContext, operationReg); + DataAccessPointRegistry.set(servletContext, dapRegistry); + } + + /*package*/ static void prepareDataServices(DataAccessPointRegistry dapRegistry, OperationRegistry operationReg) { dapRegistry.forEach((name, dap) -> { // Override for graph-level access control. if ( DataAccessCtl.isAccessControlled(dap.getDataService().getDataset()) ) { @@ -1402,21 +1480,6 @@ private static void prepareDataServices(DataAccessPointRegistry dapRegistry, Ope }); } - /** - * Given a ServletContextHandler, set the servlet attributes for - * {@link DataAccessPointRegistry} and {@link OperationRegistry}. - */ - private static void applyDatabaseSetup(ServletContextHandler handler, - DataAccessPointRegistry dapRegistry, - OperationRegistry operationReg) { - // Final wiring up of DataAccessPointRegistry - prepareDataServices(dapRegistry, operationReg); - - ServletContext cxt = handler.getServletContext(); - OperationRegistry.set(cxt, operationReg); - DataAccessPointRegistry.set(cxt, dapRegistry); - } - private ConstraintSecurityHandler buildSecurityHandler() { UserStore userStore = JettySecurityLib.makeUserStore(passwordFile); return JettySecurityLib.makeSecurityHandler(realm, userStore, authScheme); diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/JettyServer.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/JettyServer.java index 80641c76f6d..fb65a1044f2 100644 --- a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/JettyServer.java +++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/JettyServer.java @@ -393,6 +393,7 @@ private void adjustForFuseki(ServletContext cxt) { try { Fuseki.setVerbose(cxt, verbose); OperationRegistry.set(cxt, OperationRegistry.createEmpty()); + // Empty DataAccessPointRegistry, no nulls! DataAccessPointRegistry.set(cxt, new DataAccessPointRegistry()); } catch (NoClassDefFoundError err) { LOG.info("Fuseki classes not found"); diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/mgt/ActionReload.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/mgt/ActionReload.java new file mode 100644 index 00000000000..cdd8a5e4ea0 --- /dev/null +++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/mgt/ActionReload.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jena.fuseki.main.mgt; + +import org.apache.jena.atlas.logging.FmtLog; +import org.apache.jena.fuseki.Fuseki; +import org.apache.jena.fuseki.ctl.ActionCtl; +import org.apache.jena.fuseki.main.FusekiLib; +import org.apache.jena.fuseki.main.FusekiServer; +import org.apache.jena.fuseki.servlets.HttpAction; +import org.apache.jena.fuseki.servlets.ServletOps; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.riot.RDFParser; +import org.apache.jena.riot.web.HttpNames; + +/** + * Administration action to reload the server's dataset configuration. + *

+ * This is done by reading the configuration file which may have changed since server startup. + *

+ * If the server does not have a configuration file (e.g. command line or programmatic configuration) + */ +public class ActionReload extends ActionCtl { + + @Override + public void validate(HttpAction action) { + if ( action.getRequestMethod() != HttpNames.METHOD_POST ) { + ServletOps.errorMethodNotAllowed(action.getRequestMethod()); + } + } + + @Override + public void execute(HttpAction action) { + FusekiServer server = FusekiServer.get(action.getRequest().getServletContext()); + if ( server == null ) { + ServletOps.errorOccurred("Failed to find the server for this action"); + return; + } + + String configFilename = server.getConfigFilename(); + if ( configFilename == null ) { + FmtLog.warn(Fuseki.serverLog, "[%d] Server does not have an associated configuration file", action.id); + ServletOps.errorBadRequest("Server does not have an associated configuration file"); + return; + } + Model model = RDFParser.source(configFilename).toModel(); + FmtLog.info(Fuseki.serverLog, "[%d] Reload configuration", action.id); + FusekiLib.reload(server, model); + } +} + diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiAutoModules.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiAutoModules.java index ba79616b4c6..2c7ef76a0c1 100644 --- a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiAutoModules.java +++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiAutoModules.java @@ -28,17 +28,19 @@ import org.apache.jena.atlas.lib.Lib; import org.apache.jena.atlas.lib.Version; import org.apache.jena.atlas.logging.FmtLog; -import org.apache.jena.fuseki.Fuseki; import org.apache.jena.fuseki.FusekiConfigException; import org.apache.jena.fuseki.main.FusekiServer; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Control of {@link FusekiAutoModule} found via {@link ServiceLoader}. */ public class FusekiAutoModules { - private static final Logger LOG = Fuseki.serverLog; + // Separate from Fuseki.serverLog; + private static final Logger LOG = LoggerFactory.getLogger(FusekiAutoModules.class); + private static final Object lock = new Object(); public static final String logLoadingProperty = "fuseki.logLoading"; @@ -173,7 +175,7 @@ private void discoveryWarnLegacy() { try { ServiceLoader newServiceLoader = ServiceLoader.load(moduleClass, this.getClass().getClassLoader()); newServiceLoader.stream().forEach(provider->{ - FmtLog.warn(FusekiAutoModules.class, "Ignored: \"%s\" : legacy use of interface FusekiModule which has changed to FusekiAutoModule", provider.type().getSimpleName()); + FmtLog.warn(LOG, "Ignored: \"%s\" : legacy use of interface FusekiModule which has changed to FusekiAutoModule", provider.type().getSimpleName()); }); } catch (ServiceConfigurationError ex) { // Ignore - we were only checking. @@ -218,7 +220,7 @@ private FusekiModules load() { String name = m.name(); if ( name == null ) name = m.getClass().getSimpleName(); - FmtLog.info(LOG, "Module: %s (%s)", + FmtLog.debug(LOG, "Module: %s (%s)", name, Version.versionForClass(m.getClass()).orElse("unknown")); }); diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModuleStep.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModuleStep.java index 7735c187b47..1a320f5d21c 100644 --- a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModuleStep.java +++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModuleStep.java @@ -77,7 +77,7 @@ public static void serverStopped(FusekiServer server) { } /** - * Sever reload. + * Server reload. * Return true if reload happened, else false. * @see FusekiBuildCycle#serverConfirmReload * @see FusekiBuildCycle#serverReload diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/CustomTestService.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/CustomTestService.java index a7b8cdbf18f..4fc40a60c22 100644 --- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/CustomTestService.java +++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/CustomTestService.java @@ -76,6 +76,6 @@ protected void doPost(HttpAction action) { public void validate(HttpAction action) { } private void notSupported(HttpAction action) { - ServletOps.errorMethodNotAllowed(action.getMethod()+" "+action.getDatasetName()); + ServletOps.errorMethodNotAllowed(action.getRequestMethod()+" "+action.getDatasetName()); } } diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TS_FusekiMain.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TS_FusekiMain.java index c5ddd51305b..fe72f5c2d8d 100644 --- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TS_FusekiMain.java +++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TS_FusekiMain.java @@ -52,9 +52,11 @@ , TestPatchFuseki.class , TestFusekiCustomScriptFunc.class - // Test ping. , TestMetrics.class , TestFusekiShaclValidation.class + + , TestFusekiMainAdmin.class + }) public class TS_FusekiMain {} diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiMainAdmin.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiMainAdmin.java new file mode 100644 index 00000000000..57af3a985ec --- /dev/null +++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiMainAdmin.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jena.fuseki.main; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import org.apache.jena.atlas.io.IOX; +import org.apache.jena.atlas.logging.LogCtl; +import org.apache.jena.fuseki.Fuseki; +import org.apache.jena.fuseki.main.mgt.ActionReload; +import org.apache.jena.http.HttpOp; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.riot.RDFParser; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.core.DatasetGraphFactory; +import org.apache.jena.sparql.engine.http.QueryExceptionHTTP; +import org.apache.jena.sparql.exec.QueryExec; +import org.apache.jena.sparql.exec.RowSetOps; +import org.apache.jena.sparql.exec.http.QueryExecHTTP; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +public class TestFusekiMainAdmin { + + private static Path fConfigServer = Path.of("target/config-reload.ttl"); + private static Path DIR = Path.of("testing/Config/"); + private static Path fConfig1 = DIR.resolve("reload-config1.ttl"); + private static Path fConfig2 = DIR.resolve("reload-config2.ttl"); + + @Before public void before() { + // Initial state + copyFile(fConfig1, fConfigServer); + } + + @AfterClass public static void after() { + try { + Files.delete(fConfigServer); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + + @Test public void serverReload_1() { + FusekiServer server = server(fConfigServer); + try { + server.start(); + + serverBeforeReload(server); + + // Change configuration file. + copyFile(fConfig2, fConfigServer); + Model newConfig = RDFParser.source(fConfigServer).toModel(); + + // Reload operation on the server + HttpOp.httpPost("http://localhost:"+server.getPort()+"/$/reload"); + + serverAfterReload(server); + } + finally { server.stop(); } + } + + @Test public void serverReload_2() { + FusekiServer server = serverNoConfigFile(); + try { + server.start(); + + serverBeforeReload(server); + + // Change configuration file. + // Error! + copyFile(fConfig2, fConfigServer); + Model newConfig = RDFParser.source(fConfigServer).toModel(); + + LogCtl.withLevel(Fuseki.serverLog, "ERROR", ()-> + // Poke server - Operation denied - no configuration file. + FusekiTestLib.expect400(()->HttpOp.httpPost("http://localhost:"+server.getPort()+"/$/reload")) + ); + + // No change + serverBeforeReload(server); + } + finally { server.stop(); } + } + + private static void copyFile(Path pathSrc, Path pathDest) { + try { + Files.copy(pathSrc, pathDest, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException ex) { throw IOX.exception(ex); } + } + + private static void serverBeforeReload(FusekiServer server) { + String URL = "http://localhost:"+server.getPort()+"/$/ping"; + String x = HttpOp.httpGetString(URL); + query(server, "/ds", "SELECT * { }", 200); + query(server, "/dataset2", "SELECT * { ?s ?p ?o }", 404); + query(server, "/zero", "SELECT * { }", 404); + query(server, "/codedsg", "SELECT * { }", 200); + } + + private static void serverAfterReload(FusekiServer server) { + String URL = "http://localhost:"+server.getPort()+"/$/ping"; + String x = HttpOp.httpGetString(URL); + query(server, "/ds", "SELECT * { }", 404); + query(server, "/dataset2", "SELECT * { ?s ?p ?o }", 200); + query(server, "/zero", "SELECT * { }", 404); + // Replaced. + query(server, "/codedsg", "SELECT * { }", 404); + } + + private static void query(FusekiServer server, String datasetName, String queryString, int expectedStatusCode) { + QueryExec qExec = QueryExecHTTP.service(server.datasetURL(datasetName)).query(queryString).build(); + try { + RowSetOps.consume(qExec.select()); + assertEquals(datasetName, expectedStatusCode, 200); + } catch (QueryExceptionHTTP ex) { + assertEquals(datasetName, expectedStatusCode, ex.getStatusCode()); + } + } + + private static FusekiServer serverNoConfigFile() { + DatasetGraph dsg = DatasetGraphFactory.createTxnMem(); + FusekiServer server = FusekiServer.create().port(0) + // .verbose(true) + .addServlet("/$/reload", new ActionReload()) + .add("/ds", dsg) + .add("/codedsg", dsg) + .build(); + return server; + } + + + private static FusekiServer server(Path fConfig) { + DatasetGraph dsg = DatasetGraphFactory.createTxnMem(); + FusekiServer server = FusekiServer.create().port(0) + // .verbose(true) + .addServlet("/$/reload", new ActionReload()) + .parseConfigFile(fConfig) + .add("/codedsg", dsg) + .build(); + return server; + } +} diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiServerBuild.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiServerBuild.java index cefa3ae31d3..611ca3a80e0 100644 --- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiServerBuild.java +++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiServerBuild.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.util.function.Consumer; +import jakarta.servlet.ServletContext; import org.apache.jena.atlas.iterator.Iter; import org.apache.jena.atlas.logging.Log; import org.apache.jena.atlas.logging.LogCtl; @@ -60,9 +61,13 @@ public class TestFusekiServerBuild { @Test public void fuseki_build_1() { FusekiServer server = FusekiServer.create().port(3456).build(); - // Not started. Port not assigned. assertTrue(server.getHttpPort() == 3456 ); assertTrue(server.getHttpsPort() == -1 ); + + ServletContext cxt = server.getServletContext(); + FusekiServer server2 = FusekiServer.get(cxt); + assertNotNull(server2); + assertEquals(server, server2); } @Test public void fuseki_build_2() { diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestPlainServer.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestPlainServer.java index e3a27b85c1d..6c1e407a53e 100644 --- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestPlainServer.java +++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestPlainServer.java @@ -114,7 +114,7 @@ public static void afterClass() { } @Test public void plainFile3() { - String x = HttpOp.httpGetString(serverURL+"/file-top.txt"); + String x = HttpOp.httpGetString(serverURL+"/exists.txt"); assertNotNull(x); assertTrue(x.contains("CONTENT")); } diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/DemoService.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/DemoService.java index b83273b7648..6db9b378cab 100644 --- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/DemoService.java +++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/examples/DemoService.java @@ -79,6 +79,6 @@ protected void doPost(HttpAction action) { public void validate(HttpAction action) { } private void notSupported(HttpAction action) { - ServletOps.errorMethodNotAllowed(action.getMethod()+" "+action.getActionURI()); + ServletOps.errorMethodNotAllowed(action.getRequestMethod()+" "+action.getActionURI()); } } diff --git a/jena-fuseki2/jena-fuseki-main/testing/Config/reload-config1.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/reload-config1.ttl new file mode 100644 index 00000000000..3e916dbfe87 --- /dev/null +++ b/jena-fuseki2/jena-fuseki-main/testing/Config/reload-config1.ttl @@ -0,0 +1,16 @@ +PREFIX : <#> +PREFIX fuseki: +PREFIX rdf: +PREFIX rdfs: +PREFIX ja: + +:service rdf:type fuseki:Service ; + fuseki:name "ds" ; + fuseki:endpoint [ fuseki:operation fuseki:query ; ] ; + fuseki:endpoint [ fuseki:operation fuseki:update ; ] ; + fuseki:endpoint [ fuseki:operation fuseki:gsp-rw ; ] ; + fuseki:endpoint [ fuseki:operation fuseki:patch ; ] ; + fuseki:dataset :dataset ; +. + +:dataset rdf:type ja:MemoryDataset . diff --git a/jena-fuseki2/jena-fuseki-main/testing/Config/reload-config2.ttl b/jena-fuseki2/jena-fuseki-main/testing/Config/reload-config2.ttl new file mode 100644 index 00000000000..bad6403a748 --- /dev/null +++ b/jena-fuseki2/jena-fuseki-main/testing/Config/reload-config2.ttl @@ -0,0 +1,25 @@ +PREFIX : <#> +PREFIX fuseki: +PREFIX rdf: +PREFIX rdfs: +PREFIX ja: + +:service rdf:type fuseki:Service ; + fuseki:name "dataset2" ; + fuseki:endpoint [ fuseki:operation fuseki:query ; ] ; + fuseki:endpoint [ fuseki:operation fuseki:update ; ] ; + fuseki:endpoint [ fuseki:operation fuseki:gsp-rw ; ] ; + fuseki:endpoint [ fuseki:operation fuseki:patch ; ] ; + fuseki:dataset :dataset ; +. + +:dataset rdf:type ja:MemoryDataset ; + #ja:data "data.trig"; +. + +:service2 rdf:type fuseki:Service ; + fuseki:name "null" ; + ## No operations. + ## Always empty dataset. + fuseki:dataset [ rdf:type ja:RDFDatasetZero ] ; + . diff --git a/jena-fuseki2/jena-fuseki-main/testing/Files/exists.txt b/jena-fuseki2/jena-fuseki-main/testing/Files/exists.txt new file mode 100644 index 00000000000..4c5132ec87e --- /dev/null +++ b/jena-fuseki2/jena-fuseki-main/testing/Files/exists.txt @@ -0,0 +1,2 @@ +This file is used by the plain server static resource tests. +CONTENT diff --git a/jena-fuseki2/jena-fuseki-main/testing/Files/file-top.txt b/jena-fuseki2/jena-fuseki-main/testing/Files/file-top.txt deleted file mode 100644 index a980c835bc0..00000000000 --- a/jena-fuseki2/jena-fuseki-main/testing/Files/file-top.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 -CONTENT