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

org.robolectric.internal.dependency.MavenArtifactFetcher Maven / Gradle / Ivy

The newest version!
package org.robolectric.internal.dependency;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.google.common.util.concurrent.AsyncCallable;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Base64;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import javax.annotation.Nonnull;
import org.robolectric.util.Logger;

/**
 * Class responsible for fetching artifacts from Maven. This uses a thread pool of size two in order
 * to parallelize downloads. It uses the Sun JSSE provider for downloading due to its seamless
 * integration with HTTPUrlConnection.
 */
@SuppressWarnings("UnstableApiUsage")
public class MavenArtifactFetcher {
  private final String repositoryUrl;
  private final String repositoryUserName;
  private final String repositoryPassword;
  private final String proxyHost;
  private final int proxyPort;
  private final File localRepositoryDir;
  private final ExecutorService executorService;
  private File stagingRepositoryDir;

  public MavenArtifactFetcher(
      String repositoryUrl,
      String repositoryUserName,
      String repositoryPassword,
      String proxyHost,
      int proxyPort,
      File localRepositoryDir,
      ExecutorService executorService) {
    this.repositoryUrl = repositoryUrl;
    this.repositoryUserName = repositoryUserName;
    this.repositoryPassword = repositoryPassword;
    this.proxyHost = proxyHost;
    this.proxyPort = proxyPort;
    this.localRepositoryDir = localRepositoryDir;
    this.executorService = executorService;
  }

  public void fetchArtifact(MavenJarArtifact artifact) {
    // Assume that if the file exists in the local repository, it has been fetched successfully.
    if (new File(localRepositoryDir, artifact.jarPath()).exists()) {
      Logger.info(String.format("Found %s in local maven repository", artifact));
      return;
    }
    this.stagingRepositoryDir = Files.createTempDir();
    this.stagingRepositoryDir.deleteOnExit();
    try {
      createArtifactSubdirectory(artifact, stagingRepositoryDir);
      Futures.whenAllSucceed(
              fetchToStagingRepository(artifact.pomSha512Path()),
              fetchToStagingRepository(artifact.pomPath()),
              fetchToStagingRepository(artifact.jarSha512Path()),
              fetchToStagingRepository(artifact.jarPath()))
          .callAsync(
              () -> {
                // double check that the artifact has not been installed
                if (new File(localRepositoryDir, artifact.jarPath()).exists()) {
                  removeArtifactFiles(stagingRepositoryDir, artifact);
                  return Futures.immediateFuture(null);
                }
                createArtifactSubdirectory(artifact, localRepositoryDir);
                ValidationResult pomResult =
                    validateStagedFiles(artifact.pomPath(), artifact.pomSha512Path());
                if (!pomResult.isSuccess()) {
                  throw new AssertionError(
                      "SHA-512 mismatch for POM file for "
                          + artifact
                          + ", expected SHA-512="
                          + pomResult.expectedHashCode()
                          + ", actual SHA-512="
                          + pomResult.calculatedHashCode());
                }
                ValidationResult jarResult =
                    validateStagedFiles(artifact.jarPath(), artifact.jarSha512Path());
                if (!jarResult.isSuccess()) {
                  throw new AssertionError(
                      "SHA-512 mismatch for POM file for "
                          + artifact
                          + ", expected SHA-512="
                          + jarResult.expectedHashCode()
                          + ", actual SHA-512="
                          + jarResult.calculatedHashCode());
                }
                Logger.info(
                    String.format(
                        "Checksums validated, moving artifact %s to local maven directory",
                        artifact));
                commitFromStaging(artifact.pomSha512Path());
                commitFromStaging(artifact.pomPath());
                commitFromStaging(artifact.jarSha512Path());
                commitFromStaging(artifact.jarPath());
                removeArtifactFiles(stagingRepositoryDir, artifact);
                return Futures.immediateFuture(null);
              },
              executorService)
          .get();
    } catch (InterruptedException | ExecutionException | IOException e) {
      if (e instanceof InterruptedException) {
        Thread.currentThread().interrupt(); // Restore the interrupted status
      }
      removeArtifactFiles(stagingRepositoryDir, artifact);
      removeArtifactFiles(localRepositoryDir, artifact);
      Logger.error("Failed to fetch maven artifact " + artifact, e);
      throw new AssertionError("Failed to fetch maven artifact " + artifact, e);
    }
  }

