Skip to content

Commit 1f8fca1

Browse files
authored
feat(java): add support for maven-metadata.xml files for remote snapshot repositories. (#6950)
1 parent 2d85a00 commit 1f8fca1

File tree

7 files changed

+231
-5
lines changed

7 files changed

+231
-5
lines changed
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package pom
2+
3+
type Metadata struct {
4+
GroupId string `xml:"groupId"`
5+
ArtifactId string `xml:"artifactId"`
6+
Versioning Versioning `xml:"versioning"`
7+
Version string `xml:"version"`
8+
}
9+
10+
type Versioning struct {
11+
SnapshotVersions []SnapshotVersion `xml:"snapshotVersions>snapshotVersion"`
12+
}
13+
14+
type SnapshotVersion struct {
15+
Extension string `xml:"extension"`
16+
Value string `xml:"value"`
17+
}

pkg/dependency/parser/java/pom/parse.go

+76-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
multierror "github.com/hashicorp/go-multierror"
1616
"github.com/samber/lo"
17+
"golang.org/x/exp/slices"
1718
"golang.org/x/net/html/charset"
1819
"golang.org/x/xerrors"
1920

@@ -48,6 +49,12 @@ func WithReleaseRemoteRepos(repos []string) option {
4849
}
4950
}
5051

52+
func WithSnapshotRemoteRepos(repos []string) option {
53+
return func(opts *options) {
54+
opts.snapshotRemoteRepos = repos
55+
}
56+
}
57+
5158
type Parser struct {
5259
logger *log.Logger
5360
rootPath string
@@ -648,7 +655,18 @@ func (p *Parser) fetchPOMFromRemoteRepositories(paths []string, snapshot bool) (
648655

649656
// try all remoteRepositories
650657
for _, repo := range remoteRepos {
651-
fetched, err := p.fetchPOMFromRemoteRepository(repo, paths)
658+
repoPaths := slices.Clone(paths) // Clone slice to avoid overwriting last element of `paths`
659+
if snapshot {
660+
pomFileName, err := p.fetchPomFileNameFromMavenMetadata(repo, repoPaths)
661+
if err != nil {
662+
return nil, xerrors.Errorf("fetch maven-metadata.xml error: %w", err)
663+
}
664+
// Use file name from `maven-metadata.xml` if it exists
665+
if pomFileName != "" {
666+
repoPaths[len(repoPaths)-1] = pomFileName
667+
}
668+
}
669+
fetched, err := p.fetchPOMFromRemoteRepository(repo, repoPaths)
652670
if err != nil {
653671
return nil, xerrors.Errorf("fetch repository error: %w", err)
654672
} else if fetched == nil {
@@ -659,7 +677,7 @@ func (p *Parser) fetchPOMFromRemoteRepositories(paths []string, snapshot bool) (
659677
return nil, xerrors.Errorf("the POM was not found in remote remoteRepositories")
660678
}
661679

662-
func (p *Parser) fetchPOMFromRemoteRepository(repo string, paths []string) (*pom, error) {
680+
func (p *Parser) remoteRepoRequest(repo string, paths []string) (*http.Request, error) {
663681
repoURL, err := url.Parse(repo)
664682
if err != nil {
665683
p.logger.Error("URL parse error", log.String("repo", repo))
@@ -670,7 +688,6 @@ func (p *Parser) fetchPOMFromRemoteRepository(repo string, paths []string) (*pom
670688
repoURL.Path = path.Join(paths...)
671689

672690
logger := p.logger.With(log.String("host", repoURL.Host), log.String("path", repoURL.Path))
673-
client := &http.Client{}
674691
req, err := http.NewRequest("GET", repoURL.String(), http.NoBody)
675692
if err != nil {
676693
logger.Debug("HTTP request failed")
@@ -681,9 +698,54 @@ func (p *Parser) fetchPOMFromRemoteRepository(repo string, paths []string) (*pom
681698
req.SetBasicAuth(repoURL.User.Username(), password)
682699
}
683700

701+
return req, nil
702+
}
703+
704+
// fetchPomFileNameFromMavenMetadata fetches `maven-metadata.xml` file to detect file name of pom file.
705+
func (p *Parser) fetchPomFileNameFromMavenMetadata(repo string, paths []string) (string, error) {
706+
// Overwrite pom file name to `maven-metadata.xml`
707+
mavenMetadataPaths := slices.Clone(paths[:len(paths)-1]) // Clone slice to avoid shadow overwriting last element of `paths`
708+
mavenMetadataPaths = append(mavenMetadataPaths, "maven-metadata.xml")
709+
710+
req, err := p.remoteRepoRequest(repo, mavenMetadataPaths)
711+
if err != nil {
712+
return "", xerrors.Errorf("unable to create request for maven-metadata.xml file")
713+
}
714+
715+
client := &http.Client{}
684716
resp, err := client.Do(req)
685717
if err != nil || resp.StatusCode != http.StatusOK {
686-
logger.Debug("Failed to fetch")
718+
p.logger.Debug("Failed to fetch", log.String("url", req.URL.String()))
719+
return "", nil
720+
}
721+
defer resp.Body.Close()
722+
723+
mavenMetadata, err := parseMavenMetadata(resp.Body)
724+
if err != nil {
725+
return "", xerrors.Errorf("failed to parse maven-metadata.xml file: %w", err)
726+
}
727+
728+
var pomFileName string
729+
for _, sv := range mavenMetadata.Versioning.SnapshotVersions {
730+
if sv.Extension == "pom" {
731+
// mavenMetadataPaths[len(mavenMetadataPaths)-3] is always artifactID
732+
pomFileName = fmt.Sprintf("%s-%s.pom", mavenMetadataPaths[len(mavenMetadataPaths)-3], sv.Value)
733+
}
734+
}
735+
736+
return pomFileName, nil
737+
}
738+
739+
func (p *Parser) fetchPOMFromRemoteRepository(repo string, paths []string) (*pom, error) {
740+
req, err := p.remoteRepoRequest(repo, paths)
741+
if err != nil {
742+
return nil, xerrors.Errorf("unable to create request for pom file")
743+
}
744+
745+
client := &http.Client{}
746+
resp, err := client.Do(req)
747+
if err != nil || resp.StatusCode != http.StatusOK {
748+
p.logger.Debug("Failed to fetch", log.String("url", req.URL.String()))
687749
return nil, nil
688750
}
689751
defer resp.Body.Close()
@@ -709,6 +771,16 @@ func parsePom(r io.Reader) (*pomXML, error) {
709771
return parsed, nil
710772
}
711773

774+
func parseMavenMetadata(r io.Reader) (*Metadata, error) {
775+
parsed := &Metadata{}
776+
decoder := xml.NewDecoder(r)
777+
decoder.CharsetReader = charset.NewReaderLabel
778+
if err := decoder.Decode(parsed); err != nil {
779+
return nil, xerrors.Errorf("xml decode error: %w", err)
780+
}
781+
return parsed, nil
782+
}
783+
712784
func packageID(name, version string) string {
713785
return dependency.ID(ftypes.Pom, name, version)
714786
}

pkg/dependency/parser/java/pom/parse_test.go

+60-1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ func TestPom_Parse(t *testing.T) {
143143
},
144144
},
145145
},
146+
{
147+
ID: "org.example:example-api:2.0.0",
148+
Name: "org.example:example-api",
149+
Version: "2.0.0",
150+
Licenses: []string{"The Apache Software License, Version 2.0"},
151+
Relationship: ftypes.RelationshipIndirect,
152+
},
146153
},
147154
wantDeps: []ftypes.Dependency{
148155
{
@@ -151,6 +158,58 @@ func TestPom_Parse(t *testing.T) {
151158
"org.example:example-dependency:1.2.3-SNAPSHOT",
152159
},
153160
},
161+
{
162+
ID: "org.example:example-dependency:1.2.3-SNAPSHOT",
163+
DependsOn: []string{
164+
"org.example:example-api:2.0.0",
165+
},
166+
},
167+
},
168+
},
169+
{
170+
name: "snapshot repository with maven-metadata.xml",
171+
inputFile: filepath.Join("testdata", "snapshot", "with-maven-metadata", "pom.xml"),
172+
local: false,
173+
want: []ftypes.Package{
174+
{
175+
ID: "com.example:happy:1.0.0",
176+
Name: "com.example:happy",
177+
Version: "1.0.0",
178+
Relationship: ftypes.RelationshipRoot,
179+
},
180+
{
181+
ID: "org.example:example-dependency:2.17.0-SNAPSHOT",
182+
Name: "org.example:example-dependency",
183+
Version: "2.17.0-SNAPSHOT",
184+
Relationship: ftypes.RelationshipDirect,
185+
Locations: ftypes.Locations{
186+
{
187+
StartLine: 14,
188+
EndLine: 18,
189+
},
190+
},
191+
},
192+
{
193+
ID: "org.example:example-api:2.0.0",
194+
Name: "org.example:example-api",
195+
Version: "2.0.0",
196+
Licenses: []string{"The Apache Software License, Version 2.0"},
197+
Relationship: ftypes.RelationshipIndirect,
198+
},
199+
},
200+
wantDeps: []ftypes.Dependency{
201+
{
202+
ID: "com.example:happy:1.0.0",
203+
DependsOn: []string{
204+
"org.example:example-dependency:2.17.0-SNAPSHOT",
205+
},
206+
},
207+
{
208+
ID: "org.example:example-dependency:2.17.0-SNAPSHOT",
209+
DependsOn: []string{
210+
"org.example:example-api:2.0.0",
211+
},
212+
},
154213
},
155214
},
156215
{
@@ -1404,7 +1463,7 @@ func TestPom_Parse(t *testing.T) {
14041463
remoteRepos = []string{ts.URL}
14051464
}
14061465

1407-
p := pom.NewParser(tt.inputFile, pom.WithReleaseRemoteRepos(remoteRepos), pom.WithOffline(tt.offline))
1466+
p := pom.NewParser(tt.inputFile, pom.WithReleaseRemoteRepos(remoteRepos), pom.WithSnapshotRemoteRepos(remoteRepos), pom.WithOffline(tt.offline))
14081467

14091468
gotPkgs, gotDeps, err := p.Parse(f)
14101469
if tt.wantErr != "" {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>org.example</groupId>
8+
<artifactId>example-dependency</artifactId>
9+
<version>2.17.0-SNAPSHOT</version>
10+
11+
<packaging>jar</packaging>
12+
<name>Example API Dependency</name>
13+
<description>The example API</description>
14+
15+
<dependencies>
16+
<dependency>
17+
<groupId>org.example</groupId>
18+
<artifactId>example-api</artifactId>
19+
<version>2.0.0</version>
20+
</dependency>
21+
</dependencies>
22+
23+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<metadata modelVersion="1.1.0">
2+
<groupId>org.example</groupId>
3+
<artifactId>example-dependency</artifactId>
4+
<versioning>
5+
<lastUpdated>20240312035235</lastUpdated>
6+
<snapshot>
7+
<timestamp>20240312.035235</timestamp>
8+
<buildNumber>10</buildNumber>
9+
</snapshot>
10+
<snapshotVersions>
11+
<snapshotVersion>
12+
<classifier>sources</classifier>
13+
<extension>jar</extension>
14+
<value>2.17.0-20240312.035235-10</value>
15+
<updated>20240312035235</updated>
16+
</snapshotVersion>
17+
<snapshotVersion>
18+
<extension>module</extension>
19+
<value>2.17.0-20240312.035235-10</value>
20+
<updated>20240312035235</updated>
21+
</snapshotVersion>
22+
<snapshotVersion>
23+
<extension>jar</extension>
24+
<value>2.17.0-20240312.035235-10</value>
25+
<updated>20240312035235</updated>
26+
</snapshotVersion>
27+
<snapshotVersion>
28+
<extension>pom</extension>
29+
<value>2.17.0-20240312.035235-10</value>
30+
<updated>20240312035235</updated>
31+
</snapshotVersion>
32+
</snapshotVersions>
33+
</versioning>
34+
<version>2.17.0-SNAPSHOT</version>
35+
</metadata>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<groupId>com.example</groupId>
6+
<artifactId>happy</artifactId>
7+
<version>1.0.0</version>
8+
9+
<name>happy</name>
10+
<description>Example</description>
11+
12+
13+
<dependencies>
14+
<dependency>
15+
<groupId>org.example</groupId>
16+
<artifactId>example-dependency</artifactId>
17+
<version>2.17.0-SNAPSHOT</version>
18+
</dependency>
19+
</dependencies>
20+
</project>

0 commit comments

Comments
 (0)