package frameworks
import (
"fmt"
"github.com/cloudfoundry/java-buildpack/src/java/common"
"os"
"strings"
"unicode"
)
// JavaOptsFramework implements custom JAVA_OPTS configuration
type JavaOptsFramework struct {
context *common.Context
}
// JavaOptsConfig represents the java_opts.yml configuration
type JavaOptsConfig struct {
FromEnvironment bool `yaml:"from_environment"`
JavaOpts []string `yaml:"java_opts"`
}
// NewJavaOptsFramework creates a new Java Opts framework instance
func NewJavaOptsFramework(ctx *common.Context) *JavaOptsFramework {
return &JavaOptsFramework{context: ctx}
}
// Detect returns a positive result if loadConfig() finds settings (universal framework for JAVA_OPTS configuration)
func (j *JavaOptsFramework) Detect() (string, error) {
// Check if there's any configuration to apply
config, err := j.loadConfig()
if err != nil {
// if detect "fails" Finalize() is not called so log parse failures as warning
j.context.Log.Warning("Failed to load java_opts config: %s", err.Error())
return "", nil
}
// Detect if there are any custom java_opts or if from_environment is enabled
if len(config.JavaOpts) > 0 || config.FromEnvironment {
return "Java Opts", nil
}
return "", nil
}
// Supply does nothing (no dependencies to install)
func (j *JavaOptsFramework) Supply() error {
// Java Opts framework only configures environment in finalize phase
return nil
}
// Finalize applies the JAVA_OPTS configuration
func (j *JavaOptsFramework) Finalize() error {
j.context.Log.BeginStep("Configuring Java Opts")
// Load configuration
config, err := j.loadConfig()
if err != nil {
j.context.Log.Warning("Failed to load java_opts config: %s", err.Error())
return nil // Don't fail the build
}
var configuredOpts []string
// Add configured java_opts from config file
if len(config.JavaOpts) > 0 {
j.context.Log.Info("Adding configured JAVA_OPTS: %v", config.JavaOpts)
configuredOpts = append(configuredOpts, config.JavaOpts...)
}
// Build the configured JAVA_OPTS value
// Escape each opt using Ruby buildpack's strategy: backslash-escape special characters
// This allows values with spaces to be preserved when passed through shell evaluation
var escapedOpts []string
for _, opt := range configuredOpts {
escapedOpts = append(escapedOpts, rubyStyleEscape(opt))
}
optsString := strings.Join(escapedOpts, " ")
// Write user-defined JAVA_OPTS to .opts file with priority 99 (Ruby buildpack line 82)
// This ensures user opts run LAST, allowing them to override framework defaults
//
// Handle from_environment setting (matching Ruby buildpack order):
// - If true: configured opts FIRST, then append $JAVA_OPTS (allows environment to override config)
// - If false: only use configured opts (ignore environment JAVA_OPTS)
//
// Ruby buildpack order (lines 39-44):
// configured.shellsplit.map {...}.each { |java_opt| @droplet.java_opts << java_opt }
// @droplet.java_opts << '$JAVA_OPTS' if from_environment?
var finalOpts string
if config.FromEnvironment {
// Add configured opts first, then environment JAVA_OPTS (Ruby order)
if optsString != "" {
finalOpts = fmt.Sprintf("%s $JAVA_OPTS", optsString)
} else {
// No configured opts, use only environment JAVA_OPTS
finalOpts = "$JAVA_OPTS"
}
} else {
// Ignore environment JAVA_OPTS, use only configured opts
finalOpts = optsString
}
// Write to .opts file (priority 99 = always last)
if finalOpts != "" {
if err := writeJavaOptsFile(j.context, 99, "user_java_opts", finalOpts); err != nil {
return fmt.Errorf("failed to write java_opts file: %w", err)
}
}
j.context.Log.Info("Configured user JAVA_OPTS for runtime (priority 99)")
return nil
}
// shellSplit splits a string like a shell would, respecting quotes
// Similar to Ruby's Shellwords.shellsplit
func shellSplit(input string) ([]string, error) {
var tokens []string
var current strings.Builder
var inSingleQuote, inDoubleQuote bool
var escaped bool
for _, r := range input {
// Handle escape sequences
if escaped {
current.WriteRune(r)
escaped = false
continue
}
if r == '\\' {
escaped = true
continue
}
// Handle quotes
if r == '\'' && !inDoubleQuote {
inSingleQuote = !inSingleQuote
continue
}
if r == '"' && !inSingleQuote {
inDoubleQuote = !inDoubleQuote
continue
}
// Handle spaces (word separators when not quoted)
if unicode.IsSpace(r) && !inSingleQuote && !inDoubleQuote {
if current.Len() > 0 {
tokens = append(tokens, current.String())
current.Reset()
}
continue
}
// Regular character
current.WriteRune(r)
}
// Add last token if exists
if current.Len() > 0 {
tokens = append(tokens, current.String())
}
// Check for unclosed quotes
if inSingleQuote || inDoubleQuote {
return nil, fmt.Errorf("unclosed quote in string: %s", input)
}
return tokens, nil
}
// rubyStyleEscape escapes a Java option exactly like the Ruby buildpack
//
// Ruby source: lib/java_buildpack/framework/java_opts.rb:40-41
//
// .map { |java_opt| /(?