  private void removeArtifactFiles(File repositoryDir, MavenJarArtifact artifact) {
    new File(repositoryDir, artifact.jarPath()).delete();
    new File(repositoryDir, artifact.jarSha512Path()).delete();
    new File(repositoryDir, artifact.pomPath()).delete();
    new File(repositoryDir, artifact.pomSha512Path()).delete();
  }

  private ValidationResult validateStagedFiles(String filePath, String sha512Path)
      throws IOException {
    File tempFile = new File(this.stagingRepositoryDir, filePath);
    File sha512File = new File(this.stagingRepositoryDir, sha512Path);

    HashCode expected =
        HashCode.fromString(new String(Files.asByteSource(sha512File).read(), UTF_8));

    HashCode actual = Files.asByteSource(tempFile).hash(Hashing.sha512());
    return ValidationResult.create(expected.equals(actual), expected.toString(), actual.toString());
  }

  @AutoValue
  abstract static class ValidationResult {
    abstract boolean isSuccess();

    @Nonnull
    abstract String expectedHashCode();

    @Nonnull
    abstract String calculatedHashCode();

    static ValidationResult create(
        boolean isSuccess, String expectedHashCode, String calculatedHashCode) {
      return new AutoValue_MavenArtifactFetcher_ValidationResult(
          isSuccess, expectedHashCode, calculatedHashCode);
    }
  }

  private void createArtifactSubdirectory(MavenJarArtifact artifact, File repositoryDir)
      throws IOException {
    File jarPath = new File(repositoryDir, artifact.jarPath());
    Files.createParentDirs(jarPath);
  }

  private URL getRemoteUrl(String path) {
    String url = this.repositoryUrl;
    if (!url.endsWith("/")) {
      url = url + "/";
    }
    try {
      return new URI(url + path).toURL();
    } catch (URISyntaxException | MalformedURLException e) {
      throw new AssertionError(e);
    }
  }

  private ListenableFuture fetchToStagingRepository(String path) {
    URL remoteUrl = getRemoteUrl(path);
    File destination = new File(this.stagingRepositoryDir, path);
    return createFetchToFileTask(remoteUrl, destination);
  }

  protected ListenableFuture createFetchToFileTask(URL remoteUrl, File tempFile) {
    return Futures.submitAsync(
        new FetchToFileTask(
            remoteUrl, tempFile, repositoryUserName, repositoryPassword, proxyHost, proxyPort),
        this.executorService);
  }

  private void commitFromStaging(String path) throws IOException {
    File source = new File(this.stagingRepositoryDir, path);
    File destination = new File(this.localRepositoryDir, path);
    Files.move(source, destination);
  }

  static class FetchToFileTask implements AsyncCallable {

    private final URL remoteURL;
    private final File localFile;
    private String repositoryUserName;
    private String repositoryPassword;
    private String proxyHost;
    private int proxyPort;

    public FetchToFileTask(
        URL remoteURL,
        File localFile,
        String repositoryUserName,
        String repositoryPassword,
        String proxyHost,
        int proxyPort) {
      this.remoteURL = remoteURL;
      this.localFile = localFile;
      this.repositoryUserName = repositoryUserName;
      this.repositoryPassword = repositoryPassword;
      this.proxyHost = proxyHost;
      this.proxyPort = proxyPort;
    }

    @Override
    public ListenableFuture call() throws Exception {
      URLConnection connection;
      if (this.proxyHost != null && !this.proxyHost.isEmpty() && this.proxyPort > 0) {
        Proxy proxy =
            new Proxy(Proxy.Type.HTTP, new InetSocketAddress(this.proxyHost, this.proxyPort));
        connection = remoteURL.openConnection(proxy);
      } else {
        connection = remoteURL.openConnection();
      }
      // Add authorization header if applicable.
      if (!Strings.isNullOrEmpty(this.repositoryUserName)) {
        String encoded =
            Base64.getEncoder()
                .encodeToString(
                    (this.repositoryUserName + ":" + this.repositoryPassword).getBytes(UTF_8));
        connection.setRequestProperty("Authorization", "Basic " + encoded);
      }

      Logger.info("Transferring " + remoteURL);
      try (InputStream inputStream = connection.getInputStream();
          FileOutputStream outputStream = new FileOutputStream(localFile)) {
        ByteStreams.copy(inputStream, outputStream);
        // Ensure all contents are written to disk.
        outputStream.flush();
        outputStream.getFD().sync();
      }
      return Futures.immediateFuture(null);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy