8
8
import java .net .http .HttpResponse .BodyHandlers ;
9
9
import java .nio .file .Files ;
10
10
import java .nio .file .Path ;
11
+ import java .util .ArrayList ;
12
+ import java .util .List ;
13
+
11
14
import org .jsoup .Jsoup ;
12
15
import org .jsoup .nodes .Document ;
13
16
import org .jsoup .nodes .Element ;
14
17
import org .jsoup .parser .Parser ;
15
18
import org .jsoup .select .Elements ;
16
19
20
+ /**
21
+ * A very simple Maven resolver. This currently lacks support for Maven BOM
22
+ * and variable substitutions in pom.xml, but should be enough to pull
23
+ * dependencies for small libraries.
24
+ *
25
+ */
17
26
public class MavenResolver {
18
27
28
+ private Path tempDir ;
19
29
private HttpClient client ;
20
- private String repo ;
30
+ private List < String > repos ;
21
31
22
- public MavenResolver (String repo ) {
32
+ /**
33
+ * Creates a new Maven resolver.
34
+ * @param tempDir Temporary directory where downloads should be placed.
35
+ * @param repos Maven repository URLs, in order of preference.
36
+ */
37
+ public MavenResolver (Path tempDir , List <String > repos ) {
38
+ this .tempDir = tempDir ;
23
39
this .client = HttpClient .newHttpClient ();
24
- this .repo = repo ;
40
+ this .repos = repos ;
25
41
}
26
42
27
- private String getSnapshotMetadata ( String group , String artifact , String version ) throws InterruptedException , IOException {
28
- URI uri = URI . create ( repo + "/" + group . replace ( '.' , '/' )
29
- + "/" + artifact + "/" + version + "/maven-metadata.xml" ) ;
30
- HttpResponse < String > response = client . send ( HttpRequest . newBuilder ( uri ). GET (). build (), BodyHandlers . ofString ());
31
- if ( response . statusCode () == 200 ) {
32
- return response . body (); // Got a response, let's hope it is valid XML
43
+ private static class ArtifactNotFoundException extends RuntimeException {
44
+
45
+ private static final long serialVersionUID = 1L ;
46
+
47
+ public ArtifactNotFoundException ( String msg ) {
48
+ super ( msg );
33
49
}
34
- throw new IllegalArgumentException ("cannot find artifact " + group + ":" + artifact + ":" + version );
35
50
}
36
51
37
- private URI getSnapshotSources (String metadataStr ) {
52
+ private String getSnapshotUrl (String group , String artifact , String version ) throws InterruptedException , IOException {
53
+ // Try each repo in order they were specified
54
+ for (String repo : repos ) {
55
+ URI uri = URI .create (repo + "/" + group .replace ('.' , '/' )
56
+ + "/" + artifact + "/" + version + "/maven-metadata.xml" );
57
+ HttpResponse <String > response = client .send (HttpRequest .newBuilder (uri ).GET ().build (), BodyHandlers .ofString ());
58
+ if (response .statusCode () == 200 ) {
59
+ // Got a response, let's hope it is valid XML
60
+ return getSnapshotUrl (repo , response .body ());
61
+ }
62
+ }
63
+ throw new ArtifactNotFoundException ("cannot find artifact " + group + ":" + artifact + ":" + version );
64
+ }
65
+
66
+ private String getSnapshotUrl (String repo , String metadataStr ) {
38
67
Document doc = Jsoup .parse (metadataStr , "" , Parser .xmlParser ());
39
68
Element metadata = doc .selectFirst ("metadata" );
40
69
Elements snapshots = metadata .selectFirst ("versioning" )
@@ -58,33 +87,164 @@ private URI getSnapshotSources(String metadataStr) {
58
87
String group = metadata .selectFirst ("groupId" ).text ();
59
88
String artifact = metadata .selectFirst ("artifactId" ).text ();
60
89
String version = metadata .selectFirst ("version" ).text ();
61
- return URI . create ( repo + "/" + group .replace ('.' , '/' )
90
+ return repo + "/" + group .replace ('.' , '/' )
62
91
+ "/" + artifact + "/" + version
63
- + "/" + artifact + "-" + snapshotVersion + "-sources.jar" );
92
+ + "/" + artifact + "-" + snapshotVersion ;
93
+ }
94
+
95
+ private String getReleaseUrl (String group , String artifact , String version ) throws IOException , InterruptedException {
96
+ for (String repo : repos ) {
97
+ String baseUrl = repo + "/" + group .replace ('.' , '/' )
98
+ + "/" + artifact + "/" + version
99
+ + "/" + artifact + "-" + version ;
100
+ URI testUri = URI .create (baseUrl + ".pom" );
101
+ HttpResponse <String > response = client .send (HttpRequest .newBuilder (testUri )
102
+ .GET ().build (), BodyHandlers .ofString ());
103
+ if (response .statusCode () == 200 ) {
104
+ // Got .pom, artifact should exist in this repository
105
+ // TODO reuse this .pom to avoid downloading twice
106
+ return baseUrl ;
107
+ }
108
+ }
109
+ throw new ArtifactNotFoundException ("cannot find artifact " + group + ":" + artifact + ":" + version );
110
+
111
+ }
112
+
113
+ /**
114
+ * Parses the given .pom file contents and gets a list of dependencies.
115
+ * @param pomXml .pom XML content.
116
+ * @return Dependency coordinates.
117
+ */
118
+ private List <String > getDependencies (String pomXml ) {
119
+ List <String > deps = new ArrayList <>();
120
+
121
+ Document doc = Jsoup .parse (pomXml , "" , Parser .xmlParser ());
122
+ // Select last <dependencies> so we don't hit one under <dependencyManagement>
123
+ Element depsTag = doc .selectFirst ("project" ).select ("dependencies" ).last ();
124
+ if (depsTag == null ) {
125
+ return deps ; // No dependencies
126
+ }
127
+ Elements dependencies = depsTag .select ("dependency" );
128
+ if (dependencies == null ) {
129
+ return deps ; // Also no dependencies
130
+ }
131
+ for (Element dependency : dependencies ) {
132
+ Element group = dependency .selectFirst ("groupId" );
133
+ Element artifact = dependency .selectFirst ("artifactId" );
134
+ Element version = dependency .selectFirst ("version" );
135
+ if (group == null || artifact == null ) {
136
+ throw new AssertionError ("groupId: " + group + ", artifactId: " + artifact );
137
+ }
138
+ Element scopeEl = dependency .selectFirst ("scope" );
139
+ if (scopeEl != null ) {
140
+ String scope = scopeEl .text ();
141
+ if (!scope .equals ("compile" ) && !scope .equals ("provided" )) {
142
+ continue ; // Not referenced from main source code
143
+ }
144
+ }
145
+ if (version == null ) {
146
+ System .out .println (group .text () + ":" + artifact .text () + ": unknown version, Maven BOM?" );
147
+ continue ;
148
+ }
149
+ String dep = group .text () + ":" + artifact .text () + ":" + version .text ();
150
+ if (dep .contains ("${" )) {
151
+ System .out .println (dep + ": variables not supported" );
152
+ continue ;
153
+ }
154
+ deps .add (dep );
155
+ }
156
+ return deps ;
64
157
}
65
158
66
- public Path downloadSources (String coordinates ) throws InterruptedException , IOException {
159
+ public static class ArtifactResults {
160
+
161
+ /**
162
+ * Source jar. Null when {@link MavenResolver#downloadArtifacts(String, boolean)}
163
+ * is given false as {@code source} parameter.
164
+ */
165
+ public final Path sourceJar ;
166
+
167
+ /**
168
+ * Binary jars that contain symbols needed to parse the source jar.
169
+ */
170
+ public final List <Path > symbols ;
171
+
172
+ private ArtifactResults (Path sourceJar , List <Path > symbols ) {
173
+ this .sourceJar = sourceJar ;
174
+ this .symbols = symbols ;
175
+ }
176
+ }
177
+
178
+ private Path download (URI uri , String name ) throws IOException , InterruptedException {
179
+ name = name .replace (':' , '-' ); // Double colon is trouble on Windows
180
+ Path path = tempDir .resolve (name );
181
+ if (Files .exists (path )) {
182
+ return path ; // no need to download it again
183
+ }
184
+ HttpResponse <Path > response = client .send (HttpRequest .newBuilder (uri )
185
+ .GET ().build (), BodyHandlers .ofFile (path ));
186
+ if (response .statusCode () != 200 ) {
187
+ throw new IOException ("failed to GET " + uri + ": HTTP " + response .statusCode ());
188
+ }
189
+ return path ;
190
+ }
191
+
192
+ /**
193
+ * Downloads an artifact and its (non-test and supported) dependencies.
194
+ * @param coordinates Coordinates in Gradle format, i.e.
195
+ * {@code group:artifact:version}.
196
+ * @param source If source jar should be downloaded. If this is false,
197
+ * {@link ArtifactResults#sourceJar} is also null.
198
+ * @return Paths to symbol jars and, optionally, the source jar.
199
+ * @throws InterruptedException
200
+ * @throws IOException
201
+ */
202
+ public ArtifactResults downloadArtifacts (String coordinates , boolean source ) throws InterruptedException , IOException {
67
203
String [] parts = coordinates .split (":" );
68
204
String group = parts [0 ];
69
205
String artifact = parts [1 ];
70
206
String version = parts [2 ];
71
207
72
- URI sourceUri ;
208
+ // For snapshots, we need to figure out the subfolder for latest upload
209
+ // (for releases: just find the repository it exists in)
210
+ String baseUrl ;
73
211
if (version .contains ("SNAPSHOT" )) {
74
- String metadata = getSnapshotMetadata (group , artifact , version );
75
- sourceUri = getSnapshotSources (metadata );
212
+ baseUrl = getSnapshotUrl (group , artifact , version );
76
213
} else {
77
- sourceUri = URI .create (repo + "/" + group .replace ('.' , '/' )
78
- + "/" + artifact + "/" + version
79
- + "/" + artifact + "-" + version + "-sources.jar" );
214
+ baseUrl = getReleaseUrl (group , artifact , version );
215
+ }
216
+
217
+ List <Path > symbols = new ArrayList <>();
218
+
219
+ // Download source if it was requested
220
+ Path sourceJar = null ;
221
+ if (source ) {
222
+ sourceJar = download (URI .create (baseUrl + "-sources.jar" ), coordinates + "-sources.jar" );
80
223
}
81
224
82
- // Download source jar to temporary file
83
- Path tempFile = Files .createTempFile ("tsbind" , "-sources.jar" );
84
- HttpResponse <Path > response = client .send (HttpRequest .newBuilder (sourceUri ).GET ().build (), BodyHandlers .ofFile (tempFile ));
225
+ // Fetch the binary jar for symbols
226
+ symbols .add (download (URI .create (baseUrl + ".jar" ), coordinates + ".jar" ));
227
+
228
+ // ... and all compile-time dependencies for main source code, for symbols again
229
+ URI pomUri = URI .create (baseUrl + ".pom" );
230
+ HttpResponse <String > response = client .send (HttpRequest .newBuilder (pomUri )
231
+ .GET ().build (), BodyHandlers .ofString ());
85
232
if (response .statusCode () != 200 ) {
86
- throw new IOException ("failed to GET " + sourceUri + ": HTTP " + response .statusCode ());
233
+ throw new IOException ("failed to GET " + pomUri + ": HTTP " + response .statusCode ());
87
234
}
88
- return tempFile ;
235
+ for (String dependency : getDependencies (response .body ())) {
236
+ try {
237
+ ArtifactResults artifacts = downloadArtifacts (dependency , false );
238
+ System .out .println ("Fetching " + dependency );
239
+ symbols .addAll (artifacts .symbols );
240
+ } catch (ArtifactNotFoundException e ) {
241
+ // Failure to resolve a dependency is not necessarily critical
242
+ // It will also happen quite often since our .pom parsing logic
243
+ // is quite limited compared to actual build systems
244
+ System .out .println (e .getMessage ());
245
+ }
246
+ }
247
+
248
+ return new ArtifactResults (sourceJar , symbols );
89
249
}
90
250
}
0 commit comments