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

Easy Init: Enhance the Java analyzer #4640

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
70 changes: 66 additions & 4 deletions cli/azd/internal/appdetect/appdetect.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,16 @@ const (
PyFlask Dependency = "flask"
PyDjango Dependency = "django"
PyFastApi Dependency = "fastapi"

SpringFrontend Dependency = "springFrontend"
)

var WebUIFrameworks = map[Dependency]struct{}{
JsReact: {},
JsAngular: {},
JsJQuery: {},
JsVite: {},
JsReact: {},
JsAngular: {},
JsJQuery: {},
JsVite: {},
SpringFrontend: {},
}

func (f Dependency) Language() Language {
Expand Down Expand Up @@ -112,6 +115,7 @@ const (
DbMySql DatabaseDep = "mysql"
DbSqlServer DatabaseDep = "sqlserver"
DbRedis DatabaseDep = "redis"
DbCosmos DatabaseDep = "cosmos"
)

func (db DatabaseDep) Display() string {
Expand All @@ -126,11 +130,60 @@ func (db DatabaseDep) Display() string {
return "SQL Server"
case DbRedis:
return "Redis"
case DbCosmos:
return "Cosmos DB"
}

return ""
}

//type AzureDep string

type AzureDep interface {
ResourceDisplay() string
}

type AzureDepServiceBus struct {
Queues []string
IsJms bool
}

func (a AzureDepServiceBus) ResourceDisplay() string {
return "Azure Service Bus"
}

type AzureDepEventHubs struct {
EventHubsNamePropertyMap map[string]string
UseKafka bool
SpringBootVersion string
}

func (a AzureDepEventHubs) ResourceDisplay() string {
return "Azure Event Hubs"
}

type AzureDepStorageAccount struct {
ContainerNamePropertyMap map[string]string
}

func (a AzureDepStorageAccount) ResourceDisplay() string {
return "Azure Storage Account"
}

type Metadata struct {
ApplicationName string
DatabaseNameInPropertySpringDatasourceUrl map[DatabaseDep]string
ContainsDependencySpringCloudAzureStarter bool
ContainsDependencySpringCloudAzureStarterJdbcPostgresql bool
ContainsDependencySpringCloudAzureStarterJdbcMysql bool
ContainsDependencySpringCloudEurekaServer bool
ContainsDependencySpringCloudEurekaClient bool
ContainsDependencySpringCloudConfigServer bool
ContainsDependencySpringCloudConfigClient bool
}

const UnknownSpringBootVersion string = "unknownSpringBootVersion"

type Project struct {
// The language associated with the project.
Language Language
Expand All @@ -141,9 +194,18 @@ type Project struct {
// Experimental: Database dependencies inferred through heuristics while scanning dependencies in the project.
DatabaseDeps []DatabaseDep

// Experimental: Azure dependencies inferred through heuristics while scanning dependencies in the project.
AzureDeps []AzureDep

// Experimental: Metadata inferred through heuristics while scanning the project.
Metadata Metadata

// The path to the project directory.
Path string

// Options for the project.
Options map[string]interface{}

// A short description of the detection rule applied.
DetectionRule string

Expand Down
152 changes: 53 additions & 99 deletions cli/azd/internal/appdetect/java.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,142 +2,96 @@ package appdetect

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

"github.com/azure/azure-dev/cli/azd/internal/tracing"
"github.com/azure/azure-dev/cli/azd/internal/tracing/fields"
)

type javaDetector struct {
rootProjects []mavenProject
parentPoms []pom
mavenWrapperPaths []mavenWrapper
}

type mavenWrapper struct {
posixPath string
winPath string
}

// JavaProjectOptionMavenParentPath The parent module path of the maven multi-module project
const JavaProjectOptionMavenParentPath = "parentPath"

// JavaProjectOptionPosixMavenWrapperPath The path to the maven wrapper script for POSIX systems
const JavaProjectOptionPosixMavenWrapperPath = "posixMavenWrapperPath"

// JavaProjectOptionWinMavenWrapperPath The path to the maven wrapper script for Windows systems
const JavaProjectOptionWinMavenWrapperPath = "winMavenWrapperPath"

func (jd *javaDetector) Language() Language {
return Java
}

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" {
tracing.SetUsageAttributes(fields.AppInitJavaDetect.String("start"))
pomFile := filepath.Join(path, entry.Name())
project, err := readMavenProject(pomFile)
mavenProject, err := toMavenProject(pomFile)
if err != nil {
return nil, fmt.Errorf("error reading pom.xml: %w", err)
log.Printf("Please edit azure.yaml manually to satisfy your requirement. azd can not help you "+
"to that by detect your java project because error happened when reading pom.xml: %s. ", err)
return nil, nil
}

if len(project.Modules) > 0 {
if len(mavenProject.pom.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)
jd.parentPoms = append(jd.parentPoms, mavenProject.pom)
jd.mavenWrapperPaths = append(jd.mavenWrapperPaths, mavenWrapper{
posixPath: detectMavenWrapper(path, "mvnw"),
winPath: detectMavenWrapper(path, "mvnw.cmd"),
})
return nil, nil
}

var currentRoot *mavenProject
for _, rootProject := range jd.rootProjects {
var parentPom *pom
var currentWrapper mavenWrapper
for i, parentPomItem := range jd.parentPoms {
// 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
if inRoot := strings.HasPrefix(pomFile, parentPomItem.path); inRoot {
parentPom = &parentPomItem
currentWrapper = jd.mavenWrapperPaths[i]
}
}

_ = currentRoot // use currentRoot here in the analysis
result, err := detectDependencies(project, &Project{
project := Project{
Language: Java,
Path: path,
DetectionRule: "Inferred by presence of: pom.xml",
})
if err != nil {
return nil, fmt.Errorf("detecting dependencies: %w", err)
}
detectAzureDependenciesByAnalyzingSpringBootProject(parentPom, &mavenProject.pom, &project)
if parentPom != nil {
project.Options = map[string]interface{}{
JavaProjectOptionMavenParentPath: parentPom.path,
JavaProjectOptionPosixMavenWrapperPath: currentWrapper.posixPath,
JavaProjectOptionWinMavenWrapperPath: currentWrapper.winPath,
}
}

return result, nil
tracing.SetUsageAttributes(fields.AppInitJavaDetect.String("finish"))
return &project, 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))
})
func detectMavenWrapper(path string, executable string) string {
wrapperPath := filepath.Join(path, executable)
if fileExists(wrapperPath) {
return wrapperPath
}

return project, nil
return ""
}
Loading
Loading