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]
