This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit da7480a3cff125925f9326c162e8710f0ce102ee
Author: remm <[email protected]>
AuthorDate: Thu Sep 17 15:32:56 2026 +0200

    Avoid saving some Context to server.xml
    
    Use a marker flag to determine if it was deployed from server.xml in the
    first place.
    Co authored with OpenCode.
---
 java/org/apache/catalina/core/StandardContext.java |  30 +++
 .../apache/catalina/startup/ContextRuleSet.java    |  36 +++
 .../catalina/storeconfig/LocalStrings.properties   |   1 +
 .../catalina/storeconfig/StandardContextSF.java    |  15 +-
 .../catalina/storeconfig/server-registry.xml       |   1 +
 .../catalina/storeconfig/TestStoreConfig.java      | 243 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   6 +
 7 files changed, 329 insertions(+), 3 deletions(-)

diff --git a/java/org/apache/catalina/core/StandardContext.java 
b/java/org/apache/catalina/core/StandardContext.java
index a06f384ad5..7492b464f5 100644
--- a/java/org/apache/catalina/core/StandardContext.java
+++ b/java/org/apache/catalina/core/StandardContext.java
@@ -270,6 +270,13 @@ public class StandardContext extends ContainerBase 
implements Context, Notificat
     private boolean configured = false;
 
 
+    /**
+     * Indicates that this Context was deployed from a Context element defined 
in server.xml. The flag is for internal
+     * use only (it is not exposed via JMX and is not persisted by 
storeconfig).
+     */
+    private boolean deployedFromServerXml = false;
+
+
     /**
      * The security constraints for this web application.
      */
@@ -1511,6 +1518,29 @@ public class StandardContext extends ContainerBase 
implements Context, Notificat
     }
 
 
+    /**
+     * Indicates whether this Context was deployed from a Context element 
defined in server.xml. The flag is set by the
+     * server.xml digester and is for internal use only. In particular, it is 
not exposed via JMX and is not stored by
+     * storeconfig.
+     *
+     * @return <code>true</code> if the Context element was parsed from 
server.xml
+     */
+    public boolean getDeployedFromServerXml() {
+        return this.deployedFromServerXml;
+    }
+
+
+    /**
+     * Sets the flag indicating that this Context was deployed from a Context 
element defined in server.xml. The flag is
+     * for internal use only. In particular, it is not exposed via JMX and is 
not stored by storeconfig.
+     *
+     * @param deployedFromServerXml The new flag value
+     */
+    public void setDeployedFromServerXml(boolean deployedFromServerXml) {
+        this.deployedFromServerXml = deployedFromServerXml;
+    }
+
+
     @Override
     public boolean getConfigured() {
         return this.configured;
diff --git a/java/org/apache/catalina/startup/ContextRuleSet.java 
b/java/org/apache/catalina/startup/ContextRuleSet.java
index 6dccb9b6f0..8c29539c6d 100644
--- a/java/org/apache/catalina/startup/ContextRuleSet.java
+++ b/java/org/apache/catalina/startup/ContextRuleSet.java
@@ -16,8 +16,11 @@
  */
 package org.apache.catalina.startup;
 
+import org.apache.catalina.core.StandardContext;
 import org.apache.tomcat.util.digester.Digester;
+import org.apache.tomcat.util.digester.Rule;
 import org.apache.tomcat.util.digester.RuleSet;
+import org.xml.sax.Attributes;
 
 /**
  * <strong>RuleSet</strong> for processing the contents of a Context 
definition element.
@@ -80,6 +83,7 @@ public class ContextRuleSet implements RuleSet {
         if (create) {
             digester.addObjectCreate(prefix + "Context", 
"org.apache.catalina.core.StandardContext", "className");
             digester.addSetProperties(prefix + "Context");
+            digester.addRule(prefix + "Context", new 
SetDeployedFromServerXmlRule());
         } else {
             digester.addSetProperties(prefix + "Context", new String[] { 
"path", "docBase" });
         }
@@ -189,3 +193,35 @@ public class ContextRuleSet implements RuleSet {
                 "org.apache.tomcat.util.http.CookieProcessor");
     }
 }
+
+
+/**
+ * A Rule that calls <code>setDeployedFromServerXml(true)</code> for the top 
object on the stack when a Context element
+ * is created, which only occurs for Context elements defined in server.xml. 
The flag allows storeconfig to detect a
+ * Context that is defined inline in server.xml so it is not moved to a 
separate configuration file when the
+ * configuration is stored.
+ */
+final class SetDeployedFromServerXmlRule extends Rule {
+
+    SetDeployedFromServerXmlRule() {
+        // NO-OP
+    }
+
+    @Override
+    public void begin(String namespace, String name, Attributes attributes) 
throws Exception {
+        if (digester.peek() instanceof StandardContext context) {
+            context.setDeployedFromServerXml(true);
+            if (digester.getLogger().isTraceEnabled()) {
+                digester.getLogger().trace("Calling 
StandardContext.setDeployedFromServerXml(true)");
+            }
+
+            StringBuilder code = digester.getGeneratedCode();
+            if (code != null) {
+                code.append(System.lineSeparator());
+                
code.append(digester.toVariableName(context)).append(".setDeployedFromServerXml(true);");
+                code.append(System.lineSeparator());
+            }
+        }
+    }
+
+}
diff --git a/java/org/apache/catalina/storeconfig/LocalStrings.properties 
b/java/org/apache/catalina/storeconfig/LocalStrings.properties
index 280faefe5d..e26bf11e07 100644
--- a/java/org/apache/catalina/storeconfig/LocalStrings.properties
+++ b/java/org/apache/catalina/storeconfig/LocalStrings.properties
@@ -39,6 +39,7 @@ standardContextSF.canonicalPathError=Failed to obtain the 
canonical path of the
 standardContextSF.moveFailed=Context original file at [{0}] is null, not a 
file or not writable
 standardContextSF.nonFileConfigUrl=The config URL [{0}] is not file based
 standardContextSF.storeContext=Store context [{0}] configuration separately at 
path [{1}]
+standardContextSF.storeContextInlineSkipped=Context [{0}] not stored because 
it is defined in server.xml and no writer for server.xml is available
 standardContextSF.storeContextSkipped=Context [{0}] not stored because 
external context storage is not allowed and no writer for server.xml is 
available
 standardContextSF.storeContextWithBackup=Store context [{0}] configuration 
separately with backup at path [{1}]
 
diff --git a/java/org/apache/catalina/storeconfig/StandardContextSF.java 
b/java/org/apache/catalina/storeconfig/StandardContextSF.java
index 5d64479b2e..e328a1c87e 100644
--- a/java/org/apache/catalina/storeconfig/StandardContextSF.java
+++ b/java/org/apache/catalina/storeconfig/StandardContextSF.java
@@ -54,7 +54,8 @@ import org.apache.tomcat.util.http.CookieProcessor;
  * <li>Store a context that has an external configuration file to that 
file</li>
  * <li>Store a context without an external configuration file to
  * conf/enginename/hostname/context.xml, unless the registry allows inline
- * storage in server.xml</li>
+ * storage in server.xml and the context was deployed from a Context element
+ * in server.xml, in which case it is stored back inline to server.xml</li>
  * <li>Store with backup</li>
  * </ul>
  */
@@ -96,8 +97,10 @@ public class StandardContextSF extends StoreFactoryBase {
                         }
                         return;
                     }
-                } else if (desc.isExternalOnly()) {
-                    // Set a configFile so that the configuration is actually 
saved
+                } else if (desc.isExternalOnly() || !((StandardContext) 
aContext).getDeployedFromServerXml()) {
+                    // Set a configFile so that the configuration is actually 
saved. This only happens when the
+                    // registry requires external storage or when the Context 
was not deployed from a Context element
+                    // in server.xml (a Context defined in server.xml is 
stored back inline to server.xml instead).
                     Context context = ((StandardContext) aContext);
                     Host host = (Host) context.getParent();
                     File configBase = host.getConfigBaseFile();
@@ -111,6 +114,12 @@ public class StandardContextSF extends StoreFactoryBase {
                         storeContextSeparate(aWriter, indent, 
(StandardContext) aContext);
                     }
                     return;
+                } else if (aWriter == null) {
+                    if (log.isInfoEnabled()) {
+                        
log.info(sm.getString("standardContextSF.storeContextInlineSkipped",
+                                ((StandardContext) aContext).getPath()));
+                    }
+                    return;
                 }
             }
         }
diff --git a/java/org/apache/catalina/storeconfig/server-registry.xml 
b/java/org/apache/catalina/storeconfig/server-registry.xml
index ed26d451d6..2e62167491 100644
--- a/java/org/apache/catalina/storeconfig/server-registry.xml
+++ b/java/org/apache/catalina/storeconfig/server-registry.xml
@@ -70,6 +70,7 @@
         <TransientAttribute>configured</TransientAttribute>
         <TransientAttribute>displayName</TransientAttribute>
         <TransientAttribute>distributable</TransientAttribute>
