All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sandpolis.core.util.ArtifactUtil Maven / Gradle / Ivy

/*******************************************************************************
 *                                                                             *
 *                Copyright © 2015 - 2019 Subterranean Security                *
 *                                                                             *
 *  Licensed under the Apache License, Version 2.0 (the "License");            *
 *  you may not use this file except in compliance with the License.           *
 *  You may obtain a copy of the License at                                    *
 *                                                                             *
 *      http://www.apache.org/licenses/LICENSE-2.0                             *
 *                                                                             *
 *  Unless required by applicable law or agreed to in writing, software        *
 *  distributed under the License is distributed on an "AS IS" BASIS,          *
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   *
 *  See the License for the specific language governing permissions and        *
 *  limitations under the License.                                             *
 *                                                                             *
 ******************************************************************************/
package com.sandpolis.core.util;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.stream.Stream;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import com.google.common.io.Files;

/**
 * Utilities for managing dependency artifacts and interacting with the Maven
 * Central Repository.
 *
 * @author cilki
 * @since 5.0.0
 */
public final class ArtifactUtil {

	private static final Logger log = LoggerFactory.getLogger(ArtifactUtil.class);

	/**
	 * The central repository base URL.
	 */
	private static final String MAVEN_HOST = "https://repo1.maven.org";

	/**
	 * Get an artifact file from the given directory.
	 *
	 * @param directory   The directory containing artifacts
	 * @param coordinates The artifact's coordinates
	 * @return The artifact's local file
	 */
	public static Path getArtifactFile(Path directory, String coordinates) {
		checkNotNull(directory);
		checkNotNull(coordinates);

		return directory.resolve(ParsedCoordinate.fromCoordinate(coordinates).filename);
	}

	/**
	 * Find all artifacts with the given artifact name. The results will be in
	 * decreasing version order if applicable.
	 *
	 * @param directory  The directory containing artifacts
	 * @param artifactId The artifact to search for
	 * @return A stream of all matching artifacts
	 * @throws IOException
	 */
	public static Stream findArtifactFile(Path directory, String artifactId) throws IOException {
		return java.nio.file.Files.list(directory).filter(path -> path.getFileName().toString().startsWith(artifactId))
				// Sort by semantic version number
				.sorted((path1, path2) -> {
					// TODO COMPARE CORRECTLY!
					return ParsedCoordinate.fromFilename(path1.getFileName().toString()).version
							.compareTo(ParsedCoordinate.fromFilename(path2.getFileName().toString()).version);
				});
	}

	/**
	 * Download an artifact from Maven Central to the given directory.
	 *
	 * @param directory The output directory
	 * @param gav       The artifact coordinate in standard Gradle form
	 *                  (group:name:version)
	 * @return The output path
	 * @throws IOException
	 */
	public static Path download(Path directory, String gav) throws IOException {
		checkNotNull(directory);
		checkNotNull(gav);

		var coordinate = ParsedCoordinate.fromCoordinate(gav);
		String url = MAVEN_HOST + "/maven2/" // Base URL
				+ coordinate.groupId.replaceAll("\\.", "/") + "/"// Group name
				+ coordinate.artifactId + "/" // Artifact name
				+ coordinate.version + "/" // Artifact version
				+ coordinate.filename; // Artifact filename
		log.debug("Downloading artifact: {}", url);

		Path output = directory.resolve(coordinate.filename);
		NetUtil.download(url, output.toFile());
		return output;
	}

