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 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, String artifact) throws IOException, URISyntaxException { String path = mavenNotationToJarPath(artifact); Path res = MetaHolder.LIBRARIES_DIR.resolve(path); Net.downloadFile(Utils.join("/", repo, path), 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); } } } 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 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"; } }