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(),