Skip to content
Merged
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
2 changes: 2 additions & 0 deletions cli/azd/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ resource.syso
versioninfo.json
azd.sln

**/target

56 changes: 56 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,20 @@ 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",
DatabaseDeps: []DatabaseDep{
DbMySql,
DbPostgres,
},
},
{
Language: Java,
Path: "java-multimodules/library",
DetectionRule: "Inferred by presence of: pom.xml",
},
{
Language: JavaScript,
Path: "javascript",
Expand Down Expand Up @@ -111,6 +125,20 @@ 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",
DatabaseDeps: []DatabaseDep{
DbMySql,
DbPostgres,
},
},
{
Language: Java,
Path: "java-multimodules/library",
DetectionRule: "Inferred by presence of: pom.xml",
},
},
},
{
Expand All @@ -130,6 +158,20 @@ 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",
DatabaseDeps: []DatabaseDep{
DbMySql,
DbPostgres,
},
},
{
Language: Java,
Path: "java-multimodules/library",
DetectionRule: "Inferred by presence of: pom.xml",
},
},
},
{
Expand All @@ -152,6 +194,20 @@ 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",
DatabaseDeps: []DatabaseDep{
DbMySql,
DbPostgres,
},
},
{
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, err
}

var project mavenProject
if err := xml.Unmarshal(bytes, &project); err != nil {
return nil, fmt.Errorf("parsing xml: %w", err)
}

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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>application</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>application</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>library</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.example.multimodule.application;

import com.example.multimodule.service.MyService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication(scanBasePackages = "com.example.multimodule")
@RestController
public class DemoApplication {

private final MyService myService;

public DemoApplication(MyService myService) {
this.myService = myService;
}

@GetMapping("/")
public String home() {
return myService.message();
}

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
service.message=Hello, World
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.multimodule.application;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.example.multimodule.service.MyService;

@SpringBootTest
public class DemoApplicationTest {

@Autowired
private MyService myService;

@Test
public void contextLoads() {
assertThat(myService.message()).isNotNull();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>library</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>library</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.multimodule.service;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Service;

@Service
@EnableConfigurationProperties(ServiceProperties.class)
public class MyService {

private final ServiceProperties serviceProperties;

public MyService(ServiceProperties serviceProperties) {
this.serviceProperties = serviceProperties;
}

public String message() {
return this.serviceProperties.getMessage();
}
}
Loading