	/**
	 * Check an artifact's hash.
	 *
	 * @param directory The artifact directory
	 * @param gav       The artifact coordinate in standard Gradle form
	 *                  (group:name:version)
	 * @return Whether the artifact in the given directory matches the remote hash
	 * @throws IOException
	 */
	@SuppressWarnings("deprecation")
	public static boolean checkHash(Path directory, String gav) throws IOException {
		checkNotNull(directory);
		checkNotNull(gav);

		var coordinate = ParsedCoordinate.fromCoordinate(gav);
		String url = MAVEN_HOST + "/maven2/" // Base URL
				+ coordinate.groupId.replaceAll("\\.", "/") + "/"// Group name
				+ coordinate.artifactId + "/" // Artifact name
				+ coordinate.version + "/" // Artifact version
				+ coordinate.filename // Artifact filename
				+ ".sha1";
		log.debug("Downloading hash: {}", url);

		// If the directory does not contain the file, the hash doesn't match
		Path artifact = directory.resolve(coordinate.filename);
		if (!java.nio.file.Files.exists(artifact))
			throw new FileNotFoundException();

		// Download the file hash
		byte[] hash = BaseEncoding.base16().lowerCase().decode(new String(NetUtil.download(url)));

		// Compare hash
		return Arrays.equals(Files.asByteSource(artifact.toFile()).hash(Hashing.sha1()).asBytes(), hash);
	}

	/**
	 * Query the latest version of an artifact.
	 *
	 * @param gav The artifact coordinate in standard Gradle form
	 *            (group:name:version)
	 * @return The artifact's latest version string
	 * @throws IOException If the connection could not be made, the artifact could
	 *                     not be found, or the metadata could not be parsed
	 */
	public static String getLatestVersion(String gav) throws IOException {
		checkNotNull(gav);

		var coordinate = ParsedCoordinate.fromCoordinate(gav);
		String url = MAVEN_HOST + "/maven2/" // Base URL
				+ coordinate.groupId.replaceAll("\\.", "/") + "/"// Group name
				+ coordinate.artifactId // Artifact name
				+ "/maven-metadata.xml";

		try (var in = new URL(url).openStream()) {
			return XPathFactory.newDefaultInstance().newXPath().evaluate("/metadata/versioning/latest",
					DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in));
		} catch (XPathExpressionException | SAXException | ParserConfigurationException e) {
			throw new IOException(e);
		}
	}

	/**
	 * A container for an artifact's colon-delimited coordinate.
	 */
	public static class ParsedCoordinate {

		public final String coordinate;
		public final String groupId;
		public final String artifactId;
		public final String version;
		public final String filename;

		private ParsedCoordinate(String coordinate, String groupId, String artifactId, String version,
				String filename) {
			this.coordinate = coordinate == null ? "" : coordinate;
			this.groupId = groupId == null ? "" : groupId;
			this.artifactId = artifactId == null ? "" : artifactId;
			this.version = version == null ? "" : version;
			this.filename = filename == null ? "" : filename;
		}

		/**
		 * Parse an artifact's filename.
		 *
		 * @param filename The standard filename
		 * @return A new {@link ParsedCoordinate}
		 */
		public static ParsedCoordinate fromFilename(String filename) {
			checkNotNull(filename);
			filename = filename.trim();

			String version = filename.substring(filename.lastIndexOf('-') + 1, filename.lastIndexOf(".jar"));
			try {
				String artifact = filename.substring(0, filename.lastIndexOf('-'));
				return new ParsedCoordinate(":" + artifact + ":" + version, null, artifact, version, filename);
			} catch (Exception e) {
				// Missing version
				String artifact = filename.substring(0, filename.lastIndexOf(".jar"));
				return new ParsedCoordinate(":" + artifact + ":", null, artifact, null, filename);
			}
		}

		/**
		 * Parse a coordinate.
		 *
		 * @param coordinate The coordinate to parse
		 * @return A new {@link ParsedCoordinate}
		 */
		public static ParsedCoordinate fromCoordinate(String coordinate) {
			checkNotNull(coordinate);

			// Hack to produce an empty last element if necessary
			if (coordinate.endsWith(":"))
				coordinate += " ";

			String[] gav = coordinate.split(":");
			checkArgument(gav.length == 3, "Coordinate format: %s", coordinate);

			// Trim fields
			for (int i = 0; i < gav.length; i++)
				gav[i] = gav[i].trim();

			// Build canonical filename
			String filename = gav[1];
			if (!gav[2].isEmpty())
				filename += "-" + gav[2];
			filename += ".jar";

			return new ParsedCoordinate(coordinate.trim(), gav[0], gav[1], gav[2], filename);
		}
	}

	private ArtifactUtil() {
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy