This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/11.0.x by this push:
new 6a48b763e4 Avoid saving some Context to server.xml
6a48b763e4 is described below
commit 6a48b763e49b23cbd28ee3235cb3db105aede484
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 1d581e85cb..8f1bd4d8cc 100644
--- a/java/org/apache/catalina/core/StandardContext.java
+++ b/java/org/apache/catalina/core/StandardContext.java
@@ -266,6 +266,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.
*/
@@ -1541,6 +1548,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 dd0e8aa3c5..786ee35959 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" });
}
@@ -192,3 +196,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 5191a7d0ee..209c2af5c6 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 91cd7e29bf..b728402000 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 903a2f62db..06ab3785cd 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]