Improve LogCtlLog4j2 use of log4j2 internals
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtl.java b/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtl.java
index b0c67b0..bba604c 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtl.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtl.java
@@ -331,7 +331,7 @@
             }
             // Nothing found - built-in default.
             logLogging("Log4j2: built-in default");
-            LogCtlLog4j2.resetLogging(LogCtlLog4j2.log4j2setup);
+            LogCtlLog4j2.reconfigureLog4j2fromString(LogCtlLog4j2.log4j2setup, LogCtlLog4j2.SyntaxHint.PROPERTIES);
         } else {
             if ( isSetLog4j2property(log4j2ConfigFilePropertyLegacy) )
                 logLogging("Already set: %s=%s", log4j2ConfigFilePropertyLegacy, System.getProperty(log4j2ConfigFilePropertyLegacy));
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtlLog4j2.java b/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtlLog4j2.java
index 9173972..fc976bc 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtlLog4j2.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtlLog4j2.java
@@ -21,48 +21,68 @@
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Objects;
 
-import org.apache.jena.atlas.io.IO;
-import org.apache.jena.atlas.lib.StrUtils;
+import org.apache.commons.io.FilenameUtils;
 import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.config.ConfigurationFactory;
 import org.apache.logging.log4j.core.config.ConfigurationSource;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.apache.logging.log4j.core.config.json.JsonConfigurationFactory;
 import org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory;
+import org.apache.logging.log4j.core.config.xml.XmlConfigurationFactory;
+import org.apache.logging.log4j.core.config.yaml.YamlConfigurationFactory;
 
 /**
  * Additional logging control, for Log4j2 as used by jena-cmds.
- * <br/>
- * This class pulls in log4j2.
- * <br/>
- * This class is split out from {@link LogCtl} to decouple the class loading dependencies.
+ * <p>
+ * This class depends on log4j2-api and also log4j2-core.
+ * These are &lt;optional&gt; dependencies for Jena which can use any slf4j provider.
+ * <p>
+ * This class is split out from {@link LogCtl} to decouple the dependencies.
+ * <p>
+ * This class is not used if log4j2 is not used.
  */
 public class LogCtlLog4j2 {
+
+    /** Default log4j2 setup */
+    public  static String log4j2setup = String.join(log4jSetupSep(),
+                                                    log4j2setupBase(),
+                                                    log4j2setupJenaLib(),
+                                                    log4j2setupFuseki());
+
     /**
-     * Reset logging (log4j2). log4j2.properties format.
+     * Reset logging for log4j2.
+     * The string is log4j2.properties format.
      */
     public static void resetLogging(String configString) {
-        // Dispatch name to syntax.
-        try (InputStream inputStream = new ByteArrayInputStream(StrUtils.asUTF8bytes(configString))) {
-            resetLogging(inputStream, ".properties");
-        } catch (IOException ex) {
-            IO.exception(ex);
-        }
+        // This method is previous naming.
+        reconfigureLog4j2fromString(configString, SyntaxHint.PROPERTIES);
     }
 
-    public static void resetLogging(InputStream inputStream, String syntaxHint) throws IOException {
-        ConfigurationSource source = new ConfigurationSource(inputStream);
-        ConfigurationFactory factory = ( syntaxHint.endsWith(".properties") )
-            ? new PropertiesConfigurationFactory()
-            : ConfigurationFactory.getInstance();
-        Configuration configuration = factory.getConfiguration(null, source);
-        LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
-        // This changes existing loggers.
-        ctx.setConfiguration(configuration);
+    /**
+     * Reset logging for log4j2 from a string.
+     * The resourceName is used to determine the syntax.
+     */
+    public static void resetLogging(InputStream inputStream, String resourceName) {
+        resetLogging(inputStream, determineSyntax(resourceName));
     }
 
+    /**
+     * Reset logging for log4j2 from an {@link InputStream} with the given syntax.
+     */
+    public static void resetLogging(InputStream inputStream, SyntaxHint syntaxHint) {
+        Configuration config = log4j2Configuration(inputStream, syntaxHint);
+        reconfigureLog4j(config);
+    }
+
+    /** Check logging level of a Logger */
     /*package*/ static void setLoggerlevel(String logger, Level level) {
         try {
             if ( !logger.equals("") )
@@ -70,47 +90,250 @@
             else
                 org.apache.logging.log4j.core.config.Configurator.setRootLevel(level);
         } catch (NoClassDefFoundError ex) {
-            Log.warnOnce(LogCtl.class, "Log4j2 Configurator not found", LogCtl.class);
+            Log.warnOnce(LogCtlLog4j2.class, "Log4j2 Configurator not found", LogCtl.class);
         }
     }
 
-    // basic setup.
-    // @formatter:off
-    /** A basic logging setup. */
-    public static String log4j2setup = StrUtils.strjoinNL
-        ( "## Command default log4j2 setup : log4j2 properties syntax."
-        , "status = error"
-        , "name = JenaLoggingDft"
-//        , "filters = threshold"
-//        , ""
-//        , "filter.threshold.type = ThresholdFilter"
-//        , "filter.threshold.level = ALL"
-//      , ""
-        , "appender.console.type = Console"
-        , "appender.console.name = OUT"
-        , "appender.console.target = SYSTEM_OUT"
-        , "appender.console.layout.type = PatternLayout"
-        , "appender.console.layout.pattern = %d{HH:mm:ss} %-5p %-15c{1} :: %m%n"
-        , "#appender.console.layout.pattern = [%d{yyyy-MM-dd HH:mm:ss}] %-5p %-15c{1} :: %m%n"
+    /** Check logging level of a Logger */
+    /*package*/ static void setLoggerlevel(Logger logger, Level level) {
+        try {
+            org.apache.logging.log4j.core.config.Configurator.setLevel(logger, level);
+        } catch (NoClassDefFoundError ex) {
+            Log.warnOnce(LogCtlLog4j2.class, "Log4j2 Configurator not found", LogCtl.class);
+        }
+    }
 
-        , "rootLogger.level                  = INFO"
-        , "rootLogger.appenderRef.stdout.ref = OUT"
 
-        , "logger.jena.name  = org.apache.jena"
-        , "logger.jena.level = INFO"
+    /**
+     * Enum for possible syntax of a Log4j configuration file.
+     * <p>
+     * Note that the JSON and YAML forms, require additional jars. See
+     * <a href="https://logging.apache.org/log4j/2.x/runtime-dependencies.html#log4j-core"
+     * >"dependencies for log4j-core"</a> for more information.
+     */
+    public enum SyntaxHint {
+        PROPERTIES("properties"),
+        XML("xml"),
+        JSON("json"),
+        YAML("yaml");
 
-        , "logger.arq-exec.name  = org.apache.jena.arq.exec"
-        , "logger.arq-exec.level = INFO"
+        // The syntax name is assumed to be the file extension.
+        // This can be used as the name of a syntax.
+        private String syntaxName;
+        SyntaxHint(String syntaxName) { this.syntaxName = syntaxName; }
 
-        , "logger.riot.name  = org.apache.jena.riot"
-        , "logger.riot.level = INFO"
+        /** Return the {@code SyntaxHint} for a name (case insensitive) or null */
+        static SyntaxHint fromName(String name) {
+            for ( SyntaxHint hint : SyntaxHint.values() ) {
+                if ( hint.syntaxName.equalsIgnoreCase(name) )
+                    return hint;
+            }
+            return null;
+        }
+    }
 
-        // If mixed with Fuseki code, and command logging happens, then ensure Jetty is WARN.
-        , "logger.jetty.name  = org.eclipse.jetty"
-        , "logger.jetty.level = WARN"
-        );
-    // @formatter:on
+    /**
+     * Reconfigure log4j2 from a file.
+     * <p>
+     * The file syntax is determined by the file extension (".properties" or ".xml").
+     * <p>
+     * Existing loggers are reconfigured by this function.
+     */
+    public static void reconfigureLog4j2fromFile(String filename) {
+        if ( true ) {
+            // This particular case can be done with Log4J directly.
+            // That will extend to all plugins.
+            Configurator.initialize(null, filename);
+            return;
+        }
+        // Use the same logic as the other operations.
+        // JSON and YAML usage require addition jars on the classpath.
+        SyntaxHint syntax = determineSyntax(filename);
+        Configuration config = log4j2ConfigurationFromFile(filename, syntax);
+        reconfigureLog4j(config);
+    }
 
-//    /** A format for commands using stderr. */
-//    public static String log4j2setupCmd = log4j2setup.replace("SYSTEM_OUT", "SYSTEM_ERR");
+    /**
+     * Reconfigure log4j2 from a file.
+     * <p>
+     * The file syntax is determined by the syntax hint.
+     * <p>
+     * Existing loggers are reconfigured by this function.
+     */
+    public static void reconfigureLog4j2fromFile(String filename, SyntaxHint syntaxHint) {
+        Configuration config = log4j2ConfigurationFromFile(filename, syntaxHint);
+        reconfigureLog4j(config);
+    }
+
+    /**
+     * Reconfigure log4j2 from a string.
+     * <p>
+     * The syntax is given by the syntax hint.
+     * <p>
+     * Existing loggers are reconfigured by this function.
+     */
+    public static void reconfigureLog4j2fromString(String configString, SyntaxHint syntaxHint) {
+        Configuration config = log4j2ConfigurationFromString(configString, syntaxHint);
+        reconfigureLog4j(config);
+    }
+
+    /**
+     * Reconfigure log4j from a {@link Configuration}.
+     */
+    private static void reconfigureLog4j(Configuration config) {
+        config.initialize();
+        Configurator.reconfigure(config);
+    }
+
+    /**
+     * Create a log4j2 {@link Configuration} from a file.
+     * <p>
+     * The file syntax is determined by the syntax hint
+     */
+    private static Configuration log4j2ConfigurationFromFile(String filename, SyntaxHint syntaxHint) {
+        URI uri = Path.of(filename).toUri();
+        ConfigurationSource source = ConfigurationSource.fromUri(uri);
+        return createLog4jConfiguration(source, syntaxHint);
+    }
+
+    /**
+     * Create a log4j2 {@link Configuration} from a string.
+     * <p>
+     * TThe string syntax is determined by the syntax hint
+     */
+    private static Configuration log4j2ConfigurationFromString(String text, SyntaxHint syntaxHint) {
+        try(InputStream input = new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))) {
+            return log4j2Configuration(input, syntaxHint);
+        } catch (IOException ex) { throw new UncheckedIOException(ex); }
+    }
+
+    /**
+     * Create a log4j2 {@link Configuration} from an {@link InputStream}.
+     * <p>
+     * The file syntax is determined by the syntax hint
+     */
+    private static Configuration log4j2Configuration(InputStream inputStream, SyntaxHint syntaxHint) {
+        try {
+            ConfigurationSource source = new ConfigurationSource(inputStream);
+            return createLog4jConfiguration(source, syntaxHint);
+        } catch (IOException ex) {
+            throw new UncheckedIOException(ex);
+        }
+    }
+
+    /**
+     * Create a log4j2 {@link Configuration}.
+     * <p>
+     * @see org.apache.logging.log4j.core.config.Configurator
+     * @see org.apache.logging.log4j.core.config.ConfigurationSource
+     */
+    private static Configuration createLog4jConfiguration(ConfigurationSource source, SyntaxHint syntaxHint) {
+        Objects.requireNonNull(source);
+        Objects.requireNonNull(syntaxHint);
+        ConfigurationFactory factory = switch(syntaxHint) {
+            case PROPERTIES -> new PropertiesConfigurationFactory();
+            case XML        -> new XmlConfigurationFactory();
+            case JSON       -> new JsonConfigurationFactory();
+            case YAML       -> new YamlConfigurationFactory();
+            default -> ConfigurationFactory.getInstance();
+        };
+        Configuration configuration = factory.getConfiguration(null, source);
+        if ( configuration == null )
+            throw new UnsupportedOperationException("Can't create a configuration for '"+source+"' using '"+syntaxHint+"'");
+        return configuration;
+    }
+
+    /**
+     * Filename to {@link SynatxHint}.
+     * <p>
+     * Identify the likely syntax of a file, or throw IllegalArgumentException
+     * if no such determination can be made.
+     */
+    private static SyntaxHint determineSyntax(String filename) {
+        String ext = FilenameUtils.getExtension(filename);
+        if ( ext == null )
+            throw new IllegalArgumentException("No file extension");
+        SyntaxHint hint = SyntaxHint.fromName(ext);
+        if ( hint == null )
+            throw new IllegalArgumentException("File extension not recognized: '"+ext+"'");
+        return hint;
+    }
+
+    /** Line separate/blank line for concatenating log4j syntax fragments. */
+    private static String log4jSetupSep() { return "\n"; }
+
+    /**
+     * A basic logging setup. Time and level INFO.
+     */
+    private static String log4j2setupBase() {
+        return """
+                ## Log4j2 properties syntax.
+                status = error
+                name = JenaLoggingDft
+
+                # filters = threshold
+                # filter.threshold.type = ThresholdFilter
+                # filter.threshold.level = ALL
+
+                appender.console.type = Console
+                appender.console.name = OUT
+                appender.console.target = SYSTEM_OUT
+                appender.console.layout.type = PatternLayout
+                appender.console.layout.pattern = %d{HH:mm:ss} %-5p %-15c{1} :: %m%n
+                # appender.console.layout.pattern = [%d{yyyy-MM-dd HH:mm:ss}] %-5p %-15c{1} :: %m%n
+
+                rootLogger.level                  = INFO
+                rootLogger.appenderRef.stdout.ref = OUT
+                """;
+    }
+    /** Default log4j fragment needed for Jena command line tools. */
+    private static String log4j2setupJenaLib() {
+        return """
+                logger.jena.name  = org.apache.jena
+                logger.jena.level = INFO
+
+                logger.arq-exec.name  = org.apache.jena.arq.exec
+                logger.arq-exec.level = INFO
+
+                logger.riot.name  = org.apache.jena.riot
+                logger.riot.level = INFO
+                """;
+    }
+    /** Additional log4j fragment for Fuseki in case the general default is used with embedded Fuseki. */
+    private static String log4j2setupFuseki() {
+        return """
+                # Fuseki. In case this logging setup gets install for embedded Fuseki.
+
+                logger.fuseki.name  = org.apache.jena.fuseki
+                logger.fuseki.level = INFO
+                logger.fuseki-fuseki.name  = org.apache.jena.fuseki.Fuseki
+                logger.fuseki-fuseki.level = INFO
+
+                logger.fuseki-server.name  = org.apache.jena.fuseki.Server
+                logger.fuseki-server.level = INFO
+
+                logger.fuseki-config.name  = org.apache.jena.fuseki.Config
+                logger.fuseki-config.level = INFO
+
+                logger.fuseki-admin.name  = org.apache.jena.fuseki.Admin
+                logger.fuseki-admin.level = INFO
+
+                logger.jetty.name  = org.eclipse.jetty
+                logger.jetty.level = WARN
+
+                logger.shiro.name = org.apache.shiro
+                logger.shiro.level = WARN
+
+                # This goes out in NCSA format
+                appender.plain.type = Console
+                appender.plain.name = PLAIN
+                appender.plain.layout.type = PatternLayout
+                appender.plain.layout.pattern = %m%n
+
+                logger.fuseki-request.name                   = org.apache.jena.fuseki.Request
+                logger.fuseki-request.additivity             = false
+                logger.fuseki-request.level                  = OFF
+                logger.fuseki-request.appenderRef.plain.ref  = PLAIN
+                """;
+    }
 }