Skip to content
79 changes: 7 additions & 72 deletions src/java/containers/groovy_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,34 +77,6 @@ func (g *GroovyUtils) IsBeans(filePath string) bool {
return beansPattern.Match(content)
}

// HasMainMethod checks if a Groovy file contains a static void main() method
func HasMainMethod(filePath string) (bool, error) {
content, err := os.ReadFile(filePath)
if err != nil {
return false, err
}
return mainMethodPattern.Match(content), nil
}

// IsPOGO checks if a Groovy file is a Plain Old Groovy Object (contains a class definition)
// POGOs are NOT standalone runnable scripts - they need to be instantiated
func IsPOGO(filePath string) (bool, error) {
content, err := os.ReadFile(filePath)
if err != nil {
return false, err
}
return pogoPattern.Match(content), nil
}

// HasShebang checks if a Groovy file has a shebang line (#!/...)
func HasShebang(filePath string) (bool, error) {
content, err := os.ReadFile(filePath)
if err != nil {
return false, err
}
return shebangPattern.Match(content), nil
}

// isValidGroovyFile checks if a file is a valid, readable Groovy script
// Filters out binary files, empty files, and files with invalid content
func isValidGroovyFile(filePath string) bool {
Expand Down Expand Up @@ -149,60 +121,23 @@ func isPartOfUTF8Sequence(content []byte, i int) bool {
return false
}

// FindMainGroovyScript determines which Groovy script should be executed
// Following Ruby buildpack logic:
// 1. Files with static void main() method
// 2. Non-POGO files (simple scripts without class definitions)
// 3. Files with shebang
// Returns the single candidate if exactly one matches, empty string otherwise
// FindMainGroovyScript determines which Groovy script should be executed.
// Following Ruby buildpack logic, a file is a candidate if it has a static
// void main() method, is not a POGO, or has a shebang.
// Returns the single candidate if exactly one matches, empty string otherwise.
func FindMainGroovyScript(scripts []string) (string, error) {
g := &GroovyUtils{}
candidates := make(map[string]bool)

// Filter out invalid files first
validScripts := make([]string, 0, len(scripts))
for _, script := range scripts {
if isValidGroovyFile(script) {
validScripts = append(validScripts, script)
}
}

// Check for main method
for _, script := range validScripts {
hasMain, err := HasMainMethod(script)
if err != nil {
// Skip files that can't be read (like binary files)
continue
}
if hasMain {
candidates[script] = true
}
}

// Check for non-POGOs (simple scripts)
for _, script := range validScripts {
isPOGO, err := IsPOGO(script)
if err != nil {
// Skip files that can't be read
continue
}
if !isPOGO {
candidates[script] = true
}
}

// Check for shebang
for _, script := range validScripts {
hasShebang, err := HasShebang(script)
if err != nil {
// Skip files that can't be read
if !isValidGroovyFile(script) {
continue
}
if hasShebang {
if g.HasMainMethod(script) || !g.IsPOGO(script) || g.HasShebang(script) {
candidates[script] = true
}
}

// Return the candidate if exactly one matches
if len(candidates) == 1 {
for script := range candidates {
return script, nil
Expand Down
186 changes: 91 additions & 95 deletions src/java/containers/groovy_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,98 +10,108 @@ import (
"github.com/cloudfoundry/java-buildpack/src/java/containers"
)

var _ = Describe("HasMainMethod", func() {
DescribeTable("detecting main method in Groovy files",
func(content string, expected bool) {
tmpFile, err := os.CreateTemp("", "test-*.groovy")
Expect(err).NotTo(HaveOccurred())
defer os.Remove(tmpFile.Name())
// groovyFile writes content to a temp file and returns its path.
// The file is removed when the current spec ends.
func groovyFile(content string) string {
tmpFile, err := os.CreateTemp("", "test-*.groovy")
Expect(err).NotTo(HaveOccurred())
DeferCleanup(os.Remove, tmpFile.Name())
_, err = tmpFile.WriteString(content)
Expect(err).NotTo(HaveOccurred())
Expect(tmpFile.Close()).To(Succeed())
return tmpFile.Name()
}

var _ = Describe("GroovyUtils", func() {
var g *containers.GroovyUtils

_, err = tmpFile.WriteString(content)
Expect(err).NotTo(HaveOccurred())
tmpFile.Close()
BeforeEach(func() {
g = &containers.GroovyUtils{}
})

result, err := containers.HasMainMethod(tmpFile.Name())
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal(expected))
},
Entry("has static void main", `class MyApp {
Describe("HasMainMethod", func() {
DescribeTable("detecting main method in Groovy files",
func(content string, expected bool) {
Expect(g.HasMainMethod(groovyFile(content))).To(Equal(expected))
},
Entry("has static void main", `class MyApp {
static void main(String[] args) {
println "Hello"
}
}`, true),
Entry("has static void main with whitespace variations", `class MyApp {
Entry("has static void main with extra whitespace", `class MyApp {
static void main ( String[] args ) {
println "Hello"
}
}`, true),
Entry("no main method", `class Alpha {
Entry("no main method", `class Alpha {
}`, false),
Entry("simple script no main", `println 'Hello World'`, false),
Entry("instance method not static main", `class Test {
Entry("simple script no main", `println 'Hello World'`, false),
Entry("instance method not static main", `class Test {
void main() {
println "Not static"
}
}`, false),
)
})

var _ = Describe("IsPOGO", func() {
DescribeTable("detecting Plain Old Groovy Objects",
func(content string, expected bool) {
tmpFile, err := os.CreateTemp("", "test-*.groovy")
Expect(err).NotTo(HaveOccurred())
defer os.Remove(tmpFile.Name())
)

_, err = tmpFile.WriteString(content)
Expect(err).NotTo(HaveOccurred())
tmpFile.Close()
It("returns false for an unreadable file", func() {
Expect(g.HasMainMethod("/nonexistent/file.groovy")).To(BeFalse())
})
})

result, err := containers.IsPOGO(tmpFile.Name())
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal(expected))
},
Entry("simple class definition", `class Alpha {
Describe("IsPOGO", func() {
DescribeTable("detecting Plain Old Groovy Objects",
func(content string, expected bool) {
Expect(g.IsPOGO(groovyFile(content))).To(Equal(expected))
},
Entry("simple class definition", `class Alpha {
}`, true),
Entry("class with inheritance", `class MyApp extends BaseApp {
Entry("class with inheritance", `class MyApp extends BaseApp {
void run() {}
}`, true),
Entry("simple script no class", `println 'Hello World'`, false),
Entry("script with variables no class", `def name = "World"
Entry("simple script no class", `println 'Hello World'`, false),
Entry("script with variables no class", `def name = "World"
println "Hello $name"`, false),
Entry("class keyword in comment", `// This is not a class
Entry("class keyword in comment", `// This is not a class
println 'Hello'`, false),
Entry("class keyword in string", `println "This mentions class but isn't one"`, false),
)
})

var _ = Describe("HasShebang", func() {
DescribeTable("detecting shebang in Groovy files",
func(content string, expected bool) {
tmpFile, err := os.CreateTemp("", "test-*.groovy")
Expect(err).NotTo(HaveOccurred())
defer os.Remove(tmpFile.Name())
Entry("class keyword in string", `println "This mentions class but isn't one"`, false),
)

_, err = tmpFile.WriteString(content)
Expect(err).NotTo(HaveOccurred())
tmpFile.Close()
It("returns false for an unreadable file", func() {
Expect(g.IsPOGO("/nonexistent/file.groovy")).To(BeFalse())
})
})

result, err := containers.HasShebang(tmpFile.Name())
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal(expected))
},
Entry("has shebang", `#!/usr/bin/env groovy
println 'Hello World'`, true),
Entry("has groovy shebang", `#!/usr/bin/groovy
println 'Hello'`, true),
Entry("no shebang", `class Alpha {
Describe("HasShebang", func() {
DescribeTable("detecting shebang in Groovy files",
func(content string, expected bool) {
Expect(g.HasShebang(groovyFile(content))).To(Equal(expected))
},
Entry("has shebang", "#!/usr/bin/env groovy\nprintln 'Hello World'", true),
Entry("has groovy shebang", "#!/usr/bin/groovy\nprintln 'Hello'", true),
Entry("no shebang", `class Alpha {
}`, false),
Entry("shebang not at start", `
#!/usr/bin/env groovy
Entry("shebang not at start", "\n#!/usr/bin/env groovy\nprintln 'Hello'", false),
Entry("comment mentioning shebang", `// Use #!/usr/bin/env groovy at the top
println 'Hello'`, false),
Entry("comment mentioning shebang", `// Use #!/usr/bin/env groovy at the top
println 'Hello'`, false),
)
)

It("returns false for an unreadable file", func() {
Expect(g.HasShebang("/nonexistent/file.groovy")).To(BeFalse())
})
})

Describe("IsBeans", func() {
DescribeTable("detecting beans-style configuration",
func(content string, expected bool) {
Expect(g.IsBeans(groovyFile(content))).To(Equal(expected))
},
Entry("has beans block", `beans {
bean(MyBean)
}`, true),
Entry("no beans block", `class Alpha {}`, false),
)
})
})

var _ = Describe("FindMainGroovyScript", func() {
Expand All @@ -117,32 +127,24 @@ var _ = Describe("FindMainGroovyScript", func() {
os.RemoveAll(tmpDir)
})

writeFile := func(name, content string) string {
path := filepath.Join(tmpDir, name)
Expect(os.WriteFile(path, []byte(content), 0644)).To(Succeed())
return path
}

Context("with various Groovy script types", func() {
var pogoFile, nonPogoFile, mainMethodFile, shebangFile string

BeforeEach(func() {
var err error

pogoFile = filepath.Join(tmpDir, "Alpha.groovy")
err = os.WriteFile(pogoFile, []byte("class Alpha {}"), 0644)
Expect(err).NotTo(HaveOccurred())

nonPogoFile = filepath.Join(tmpDir, "Application.groovy")
err = os.WriteFile(nonPogoFile, []byte("println 'Hello World'"), 0644)
Expect(err).NotTo(HaveOccurred())

mainMethodFile = filepath.Join(tmpDir, "Main.groovy")
mainContent := `class Main {
pogoFile = writeFile("Alpha.groovy", "class Alpha {}")
nonPogoFile = writeFile("Application.groovy", "println 'Hello World'")
mainMethodFile = writeFile("Main.groovy", `class Main {
static void main(String[] args) {
println "Main"
}
}`
err = os.WriteFile(mainMethodFile, []byte(mainContent), 0644)
Expect(err).NotTo(HaveOccurred())

shebangFile = filepath.Join(tmpDir, "Script.groovy")
err = os.WriteFile(shebangFile, []byte("#!/usr/bin/env groovy\nprintln 'Script'"), 0644)
Expect(err).NotTo(HaveOccurred())
}`)
shebangFile = writeFile("Script.groovy", "#!/usr/bin/env groovy\nprintln 'Script'")
})

DescribeTable("finding the main Groovy script",
Expand Down Expand Up @@ -176,17 +178,11 @@ var _ = Describe("FindMainGroovyScript", func() {
})

Context("with invalid files", func() {
It("should skip invalid files and select valid ones", func() {
invalidFile := filepath.Join(tmpDir, "invalid.groovy")
err := os.WriteFile(invalidFile, []byte{0xff, 0xfe}, 0644)
Expect(err).NotTo(HaveOccurred())

validFile := filepath.Join(tmpDir, "valid.groovy")
err = os.WriteFile(validFile, []byte("println 'Hello'"), 0644)
Expect(err).NotTo(HaveOccurred())
It("skips invalid files and selects the valid one", func() {
invalidFile := writeFile("invalid.groovy", string([]byte{0xff, 0xfe}))
validFile := writeFile("valid.groovy", "println 'Hello'")

scripts := []string{invalidFile, validFile}
result, err := containers.FindMainGroovyScript(scripts)
result, err := containers.FindMainGroovyScript([]string{invalidFile, validFile})
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal(validFile))
})
Expand Down
Loading