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 17cb4d1b007d2b568820f54cdca197dd88ec1003
Author: Andy Seaborne <[email protected]>
AuthorDate: Wed Nov 13 17:04:05 2024 +0000

    GH-2753: Custom arguments for FusekiMain CLI
---
 .../org/apache/jena/fuseki/main/FusekiServer.java  |   7 +-
 .../org/apache/jena/fuseki/main/cmds/DSGSetup.java |  30 +-
 .../apache/jena/fuseki/main/cmds/FusekiMain.java   | 345 ++++++++++++---------
 .../cmds/{ServerConfig.java => ServerArgs.java}    |  10 +-
 .../apache/jena/fuseki/main/sys/FusekiModule.java  |   4 +-
 .../main/sys/FusekiServerArgsCustomiser.java       | 113 +++++++
 .../org/apache/jena/fuseki/main/TS_FusekiMain.java |   1 +
 .../fuseki/main/TestFusekiMainCmdArguments.java    |  24 +-
 .../main/TestFusekiMainCmdCustomArguments.java     | 227 ++++++++++++++
 9 files changed, 594 insertions(+), 167 deletions(-)

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 3e00e103d5..ef3c4622bd 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
@@ -800,6 +800,11 @@ public class FusekiServer {
             return this;
         }
 
+        /**
+         * Return the config model
+         */
+        public Model configModel() { return configModel; }
+
         /**
          * Configure using a Fuseki services/datasets assembler in a {@link 
Graph}.
          * <p>
@@ -849,7 +854,7 @@ public class FusekiServer {
         public Builder jettyServerConfig(String filename) {
             requireNonNull(filename, "filename");
             if ( ! FileOps.exists(filename) )
-                throw new FusekiConfigException("File no found: "+filename);
+                throw new FusekiConfigException("File not found: "+filename);
             this.jettyServerConfig = filename;
             return this;
         }
diff --git 
a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/DSGSetup.java
 
b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/DSGSetup.java
index 4610bcfa14..87a11e5e45 100644
--- 
a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/DSGSetup.java
+++ 
b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/DSGSetup.java
@@ -33,7 +33,7 @@ import org.apache.jena.tdb2.DatabaseMgr;
      * Given a path name and a preference of TDB1/TDB2 for new databases, 
return
      * details of the setup to use.
      */
