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
79 changes: 79 additions & 0 deletions src/integration/tomcat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,41 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T,
})

context("with a simple servlet app", func() {
it("successfully deploys and runs with Java 11 (Javax)", func() {
deployment, logs, err := platform.Deploy.
WithEnv(map[string]string{
"BP_JAVA_VERSION": "11",
"JBP_CONFIG_TOMCAT": "{tomcat: { version: \"9.+\" }, access_logging_support: {access_logging: enabled}}",
}).
Execute(name, filepath.Join(fixtures, "containers", "tomcat_javax"))

Expect(err).NotTo(HaveOccurred(), logs.String)

// Verify embedded Cloud Foundry-optimized Tomcat configuration was installed
Expect(logs.String()).To(ContainSubstring("Installing Cloud Foundry-optimized Tomcat configuration defaults"))
Expect(logs.String()).To(ContainSubstring("Dynamic port binding (${http.port} from $PORT)"))
Expect(logs.String()).To(ContainSubstring("HTTP/2 support enabled"))
Expect(logs.String()).To(ContainSubstring("RemoteIpValve for X-Forwarded-* headers"))
Expect(logs.String()).To(ContainSubstring("CloudFoundryAccessLoggingValve with vcap_request_id"))
Expect(logs.String()).To(ContainSubstring("Stdout logging via CloudFoundryConsoleHandler"))

Eventually(deployment).Should(matchers.Serve(ContainSubstring("OK")))

// Verify runtime logs contain CloudFoundry-specific Tomcat features
// Use Eventually to wait for logs to be flushed, as they may not appear immediately

// Check for HTTP/2 support in runtime logs (Tomcat startup messages)
// These should appear quickly during Tomcat startup
Eventually(func() string {
logs, _ := deployment.RuntimeLogs()
return logs
}, "10s", "1s").Should(Or(
ContainSubstring("Http11NioProtocol"),
ContainSubstring("Starting ProtocolHandler"),
ContainSubstring("HTTP/1.1"),
))
})

it("successfully deploys and runs with Java 11 (Jakarta EE)", func() {
deployment, logs, err := platform.Deploy.
WithEnv(map[string]string{
Expand Down Expand Up @@ -140,6 +175,50 @@ func testTomcat(platform switchblade.Platform, fixtures string) func(*testing.T,
Eventually(deployment).Should(matchers.Serve(ContainSubstring("OK")))
})

it("deploys with Java 11 (Tomcat 9 + javax.servlet)", func() {
deployment, logs, err := platform.Deploy.
WithEnv(map[string]string{
"BP_JAVA_VERSION": "11",
"JBP_CONFIG_TOMCAT": "{tomcat: { version: \"9.+\" }",
}).
Execute(name, filepath.Join(fixtures, "containers", "tomcat_javax"))
Expect(err).NotTo(HaveOccurred(), logs.String)

Expect(logs.String()).To(ContainSubstring("Installing OpenJDK 11."))
Expect(logs.String()).To(ContainSubstring("Tomcat 9"))
Eventually(deployment).Should(matchers.Serve(ContainSubstring("OK")))
})

it("deploys with default Java (Tomcat 9 + javax.servlet)", func() {
deployment, logs, err := platform.Deploy.
WithEnv(map[string]string{
"JBP_CONFIG_TOMCAT": "{ tomcat: { version: 9.+ } }",
}).
Execute(name, filepath.Join(fixtures, "containers", "tomcat_javax"))
Expect(err).NotTo(HaveOccurred(), logs.String)

Expect(logs.String()).To(ContainSubstring("Installing OpenJDK 17."))
Expect(logs.String()).To(ContainSubstring("Tomcat 9"))
Eventually(deployment).Should(matchers.Serve(ContainSubstring("OK")))
})

it("fails staging with a compatibility error for Tomcat 10 with Java 8 (javax)", func() {
if settings.Platform == "docker" {
t.Skip("Tomcat 10 + Java 8 compatibility enforcement is only guaranteed on CF platform")
}

_, logs, err := platform.Deploy.
WithEnv(map[string]string{
"BP_JAVA_VERSION": "8",
"JBP_CONFIG_TOMCAT": "{ tomcat: { version: \"10.+\" } }",
}).
Execute(name, filepath.Join(fixtures, "containers", "tomcat_javax"))

// Now we expect staging to fail
Expect(err).To(HaveOccurred())
Expect(logs.String()).To(ContainSubstring("Tomcat 10.x requires Java 11+, but Java 8 detected"))
})

it("deploys with Java 11 (Tomcat 10 + jakarta.servlet)", func() {
deployment, logs, err := platform.Deploy.
WithEnv(map[string]string{
Expand Down
73 changes: 60 additions & 13 deletions src/java/containers/tomcat.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/cloudfoundry/java-buildpack/src/java/common"
Expand Down Expand Up @@ -60,30 +61,40 @@ func (t *TomcatContainer) Supply() error {
if javaHome != "" {
javaMajorVersion, versionErr := common.DetermineJavaVersion(javaHome)
if versionErr == nil {
tomcatVersion := determineTomcatVersion(os.Getenv("JBP_CONFIG_TOMCAT"))
t.context.Log.Debug("Detected Java major version: %d", javaMajorVersion)

// Select Tomcat version pattern based on Java version
var versionPattern string
if javaMajorVersion >= 11 {
// Java 11+: Use Tomcat 10.x (Jakarta EE 9+)
versionPattern = "10.x"
t.context.Log.Info("Using Tomcat 10.x for Java %d", javaMajorVersion)
if tomcatVersion == "" {
if javaMajorVersion >= 11 {
// Java 11+: Use Tomcat 10.x (Jakarta EE 9+)
versionPattern = "10.x"
t.context.Log.Info("Using Tomcat 10.x for Java %d", javaMajorVersion)
} else {
// Java 8-10: Use Tomcat 9.x (Java EE 8)
versionPattern = "9.x"
t.context.Log.Info("Using Tomcat 9.x for Java %d", javaMajorVersion)
}
} else {
// Java 8-10: Use Tomcat 9.x (Java EE 8)
versionPattern = "9.x"
t.context.Log.Info("Using Tomcat 9.x for Java %d", javaMajorVersion)
versionPattern = tomcatVersion
t.context.Log.Info("Using Tomcat %s for Java %d", versionPattern, javaMajorVersion)
}

if strings.HasPrefix(versionPattern, "10.") && javaMajorVersion < 11 {
t.context.Log.Warning("Tomcat %s requires Java 11+, but Java %d detected. Tomcat may fail to start.", versionPattern, javaMajorVersion)
}

// Resolve the version pattern to actual version using libbuildpack
allVersions := t.context.Manifest.AllDependencyVersions("tomcat")
resolvedVersion, err := libbuildpack.FindMatchingVersion(versionPattern, allVersions)
if err == nil {
dep.Name = "tomcat"
dep.Version = resolvedVersion
t.context.Log.Debug("Resolved Tomcat version pattern '%s' to %s", versionPattern, resolvedVersion)
} else {
t.context.Log.Warning("Unable to resolve Tomcat version pattern '%s': %s", versionPattern, err.Error())
if err != nil {
return fmt.Errorf("tomcat version resolution error for pattern %q: %w", versionPattern, err)
}

dep.Name = "tomcat"
dep.Version = resolvedVersion
t.context.Log.Debug("Resolved Tomcat version pattern '%s' to %s", versionPattern, resolvedVersion)
} else {
t.context.Log.Warning("Unable to determine Java version: %s", versionErr.Error())
}
Expand Down Expand Up @@ -442,6 +453,42 @@ func getKeys(m map[string]string) []string {
return keys
}

// DetermineTomcatVersion is an exported wrapper around determineTomcatVersion.
// It exists primarily to allow unit tests in the containers_test package to
// verify Tomcat version parsing behavior without changing production semantics.
func DetermineTomcatVersion(raw string) string {
return determineTomcatVersion(raw)
}

// determineTomcatVersion determines the version of the tomcat
// based on the JBP_CONFIG_TOMCAT field from manifest.
// It looks for a tomcat block with a version of the form "<major>.+" (e.g. "9.+", "10.+").
// Returns "<major>.x" (e.g. "9.x", "10.x") so libbuildpack can resolve it,
func determineTomcatVersion(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}

re := regexp.MustCompile(`(?i)tomcat\s*:\s*\{[\s\S]*?version\s*:\s*["']?([\d.]+\.\+)`)
match := re.FindStringSubmatch(raw)
if len(match) < 2 {
return ""
}

pattern := match[1] // e.g. "9.+", "10.+", "10.23.+"

// If it's just "<major>.+" (no additional dot), convert to "<major>.x"
if !strings.Contains(strings.TrimSuffix(pattern, ".+"), ".") {
// "9.+" -> "9.x"
major := strings.TrimSuffix(pattern, ".+")
return major + ".x"
}

// Otherwise, it's something like "10.23.+": pass it through unchanged
return pattern
}

// isAccessLoggingEnabled checks if access logging is enabled in configuration
// Returns: "true" or "false" as a string (for use in JAVA_OPTS)
// Default: "false" (disabled, matching Ruby buildpack behavior)
Expand Down
31 changes: 31 additions & 0 deletions src/java/containers/tomcat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,35 @@ var _ = Describe("Tomcat Container", func() {
Expect(contentStr).To(ContainSubstring("org.apache.catalina.realm.UserDatabaseRealm"))
})
})

Describe("determineTomcatVersion", func() {
It("returns empty string when JBP_CONFIG_TOMCAT is empty", func() {
v := containers.DetermineTomcatVersion("")
Expect(v).To(Equal(""))
})

It("returns 9.x for tomcat version 9.+", func() {
raw := `{ tomcat: { version: "9.+" } }`
v := containers.DetermineTomcatVersion(raw)
Expect(v).To(Equal("9.x"))
})

It("returns 10.x for tomcat version 10.+", func() {
raw := `{ tomcat: { version: "10.+" } }`
v := containers.DetermineTomcatVersion(raw)
Expect(v).To(Equal("10.x"))
})

It("returns 10.23.+ for tomcat version 10.23.+", func() {
raw := `{ tomcat: { version: "10.23.+" } }`
v := containers.DetermineTomcatVersion(raw)
Expect(v).To(Equal("10.23.+"))
})

It("returns empty string when only access logging is configured", func() {
raw := `{access_logging_support: {access_logging: enabled}}`
v := containers.DetermineTomcatVersion(raw)
Expect(v).To(Equal(""))
})
})
})