From b70c01d1d676debd1931771ee56d33f04e1bf475 Mon Sep 17 00:00:00 2001
From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Date: Thu, 5 Nov 2026 22:53:08 +0800
Subject: [PATCH] fix(tomcat): keep the web app's META-INF/context.xml in the
generated descriptor (#1576)
Tomcat ignores an app's META-INF/context.xml once conf/Catalina/localhost has a descriptor for it, and the run configuration always wrote a bare , dropping the app's Resource/Environment/Valve/Realm entries. The descriptor now starts from the app's own context.xml (exploded directory or WAR, all three deployment kinds) with docBase and reloadable set by the IDE and path dropped; unreadable or non- files fall back to the bare descriptor.
Fixes #1514
Co-Authored-By: Claude Opus 5.5
---
crates/ide-run/src/tomcat.rs | 100 ++++++++++++++++++++++++++++++++++-
1 file changed, 99 insertions(+), 1 deletion(-)
diff --git a/crates/ide-run/src/tomcat.rs b/crates/ide-run/src/tomcat.rs
index ab49e617..bcccc221 100644
--- a/crates/ide-run/src/tomcat.rs
+++ b/crates/ide-run/src/tomcat.rs
@@ -71,6 +71,62 @@ pub fn context_xml(doc_base: &Path, reload: bool) -> String {
format!("\n", escape(&doc_base.to_string_lossy()))
}
+/// The web app's own `META-INF/context.xml`, from an exploded directory or a WAR.
+fn app_context_xml(doc_base: &Path) -> Option {
+ if doc_base.is_dir() {
+ return fs::read_to_string(doc_base.join("META-INF/context.xml")).ok();
+ }
+ let mut zip = zip::ZipArchive::new(fs::File::open(doc_base).ok()?).ok()?;
+ let mut text = String::new();
+ zip.by_name("META-INF/context.xml").ok()?.read_to_string(&mut text).ok()?;
+ Some(text)
+}
+
+/// The context descriptor for `conf/Catalina/localhost`. Tomcat ignores the app's
+/// `META-INF/context.xml` once a descriptor exists there, so its content (``,
+/// ``, ``…) is carried over, with `docBase` and `reloadable` set by the IDE.
+pub fn deployed_context_xml(doc_base: &Path, reload: bool) -> String {
+ let Some(app) = app_context_xml(doc_base) else { return context_xml(doc_base, reload) };
+ merge_context_xml(&app, doc_base, reload).unwrap_or_else(|e| {
+ tracing::warn!("ignoring the web app's META-INF/context.xml: {e}");
+ context_xml(doc_base, reload)
+ })
+}
+
+fn merge_context_xml(app: &str, doc_base: &Path, reload: bool) -> IdeResult {
+ let mut reader = Reader::from_str(app);
+ let mut writer = Writer::new(Vec::new());
+ let mut root = false;
+ loop {
+ let event = reader.read_event().map_err(invalid)?;
+ let event = match event {
+ Event::Eof => break,
+ Event::Start(ref tag) | Event::Empty(ref tag) if !root => {
+ if tag.name().as_ref() != "Context" {
+ return Err(invalid("the root element is not "));
+ }
+ root = true;
+ let mut new = BytesStart::new("Context");
+ for a in tag.attributes() {
+ let a = a.map_err(invalid)?;
+ if !matches!(a.key.as_ref(), "docBase" | "reloadable" | "path") {
+ new.push_attribute(a);
+ }
+ }
+ new.push_attribute(("docBase", doc_base.to_string_lossy().as_ref()));
+ new.push_attribute(("reloadable", if reload { "true" } else { "false" }));
+ if matches!(event, Event::Empty(_)) { Event::Empty(new) } else { Event::Start(new) }
+ }
+ e => e,
+ };
+ writer.write_event(event).map_err(invalid)?;
+ }
+ if !root {
+ return Err(invalid("no element"));
+ }
+ String::from_utf8(writer.into_inner()).map_err(invalid)
+}
+
pub fn patch_server_xml(xml: &str, http: u16, shutdown: u16, secret: &str) -> IdeResult {
let mut reader = Reader::from_str(xml);
let mut writer = Writer::new(Vec::new());
@@ -442,7 +498,7 @@ pub fn prepare(root: &Path, cache: &Path, config: &mut RunConfig) -> IdeResult<(
let server = patch_server_xml(&fs::read_to_string(home.join("conf/server.xml"))?, port, shutdown, &secret)?;
fs::write(base.join("conf/server.xml"), server)?;
let debug = config.extras.params.get("tomcatDebug").is_some_and(|v| v == "true");
- fs::write(contexts.join(format!("{context}.xml")), context_xml(&doc, !debug))?;
+ fs::write(contexts.join(format!("{context}.xml")), deployed_context_xml(&doc, !debug))?;
config.env.insert("CATALINA_HOME".into(), home.to_string_lossy().into_owned());
config.env.insert("CATALINA_BASE".into(), base.to_string_lossy().into_owned());
if let Some(jre) = config.extras.params.get("jre").filter(|s| !s.trim().is_empty()) {
@@ -501,6 +557,48 @@ mod tests {
}
zip.finish().unwrap();
}
+ #[test]
+ fn keeps_the_apps_own_context_descriptor() {
+ let tmp = tempfile::tempdir().unwrap();
+ let app = "\n\n \n \n \n\n";
+ let dir = tmp.path().join("web app");
+ fs::create_dir_all(dir.join("META-INF")).unwrap();
+ fs::write(dir.join("META-INF/context.xml"), app).unwrap();
+ let war = tmp.path().join("app.war");
+ archive(&war, &[("META-INF/context.xml", app), ("index.jsp", "")]);
+ for doc in [&dir, &war] {
+ let xml = deployed_context_xml(doc, true);
+ let doc_base = escape(&doc.to_string_lossy());
+ assert!(
+ xml.starts_with("\n")), "{xml}");
+ assert!(!xml.contains("/old") && !xml.contains("\"old\"") && !xml.contains("\"false\""), "{xml}");
+ assert!(xml.contains(""));
+ assert!(xml.contains(""));
+ assert!(xml.contains(""));
+ }
+
+ fs::write(dir.join("META-INF/context.xml"), "").unwrap();
+ let doc_base = escape(&dir.to_string_lossy());
+ assert_eq!(
+ deployed_context_xml(&dir, false),
+ format!("")
+ );
+ // Unreadable or not a context: the IDE's own descriptor, as before.
+ for broken in ["", "", ""] {
+ fs::write(dir.join("META-INF/context.xml"), broken).unwrap();
+ assert_eq!(deployed_context_xml(&dir, false), context_xml(&dir, false), "{broken}");
+ }
+ let plain = tmp.path().join("plain");
+ fs::create_dir_all(&plain).unwrap();
+ assert_eq!(deployed_context_xml(&plain, true), context_xml(&plain, true));
+ let bare = tmp.path().join("bare.war");
+ archive(&bare, &[("index.jsp", "")]);
+ assert_eq!(deployed_context_xml(&bare, true), context_xml(&bare, true));
+ }
fn config() -> RunConfig {
RunConfig {
id: "local:demo".into(),