Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions RUBY_VS_GO_BUILDPACK_COMPARISON.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ func (t *TomcatContainer) Supply() error {
| **Logging Support** | `TomcatLoggingSupport` | `installTomcatLoggingSupport()` | ✅ Complete | Installs `tomcat-logging-support.jar` (CloudFoundryConsoleHandler) |
| **setenv.sh Generation** | `TomcatSetenv` | `createSetenvScript()` | ✅ Complete | Creates `bin/setenv.sh` for CLASSPATH |
| **Utils (XML helpers)** | `TomcatUtils` | N/A | ✅ Complete | Go uses standard library XML parsing |
| **Context Path** | `TomcatInstance#root` (webapps dir rename) | `contextXMLFilename()` + context descriptor | ✅ Complete | Same config key; different mechanism — see §2A.11 |
| **Geode/GemFire Session Store** | `TomcatGeodeStore` (199 lines) | **❌ Missing** | ❌ Not Implemented | Session clustering for Tanzu GemFire |
| **Redis Session Store** | `TomcatRedisStore` (118 lines) | **❌ Missing** | ❌ Not Implemented | Session clustering for Redis |
| **Spring Insight Support** | `TomcatInsightSupport` (51 lines) | **❌ Missing** | ⚠️ Deprecated | Spring Insight deprecated by VMware |
Expand Down Expand Up @@ -849,6 +850,7 @@ cf set-env myapp JBP_CONFIG_TOMCAT '{tomcat: {version: 9.0.+}}'
| **External Configuration** | ⚠️ 90% | Go requires manifest (no runtime repository_root) |
| **Lifecycle Support** | ✅ 100% | Both detect startup failures |
| **Logging Support** | ✅ 100% | Both use CloudFoundryConsoleHandler |
| **Context Path** | ✅ 100% | Same config key and URL behavior; implementation differs (see §2A.11) |
| **Session Store Auto-Config** | ⚠️ 0% | Go missing convenience auto-configuration (manual setup possible) |
| **Overall** | ⚠️ **95%** | Core features complete; auto-config conveniences missing |

Expand All @@ -875,6 +877,22 @@ cf set-env myapp JBP_CONFIG_TOMCAT '{tomcat: {version: 9.0.+}}'
3. Read `VCAP_SERVICES` in application code (if needed)
4. Test with Go buildpack → Deploy

### 2A.11 Context Path: Implementation Difference

Both buildpacks support `context_path` via `JBP_CONFIG_TOMCAT`, but use different Tomcat mechanisms:

| Aspect | Ruby (4.x) | Go (5.x) |
|--------|-----------|---------|
| **Mechanism** | Deploys app into `tomcat/webapps/<name>/` | Writes `conf/Catalina/localhost/<name>.xml` context descriptor |
| **App location** | `tomcat/webapps/foo#bar/` | `${user.home}/app` (unchanged) |
| **Root context prevention** | No `ROOT` directory in webapps | `ROOT.xml` explicitly removed when non-root path set |
| **Config key** | `tomcat.context_path` | `tomcat.context_path` (identical) |
| **URL result** | App at `/foo/bar` only | App at `/foo/bar` only (identical) |

**Migration impact**: None for users. The `context_path` config key and resulting URL routing are identical between 4.x and 5.x. This is a smooth transition.

**Caveat**: `ServletContext.getRealPath()` and webapps-relative path assumptions may behave differently — the Go buildpack serves from `${user.home}/app` rather than a webapps subdirectory. This is a pre-existing structural difference between the two buildpacks, not specific to `context_path`.

---

## 2B. Container Feature Parity: Complete Analysis
Expand Down
15 changes: 15 additions & 0 deletions src/integration/tomcat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,5 +441,20 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T,
Eventually(deployment).Should(matchers.Serve(ContainSubstring("OK")))
})
})

context("with context_path configured", func() {
it("serves app at configured path and not at root", func() {
deployment, logs, err := platform.Deploy.
WithEnv(map[string]string{
"BP_JAVA_VERSION": "11",
"JBP_CONFIG_TOMCAT": `{tomcat: {context_path: /my/app}}`,
}).
Execute(name, filepath.Join(fixtures, "containers", "tomcat_jakarta"))
Expect(err).NotTo(HaveOccurred(), logs.String())

Eventually(deployment).Should(matchers.Serve(ContainSubstring("OK")).WithEndpoint("/my/app"))
Eventually(deployment).ShouldNot(matchers.Serve(ContainSubstring("OK")).WithEndpoint("/"))
})
})
}
}
101 changes: 84 additions & 17 deletions src/java/containers/tomcat.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,12 +553,32 @@ func injectDocBase(xmlContent string, docBase string) string {
return xmlContent[:idx] + newContextTag + xmlContent[endIdx:]
}

// contextXMLFilename converts a context path to a Tomcat context XML filename.
// Tomcat convention: /foo/bar → foo#bar.xml, / or empty → ROOT.xml
func contextXMLFilename(contextPath string) string {
name := strings.Trim(contextPath, "/")
if name == "" {
return "ROOT.xml"
}
name = strings.ReplaceAll(name, "/", "#")
return name + ".xml"
}

// Finalize performs final Tomcat configuration
func (t *TomcatContainer) Finalize() error {
t.context.Log.BeginStep("Finalizing Tomcat")

if t.config == nil {
var err error
t.config, err = t.loadConfig()
if err != nil {
return fmt.Errorf("failed to load tomcat config: %w", err)
}
}

buildDir := t.context.Stager.BuildDir()
contextXMLPath := filepath.Join(t.tomcatDir(), "conf", "Catalina", "localhost", "ROOT.xml")
contextXMLName := contextXMLFilename(t.config.Tomcat.ContextPath)
contextXMLPath := filepath.Join(t.tomcatDir(), "conf", "Catalina", "localhost", contextXMLName)

Comment thread
stokpop marked this conversation as resolved.
webInf := filepath.Join(buildDir, "WEB-INF")
if _, err := os.Stat(webInf); err == nil {
Expand All @@ -573,27 +593,73 @@ func (t *TomcatContainer) Finalize() error {
return fmt.Errorf("failed to create context directory: %w", err)
}

appContextXML := filepath.Join(buildDir, "META-INF", "context.xml")
var contextContent string

if _, err := os.Stat(appContextXML); err == nil {
xmlBytes, err := os.ReadFile(appContextXML)
if err != nil {
return fmt.Errorf("failed to read META-INF/context.xml: %w", err)
_, statErr := os.Stat(contextXMLPath)
if statErr != nil && !os.IsNotExist(statErr) {
return fmt.Errorf("failed to check context XML %s: %w", contextXMLName, statErr)
}
if os.IsNotExist(statErr) {
appContextXML := filepath.Join(buildDir, "META-INF", "context.xml")
var contextContent string

if _, err := os.Stat(appContextXML); err == nil {
xmlBytes, err := os.ReadFile(appContextXML)
if err != nil {
return fmt.Errorf("failed to read META-INF/context.xml: %w", err)
}

xmlStr := string(xmlBytes)
xmlStr = strings.TrimSpace(xmlStr)

contextContent = injectDocBase(xmlStr, "${user.home}/app")
t.context.Log.Info("Merged META-INF/context.xml with %s - realm and resource configurations preserved", contextXMLName)
} else {
contextContent = fmt.Sprintf("<Context docBase=\"${user.home}/app\" reloadable=\"false\">\n</Context>\n")
t.context.Log.Info("Created %s with docBase pointing to application directory", contextXMLName)
}

xmlStr := string(xmlBytes)
xmlStr = strings.TrimSpace(xmlStr)

contextContent = injectDocBase(xmlStr, "${user.home}/app")
t.context.Log.Info("Merged META-INF/context.xml with ROOT.xml - realm and resource configurations preserved")
if err := os.WriteFile(contextXMLPath, []byte(contextContent), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", contextXMLName, err)
}
} else {
contextContent = fmt.Sprintf("<Context docBase=\"${user.home}/app\" reloadable=\"false\">\n</Context>\n")
t.context.Log.Info("Created ROOT.xml with docBase pointing to application directory")
t.context.Log.Info("Context XML %s already exists (e.g. from external config), skipping generation", contextXMLName)
}
Comment thread
stokpop marked this conversation as resolved.
if contextXMLName != "ROOT.xml" {
rootXMLPath := filepath.Join(t.tomcatDir(), "conf", "Catalina", "localhost", "ROOT.xml")
if err := os.Remove(rootXMLPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove ROOT.xml: %w", err)
}
}
} else {
warMatches, err := filepath.Glob(filepath.Join(buildDir, "*.war"))
if err == nil && len(warMatches) == 1 {
warFilename := filepath.Base(warMatches[0])
contextContent := fmt.Sprintf("<Context docBase=\"${user.home}/app/%s\" reloadable=\"false\">\n</Context>\n", warFilename)

contextXMLDir := filepath.Dir(contextXMLPath)
if err := os.MkdirAll(contextXMLDir, 0755); err != nil {
return fmt.Errorf("failed to create context directory: %w", err)
}

if err := os.WriteFile(contextXMLPath, []byte(contextContent), 0644); err != nil {
return fmt.Errorf("failed to write ROOT.xml: %w", err)
_, statErr := os.Stat(contextXMLPath)
if statErr != nil && !os.IsNotExist(statErr) {
return fmt.Errorf("failed to check context XML %s: %w", contextXMLName, statErr)
}
if os.IsNotExist(statErr) {
if err := os.WriteFile(contextXMLPath, []byte(contextContent), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", contextXMLName, err)
}
t.context.Log.Info("Created %s with docBase pointing to %s", contextXMLName, warFilename)
} else {
t.context.Log.Info("Context XML %s already exists (e.g. from external config), skipping generation", contextXMLName)
}
if contextXMLName != "ROOT.xml" {
rootXMLPath := filepath.Join(t.tomcatDir(), "conf", "Catalina", "localhost", "ROOT.xml")
if err := os.Remove(rootXMLPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove ROOT.xml: %w", err)
}
}
} else if len(warMatches) > 1 {
t.context.Log.Warning("Multiple WAR files found in build directory; context_path not applied")
}
}

Expand Down Expand Up @@ -647,6 +713,7 @@ type tomcatConfig struct {
type Tomcat struct {
Version string `yaml:"version"`
ExternalConfigurationEnabled bool `yaml:"external_configuration_enabled"`
ContextPath string `yaml:"context_path"`
}

type ExternalConfiguration struct {
Expand Down
156 changes: 156 additions & 0 deletions src/java/containers/tomcat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,162 @@ var _ = Describe("Tomcat Container", func() {
Expect(contentStr).NotTo(ContainSubstring("/old/path"))
Expect(contentStr).To(ContainSubstring("org.apache.catalina.realm.UserDatabaseRealm"))
})

It("creates context XML named after context_path when set", func() {
os.Setenv("JBP_CONFIG_TOMCAT", `{tomcat: {context_path: /the/intended/path}}`)
defer os.Unsetenv("JBP_CONFIG_TOMCAT")

err := container.Finalize()
Expect(err).NotTo(HaveOccurred())

tomcatDir := filepath.Join(depsDir, "0", "tomcat")
contextFile := filepath.Join(tomcatDir, "conf", "Catalina", "localhost", "the#intended#path.xml")
Expect(contextFile).To(BeAnExistingFile())
Expect(filepath.Join(tomcatDir, "conf", "Catalina", "localhost", "ROOT.xml")).NotTo(BeAnExistingFile())

content, err := os.ReadFile(contextFile)
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(ContainSubstring("docBase=\"${user.home}/app\""))
})

It("uses ROOT.xml when context_path is empty", func() {
os.Setenv("JBP_CONFIG_TOMCAT", `{tomcat: {context_path: ""}}`)
defer os.Unsetenv("JBP_CONFIG_TOMCAT")

err := container.Finalize()
Expect(err).NotTo(HaveOccurred())

tomcatDir := filepath.Join(depsDir, "0", "tomcat")
Expect(filepath.Join(tomcatDir, "conf", "Catalina", "localhost", "ROOT.xml")).To(BeAnExistingFile())
})

It("uses ROOT.xml when context_path is /", func() {
os.Setenv("JBP_CONFIG_TOMCAT", `{tomcat: {context_path: /}}`)
defer os.Unsetenv("JBP_CONFIG_TOMCAT")

err := container.Finalize()
Expect(err).NotTo(HaveOccurred())

tomcatDir := filepath.Join(depsDir, "0", "tomcat")
Expect(filepath.Join(tomcatDir, "conf", "Catalina", "localhost", "ROOT.xml")).To(BeAnExistingFile())
})

It("normalizes trailing slash in context_path", func() {
os.Setenv("JBP_CONFIG_TOMCAT", `{tomcat: {context_path: /the/intended/path/}}`)
defer os.Unsetenv("JBP_CONFIG_TOMCAT")

err := container.Finalize()
Expect(err).NotTo(HaveOccurred())

tomcatDir := filepath.Join(depsDir, "0", "tomcat")
contextFile := filepath.Join(tomcatDir, "conf", "Catalina", "localhost", "the#intended#path.xml")
Expect(contextFile).To(BeAnExistingFile())
})

It("removes pre-existing ROOT.xml when context_path is non-root", func() {
os.Setenv("JBP_CONFIG_TOMCAT", `{tomcat: {context_path: /the/intended/path}}`)
defer os.Unsetenv("JBP_CONFIG_TOMCAT")

tomcatDir := filepath.Join(depsDir, "0", "tomcat")
contextDir := filepath.Join(tomcatDir, "conf", "Catalina", "localhost")
Expect(os.MkdirAll(contextDir, 0755)).To(Succeed())
rootXML := filepath.Join(contextDir, "ROOT.xml")
Expect(os.WriteFile(rootXML, []byte("<Context/>"), 0644)).To(Succeed())

err := container.Finalize()
Expect(err).NotTo(HaveOccurred())
Expect(rootXML).NotTo(BeAnExistingFile())
Expect(filepath.Join(contextDir, "the#intended#path.xml")).To(BeAnExistingFile())
})

It("skips writing context XML when file already exists (external config)", func() {
tomcatDir := filepath.Join(depsDir, "0", "tomcat")
contextDir := filepath.Join(tomcatDir, "conf", "Catalina", "localhost")
Expect(os.MkdirAll(contextDir, 0755)).To(Succeed())
preExistingContent := "<Context docBase=\"/external/path\"/>"
rootXML := filepath.Join(contextDir, "ROOT.xml")
Expect(os.WriteFile(rootXML, []byte(preExistingContent), 0644)).To(Succeed())

err := container.Finalize()
Expect(err).NotTo(HaveOccurred())

content, readErr := os.ReadFile(rootXML)
Expect(readErr).NotTo(HaveOccurred())
Expect(string(content)).To(Equal(preExistingContent))
})

It("removes ROOT.xml even when non-root context XML already exists (external config)", func() {
os.Setenv("JBP_CONFIG_TOMCAT", `{tomcat: {context_path: /my/path}}`)
defer os.Unsetenv("JBP_CONFIG_TOMCAT")

tomcatDir := filepath.Join(depsDir, "0", "tomcat")
contextDir := filepath.Join(tomcatDir, "conf", "Catalina", "localhost")
Expect(os.MkdirAll(contextDir, 0755)).To(Succeed())
externalContent := "<Context docBase=\"/external/path\"/>"
contextXML := filepath.Join(contextDir, "my#path.xml")
Expect(os.WriteFile(contextXML, []byte(externalContent), 0644)).To(Succeed())
rootXML := filepath.Join(contextDir, "ROOT.xml")
Expect(os.WriteFile(rootXML, []byte("<Context/>"), 0644)).To(Succeed())

err := container.Finalize()
Expect(err).NotTo(HaveOccurred())

content, readErr := os.ReadFile(contextXML)
Expect(readErr).NotTo(HaveOccurred())
Expect(string(content)).To(Equal(externalContent))
Expect(rootXML).NotTo(BeAnExistingFile())
})

Context("with packaged WAR (no WEB-INF)", func() {
BeforeEach(func() {
Expect(os.RemoveAll(filepath.Join(buildDir, "WEB-INF"))).To(Succeed())
})

It("creates context XML pointing to WAR file when context_path is set", func() {
Expect(os.WriteFile(filepath.Join(buildDir, "myapp.war"), []byte("fakewar"), 0644)).To(Succeed())
os.Setenv("JBP_CONFIG_TOMCAT", `{tomcat: {context_path: /my/path}}`)
defer os.Unsetenv("JBP_CONFIG_TOMCAT")

err := container.Finalize()
Expect(err).NotTo(HaveOccurred())

tomcatDir := filepath.Join(depsDir, "0", "tomcat")
contextFile := filepath.Join(tomcatDir, "conf", "Catalina", "localhost", "my#path.xml")
Expect(contextFile).To(BeAnExistingFile())
content, _ := os.ReadFile(contextFile)
Expect(string(content)).To(ContainSubstring(`docBase="${user.home}/app/myapp.war"`))
})

It("creates ROOT.xml pointing to WAR file when no context_path set", func() {
Expect(os.WriteFile(filepath.Join(buildDir, "myapp.war"), []byte("fakewar"), 0644)).To(Succeed())

err := container.Finalize()
Expect(err).NotTo(HaveOccurred())

tomcatDir := filepath.Join(depsDir, "0", "tomcat")
contextFile := filepath.Join(tomcatDir, "conf", "Catalina", "localhost", "ROOT.xml")
Expect(contextFile).To(BeAnExistingFile())
content, _ := os.ReadFile(contextFile)
Expect(string(content)).To(ContainSubstring(`docBase="${user.home}/app/myapp.war"`))
})

It("removes ROOT.xml when packaged WAR uses non-root context_path", func() {
Expect(os.WriteFile(filepath.Join(buildDir, "myapp.war"), []byte("fakewar"), 0644)).To(Succeed())
os.Setenv("JBP_CONFIG_TOMCAT", `{tomcat: {context_path: /my/path}}`)
defer os.Unsetenv("JBP_CONFIG_TOMCAT")

tomcatDir := filepath.Join(depsDir, "0", "tomcat")
contextDir := filepath.Join(tomcatDir, "conf", "Catalina", "localhost")
Expect(os.MkdirAll(contextDir, 0755)).To(Succeed())
rootXML := filepath.Join(contextDir, "ROOT.xml")
Expect(os.WriteFile(rootXML, []byte("<Context/>"), 0644)).To(Succeed())

err := container.Finalize()
Expect(err).NotTo(HaveOccurred())
Expect(rootXML).NotTo(BeAnExistingFile())
Expect(filepath.Join(contextDir, "my#path.xml")).To(BeAnExistingFile())
})
})
})

Describe("SelectTomcatVersionPattern", func() {
Expand Down