Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more java related analysis code #4437

Merged
merged 9 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
40 changes: 40 additions & 0 deletions cli/azd/internal/appdetect/appdetect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ func TestDetect(t *testing.T) {
Path: "java",
saragluna marked this conversation as resolved.
Show resolved Hide resolved
DetectionRule: "Inferred by presence of: pom.xml",
},
{
Language: Java,
Path: "java-multimodules/application",
DetectionRule: "Inferred by presence of: pom.xml",
},
{
Language: Java,
Path: "java-multimodules/library",
DetectionRule: "Inferred by presence of: pom.xml",
},
{
Language: JavaScript,
Path: "javascript",
Expand Down Expand Up @@ -111,6 +121,16 @@ func TestDetect(t *testing.T) {
Path: "java",
DetectionRule: "Inferred by presence of: pom.xml",
},
{
Language: Java,
Path: "java-multimodules/application",
DetectionRule: "Inferred by presence of: pom.xml",
},
{
Language: Java,
Path: "java-multimodules/library",
DetectionRule: "Inferred by presence of: pom.xml",
},
},
},
{
Expand All @@ -130,6 +150,16 @@ func TestDetect(t *testing.T) {
Path: "java",
DetectionRule: "Inferred by presence of: pom.xml",
},
{
Language: Java,
Path: "java-multimodules/application",
DetectionRule: "Inferred by presence of: pom.xml",
},
{
Language: Java,
Path: "java-multimodules/library",
DetectionRule: "Inferred by presence of: pom.xml",
},
},
},
{
Expand All @@ -152,6 +182,16 @@ func TestDetect(t *testing.T) {
Path: "java",
DetectionRule: "Inferred by presence of: pom.xml",
},
{
Language: Java,
Path: "java-multimodules/application",
DetectionRule: "Inferred by presence of: pom.xml",
},
{
Language: Java,
Path: "java-multimodules/library",
DetectionRule: "Inferred by presence of: pom.xml",
},
{
Language: Python,
Path: "python",
Expand Down
121 changes: 118 additions & 3 deletions cli/azd/internal/appdetect/java.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ package appdetect

import (
"context"
"encoding/xml"
"fmt"
"io/fs"
"maps"
"os"
"path/filepath"
"slices"
"strings"
)

type javaDetector struct {
rootProjects []mavenProject
}

func (jd *javaDetector) Language() Language {
Expand All @@ -16,13 +23,121 @@ func (jd *javaDetector) Language() Language {
func (jd *javaDetector) DetectProject(ctx context.Context, path string, entries []fs.DirEntry) (*Project, error) {
for _, entry := range entries {
if strings.ToLower(entry.Name()) == "pom.xml" {
return &Project{
pomFile := filepath.Join(path, entry.Name())
project, err := readMavenProject(pomFile)
if err != nil {
return nil, fmt.Errorf("error reading pom.xml: %w", err)
}

if len(project.Modules) > 0 {
// This is a multi-module project, we will capture the analysis, but return nil
// to continue recursing
jd.rootProjects = append(jd.rootProjects, *project)
return nil, nil
}

var currentRoot *mavenProject
for _, rootProject := range jd.rootProjects {
// we can say that the project is in the root project if the path is under the project
if inRoot := strings.HasPrefix(pomFile, rootProject.path); inRoot {
currentRoot = &rootProject
}
}

_ = currentRoot // use currentRoot here in the analysis
result, err := detectDependencies(project, &Project{
Language: Java,
Path: path,
DetectionRule: "Inferred by presence of: " + entry.Name(),
}, nil
DetectionRule: "Inferred by presence of: pom.xml",
})
if err != nil {
return nil, fmt.Errorf("detecting dependencies: %w", err)
}

return result, nil
}
}

return nil, nil
}

// mavenProject represents the top-level structure of a Maven POM file.
type mavenProject struct {
XmlName xml.Name `xml:"project"`
Parent parent `xml:"parent"`
Modules []string `xml:"modules>module"` // Capture the modules
Dependencies []dependency `xml:"dependencies>dependency"`
DependencyManagement dependencyManagement `xml:"dependencyManagement"`
Build build `xml:"build"`
path string
}

// Parent represents the parent POM if this project is a module.
type parent struct {
GroupId string `xml:"groupId"`
ArtifactId string `xml:"artifactId"`
Version string `xml:"version"`
}

// Dependency represents a single Maven dependency.
type dependency struct {
GroupId string `xml:"groupId"`
ArtifactId string `xml:"artifactId"`
Version string `xml:"version"`
Scope string `xml:"scope,omitempty"`
}

// DependencyManagement includes a list of dependencies that are managed.
type dependencyManagement struct {
Dependencies []dependency `xml:"dependencies>dependency"`
}

// Build represents the build configuration which can contain plugins.
type build struct {
Plugins []plugin `xml:"plugins>plugin"`
}

// Plugin represents a build plugin.
type plugin struct {
GroupId string `xml:"groupId"`
ArtifactId string `xml:"artifactId"`
Version string `xml:"version"`
}

func readMavenProject(filePath string) (*mavenProject, error) {
bytes, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("error reading file: %w", err)
saragluna marked this conversation as resolved.
Show resolved Hide resolved
}

var project mavenProject
if err := xml.Unmarshal(bytes, &project); err != nil {
return nil, fmt.Errorf("error parsing XML: %w", err)
saragluna marked this conversation as resolved.
Show resolved Hide resolved
}

project.path = filepath.Dir(filePath)

return &project, nil
}

func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, error) {
databaseDepMap := map[DatabaseDep]struct{}{}
for _, dep := range mavenProject.Dependencies {
if dep.GroupId == "com.mysql" && dep.ArtifactId == "mysql-connector-j" {
databaseDepMap[DbMySql] = struct{}{}
}

if dep.GroupId == "org.postgresql" && dep.ArtifactId == "postgresql" {
databaseDepMap[DbPostgres] = struct{}{}
}
}

if len(databaseDepMap) > 0 {
project.DatabaseDeps = slices.SortedFunc(maps.Keys(databaseDepMap),
func(a, b DatabaseDep) int {
return strings.Compare(string(a), string(b))
})
}

return project, nil
}
154 changes: 154 additions & 0 deletions cli/azd/internal/appdetect/java_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package appdetect

import (
"context"
"io/fs"
"os"
"path/filepath"
"testing"
)

func TestJavaDetector_Language(t *testing.T) {
jd := &javaDetector{}
if jd.Language() != Java {
t.Errorf("expected language to be Java, got %v", jd.Language())
}
}

func TestJavaDetector_DetectProject_WithPomXml(t *testing.T) {
saragluna marked this conversation as resolved.
Show resolved Hide resolved
jd := &javaDetector{}
entries := []fs.DirEntry{
mockDirEntry{name: "pom.xml"},
}
tempDir := t.TempDir()
err := os.WriteFile(filepath.Join(tempDir, "pom.xml"), []byte(`
<project>

</project>`), 0600)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
project, err := jd.DetectProject(context.Background(), tempDir, entries)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if project == nil {
t.Fatal("expected project to be detected, got nil")
}
}

func TestJavaDetector_DetectProject_WithoutPomXml(t *testing.T) {
saragluna marked this conversation as resolved.
Show resolved Hide resolved
jd := &javaDetector{}
entries := []fs.DirEntry{
mockDirEntry{name: "not_pom.xml"},
}
project, err := jd.DetectProject(context.Background(), ".", entries)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if project != nil {
t.Fatalf("expected no project to be detected, got %v", project)
}
}

func TestJavaDetector_DetectProject_WithSubmodules(t *testing.T) {
// Set up a temporary directory with a root pom.xml and submodule poms
tempDir := t.TempDir()
err := os.WriteFile(filepath.Join(tempDir, "pom.xml"), []byte(`
<project>
<modules>
<module>submodule</module>
</modules>
</project>`), 0600)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
err = os.Mkdir(filepath.Join(tempDir, "submodule"), 0755)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
err = os.WriteFile(filepath.Join(tempDir, "submodule", "pom.xml"), []byte(`
<project>
<dependencies>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
</dependencies>
</project>`), 0600)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

jd := &javaDetector{}
entries, err := os.ReadDir(tempDir)
if err != nil {
t.Fatalf("reading directory: %v", err)
}

project, err := jd.DetectProject(context.Background(), tempDir, entries)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if project != nil {
t.Fatalf("expected no project to be detected, got %v", project)
}
if len(jd.rootProjects) != 1 {
t.Fatalf("expected 1 root project, got %d", len(jd.rootProjects))
}

entries, err = os.ReadDir(filepath.Join(tempDir, "submodule"))
if err != nil {
t.Fatalf("reading directory: %v", err)
}
project, err = jd.DetectProject(context.Background(), filepath.Join(tempDir, "submodule"), entries)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if project == nil {
t.Fatalf("expected project to be detected, got nil")
}
}

func TestDetectDependencies_WithDatabaseDeps(t *testing.T) {
saragluna marked this conversation as resolved.
Show resolved Hide resolved
mavenProj := &mavenProject{
Dependencies: []dependency{
{GroupId: "com.mysql", ArtifactId: "mysql-connector-j"},
{GroupId: "org.postgresql", ArtifactId: "postgresql"},
},
}
project := &Project{}
project, err := detectDependencies(mavenProj, project)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(project.DatabaseDeps) != 2 {
t.Fatalf("expected 2 database dependencies, got %d", len(project.DatabaseDeps))
}
}

func TestDetectDependencies_WithoutDatabaseDeps(t *testing.T) {
mavenProj := &mavenProject{
Dependencies: []dependency{
{GroupId: "com.example", ArtifactId: "example-dependency"},
},
}
project := &Project{}
project, err := detectDependencies(mavenProj, project)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(project.DatabaseDeps) != 0 {
t.Fatalf("expected 0 database dependencies, got %d", len(project.DatabaseDeps))
}
}

// Mock implementation of fs.DirEntry for testing purposes
type mockDirEntry struct {
name string
}

func (m mockDirEntry) Name() string { return m.name }
func (m mockDirEntry) IsDir() bool { return false }
func (m mockDirEntry) Type() fs.FileMode { return 0 }
func (m mockDirEntry) Info() (fs.FileInfo, error) { return nil, nil }
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
plugins {
saragluna marked this conversation as resolved.
Show resolved Hide resolved
id 'org.springframework.boot' version '3.3.0'
id 'io.spring.dependency-management' version '1.1.5'
id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '17'
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation project(':library')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Loading
Loading