+        <TransientAttribute>deployedFromServerXml</TransientAttribute>
         <TransientAttribute>domain</TransientAttribute>
         <TransientAttribute>name</TransientAttribute>
         <TransientAttribute>publicId</TransientAttribute>
diff --git a/test/org/apache/catalina/storeconfig/TestStoreConfig.java 
b/test/org/apache/catalina/storeconfig/TestStoreConfig.java
index c987a1ca84..e63c34a342 100644
--- a/test/org/apache/catalina/storeconfig/TestStoreConfig.java
+++ b/test/org/apache/catalina/storeconfig/TestStoreConfig.java
@@ -18,6 +18,7 @@ package org.apache.catalina.storeconfig;
 
 import java.io.File;
 import java.io.FileReader;
+import java.io.PrintWriter;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
@@ -29,6 +30,7 @@ import org.junit.Assert;
 import org.junit.Test;
 
 import org.apache.catalina.Context;
+import org.apache.catalina.Host;
 import org.apache.catalina.connector.Connector;
 import org.apache.catalina.core.StandardContext;
 import org.apache.catalina.realm.LockOutRealm;
@@ -313,4 +315,245 @@ public class TestStoreConfig extends TomcatBaseTest {
                 .parse(new InputSource(new StringReader(contextXmlDump)));
     }
 
