diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml
index a10ce775337..47d1607fd70 100644
--- a/cli/azd/.vscode/cspell.yaml
+++ b/cli/azd/.vscode/cspell.yaml
@@ -16,6 +16,7 @@ words:
- runcontext
- unmarshals
- usgovcloudapi
+ - jdbc
languageSettings:
- languageId: go
ignoreRegExpList:
diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go
index e6103d3cbd7..b35dc531000 100644
--- a/cli/azd/internal/appdetect/appdetect.go
+++ b/cli/azd/internal/appdetect/appdetect.go
@@ -14,6 +14,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/exec"
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
+ "github.com/azure/azure-dev/cli/azd/pkg/tools/maven"
"github.com/bmatcuk/doublestar/v4"
)
@@ -179,7 +180,9 @@ type projectDetector interface {
var allDetectors = []projectDetector{
// Order here determines precedence when two projects are in the same directory.
// This is unlikely to occur in practice, but reordering could help to break the tie in these cases.
- &javaDetector{},
+ &javaDetector{
+ mvnCli: maven.NewCli(exec.NewCommandRunner(nil)),
+ },
&dotNetAppHostDetector{
// TODO(ellismg): Remove ambient authority.
dotnetCli: dotnet.NewCli(exec.NewCommandRunner(nil)),
diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go
index fe6fec3ea65..5f83a6606fa 100644
--- a/cli/azd/internal/appdetect/java.go
+++ b/cli/azd/internal/appdetect/java.go
@@ -6,13 +6,15 @@ import (
"fmt"
"io/fs"
"maps"
- "os"
"path/filepath"
"slices"
"strings"
+
+ "github.com/azure/azure-dev/cli/azd/pkg/tools/maven"
)
type javaDetector struct {
+ mvnCli *maven.Cli
rootProjects []mavenProject
}
@@ -24,7 +26,7 @@ func (jd *javaDetector) DetectProject(ctx context.Context, path string, entries
for _, entry := range entries {
if strings.ToLower(entry.Name()) == "pom.xml" {
pomFile := filepath.Join(path, entry.Name())
- project, err := readMavenProject(pomFile)
+ project, err := readMavenProject(ctx, jd.mvnCli, pomFile)
if err != nil {
return nil, fmt.Errorf("error reading pom.xml: %w", err)
}
@@ -104,19 +106,16 @@ type plugin struct {
Version string `xml:"version"`
}
-func readMavenProject(filePath string) (*mavenProject, error) {
- bytes, err := os.ReadFile(filePath)
+func readMavenProject(ctx context.Context, mvnCli *maven.Cli, filePath string) (*mavenProject, error) {
+ effectivePom, err := mvnCli.EffectivePom(ctx, filePath)
if err != nil {
return nil, err
}
-
var project mavenProject
- if err := xml.Unmarshal(bytes, &project); err != nil {
+ if err := xml.Unmarshal([]byte(effectivePom), &project); err != nil {
return nil, fmt.Errorf("parsing xml: %w", err)
}
-
project.path = filepath.Dir(filePath)
-
return &project, nil
}
diff --git a/cli/azd/internal/appdetect/java_test.go b/cli/azd/internal/appdetect/java_test.go
new file mode 100644
index 00000000000..0d62858ae19
--- /dev/null
+++ b/cli/azd/internal/appdetect/java_test.go
@@ -0,0 +1,307 @@
+package appdetect
+
+import (
+ "context"
+ "log/slog"
+ "os"
+ osexec "os/exec"
+ "path/filepath"
+ "testing"
+
+ "github.com/azure/azure-dev/cli/azd/pkg/exec"
+ "github.com/azure/azure-dev/cli/azd/pkg/tools/maven"
+)
+
+func TestToMavenProject(t *testing.T) {
+ path, err := osexec.LookPath("java")
+ if err != nil {
+ t.Skip("Skip readMavenProject because java command doesn't exist.")
+ } else {
+ slog.Info("Java command found.", "path", path)
+ }
+ path, err = osexec.LookPath("mvn")
+ if err != nil {
+ t.Skip("Skip readMavenProject because mvn command doesn't exist.")
+ } else {
+ slog.Info("Java command found.", "path", path)
+ }
+ tests := []struct {
+ name string
+ testPoms []testPom
+ expected []dependency
+ }{
+ {
+ name: "Test with two dependencies",
+ testPoms: []testPom{
+ {
+ pomFilePath: "pom.xml",
+ pomContentString: `
+
+ 4.0.0
+ com.example
+ example-project
+ 1.0.0
+
+
+ org.springframework
+ spring-core
+ 5.3.8
+ compile
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+
+ `,
+ },
+ },
+ expected: []dependency{
+ {
+ GroupId: "org.springframework",
+ ArtifactId: "spring-core",
+ Version: "5.3.8",
+ Scope: "compile",
+ },
+ {
+ GroupId: "junit",
+ ArtifactId: "junit",
+ Version: "4.13.2",
+ Scope: "test",
+ },
+ },
+ },
+ {
+ name: "Test with no dependencies",
+ testPoms: []testPom{
+ {
+ pomFilePath: "pom.xml",
+ pomContentString: `
+
+ 4.0.0
+ com.example
+ example-project
+ 1.0.0
+
+
+
+ `,
+ },
+ },
+ expected: []dependency{},
+ },
+ {
+ name: "Test with one dependency which version is decided by dependencyManagement",
+ testPoms: []testPom{
+ {
+ pomFilePath: "pom.xml",
+ pomContentString: `
+
+ 4.0.0
+ com.example
+ example-project
+ 1.0.0
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ 3.0.0
+ pom
+ import
+
+
+
+
+ `,
+ },
+ },
+ expected: []dependency{
+ {
+ GroupId: "org.slf4j",
+ ArtifactId: "slf4j-api",
+ Version: "2.0.4",
+ Scope: "compile",
+ },
+ },
+ },
+ {
+ name: "Test with one dependency which version is decided by parent",
+ testPoms: []testPom{
+ {
+ pomFilePath: "pom.xml",
+ pomContentString: `
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.0.0
+
+
+ 4.0.0
+ com.example
+ example-project
+ 1.0.0
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+ `,
+ },
+ },
+ expected: []dependency{
+ {
+ GroupId: "org.slf4j",
+ ArtifactId: "slf4j-api",
+ Version: "2.0.4",
+ Scope: "compile",
+ },
+ },
+ },
+ {
+ name: "Test pom with multi modules",
+ testPoms: []testPom{
+ {
+ pomFilePath: "pom.xml",
+ pomContentString: `
+
+ 4.0.0
+ org.springframework
+ gs-multi-module
+ 0.1.0
+ pom
+
+ library
+ application
+
+
+ `,
+ },
+ {
+ pomFilePath: filepath.Join("application", "pom.xml"),
+ pomContentString: `
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.0
+
+
+ com.example
+ application
+ 0.0.1-SNAPSHOT
+ application
+ Demo project for Spring Boot
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+ `,
+ },
+ {
+ pomFilePath: filepath.Join("library", "pom.xml"),
+ pomContentString: `
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.2
+
+
+ com.example
+ library
+ 0.0.1-SNAPSHOT
+ library
+ Demo project for Spring Boot
+
+
+ org.springframework.boot
+ spring-boot
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ `,
+ },
+ },
+ expected: []dependency{},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ workingDir, err := prepareTestPomFiles(tt.testPoms)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ testPom := tt.testPoms[0]
+ pomFilePath := filepath.Join(workingDir, testPom.pomFilePath)
+
+ mavenProject, err := readMavenProject(context.TODO(), maven.NewCli(exec.NewCommandRunner(nil)),
+ pomFilePath)
+ if err != nil {
+ t.Fatalf("readMavenProject failed: %v", err)
+ }
+
+ if len(mavenProject.Dependencies) != len(tt.expected) {
+ t.Fatalf("Expected: %d\nActual: %d", len(tt.expected), len(mavenProject.Dependencies))
+ }
+
+ for i, dep := range mavenProject.Dependencies {
+ if dep != tt.expected[i] {
+ t.Errorf("\nExpected: %s\nActual: %s", tt.expected[i], dep)
+ }
+ }
+ })
+ }
+}
+
+type testPom struct {
+ pomFilePath string
+ pomContentString string
+}
+
+func prepareTestPomFiles(testPoms []testPom) (string, error) {
+ tempDir, err := os.MkdirTemp("", "prepareTestPomFiles")
+ if err != nil {
+ return "", err
+ }
+ for _, testPom := range testPoms {
+ pomPath := filepath.Join(tempDir, testPom.pomFilePath)
+ err := os.MkdirAll(filepath.Dir(pomPath), 0755)
+ if err != nil {
+ return "", err
+ }
+ err = os.WriteFile(pomPath, []byte(testPom.pomContentString), 0600)
+ if err != nil {
+ return "", err
+ }
+ }
+ return tempDir, nil
+}
diff --git a/cli/azd/internal/appdetect/testdata/java/pom.xml b/cli/azd/internal/appdetect/testdata/java/pom.xml
index 09cb26061ae..1ff4713938a 100644
--- a/cli/azd/internal/appdetect/testdata/java/pom.xml
+++ b/cli/azd/internal/appdetect/testdata/java/pom.xml
@@ -2,7 +2,6 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
4.0.0
-
nothing
basic-pom
0.0.1-SNAPSHOT
diff --git a/cli/azd/pkg/tools/maven/maven.go b/cli/azd/pkg/tools/maven/maven.go
index 43d8d91db59..0b974d1a803 100644
--- a/cli/azd/pkg/tools/maven/maven.go
+++ b/cli/azd/pkg/tools/maven/maven.go
@@ -1,6 +1,7 @@
package maven
import (
+ "bufio"
"context"
"errors"
"fmt"
@@ -237,6 +238,51 @@ func (cli *Cli) GetProperty(ctx context.Context, propertyPath string, projectPat
return result, nil
}
+func (cli *Cli) EffectivePom(ctx context.Context, pomPath string) (string, error) {
+ mvnCmd, err := cli.mvnCmd()
+ if err != nil {
+ return "", err
+ }
+ pomDir := filepath.Dir(pomPath)
+ runArgs := exec.NewRunArgs(mvnCmd, "help:effective-pom", "-f", pomPath).WithCwd(pomDir)
+ result, err := cli.commandRunner.Run(ctx, runArgs)
+ if err != nil {
+ return "", fmt.Errorf("mvn help:effective-pom on project '%s' failed: %w", pomPath, err)
+ }
+ return getEffectivePomFromConsoleOutput(result.Stdout)
+}
+
+var projectStart = regexp.MustCompile(`^\s*\s*$`)
+
+func getEffectivePomFromConsoleOutput(consoleOutput string) (string, error) {
+ var builder strings.Builder
+ scanner := bufio.NewScanner(strings.NewReader(consoleOutput))
+ inProject := false
+
+ for scanner.Scan() {
+ line := scanner.Text()
+ if projectStart.MatchString(line) {
+ inProject = true
+ builder.Reset() // for a pom which contains submodule, the effective pom for root pom appears at last.
+ } else if projectEnd.MatchString(line) {
+ builder.WriteString(line)
+ inProject = false
+ }
+ if inProject {
+ builder.WriteString(line)
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return "", fmt.Errorf("failed to scan console output: %w", err)
+ }
+ result := builder.String()
+ if result == "" {
+ return "", fmt.Errorf("failed to get effective pom from console: empty content")
+ }
+ return result, nil
+}
+
func NewCli(commandRunner exec.CommandRunner) *Cli {
return &Cli{
commandRunner: commandRunner,