Skip to content

Commit 2d836a4

Browse files
committed
Add support for basic Maven dependency resolution. Add logging to CLI.
1 parent 7986d80 commit 2d836a4

File tree

3 files changed

+201
-31
lines changed

3 files changed

+201
-31
lines changed

src/main/java/io/github/bensku/tsbind/cli/Args.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.bensku.tsbind.cli;
22

33
import java.nio.file.Path;
4+
import java.util.ArrayList;
45
import java.util.List;
56

67
import com.beust.jcommander.Parameter;
@@ -30,10 +31,10 @@ public enum OutputFormat {
3031
public Path in;
3132

3233
@Parameter(names = "--symbols")
33-
public List<Path> symbols = List.of();
34+
public List<Path> symbols = new ArrayList<>();
3435

3536
@Parameter(names = "--repo")
36-
public String repo;
37+
public List<String> repos = new ArrayList<>();
3738

3839
@Parameter(names = "--artifact")
3940
public String artifact;

src/main/java/io/github/bensku/tsbind/cli/BindGenApp.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,23 @@ public Path read(JsonReader in) throws IOException {
5151
.create()
5252
.fromJson(Files.readString(args.packageJson), PackageJson.class).tsbindOptions;
5353
if (args == null) {
54-
throw new IllegalArgumentException("missing tsbindOptions in --package");
54+
throw new IllegalArgumentException("missing tsbindOptions in --packageJson");
5555
}
5656
}
5757

5858
// Download the --artifact from Maven if provided
5959
Path inputPath;
6060
if (args.artifact != null) {
61-
MavenResolver resolver = new MavenResolver(args.repo);
62-
inputPath = resolver.downloadSources(args.artifact);
61+
System.out.println("Resolving Maven artifact " + args.artifact);
62+
args.repos.add("https://repo1.maven.org/maven2"); // Maven central as last resort
63+
MavenResolver resolver = new MavenResolver(Files.createTempDirectory("tsbind"), args.repos);
64+
MavenResolver.ArtifactResults results = resolver.downloadArtifacts(args.artifact, true);
65+
inputPath = results.sourceJar;
66+
args.symbols.addAll(results.symbols);
6367
} else {
6468
inputPath = args.in;
6569
}
70+
System.out.println("Generating types for " + inputPath + " to " + args.out);
6671

6772
// Create path to root of input files we have
6873
Path inputDir;
@@ -97,11 +102,15 @@ public Path read(JsonReader in) throws IOException {
97102
throw new RuntimeException(e);
98103
}
99104
}).map(astGenerator::parseType)
100-
.flatMap(Optional::stream).forEach(type -> types.put(type.name(), type));
105+
.flatMap(Optional::stream).forEach(type -> {
106+
System.out.println("Parsed type " + type.name());
107+
types.put(type.name(), type);
108+
});
101109

102110
Stream<Result<String>> results = args.format.consumerSource.apply(args)
103111
.consume(types);
104112
results.forEach(result -> {
113+
System.out.println("Writing module " + result.name);
105114
try {
106115
Files.writeString(outDir.resolve(result.name), result.result);
107116
} catch (IOException e) {

src/main/java/io/github/bensku/tsbind/cli/MavenResolver.java

Lines changed: 185 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,62 @@
88
import java.net.http.HttpResponse.BodyHandlers;
99
import java.nio.file.Files;
1010
import java.nio.file.Path;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
1114
import org.jsoup.Jsoup;
1215
import org.jsoup.nodes.Document;
1316
import org.jsoup.nodes.Element;
1417
import org.jsoup.parser.Parser;
1518
import org.jsoup.select.Elements;
1619

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+
*/
1726
public class MavenResolver {
1827

28+
private Path tempDir;
1929
private HttpClient client;
20-
private String repo;
30+
private List<String> repos;
2131

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;
2339
this.client = HttpClient.newHttpClient();
24-
this.repo = repo;
40+
this.repos = repos;
2541
}
2642

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);
3349
}
34-
throw new IllegalArgumentException("cannot find artifact " + group + ":" + artifact + ":" + version);
3550
}
3651

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) {
3867
Document doc = Jsoup.parse(metadataStr, "", Parser.xmlParser());
3968
Element metadata = doc.selectFirst("metadata");
4069
Elements snapshots = metadata.selectFirst("versioning")
@@ -58,33 +87,164 @@ private URI getSnapshotSources(String metadataStr) {
5887
String group = metadata.selectFirst("groupId").text();
5988
String artifact = metadata.selectFirst("artifactId").text();
6089
String version = metadata.selectFirst("version").text();
61-
return URI.create(repo + "/" + group.replace('.', '/')
90+
return repo + "/" + group.replace('.', '/')
6291
+ "/" + 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;
64157
}
65158

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 {
67203
String[] parts = coordinates.split(":");
68204
String group = parts[0];
69205
String artifact = parts[1];
70206
String version = parts[2];
71207

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;
73211
if (version.contains("SNAPSHOT")) {
74-
String metadata = getSnapshotMetadata(group, artifact, version);
75-
sourceUri = getSnapshotSources(metadata);
212+
baseUrl = getSnapshotUrl(group, artifact, version);
76213
} 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");
80223
}
81224

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());
85232
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());
87234
}
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);
89249
}
90250
}

0 commit comments

Comments
 (0)