+    /**
+     * Verify that a Context parsed from a Context element in server.xml is 
flagged as deployed from server.xml, so
+     * storeconfig can detect it. The flag must also be set when server.xml is 
processed through the generated code
+     * path, so the generated code is checked as well.
+     *
+     * @throws Exception if the test experiences an unexpected error
+     */
+    @Test
+    public void testContextFromServerXmlIsFlagged() throws Exception {
+        File appDir = new File(getTemporaryDirectory(), "webapps/inline");
+        if (!appDir.mkdirs()) {
+            Assert.fail("Unable to create the webapp directory");
+        }
+
+        File conf = new File(getTemporaryDirectory(), "conf");
+        if (!conf.mkdirs()) {
+            Assert.fail("Unable to create conf directory");
+        }
+        addDeleteOnTearDown(conf);
+
+        File serverXml = new File(conf, "server.xml");
+        Files.write(serverXml.toPath(), String.join("\n",
+                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
+                "<Server port=\"-1\" shutdown=\"SHUTDOWN\">",
+                "    <Service name=\"Catalina\">",
+                "        <Engine name=\"Catalina\" defaultHost=\"localhost\">",
+                "            <Host name=\"localhost\" appBase=\"webapps\"",
+                "                  deployOnStartup=\"false\" 
autoDeploy=\"false\">",
+                "                <Context path=\"/inline\" 
docBase=\"inline\"/>",
+                "            </Host>",
+                "        </Engine>",
+                "    </Service>",
+                "</Server>",
+                "").getBytes(StandardCharsets.UTF_8));
+
+        // Parse server.xml (with code generation enabled) without starting 
the server
+        File generatedCodeLocation = new File(getTemporaryDirectory(), 
"generated");
+        Catalina catalina = new Catalina();
+        catalina.load(new String[] { "start", "-generateCode", 
generatedCodeLocation.getAbsolutePath() });
+
+        Context inline = (Context) 
catalina.getServer().findServices()[0].getContainer().findChildren()[0]
+                .findChild("/inline");
+        Assert.assertNotNull("Context element from server.xml not found", 
inline);
+        Assert.assertTrue("Context from server.xml must be flagged as deployed 
from server.xml",
+                ((StandardContext) inline).getDeployedFromServerXml());
+
+        // The generated code must set the flag as well, so that contexts 
parsed through the generated code path are
+        // also flagged
+        File generatedClass = new File(generatedCodeLocation, 
"catalinaembedded/ServerXml.java");
+        Assert.assertTrue("Generated code was not created: " + generatedClass, 
generatedClass.exists());
+        String generatedCode;
+        try (FileReader reader = new FileReader(generatedClass);
+                StringWriter writer = new StringWriter()) {
+            IOTools.flow(reader, writer);
+            generatedCode = writer.toString();
+        }
+        Assert.assertTrue(generatedCode, 
generatedCode.contains(".setDeployedFromServerXml(true);"));
+    }
+
+    /**
+     * Verify that a context flagged as deployed from server.xml is stored 
back inline to the server.xml writer (and
+     * not moved to a new context configuration file) when inline storage is 
allowed (externalOnly is false).
+     *
+     * @throws Exception if the test experiences an unexpected error
+     */
+    @Test
+    public void testContextFromServerXmlStoredInline() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+        StoreConfigLifecycleListener storeConfigListener = new 
StoreConfigLifecycleListener();
+        tomcat.getServer().addLifecycleListener(storeConfigListener);
+
+        // Use a storable realm. The default embedded realm 
(Tomcat.SimpleRealm) is an inner class that the store
+        // path cannot instantiate a default instance of.
+        tomcat.getEngine().setRealm(new LockOutRealm());
+
+        File appDir = new File(getTemporaryDirectory(), "webapps/inline");
+        if (!appDir.mkdirs()) {
+            Assert.fail("Unable to create the webapp directory");
+        }
+        Context context = tomcat.addContext("/inline", 
appDir.getAbsolutePath());
+        ((StandardContext) context).setDeployedFromServerXml(true);
+
+        File conf = new File(getTemporaryDirectory(), "conf");
+        if (!conf.mkdirs()) {
+            Assert.fail("Unable to create conf directory");
+        }
+        addDeleteOnTearDown(conf);
+
+        tomcat.start();
+
+        IStoreConfig storeConfig = storeConfigListener.getStoreConfig();
+        StoreDescription desc = 
storeConfig.getRegistry().findDescription(StandardContext.class);
+        Assert.assertNotNull(desc);
+        boolean oldSeparate = desc.isStoreSeparate();
+        boolean oldExternalAllowed = desc.isExternalAllowed();
+        boolean oldExternalOnly = desc.isExternalOnly();
+        String serverXmlDump;
+        try {
+            desc.setStoreSeparate(true);
+            desc.setExternalAllowed(true);
+            desc.setExternalOnly(false);
+            StringWriter buffer = new StringWriter();
+            storeConfig.store(new PrintWriter(buffer), -2, tomcat.getServer());
+            serverXmlDump = buffer.toString();
+        } finally {
+            desc.setStoreSeparate(oldSeparate);
+            desc.setExternalAllowed(oldExternalAllowed);
+            desc.setExternalOnly(oldExternalOnly);
+        }
+
+        // The context must be stored inline in server.xml, without the 
internal flag attribute
+        Assert.assertTrue(serverXmlDump, serverXmlDump.contains("<Context"));
+        Assert.assertTrue(serverXmlDump, 
serverXmlDump.contains("path=\"/inline\""));
+        Assert.assertFalse(serverXmlDump, 
serverXmlDump.contains("deployedFromServerXml"));
+        // The stored configuration must remain well-formed
+        SAXParserFactory.newInstance().newSAXParser().getXMLReader()
+                .parse(new InputSource(new StringReader(serverXmlDump)));
+
+        // No new context configuration file must have been created
+        Host host = tomcat.getHost();
+        Assert.assertFalse(new File(host.getConfigBaseFile(), 
"inline.xml").exists());
+    }
+
+    /**
+     * Verify that a context that was not deployed from server.xml and that 
has no configuration file is stored to a
+     * new context configuration file even when inline storage is allowed 
(externalOnly is false).
+     *
+     * @throws Exception if the test experiences an unexpected error
+     */
+    @Test
+    public void testContextNotFromServerXmlStoredToNewFile() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+        StoreConfigLifecycleListener storeConfigListener = new 
StoreConfigLifecycleListener();
+        tomcat.getServer().addLifecycleListener(storeConfigListener);
+
+        // Use a storable realm. The default embedded realm 
(Tomcat.SimpleRealm) is an inner class that the store
+        // path cannot instantiate a default instance of.
+        tomcat.getEngine().setRealm(new LockOutRealm());
+
+        File appDir = new File(getTemporaryDirectory(), "webapps/standalone");
+        if (!appDir.mkdirs()) {
+            Assert.fail("Unable to create the webapp directory");
+        }
+        Context context = tomcat.addContext("/standalone", 
appDir.getAbsolutePath());
+        Assert.assertFalse("Programmatic context must not be flagged as 
deployed from server.xml",
+                ((StandardContext) context).getDeployedFromServerXml());
+
+        File conf = new File(getTemporaryDirectory(), "conf");
+        if (!conf.mkdirs()) {
+            Assert.fail("Unable to create conf directory");
+        }
+        addDeleteOnTearDown(conf);
+
+        tomcat.start();
+
+        IStoreConfig storeConfig = storeConfigListener.getStoreConfig();
+        StoreDescription desc = 
storeConfig.getRegistry().findDescription(StandardContext.class);
+        Assert.assertNotNull(desc);
+        boolean oldSeparate = desc.isStoreSeparate();
+        boolean oldExternalAllowed = desc.isExternalAllowed();
+        boolean oldExternalOnly = desc.isExternalOnly();
+        String serverXmlDump;
+        try {
+            desc.setStoreSeparate(true);
+            desc.setExternalAllowed(true);
+            desc.setExternalOnly(false);
+            StringWriter buffer = new StringWriter();
+            storeConfig.store(new PrintWriter(buffer), -2, tomcat.getServer());
+            serverXmlDump = buffer.toString();
+        } finally {
+            desc.setStoreSeparate(oldSeparate);
+            desc.setExternalAllowed(oldExternalAllowed);
+            desc.setExternalOnly(oldExternalOnly);
+        }
+
+        // The context must not be stored inline in server.xml
+        Assert.assertFalse(serverXmlDump, 
serverXmlDump.contains("standalone"));
+
+        // A new context configuration file must have been created instead
+        File contextXml = new File(tomcat.getHost().getConfigBaseFile(), 
"standalone.xml");
+        Assert.assertTrue("Context file was not created: " + contextXml, 
contextXml.exists());
+        String contextXmlDump;
+        try (FileReader reader = new FileReader(contextXml);
+                StringWriter writer = new StringWriter()) {
+            IOTools.flow(reader, writer);
+            contextXmlDump = writer.toString();
+        }
+        Assert.assertTrue(contextXmlDump, contextXmlDump.contains("<Context"));
+        // The stored configuration must remain well-formed
+        SAXParserFactory.newInstance().newSAXParser().getXMLReader()
+                .parse(new InputSource(new StringReader(contextXmlDump)));
+    }
+
+    /**
+     * Verify that storing a context flagged as deployed from server.xml with 
no writer for server.xml is skipped
+     * gracefully (no error, no new context configuration file).
+     *
+     * @throws Exception if the test experiences an unexpected error
+     */
+    @Test
+    public void testContextFromServerXmlStoreWithoutWriterSkipped() throws 
Exception {
+        Tomcat tomcat = getTomcatInstance();
+        StoreConfigLifecycleListener storeConfigListener = new 
StoreConfigLifecycleListener();
+        tomcat.getServer().addLifecycleListener(storeConfigListener);
+
+        File appDir = new File(getTemporaryDirectory(), "webapps/skipped");
+        if (!appDir.mkdirs()) {
+            Assert.fail("Unable to create the webapp directory");
+        }
+        Context context = tomcat.addContext("/skipped", 
appDir.getAbsolutePath());
+        ((StandardContext) context).setDeployedFromServerXml(true);
+
+        File conf = new File(getTemporaryDirectory(), "conf");
+        if (!conf.mkdirs()) {
+            Assert.fail("Unable to create conf directory");
+        }
+        addDeleteOnTearDown(conf);
+
+        tomcat.start();
+
+        IStoreConfig storeConfig = storeConfigListener.getStoreConfig();
+        StoreDescription desc = 
storeConfig.getRegistry().findDescription(StandardContext.class);
+        Assert.assertNotNull(desc);
+        boolean oldSeparate = desc.isStoreSeparate();
+        boolean oldExternalAllowed = desc.isExternalAllowed();
+        boolean oldExternalOnly = desc.isExternalOnly();
+        try {
+            desc.setStoreSeparate(true);
+            desc.setExternalAllowed(true);
+            desc.setExternalOnly(false);
+            // No writer available for server.xml and no config file: storing 
must be skipped without an error
+            desc.getStoreFactory().store(null, -2, context);
+        } finally {
+            desc.setStoreSeparate(oldSeparate);
+            desc.setExternalAllowed(oldExternalAllowed);
+            desc.setExternalOnly(oldExternalOnly);
+        }
+
+        Assert.assertFalse(new File(tomcat.getHost().getConfigBaseFile(), 
"skipped.xml").exists());
+    }
+
 }
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 7999256f8f..2612650ea1 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -130,6 +130,12 @@
         layout of the existing file (comments, blank lines and attribute
         order) on a best effort basis. (remm)
       </update>
+      <fix>
+        Track in <code>StandardContext</code> whether the context was
+        deployed from a <code>Context</code> element in
+        <code>server.xml</code>. Use it to have <code>StoreConfig</code>
+        save to a new config file. (remm)
+      </fix>
     </changelog>
   </subsection>
 </section>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to