-    /*package*/ static void setupTDB(String directory, boolean useTDB2, 
ServerConfig serverConfig) {
+    /*package*/ static void setupTDB(String directory, boolean useTDB2, 
ServerArgs serverArgs) {
         File dir = Path.of(directory).toFile();
         if ( ! dir.exists() )
             throw new CmdException("Directory does not exist: " + directory);
@@ -46,40 +46,40 @@ import org.apache.jena.tdb2.DatabaseMgr;
 
         if ( IO.isEmptyDirectory(directory) ) {
             if ( useTDB2 )
-                setupTDB2(directory, serverConfig);
+                setupTDB2(directory, serverArgs);
             else
-                setupTDB1(directory, serverConfig);
+                setupTDB1(directory, serverArgs);
             return;
         }
 
         // Exists, not empty or does not exist
         if ( TDBOps.isTDB1(directory) ) {
-            setupTDB1(directory, serverConfig);
+            setupTDB1(directory, serverArgs);
             return;
         } else if ( TDBOps.isTDB2(directory) ) {
-            setupTDB2(directory, serverConfig);
+            setupTDB2(directory, serverArgs);
             return;
         } else
             throw new CmdException("Directory not a database: " + directory);
     }
 
-    private static void setupTDB1(String directory, ServerConfig serverConfig) 
{
-        serverConfig.datasetDescription = "TDB1 dataset: location="+directory;
-        serverConfig.dsg = TDB1Factory.createDatasetGraph(directory);
+    private static void setupTDB1(String directory, ServerArgs serverArgs) {
+        serverArgs.datasetDescription = "TDB1 dataset: location="+directory;
+        serverArgs.dsg = TDB1Factory.createDatasetGraph(directory);
     }
 
-    private static void setupTDB2(String directory, ServerConfig serverConfig) 
{
-        serverConfig.datasetDescription = "TDB2 dataset: location="+directory;
-        serverConfig.dsg = DatabaseMgr.connectDatasetGraph(directory);
+    private static void setupTDB2(String directory, ServerArgs serverArgs) {
+        serverArgs.datasetDescription = "TDB2 dataset: location="+directory;
+        serverArgs.dsg = DatabaseMgr.connectDatasetGraph(directory);
     }
 
-    public static void setupMemTDB(boolean useTDB2, ServerConfig serverConfig) 
{
+    public static void setupMemTDB(boolean useTDB2, ServerArgs serverArgs) {
         String tag = useTDB2 ? "TDB2" : "TDB1";
-        serverConfig.datasetDescription = tag+" dataset in-memory";
-        serverConfig.dsg = useTDB2
+        serverArgs.datasetDescription = tag+" dataset in-memory";
+        serverArgs.dsg = useTDB2
             ? DatabaseMgr.createDatasetGraph()
             : TDB1Factory.createDatasetGraph();
-        serverConfig.allowUpdate = true;
+        serverArgs.allowUpdate = true;
 
     }
 }
diff --git 
a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiMain.java
 
b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiMain.java
index 998c79b979..181756c244 100644
--- 
a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiMain.java
+++ 
b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/FusekiMain.java
@@ -25,6 +25,7 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 import arq.cmdline.CmdARQ;
 import arq.cmdline.ModDatasetAssembler;
@@ -40,8 +41,9 @@ import org.apache.jena.fuseki.Fuseki;
 import org.apache.jena.fuseki.FusekiException;
 import org.apache.jena.fuseki.main.FusekiMainInfo;
 import org.apache.jena.fuseki.main.FusekiServer;
-import org.apache.jena.fuseki.main.sys.FusekiModules;
 import org.apache.jena.fuseki.main.sys.FusekiAutoModules;
+import org.apache.jena.fuseki.main.sys.FusekiServerArgsCustomiser;
+import org.apache.jena.fuseki.main.sys.FusekiModules;
 import org.apache.jena.fuseki.server.DataAccessPoint;
 import org.apache.jena.fuseki.server.DataAccessPointRegistry;
 import org.apache.jena.fuseki.server.FusekiCoreInfo;
@@ -53,10 +55,7 @@ import org.apache.jena.fuseki.validation.UpdateValidator;
 import org.apache.jena.query.ARQ;
 import org.apache.jena.query.Dataset;
 import org.apache.jena.rdfs.RDFSFactory;
-import org.apache.jena.riot.Lang;
-import org.apache.jena.riot.RDFDataMgr;
-import org.apache.jena.riot.RDFLanguages;
-import org.apache.jena.riot.RiotException;
+import org.apache.jena.riot.*;
 import org.apache.jena.sparql.core.DatasetGraphFactory;
 import org.apache.jena.sys.JenaSystem;
 import org.apache.jena.system.Txn;
@@ -123,22 +122,15 @@ public class FusekiMain extends CmdARQ {
     private static ArgDecl  argValidators   = new ArgDecl(ArgDecl.NoValue,  
"validators");
 
     private static List<ArgModuleGeneral> additionalArgs = new ArrayList<>();
-    public static void addArgModule(ArgModuleGeneral argModule) { 
additionalArgs.add(argModule); }
 
     // private static ModLocation modLocation = new ModLocation();
     private static ModDatasetAssembler modDataset      = new 
ModDatasetAssembler();
 
-    private final ServerConfig serverConfig  = new ServerConfig();
+    private final ServerArgs serverArgs  = new ServerArgs();
     // Default
     private boolean useTDB2 = true;
 
-    // -- Programmatic ways to create a server
-
-    /** Build, but do not start, a server based on command line syntax. */
-    public static FusekiServer build(String... args) {
-        FusekiServer.Builder builder = builder(args);
-        return builder.build();
-    }
+    // -- Programmatic ways to create a server using command line syntax.
 
     /**
      * Create a {@link org.apache.jena.fuseki.main.FusekiServer.Builder} which 
has
@@ -147,25 +139,68 @@ public class FusekiMain extends CmdARQ {
      */
     public static FusekiServer.Builder builder(String... args) {
         // Parses command line, sets arguments.
-        FusekiMain inner = new FusekiMain(args);
+        FusekiMain fusekiMain = new FusekiMain(args);
         // Process command line args according to the argument specified.
-        inner.process();
-        // Apply command line/serverConfig to a builder.
-        FusekiServer.Builder builder = inner.builder();
-        applyServerArgs(builder, inner.serverConfig);
+        fusekiMain.process();
+        // Apply command line/serverArgs to a builder.
+        FusekiServer.Builder builder = fusekiMain.builder();
+        applyServerArgs(builder, fusekiMain.serverArgs);
         return builder;
     }
 
+    /**
+     * Build, but do not start, a server based on command line syntax.
+     */
+    public static FusekiServer build(String... args) {
+        FusekiServer.Builder builder = builder(args);
+        return builder.build();
+    }
+
     /**
      * Create a server and run, within the same JVM.
      * This is the command line entry point.
      */
-
     static void run(String... argv) {
         JenaSystem.init();
         new FusekiMain(argv).mainRun();
     }
 
+    /**
+     * Registers a custom arguments module
+     * <p>
+     * This approach is useful when your custom arguments affect global 
server/runtime setup and don't need to directly
+     * impact Fuseki Server building.  If you need to impact server building 
then use
+     * {@link #addCustomiser(FusekiServerArgsCustomiser)} instead
+     * </p>
+     * @param argModule Arguments module
+     * @deprecated Register a {@link 
org.apache.jena.fuseki.main.sys.FusekiServerArgsCustomiser} via
+     * {@link #addCustomiser(FusekiServerArgsCustomiser)} instead.
+     */
+    @Deprecated(forRemoval =  true)
+    public static void addArgModule(ArgModuleGeneral argModule) { 
additionalArgs.add(argModule); }
+
+    private static final List<FusekiServerArgsCustomiser> customiseServerArgs 
= new ArrayList<>();
+    /**
+     * Registers a CLI customiser
+     * <p>
+     * A CLI customiser can add one/more custom arguments into the Fuseki 
Server CLI arguments and then can apply those
+     * to the Fuseki server being built during the processing of {@link 
#processModulesAndArgs()}.  This allows for
+     * custom arguments that directly affect how the Fuseki server is built to 
be created.
+     * </p>
+     * @param customiser CLI customiser
+     */
+    public static void addCustomiser(FusekiServerArgsCustomiser customiser) {
+        Objects.requireNonNull(customiser);
+        customiseServerArgs.add(customiser);
+    }
+
+    /**
+     * Resets any previously registered CLI customisers
+     */
+    public static void resetCustomisers() {
+        customiseServerArgs.clear();
+    }
+
     // --
 
     protected FusekiMain(String... argv) {
@@ -245,6 +280,10 @@ public class FusekiMain extends CmdARQ {
         add(argEnableModules, "--modules=true|false", "Enable Fuseki modules");
 
         super.modVersion.addClass("Fuseki", Fuseki.class);
+
+        for (FusekiServerArgsCustomiser customiser : customiseServerArgs) {
+            customiser.serverArgsModify(this);
+        }
     }
 
     static String argUsage = "[--config=FILE] 
[--mem|--desc=AssemblerFile|--file=FILE] [--port PORT] /DatasetPathName";
@@ -258,7 +297,7 @@ public class FusekiMain extends CmdARQ {
     protected void processModulesAndArgs() {
         Logger log = Fuseki.serverLog;
 
-        serverConfig.verboseLogging = super.isVerbose();
+        serverArgs.verboseLogging = super.isVerbose();
 
         boolean allowEmpty = contains(argEmpty) || contains(argSparqler);
 
@@ -299,32 +338,32 @@ public class FusekiMain extends CmdARQ {
             if ( getPositional().size() > 1 )
                 throw new CmdException("Multiple dataset path names given");
             if ( getPositional().size() != 0 )
-                serverConfig.datasetPath = 
DataAccessPoint.canonical(getPositionalArg(0));
+                serverArgs.datasetPath = 
DataAccessPoint.canonical(getPositionalArg(0));
         }
 
-        serverConfig.datasetDescription = "<unset>";
+        serverArgs.datasetDescription = "<unset>";
 
         // ---- check: Invalid: --update + --conf
         if ( contains(argUpdate) && contains(argConfig) )
             throw new CmdException("--update and a configuration file does not 
make sense (control using the configuration file only)");
         boolean allowUpdate = contains(argUpdate);
-        serverConfig.allowUpdate = allowUpdate;
+        serverArgs.allowUpdate = allowUpdate;
 
         boolean hasJettyConfigFile = contains(argJettyConfig);
 
         // ---- Port
-        serverConfig.port = defaultPort;
+        serverArgs.port = defaultPort;
 
         if ( contains(argPort) ) {
             if ( hasJettyConfigFile )
                 throw new CmdException("Cannot specify the port and also 
provide a Jetty configuration file");
-            serverConfig.port = portNumber(argPort);
+            serverArgs.port = portNumber(argPort);
         }
 
         if ( contains(argLocalhost) ) {
             if ( hasJettyConfigFile )
                 throw new CmdException("Cannot specify 'localhost' and also 
provide a Jetty configuration file");
-            serverConfig.loopback = true;
+            serverArgs.loopback = true;
         }
 
         // ---- Dataset
@@ -337,41 +376,30 @@ public class FusekiMain extends CmdARQ {
             useTDB2 = true;
 
         if ( allowEmpty ) {
-            serverConfig.empty = true;
-            serverConfig.datasetDescription = "No dataset";
+            serverArgs.empty = true;
+            serverArgs.datasetDescription = "No dataset";
         }
 
         // Fuseki config file
-        if ( contains(argConfig) ) {
-            String file = getValue(argConfig);
-            if ( file.startsWith("file:") )
-                file = file.substring("file:".length());
-
-            Path path = Path.of(file);
-            if ( ! Files.exists(path) )
-                throw new CmdException("File not found: "+file);
-            if ( Files.isDirectory(path) )
-                throw new CmdException("Is a directory: "+file);
-            serverConfig.datasetDescription = "Configuration: 
"+path.toAbsolutePath();
-            serverConfig.serverConfig = getValue(argConfig);
-        }
+        if ( contains(argConfig) )
+            serverArgs.serverConfigFile = getValue(argConfig);
 
         // Ways to set up a dataset.
         if ( contains(argMem) ) {
-            serverConfig.datasetDescription = "in-memory";
+            serverArgs.datasetDescription = "in-memory";
             // Only one setup should be called by the test above but to be safe
             // and in case of future changes, clear the configuration.
-            serverConfig.dsg = DatasetGraphFactory.createTxnMem();
+            serverArgs.dsg = DatasetGraphFactory.createTxnMem();
             // Always allow, else you can't do very much!
-            serverConfig.allowUpdate = true;
+            serverArgs.allowUpdate = true;
         }
 
         if ( contains(argFile) ) {
             List<String> filenames = getValues(argFile);
-            serverConfig.datasetDescription = "in-memory, with files loaded";
+            serverArgs.datasetDescription = "in-memory, with files loaded";
             // Update is not enabled by default for --file
-            serverConfig.allowUpdate = contains(argUpdate);
-            serverConfig.dsg = DatasetGraphFactory.createTxnMem();
+            serverArgs.allowUpdate = contains(argUpdate);
+            serverArgs.dsg = DatasetGraphFactory.createTxnMem();
 
             for(String filename : filenames ) {
                 String pathname = filename;
@@ -384,10 +412,10 @@ public class FusekiMain extends CmdARQ {
                 Lang language = RDFLanguages.filenameToLang(filename);
                 if ( language == null )
                     throw new CmdException("Cannot guess language for file: " 
+ filename);
-                Txn.executeWrite(serverConfig.dsg,  ()-> {
+                Txn.executeWrite(serverArgs.dsg,  ()-> {
                     try {
                         log.info("Dataset: in-memory: load file: " + filename);
-                        RDFDataMgr.read(serverConfig.dsg, filename);
+                        RDFDataMgr.read(serverArgs.dsg, filename);
                     } catch (RiotException ex) {
                         throw new CmdException("Failed to load file: " + 
filename);
                     }
@@ -396,28 +424,28 @@ public class FusekiMain extends CmdARQ {
         }
 
         if ( contains(argMemTDB) ) {
-            DSGSetup.setupMemTDB(useTDB2, serverConfig);
+            DSGSetup.setupMemTDB(useTDB2, serverArgs);
         }
 
         if ( contains(argTDB) ) {
             String directory = getValue(argTDB);
-            DSGSetup.setupTDB(directory, useTDB2, serverConfig);
+            DSGSetup.setupTDB(directory, useTDB2, serverArgs);
         }
 
         if ( contains(assemblerDescDecl) ) {
-            serverConfig.datasetDescription = "Assembler: "+ 
getValue(assemblerDescDecl);
+            serverArgs.datasetDescription = "Assembler: "+ 
getValue(assemblerDescDecl);
             // Need to add service details.
             Dataset ds = modDataset.createDataset();
-            serverConfig.dsg = ds.asDatasetGraph();
+            serverArgs.dsg = ds.asDatasetGraph();
         }
 
         if ( contains(argRDFS) ) {
             String rdfsVocab = getValue(argRDFS);
             if ( !FileOps.exists(rdfsVocab) )
                 throw new CmdException("No such file for RDFS: "+rdfsVocab);
-            serverConfig.rdfsGraph = RDFDataMgr.loadGraph(rdfsVocab);
-            serverConfig.datasetDescription = serverConfig.datasetDescription+ 
" (with RDFS)";
-            serverConfig.dsg = RDFSFactory.datasetRDFS(serverConfig.dsg, 
serverConfig.rdfsGraph);
+            serverArgs.rdfsGraph = RDFDataMgr.loadGraph(rdfsVocab);
+            serverArgs.datasetDescription = serverArgs.datasetDescription+ " 
(with RDFS)";
+            serverArgs.dsg = RDFSFactory.datasetRDFS(serverArgs.dsg, 
serverArgs.rdfsGraph);
         }
 
         // ---- Misc features.
@@ -430,21 +458,21 @@ public class FusekiMain extends CmdARQ {
             String filebase = getValue(argSparqler);
             if ( ! FileOps.exists(filebase) )
                 throw new CmdException("File area not found: "+filebase);
-            serverConfig.contentDirectory = filebase;
-            serverConfig.addGeneral = "/sparql";
-            serverConfig.empty = true;
-            serverConfig.validators = true;
+            serverArgs.contentDirectory = filebase;
+            serverArgs.addGeneral = "/sparql";
+            serverArgs.empty = true;
+            serverArgs.validators = true;
         }
 
         if ( contains(argGeneralQuerySvc) ) {
             String z = getValue(argGeneralQuerySvc);
             if ( ! z.startsWith("/") )
                 z = "/"+z;
-            serverConfig.addGeneral = z;
+            serverArgs.addGeneral = z;
         }
 
         if ( contains(argValidators) ) {
-            serverConfig.validators = true;
+            serverArgs.validators = true;
         }
 
         // -- Server setup.
@@ -453,7 +481,7 @@ public class FusekiMain extends CmdARQ {
             String contextPath = getValue(argContextPath);
             contextPath = sanitizeContextPath(contextPath);
             if ( contextPath != null )
-                serverConfig.servletContextPath = contextPath;
+                serverArgs.servletContextPath = contextPath;
         }
 
         if ( contains(argBase) ) {
@@ -463,17 +491,17 @@ public class FusekiMain extends CmdARQ {
                 throw new CmdException("File area not found: "+filebase);
                 //FmtLog.warn(Fuseki.configLog, "File area not found: 
"+filebase);
             }
-            serverConfig.contentDirectory = filebase;
+            serverArgs.contentDirectory = filebase;
         }
 
         if ( contains(argPasswdFile) ) {
             if ( hasJettyConfigFile )
                 throw new CmdException("Can't specify a password file and also 
provide a Jetty configuration file");
-            serverConfig.passwdFile = getValue(argPasswdFile);
+            serverArgs.passwdFile = getValue(argPasswdFile);
         }
 
         if ( contains(argRealm) )
-            serverConfig.realm =  getValue(argRealm);
+            serverArgs.realm =  getValue(argRealm);
 
         if ( contains(argHttpsPort) && ! contains(argHttps) )
             throw new CmdException("https port given but not certificate 
details via --"+argHttps.getKeyName());
@@ -481,19 +509,19 @@ public class FusekiMain extends CmdARQ {
         if ( contains(argHttps) ) {
             if ( hasJettyConfigFile )
                 throw new CmdException("Can't specify \"https\" and also 
provide a Jetty configuration file");
-            serverConfig.httpsPort = defaultHttpsPort;
+            serverArgs.httpsPort = defaultHttpsPort;
             if (  contains(argHttpsPort) )
-                serverConfig.httpsPort = portNumber(argHttpsPort);
+                serverArgs.httpsPort = portNumber(argHttpsPort);
             String httpsSetup = getValue(argHttps);
             // The details go in a separate file that can be secured.
-            serverConfig.httpsKeysDetails = httpsSetup;
+            serverArgs.httpsKeysDetails = httpsSetup;
         }
 
         if ( contains(argAuth) ) {
             if ( hasJettyConfigFile )
                 throw new CmdException("Can't specify authentication and also 
provide a Jetty configuration file");
             String schemeStr = getValue(argAuth);
-            serverConfig.authScheme = AuthScheme.scheme(schemeStr);
+            serverArgs.authScheme = AuthScheme.scheme(schemeStr);
         }
 
         // Jetty server : this will be the server configuration regardless of 
other settings.
@@ -501,24 +529,24 @@ public class FusekiMain extends CmdARQ {
             String jettyConfigFile = getValue(argJettyConfig);
             if ( ! FileOps.exists(jettyConfigFile) )
                 throw new CmdException("Jetty config file not found: 
"+jettyConfigFile);
-            serverConfig.jettyConfigFile = jettyConfigFile;
+            serverArgs.jettyConfigFile = jettyConfigFile;
         }
 
         boolean withModules = hasValueOfTrue(argEnableModules);
         if ( withModules ) {
             // Use the discovered ones.
             FusekiAutoModules.enable(true);
-            // Allows for external setting of serverConfig.fusekiModules
-            if ( serverConfig.fusekiModules == null ) {
+            // Allows for external setting of serverArgs.fusekiModules
+            if ( serverArgs.fusekiModules == null ) {
                 FusekiAutoModules.setup();
-                serverConfig.fusekiModules = FusekiAutoModules.load();
+                serverArgs.fusekiModules = FusekiAutoModules.load();
             }
         } else {
             // Disabled module discovery.
             FusekiAutoModules.enable(false);
-            // Allows for external setting of serverConfig.fusekiModules
-            if ( serverConfig.fusekiModules == null ) {
-                serverConfig.fusekiModules = FusekiModules.empty();
+            // Allows for external setting of serverArgs.fusekiModules
+            if ( serverArgs.fusekiModules == null ) {
+                serverArgs.fusekiModules = FusekiModules.empty();
             }
         }
 
@@ -526,15 +554,20 @@ public class FusekiMain extends CmdARQ {
             String corsConfigFile = getValue(argCORS);
             if ( ! FileOps.exists(corsConfigFile) )
                 throw new CmdException("CORS config file not found: 
"+corsConfigFile);
-            serverConfig.corsConfigFile = corsConfigFile;
+            serverArgs.corsConfigFile = corsConfigFile;
         } else if (contains(argNoCORS)) {
-            serverConfig.withCORS = ! contains(argNoCORS);
+            serverArgs.withCORS = ! contains(argNoCORS);
         }
 
-        serverConfig.withPing = contains(argWithPing);
-        serverConfig.withStats = contains(argWithStats);
-        serverConfig.withMetrics = contains(argWithMetrics);
-        serverConfig.withCompact = contains(argWithCompact);
+        serverArgs.withPing = contains(argWithPing);
+        serverArgs.withStats = contains(argWithStats);
+        serverArgs.withMetrics = contains(argWithMetrics);
+        serverArgs.withCompact = contains(argWithCompact);
+
+        // Deal with any customisers
+        for (FusekiServerArgsCustomiser customiser : customiseServerArgs) {
+            customiser.serverArgsPrepare(this, serverArgs);
+        }
     }
 
     private int portNumber(ArgDecl arg) {
@@ -568,14 +601,14 @@ public class FusekiMain extends CmdARQ {
         try {
             Logger log = Fuseki.serverLog;
             FusekiMainInfo.logServerCode(log);
-            FusekiServer server = makeServer(serverConfig);
+            FusekiServer server = makeServer(serverArgs);
             infoCmd(server, log);
             try {
                 server.start();
             } catch (FusekiException ex) {
                 if ( ex.getCause() instanceof BindException ) {
-                    if ( serverConfig.jettyConfigFile == null )
-                        Fuseki.serverLog.error("Failed to start server: 
"+ex.getCause().getMessage()+ ": port="+serverConfig.port);
+                    if ( serverArgs.jettyConfigFile == null )
+                        Fuseki.serverLog.error("Failed to start server: 
"+ex.getCause().getMessage()+ ": port="+serverArgs.port);
                     else
                         Fuseki.serverLog.error("Failed to start server: 
"+ex.getCause().getMessage()+ ": port in use");
                     System.exit(1);
@@ -584,6 +617,7 @@ public class FusekiMain extends CmdARQ {
             } catch (Exception ex) {
                 throw new FusekiException("Failed to start server: " + 
ex.getMessage(), ex);
             }
+            // This does not normally return.
             server.join();
             System.exit(0);
         }
@@ -597,13 +631,12 @@ public class FusekiMain extends CmdARQ {
     }
 
     /**
-     * Take a {@link ServerConfig} and make a {@Link FusekiServer}.
+     * Take a {@link ServerArgs} and make a {@Link FusekiServer}.
      * The server has not been started.
      */
-    private FusekiServer makeServer(ServerConfig serverConfig) {
+    private FusekiServer makeServer(ServerArgs serverArgs) {
         FusekiServer.Builder builder = builder();
-        applyServerArgs(builder, serverConfig);
-        return builder.build();
+        return buildServer(builder, serverArgs);
     }
 
     protected FusekiServer.Builder builder() {
@@ -611,27 +644,27 @@ public class FusekiMain extends CmdARQ {
     }
 
     /**
-     * Process {@link ServerConfig} and build a server.
+     * Process {@link ServerArgs} and build a server.
      * The server has not been started.
      */
-    private static FusekiServer buildServer(FusekiServer.Builder builder, 
ServerConfig serverConfig) {
-        applyServerArgs(builder, serverConfig);
+    private static FusekiServer buildServer(FusekiServer.Builder builder, 
ServerArgs serverArgs) {
+        applyServerArgs(builder, serverArgs);
         return builder.build();
     }
 
-    /** Apply {@link ServerConfig} to a {@link FusekiServer.Builder}. */
-    private static void applyServerArgs(FusekiServer.Builder builder, 
ServerConfig serverConfig) {
-        if ( serverConfig.jettyConfigFile != null )
-            builder.jettyServerConfig(serverConfig.jettyConfigFile);
-        builder.port(serverConfig.port);
-        builder.loopback(serverConfig.loopback);
-        builder.verbose(serverConfig.verboseLogging);
+    /** Apply {@link ServerArgs} to a {@link FusekiServer.Builder}. */
+    private static void applyServerArgs(FusekiServer.Builder builder, 
ServerArgs serverArgs) {
+        if ( serverArgs.jettyConfigFile != null )
+            builder.jettyServerConfig(serverArgs.jettyConfigFile);
+        builder.port(serverArgs.port);
+        builder.loopback(serverArgs.loopback);
+        builder.verbose(serverArgs.verboseLogging);
 
-        if ( serverConfig.addGeneral != null )
+        if ( serverArgs.addGeneral != null )
             // Add SPARQL_QueryGeneral as a general servlet, not reached by 
the service router.
-            builder.addServlet(serverConfig.addGeneral,  new 
SPARQL_QueryGeneral());
+            builder.addServlet(serverArgs.addGeneral,  new 
SPARQL_QueryGeneral());
 
-        if ( serverConfig.validators ) {
+        if ( serverArgs.validators ) {
             // Validators.
             builder.addServlet("/$/validate/query",  new QueryValidator());
             builder.addServlet("/$/validate/update", new UpdateValidator());
@@ -639,50 +672,84 @@ public class FusekiMain extends CmdARQ {
             builder.addServlet("/$/validate/data",   new DataValidator());
         }
 
-        if ( ! serverConfig.empty ) {
-            if ( serverConfig.serverConfig != null )
-                // Config file.
-                builder.parseConfigFile(serverConfig.serverConfig);
-            else
-                // One dataset.
-                builder.add(serverConfig.datasetPath, serverConfig.dsg, 
serverConfig.allowUpdate);
+//        if ( ! serverArgs.empty ) {
+//            if ( serverArgs.serverConfig != null )
+//                // Config file.
+//                builder.parseConfigFile(serverArgs.serverConfig);
+//            else
+//                // One dataset.
+//                builder.add(serverArgs.datasetPath, serverArgs.dsg, 
serverArgs.allowUpdate);
+//        }
+
+        if ( ! serverArgs.empty ) {
+            // A CLI customiser may have already set the model.
+            if (serverArgs.serverConfigModel != null ) {
+                builder.parseConfig(serverArgs.serverConfigModel);
+                serverArgs.datasetDescription = "Configuration: provided";
+            } else if ( serverArgs.serverConfigFile != null ) {
+                // if there is a configuration file, read it.
+                // Read the model.
+                String file = serverArgs.serverConfigFile;
+                if ( file.startsWith("file:") )
+                    file = file.substring("file:".length());
+                Path path = Path.of(file);
+                if ( ! Files.exists(path) )
+                    throw new CmdException("File not found: "+file);
+                if ( Files.isDirectory(path) )
+                    throw new CmdException("Is a directory: "+file);
+                serverArgs.datasetDescription = "Configuration: 
"+path.toAbsolutePath();
+                serverArgs.serverConfigModel = 
RDFParser.source(path).toModel();
+                builder.parseConfig(serverArgs.serverConfigModel);
+            } else {
+                // One dataset up by command line arguments.
+                if ( serverArgs.dsg == null || serverArgs.datasetPath == null 
) {
+                    // Internal error: should have happened during checking 
earlier.
+                    throw new CmdException("Failed to set the dataset 
service");
+                }
+                builder.add(serverArgs.datasetPath, serverArgs.dsg, 
serverArgs.allowUpdate);
+            }
         }
 
-        if ( serverConfig.fusekiModules != null )
-            builder.fusekiModules(serverConfig.fusekiModules);
+        if ( serverArgs.fusekiModules != null )
+            builder.fusekiModules(serverArgs.fusekiModules);
 
-        if ( serverConfig.servletContextPath != null )
-            builder.contextPath(serverConfig.servletContextPath);
+        if ( serverArgs.servletContextPath != null )
+            builder.contextPath(serverArgs.servletContextPath);
 
-        if ( serverConfig.contentDirectory != null )
-            builder.staticFileBase(serverConfig.contentDirectory);
+        if ( serverArgs.contentDirectory != null )
+            builder.staticFileBase(serverArgs.contentDirectory);
 
-        if ( serverConfig.passwdFile != null )
-            builder.passwordFile(serverConfig.passwdFile);
+        if ( serverArgs.passwdFile != null )
+            builder.passwordFile(serverArgs.passwdFile);
 
-        if ( serverConfig.realm != null )
-            builder.realm(serverConfig.realm);
+        if ( serverArgs.realm != null )
+            builder.realm(serverArgs.realm);
 
-        if ( serverConfig.httpsKeysDetails != null)
-            builder.https(serverConfig.httpsPort, 
serverConfig.httpsKeysDetails);
+        if ( serverArgs.httpsKeysDetails != null)
+            builder.https(serverArgs.httpsPort, serverArgs.httpsKeysDetails);
 
-        if ( serverConfig.authScheme != null )
-            builder.auth(serverConfig.authScheme);
+        if ( serverArgs.authScheme != null )
+            builder.auth(serverArgs.authScheme);
 
-        if ( serverConfig.withCORS )
-            builder.enableCors(true, serverConfig.corsConfigFile);
+        if ( serverArgs.withCORS )
+            builder.enableCors(true, serverArgs.corsConfigFile);
 
-        if ( serverConfig.withPing )
+        if ( serverArgs.withPing )
             builder.enablePing(true);
 
-        if ( serverConfig.withStats )
+        if ( serverArgs.withStats )
             builder.enableStats(true);
 
-        if ( serverConfig.withMetrics )
+        if ( serverArgs.withMetrics )
             builder.enableMetrics(true);
 
-        if ( serverConfig.withCompact )
+        if ( serverArgs.withCompact )
             builder.enableCompact(true);
+
+        // Allow customisers to process their own arguments.
+        for (FusekiServerArgsCustomiser customiser : customiseServerArgs) {
+            customiser.serverArgsBuilder(builder, builder.configModel());
+        }
     }
 
     /** Information from the command line setup */
@@ -690,25 +757,25 @@ public class FusekiMain extends CmdARQ {
         if ( super.isQuiet() )
             return;
 
-        if ( serverConfig.empty ) {
+        if ( serverArgs.empty ) {
             FmtLog.info(log, "No SPARQL datasets services");
         } else {
-            if ( serverConfig.datasetPath == null && serverConfig.serverConfig 
== null )
+            if ( serverArgs.datasetPath == null && 
serverArgs.serverConfigModel == null )
                 log.error("No dataset path nor server configuration file");
         }
 
         DataAccessPointRegistry dapRegistry = 
DataAccessPointRegistry.get(server.getServletContext());
-        if ( serverConfig.datasetPath != null ) {
+        if ( serverArgs.datasetPath != null ) {
             if ( dapRegistry.size() != 1 )
                 log.error("Expected only one dataset in the 
DataAccessPointRegistry");
         }
 
         // Log details on startup.
-        String datasetPath = serverConfig.datasetPath;
-        String datasetDescription = serverConfig.datasetDescription;
-        String serverConfigFile = serverConfig.serverConfig;
-        String staticFiles = serverConfig.contentDirectory;
-        boolean verbose = serverConfig.verboseLogging;
+        String datasetPath = serverArgs.datasetPath;
+        String datasetDescription = serverArgs.datasetDescription;
+        String serverConfigFile = serverArgs.serverConfigFile;
+        String staticFiles = serverArgs.contentDirectory;
+        boolean verbose = serverArgs.verboseLogging;
 
         if ( ! super.isQuiet() )
             FusekiCoreInfo.logServerCmdSetup(log, verbose, dapRegistry,
diff --git 
a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/ServerConfig.java
 
b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/ServerArgs.java
similarity index 91%
rename from 
jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/ServerConfig.java
rename to 
jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/ServerArgs.java
index 5e1ac68fd1..62dc3f3261 100644
--- 
a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/ServerConfig.java
+++ 
b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/cmds/ServerArgs.java
@@ -21,14 +21,15 @@ package org.apache.jena.fuseki.main.cmds;
 import org.apache.jena.atlas.web.AuthScheme;
 import org.apache.jena.fuseki.main.sys.FusekiModules;
 import org.apache.jena.graph.Graph;
+import org.apache.jena.rdf.model.Model;
 import org.apache.jena.sparql.core.DatasetGraph;
 
 /**
  * Setup details (command line, config file) from command line processing.
- * This is built by {@link FusekiMain#exec}.
- * This is processed by {@link FusekiMain#buildServer}.
+ * This is built by {@link FusekiMain#processModulesAndArgs}.
+ * This is processed by {@link FusekiMain#applyServerArgs}.
  */
-class ServerConfig {
+public class ServerArgs {
     /** Server port. This is the http port when both http and https are 
active. */
     public int port                     = -1;
     /** Loopback */
@@ -64,7 +65,8 @@ class ServerConfig {
     public Graph rdfsGraph              = null;
 
     // ... or this.
-    public String serverConfig          = null;
+    public String serverConfigFile      = null;
+    public Model serverConfigModel      = null;
 
     /** No registered datasets without it being an error. */
     public boolean empty                = false;
diff --git 
a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModule.java
 
b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModule.java
index d34292e272..63196f2b31 100644
--- 
a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModule.java
+++ 
b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModule.java
@@ -56,7 +56,7 @@ import org.apache.jena.rdf.model.Model;
  *     Modules must not rely on a call to {@code serverStopped} happening.</li>
  * </ul>
  */
-public interface FusekiModule extends FusekiBuildCycle, FusekiStartStop, 
FusekiActionCycle {
+public interface FusekiModule extends FusekiServerArgsCustomiser, 
FusekiBuildCycle, FusekiStartStop, FusekiActionCycle {
     // Gather all interface method together.
     // Inherited javadoc.
 
@@ -65,9 +65,11 @@ public interface FusekiModule extends FusekiBuildCycle, 
FusekiStartStop, FusekiA
 
     // ---- Build cycle
 
+    /** {@inheritDoc} */
     @Override
     public default void prepare(FusekiServer.Builder serverBuilder, 
Set<String> datasetNames, Model configModel) { }
 
+    /** {@inheritDoc} */
     @Override
     public default void configured(FusekiServer.Builder serverBuilder, 
DataAccessPointRegistry dapRegistry, Model configModel) {
         
dapRegistry.accessPoints().forEach(accessPoint->configDataAccessPoint(accessPoint,
 configModel));
diff --git 
a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiServerArgsCustomiser.java
 
b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiServerArgsCustomiser.java
new file mode 100644
index 0000000000..a43ff2d972
--- /dev/null
+++ 
b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiServerArgsCustomiser.java
@@ -0,0 +1,113 @@
+/*
+ * 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.sys;
+
+import org.apache.jena.cmd.CmdException;
+import org.apache.jena.cmd.CmdGeneral;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.main.cmds.FusekiMain;
+import org.apache.jena.fuseki.main.cmds.ServerArgs;
+import org.apache.jena.rdf.model.Model;
+
+
+
+/**
+ * Interface to implement for extending the CLI argument parsing portion of a
+ * {@link FusekiServer}.
+ * <p>
+ * Customisation code is registered by calling {@link FusekiMain#addCustomiser}
+ * before invoking {@link FusekiMain#build}. This can be done from Java code, 
or
+ * during {@link FusekiAutoModule#start()} for dynamically loaded code.
+ * <p>
+ * The customiser modifies the Fuseki arguments setup after the standard 
Fuseki main
+ * arguments have been registered. The customiser is then called
+ * after the standard arguments have been used to produce the {@link 
ServerArgs}.
+ * <p>
+ * The lifecycle for command line argument processing is:
+ * <ul>
+ * <li>
+ *   Add customisers by calling {@link FusekiMain#addCustomiser} from
+ *   {@link FusekiAutoModule#start()} or from Java application code.
+ * </li>
+ * <li>
+ *    {@link #serverArgsModify} &mdash; Register or modify the argument setup 
to be
+ *     used to parse the command line.
+ * </li>
+ * <li>
+ *   {@link #serverArgsPrepare} &mdash; Called after parsing the command line 
and
+ *   recoding the command line settings in {@link ServerArgs}. Customisers can 
record
+ *   argument values and flags.
+ * </li>
+ * <li>
+ *   {@link #serverArgsBuilder} &mdash; Called after the {@link ServerArgs} 
have
+ *   been used to construct a server builder.
+ * </li>
+ * </ul>
+ * Following command like processing, server construction proceeds with the 
{@link FusekiBuildCycle},
+ * the first step of which is {@link FusekiBuildCycle#prepare}.
+ */
+public interface FusekiServerArgsCustomiser {
+
+    /**
+     * Called after the standard Fuseki main arguments have been added
+     * and before argument processing of the command line.
+     * This allows a Fuseki module to add custom arguments via
+     * {@link CmdGeneral#addArg(String, String)} and
+     * {@link CmdGeneral#addModule(org.apache.jena.cmd.ArgModuleGeneral)}.
+     * <p>
+     * This method can throw {@link CmdException} to indicate errors.
+     * This will cause a error message to be printed, without the stack trace.
+     * The server construction is aborted.
+     *
+     * @param fusekiCmd Fuseki Main command line arguments
+     */
+    public default void serverArgsModify(CmdGeneral fusekiCmd) { }
+
+    /**
+     * Called at the end command line argument processing.
+     * <p>
+     * This allows a Fuseki module to pull out custom arguments it has added 
and
+     * process them appropriately, including validating or modifying the
+     * {@link ServerArgs} that will be used to build the server.
+     * <p>
+     * This method can throw {@link CmdException} to indicate errors.
+     * This will cause a error message to be printed, without the stack trace.
+     * The server construction is aborted.
+     *
+     * @param fusekiCmd Fuseki Main
+     * @param serverArgs Standard server argument settings, before building the
+     *     server.
+     */
+    public default void serverArgsPrepare(CmdGeneral fusekiCmd, ServerArgs 
serverArgs) { }
+
+    /**
+     * Called at the end of applying the {@link ServerArgs} to the builder.
+     * <p>
+     * The configuration model (if any) has been setup. This step can do 
validation
+     * and argument processing dependent on the configuration model.
+     * <p>
+     * This method can throw {@link CmdException} to indicate errors.
+     * This will cause a error message to be printed, without the stack trace.
+     * The server construction is aborted.
+     *
+     * @param serverBuilder The server builder.
+     * @param configModel The configuration model; this may be null.
+     */
+    public default void serverArgsBuilder(FusekiServer.Builder serverBuilder, 
Model configModel) {}
+}
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 78b902b670..e0fa22d5e8 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
@@ -35,6 +35,7 @@ import org.junit.platform.suite.api.Suite;
   , TestFusekiCustomOperation.class
   , TestFusekiMainCmd.class
   , TestFusekiMainCmdArguments.class
+  , TestFusekiMainCmdCustomArguments.class
   , TestFusekiStdSetup.class
   , TestFusekiStdReadOnlySetup.class
   , TestConfigFile.class
diff --git 
a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiMainCmdArguments.java
 
b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiMainCmdArguments.java
index 082475bdd9..9992139d88 100644
--- 
a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiMainCmdArguments.java
+++ 
b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiMainCmdArguments.java
@@ -23,6 +23,9 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
 import java.util.List;
 
 import org.apache.jena.atlas.logging.LogCtl;
@@ -43,17 +46,24 @@ import org.junit.Test;
 public class TestFusekiMainCmdArguments {
 
     private static String level = null;
+    private static File jettyConfigFile;
+    private static String jettyConfigFilename;
 
-    @BeforeClass public static void beforeClass() {
+    @BeforeClass public static void beforeClass() throws IOException {
         // This is not reset by each running server.
         FusekiLogging.setLogging();
         level = LogCtl.getLevel(Fuseki.serverLog);
         LogCtl.setLevel(Fuseki.serverLog, "WARN");
+
+        // Create a fake Jetty config file just to avoid File Not Found error
+        jettyConfigFile = Files.createTempFile("jetty-config",".xml").toFile();
+        jettyConfigFilename = jettyConfigFile.getAbsolutePath();
     }
 
     @AfterClass public static void afterClass() {
         if ( level != null )
             LogCtl.setLevel(Fuseki.serverLog, level);
+        jettyConfigFile.delete();
     }
 
     private FusekiServer server = null;
@@ -272,7 +282,7 @@ public class TestFusekiMainCmdArguments {
     @Test
     public void test_error_jettyConfigFileAndPort() {
         // given
-        List<String> arguments = List.of("--mem", "--jetty=file", "--port=99", 
"/path");
+        List<String> arguments = List.of("--mem", "--jetty=" + 
jettyConfigFilename, "--port=99", "/path");
         String expectedMessage = "Cannot specify the port and also provide a 
Jetty configuration file";
         // when, then
         testForCmdException(arguments, expectedMessage);
@@ -281,7 +291,7 @@ public class TestFusekiMainCmdArguments {
     @Test
     public void test_error_jettyConfigFileAndLocalHost() {
         // given
-        List<String> arguments = List.of("--mem", "--jetty=file", 
"--localhost", "/path");
+        List<String> arguments = List.of("--mem", "--jetty=" + 
jettyConfigFilename, "--localhost", "/path");
         String expectedMessage = "Cannot specify 'localhost' and also provide 
a Jetty configuration file";
         // when, then
         testForCmdException(arguments, expectedMessage);
@@ -357,7 +367,7 @@ public class TestFusekiMainCmdArguments {
     @Test
     public void test_error_passwdFile_withJetty() {
         // given
-        List<String> arguments = List.of("--mem", "--passwd=missing_file", 
"--jetty=file", "/dataset");
+        List<String> arguments = List.of("--mem", "--passwd=missing_file", 
"--jetty=" + jettyConfigFilename, "/dataset");
         String expectedMessage = "Can't specify a password file and also 
provide a Jetty configuration file";
         // when, then
         testForCmdException(arguments, expectedMessage);
@@ -375,7 +385,7 @@ public class TestFusekiMainCmdArguments {
     @Test
     public void test_error_httpsConfig_withJetty() {
         // given
-        List<String> arguments = List.of("--mem", "--httpsPort=12345", 
"--https=something", "--jetty=file", "/dataset");
+        List<String> arguments = List.of("--mem", "--httpsPort=12345", 
"--https=something", "--jetty=" + jettyConfigFilename, "/dataset");
         String expectedMessage = "Can't specify \"https\" and also provide a 
Jetty configuration file";
         // when, then
         testForCmdException(arguments, expectedMessage);
@@ -384,7 +394,7 @@ public class TestFusekiMainCmdArguments {
     @Test
     public void test_error_auth_withJetty() {
         // given
-        List<String> arguments = List.of("--mem", "--auth=12345", 
"--jetty=file", "/dataset");
+        List<String> arguments = List.of("--mem", "--auth=12345", "--jetty=" + 
jettyConfigFilename, "/dataset");
         String expectedMessage = "Can't specify authentication and also 
provide a Jetty configuration file";
         // when, then
         testForCmdException(arguments, expectedMessage);
@@ -440,7 +450,7 @@ public class TestFusekiMainCmdArguments {
     // Build and set the server
     private void buildServer(String... cmdline) {
         if ( server != null )
-            fail("Bad test - a server has aleardy been created");
+            fail("Bad test - a server has already been created");
         server = FusekiMain.build(cmdline);
         server.start();
     }
diff --git 
a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiMainCmdCustomArguments.java
 
b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiMainCmdCustomArguments.java
new file mode 100644
index 0000000000..cae8c05e03
--- /dev/null
+++ 
b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiMainCmdCustomArguments.java
@@ -0,0 +1,227 @@
+/*
+ * 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.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.jena.atlas.logging.LogCtl;
+import org.apache.jena.cmd.ArgDecl;
+import org.apache.jena.cmd.CmdException;
+import org.apache.jena.cmd.CmdGeneral;
+import org.apache.jena.fuseki.Fuseki;
+import org.apache.jena.fuseki.main.cmds.FusekiMain;
+import org.apache.jena.fuseki.main.cmds.ServerArgs;
+import org.apache.jena.fuseki.main.sys.FusekiServerArgsCustomiser;
+import org.apache.jena.fuseki.server.DataAccessPoint;
+import org.apache.jena.fuseki.system.FusekiLogging;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFParser;
+
+public class TestFusekiMainCmdCustomArguments {
+
+    static { FusekiLogging.setLogging(); }
+
+    private static String confFixedStr = """
+            PREFIX :        <http://example/base/>
+            PREFIX fuseki:  <http://jena.apache.org/fuseki#>
+            PREFIX rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+            PREFIX ja:      <http://jena.hpl.hp.com/2005/11/Assembler#>
+
+            :service rdf:type fuseki:Service ;
+                fuseki:name "dataset" ;
+                fuseki:endpoint [ fuseki:operation fuseki:query ; ] ;
+                fuseki:dataset [ rdf:type ja:MemoryDataset ]
+                .
+                """;
+    private static Model confFixed = RDFParser.fromString(confFixedStr,  
Lang.TTL).toModel();
+
+    private static String level = null;
+
+    @BeforeClass public static void beforeClass() {
+        FusekiLogging.setLogging();
+        level = LogCtl.getLevel(Fuseki.serverLog);
+        LogCtl.setLevel(Fuseki.serverLog, "WARN");
+    }
+
+    @AfterClass public static void afterClass() {
+        if ( level != null )
+            LogCtl.setLevel(Fuseki.serverLog, level);
+    }
+
+    private FusekiServer server = null;
+
+    @After public void after() {
+        if ( server != null )
+            server.stop();
+    }
+
+    @Test
+    public void test_custom_no_custom_args() {
+        String[] arguments = {"--port=0", "--mem","/ds"};
+        test(new ArgDecl(false, "special"), arguments, false, null);
+    }
+
+    @Test(expected = CmdException.class)
+    public void test_custom_no_custom_args_decl() {
+        String[] arguments = {"--port=0", "--special", "--mem","/ds"};
+        FusekiServer server = FusekiMain.build(arguments);
+    }
+
+    @Test
+    public void test_custom_flag() {
+        String[] arguments = {"--port=0", "--mem", "--custom-flag", "/ds"};
+        test(new ArgDecl(false, "custom-flag"), arguments, true, null);
+    }
+
+    @Test
+    public void test_custom_arg() {
+        String[] arguments = {"--port=0", "--mem", "--custom-arg", "test", 
"/ds"};
+        test(new ArgDecl(true, "custom-arg"), arguments, true, "test");
+    }
+
+    @Test
+    public void test_custom_confModel_noFlag() {
+        String[] arguments = {"--port=0", "--mem", "/ds"};
+        FusekiCliCustomModel customiser = new FusekiCliCustomModel(new 
ArgDecl(false, "fixed"), confFixed);
+        test(customiser, arguments, server->{
+            assertSame(null, customiser.notedServerConfigModel);
+            DataAccessPoint dap1 = 
server.getDataAccessPointRegistry().get("/ds");
+            assertNotNull(dap1);
+            DataAccessPoint dap2 = 
server.getDataAccessPointRegistry().get("/dataset");
+            assertNull(dap2);
+        });
+    }
+
+    @Test
+    public void test_custom_confModel_withFlag() {
+        // Ignores command line.
+        String[] arguments = {"--port=0", "--mem", "--fixed", "/ds"};
+        FusekiCliCustomModel customiser = new FusekiCliCustomModel(new 
ArgDecl(false, "fixed"), confFixed);
+        FusekiServer server = test(customiser, arguments, serv->{
+            DataAccessPoint dap1 = 
serv.getDataAccessPointRegistry().get("/ds");
+            assertNull(dap1);
+            DataAccessPoint dap2 = 
serv.getDataAccessPointRegistry().get("/dataset");
+            assertNotNull(dap2);
+        });
+        
assertTrue(server.getDataAccessPointRegistry().isRegistered("/dataset"));
+        assertFalse(server.getDataAccessPointRegistry().isRegistered("/ds"));
+    }
+
+    // ----
+
+    private void test(ArgDecl argDecl, String[] arguments, boolean seen, 
String value) {
+        FusekiCliCustomArg customiser = new FusekiCliCustomArg(argDecl);
+        test(customiser, arguments, (server)->{
+            assertEquals(seen, customiser.argSeen);
+            assertEquals(value, customiser.argValue);
+        });
+    }
+
+    private FusekiServer test(FusekiServerArgsCustomiser customiser, String[] 
arguments, Consumer<FusekiServer> checker) {
+        FusekiMain.resetCustomisers();
+        FusekiMain.addCustomiser(customiser);
+        FusekiServer server = FusekiMain.build(arguments);
+        if ( checker != null )
+            checker.accept(server);
+        return server;
+    }
+
+    // ---- Test FusekiCliCustomisers
+
+    static class FusekiCliCustomArg implements FusekiServerArgsCustomiser {
+        final ArgDecl argDecl;
+        String argValue = null;
+        boolean argSeen = false;
+
+        FusekiCliCustomArg(ArgDecl argDecl) {
+            this.argDecl = argDecl;
+        }
+
+        @Override
+        public void serverArgsModify(CmdGeneral fusekiCmd) {
+            fusekiCmd.add(argDecl);
+        }
+
+        @Override
+        public void serverArgsPrepare(CmdGeneral fusekiCmd, ServerArgs 
serverArgs) {
+            argSeen = fusekiCmd.contains(argDecl);
+            argValue = fusekiCmd.getValue(argDecl);
+        }
+    };
+
+    static class FusekiCliCustomModel implements FusekiServerArgsCustomiser {
+        final ArgDecl argDecl;
+        final Model fixedModel;
+        Model notedServerConfigModel = null;
+        boolean argSeen = false;
+
+        FusekiCliCustomModel(ArgDecl argDecl, Model conf) {
+            this.argDecl = argDecl;
+            this.fixedModel = conf;
+        }
+
+        @Override
+        public void serverArgsModify(CmdGeneral fusekiCmd) {
+            fusekiCmd.add(argDecl);
+        }
+
+        @Override
+        public void serverArgsPrepare(CmdGeneral fusekiCmd, ServerArgs 
serverArgs) {
+            argSeen = fusekiCmd.contains(argDecl);
+            if ( argSeen ) {
+                serverArgs.serverConfigModel = fixedModel;
+                notedServerConfigModel = fixedModel;
+                serverArgs.dsg = null;
+            }
+        }
+
+        @Override
+        public void serverArgsBuilder(FusekiServer.Builder serverBuilder, 
Model configModel) {
+            if ( argSeen )
+                assertSame(notedServerConfigModel, configModel);
+            else
+                assertNull(configModel);
+        }
+    };
+
+    static class FusekiCliCustomConfiguration implements 
FusekiServerArgsCustomiser {
+        @Override public void serverArgsModify(CmdGeneral fusekiCmd) { }
+        @Override public void serverArgsPrepare(CmdGeneral fusekiCmd, 
ServerArgs serverArgs) {
+            if ( Objects.equals(serverArgs.serverConfigFile, "placeholder") ) {
+                serverArgs.serverConfigModel = confFixed;
+            }
+
+            //serverArgs.serverConfigFile;
+            //serverArgs.serverConfigModel;
+        }
+
+        @Override public void serverArgsBuilder(FusekiServer.Builder 
serverBuilder, Model configModel) {}
+    }
+
+}

Reply via email to