Inceptum/common/src/main/java/io/gitlab/jfronny/inceptum/common/api/MavenApi.java

250 lines
12 KiB
Java

package io.gitlab.jfronny.inceptum.common.api;
import io.gitlab.jfronny.commons.HttpUtils;
import io.gitlab.jfronny.inceptum.common.*;
import io.gitlab.jfronny.inceptum.common.model.maven.MavenDependency;
import io.gitlab.jfronny.inceptum.common.model.maven.Pom;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import javax.xml.parsers.*;
import javax.xml.stream.XMLStreamException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.*;
public class MavenApi {
private static final DocumentBuilder FACTORY;
private static final Set<String> RUNTIME_SCOPES = Set.of("compile", "runtime");
static {
try {
FACTORY = DocumentBuilderFactory.newInstance().newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new RuntimeException("Could not create document builder", e);
}
}
public static Path downloadLibrary(String repo, Pom pom) throws IOException, URISyntaxException {
String artifact = pom.groupId + ':' + pom.artifactId + ':' + pom.version;
String rawArtifact = artifact;
if (pom.classifier != null || pom.snapshotVersion != null) {
artifact += ':';
if (pom.snapshotVersion != null) artifact += pom.snapshotVersion;
if (pom.classifier != null) {
if (pom.snapshotVersion != null) artifact += '-';
artifact += pom.classifier;
}
}
if (pom.classifier != null) rawArtifact += ':' + pom.classifier;
Path res = MetaHolder.LIBRARIES_DIR.resolve(mavenNotationToJarPath(rawArtifact));
Net.downloadFile(Utils.join("/", repo, mavenNotationToJarPath(artifact)), res);
return res;
}
public static Pom getPom(String repo, String artifact) throws IOException, SAXException, URISyntaxException, XMLStreamException {
try (InputStream is = HttpUtils.get(Utils.join("/", repo, mavenNotationToPomPath(artifact))).sendInputStream()) {
Document doc = FACTORY.parse(is);
doc.getDocumentElement().normalize();
Pom result = new Pom();
if (!"project".equals(doc.getDocumentElement().getNodeName())) throw new IOException("Illegal document name");
boolean hasModelVersion = false;
boolean hasGroupId = false;
boolean hasArtifactId = false;
boolean hasVersion = false;
for (Node node : iterable(doc.getDocumentElement().getChildNodes())) {
switch (node.getNodeName()) {
case "modelVersion" -> {
hasModelVersion = true;
result.modelVersion = node.getTextContent();
}
case "parent" -> {
// Dirty hack to get slf4j working: simply assume the groupId and version of the parent is also the groupId of this
if (!hasGroupId) {
for (Node child : iterable(node.getChildNodes())) {
switch (child.getNodeName()) {
case "groupId" -> {
if (!hasGroupId) {
hasGroupId = true;
result.groupId = node.getTextContent();
}
}
case "version" -> {
if (!hasVersion) {
hasVersion = true;
result.version = node.getTextContent();
}
}
}
}
}
}
case "groupId" -> {
hasGroupId = true;
result.groupId = node.getTextContent();
}
case "artifactId" -> {
hasArtifactId = true;
result.artifactId = node.getTextContent();
}
case "version" -> {
hasVersion = true;
result.version = node.getTextContent();
}
case "packaging" -> result.packaging = node.getTextContent();
case "dependencies" -> {
result.dependencies = new LinkedList<>();
for (Node dep : iterable(node.getChildNodes())) {
MavenDependency resolved = parseDependency(dep);
if (resolved != null) {
result.dependencies.add(resolved);
}
}
}
case "classifier" -> result.classifier = node.getTextContent();
case "versioning" -> {
for (Node node1 : iterable(node.getChildNodes())) {
if (node1.getNodeName().equals("snapshot")) {
String timestamp = null;
String buildNumber = null;
for (Node node2 : iterable(node1.getChildNodes())) {
switch (node2.getNodeName()) {
case "timestamp" -> timestamp = node2.getTextContent();
case "buildNumber" -> buildNumber = node2.getTextContent();
default -> {}
}
}
if (timestamp == null) throw new IOException("Pom snapshots lack timestamp");
if (buildNumber == null) throw new IOException("Pom snapshots lack buildNumber");
result.snapshotVersion = timestamp + '-' + buildNumber;
}
}
}
default -> {}
}
}
if (!hasModelVersion) throw new IOException("Pom lacks modelVersion");
if (!hasGroupId) throw new IOException("Pom lacks groupId");
if (!hasArtifactId) throw new IOException("Pom lacks artifactId");
if (!hasVersion) throw new IOException("Pom lacks version");
return result;
}
}
private static @Nullable MavenDependency parseDependency(Node doc) throws IOException {
MavenDependency result = new MavenDependency();
boolean hasGroupId = false;
boolean hasArtifactId = false;
boolean hasVersion = false;
boolean hasScope = false;
for (Node node : iterable(doc.getChildNodes())) {
switch (node.getNodeName()) {
case "groupId" -> {
hasGroupId = true;
result.groupId = node.getTextContent();
}
case "artifactId" -> {
hasArtifactId = true;
result.artifactId = node.getTextContent();
}
case "version" -> {
hasVersion = true;
result.version = node.getTextContent();
}
case "scope" -> {
hasScope = true;
result.scope = node.getTextContent();
if (!RUNTIME_SCOPES.contains(result.scope)) return null;
}
case "optional" -> {
if (node.getTextContent().equals("true")) return null;
}
}
}
if (!hasGroupId) throw new IOException("Pom lacks groupId");
if (!hasArtifactId) throw new IOException("Pom lacks artifactId");
if (!hasVersion) {
if (result.groupId.equals("org.lwjgl")) {
// Lwjgl uses a shared bom for versions which I don't want to support
// The required modules are explicit dependencies of launcher-imgui anyway
return null;
}
throw new IOException("Dependency " + result.groupId + ":" + result.artifactId + " lacks version");
}
if (!hasScope) throw new IOException("Pom lacks scope");
return result;
}
private static boolean isWhitespace(Node node) {
if (node.getNodeType() == Node.TEXT_NODE && node.getTextContent().isBlank()) return true;
if (node.getNodeType() == Node.COMMENT_NODE) return true;
return false;
}
private static Iterable<Node> iterable(NodeList list) {
return () -> new Iterator<>() {
int index = 0;
@Override
public boolean hasNext() {
while (index < list.getLength() && isWhitespace(list.item(index))) {
index++;
}
return index < list.getLength();
}
@Override
public Node next() {
if (!hasNext()) throw new NoSuchElementException();
return list.item(index++);
}
};
}
/**
* Converts an artifact in maven notation to a jar file path. The following are supported:
* - some.base.path:artifact:version -> some/base/path/artifact/version/artifact-version.jar
* - some.base.path:artifact:version:classifier -> some/base/path/artifact/version/artifact-version-classifier.jar
* @param mavenNotation An artifact in maven notation
* @return A file path
*/
public static String mavenNotationToJarPath(String mavenNotation) {
if (Objects.requireNonNull(mavenNotation).isEmpty()) throw new IllegalArgumentException("The notation is empty");
String[] lib = mavenNotation.split(":");
if (lib.length <= 1) throw new IllegalArgumentException("Not in maven notation");
if (lib.length == 2) throw new IllegalArgumentException("Skipping versions is not supported");
if (lib.length >= 5) throw new IllegalArgumentException("Unkown elements in maven notation");
String path = lib[0].replace('.', '/') + '/'; // Base
path += lib[1] + '/'; // Artifact name
path += lib[2] + '/'; // Version
if (lib.length == 3) { // artifact-version.jar
path += lib[1] + '-' + lib[2];
} else { // artifact-version-classifier.jar
path += lib[1] + '-' + lib[2] + "-" + lib[3];
}
return path + ".jar";
}
/**
* Converts an artifact in maven notation to a pom file path. The following are supported:
* - some.base.path:artifact:version -> some/base/path/artifact/version/artifact-version.pom
* - some.base.path:artifact:version:classifier -> some/base/path/artifact/version/artifact-version.pom
* @param mavenNotation An artifact in maven notation
* @return A file path
*/
public static String mavenNotationToPomPath(String mavenNotation) {
if (Objects.requireNonNull(mavenNotation).isEmpty()) throw new IllegalArgumentException("The notation is empty");
String[] lib = mavenNotation.split(":");
if (lib.length <= 1) throw new IllegalArgumentException("Not in maven notation");
if (lib.length == 2) throw new IllegalArgumentException("Skipping versions is not supported");
if (lib.length >= 5) throw new IllegalArgumentException("Unkown elements in maven notation");
String path = lib[0].replace('.', '/') + '/'; // Base
path += lib[1] + '/'; // Artifact name
path += lib[2] + '/'; // Version
path += lib[1] + '-' + lib[2]; // artifact-version
return path + ".pom";
}
}