Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
de.undercouch.gradle.tasks.download.DownloadAction Maven / Gradle / Ivy
Go to download
Adds a download task to Gradle that displays progress information
package de.undercouch.gradle.tasks.download;
import de.undercouch.gradle.tasks.download.internal.CachingHttpClientFactory;
import de.undercouch.gradle.tasks.download.internal.DefaultDownloadDetails;
import de.undercouch.gradle.tasks.download.internal.HttpClientFactory;
import de.undercouch.gradle.tasks.download.internal.ProgressLoggerWrapper;
import de.undercouch.gradle.tasks.download.internal.WorkerExecutorFuture;
import de.undercouch.gradle.tasks.download.internal.WorkerExecutorHelper;
import groovy.json.JsonOutput;
import groovy.json.JsonSlurper;
import groovy.lang.Closure;
import kotlin.jvm.functions.Function0;
import org.apache.hc.client5.http.ClientProtocolException;
import org.apache.hc.client5.http.auth.AuthCache;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.Credentials;
import org.apache.hc.client5.http.auth.CredentialsProvider;
import org.apache.hc.client5.http.auth.CredentialsStore;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.auth.BasicScheme;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.util.Timeout;
import org.gradle.api.Action;
import org.gradle.api.JavaVersion;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.Transformer;
import org.gradle.api.UncheckedIOException;
import org.gradle.api.file.Directory;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.file.RegularFile;
import org.gradle.api.file.RelativePath;
import org.gradle.api.logging.Logger;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.Provider;
import org.gradle.util.GradleVersion;
import javax.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Downloads a file and displays progress
* @author Michel Kraemer
*/
@SuppressWarnings({"ResultOfMethodCallIgnored", "CommentedOutCode"})
public class DownloadAction implements DownloadSpec, Serializable {
private static final GradleVersion HARD_MIN_GRADLE_VERSION =
GradleVersion.version("5.0");
// private static final GradleVersion SOFT_MIN_GRADLE_VERSION =
// GradleVersion.version("5.0");
// HEADS UP: FIELDS ARE POTENTIALLY ACCESSED BY MULTIPLE THREADS!
private final ProjectLayout projectLayout;
private final Logger logger;
private final Object servicesOwner;
private final ObjectFactory objectFactory;
private final boolean isOffline;
private final List sourceObjects = new ArrayList<>(1);
private List cachedSources;
private int sourceObjectsCached;
private transient Lock cachedSourcesLock = new ReentrantLock();
private Object destObject;
private File cachedDest;
private transient Lock cachedDestLock = new ReentrantLock();
private List cachedOutputFiles;
private transient Lock cachedOutputFilesLock = new ReentrantLock();
private boolean quiet = false;
private boolean overwrite = true;
private boolean onlyIfModified = false;
private boolean compress = true;
private String username;
private String password;
private boolean preemptiveAuth = false;
private Map headers;
private boolean acceptAnyCertificate = false;
private int connectTimeoutMs = 30 * 1000;
private int readTimeoutMs = 30 * 1000;
private int retries = 0;
private File downloadTaskDir;
private boolean tempAndMove = false;
private UseETag useETag = UseETag.FALSE;
private String method = "GET";
private String body;
private Transformer statusValidator;
private File cachedETagsFile;
private transient Lock cachedETagsFileLock = new ReentrantLock();
private final List> eachFileActions = new ArrayList<>();
private final AtomicInteger upToDate = new AtomicInteger(0);
/**
* Creates a new download action
* @param project the project to be built
*/
public DownloadAction(Project project) {
this(project, null);
}
/**
* Creates a new download action
* @param project the project to be built
* @param task the task to be executed, if applicable
*/
public DownloadAction(Project project, @Nullable Task task) {
// get required project properties now to enable configuration cache
this(project.getLayout(), project.getLogger(),
task != null ? task : project, project.getObjects(),
project.getGradle().getStartParameter().isOffline(),
project.getLayout().getBuildDirectory().getAsFile().get());
}
/**
* Creates a new download action
* @param projectLayout the project layout
* @param logger the project logger
* @param servicesOwner either the current project or (preferably) the
* current task
* @param objectFactory the project's object factory
* @param isOffline whether Gradle has been started in offline mode or not
* @param buildDir the project's build directory
*/
DownloadAction(ProjectLayout projectLayout, Logger logger,
Object servicesOwner, ObjectFactory objectFactory, boolean isOffline,
File buildDir) {
this.projectLayout = projectLayout;
this.logger = logger;
this.servicesOwner = servicesOwner;
this.objectFactory = objectFactory;
this.isOffline = isOffline;
this.downloadTaskDir = new File(buildDir, "download-task");
}
/**
* Starts downloading
* @return a {@link CompletableFuture} that completes once the download
* has finished
* @throws IOException if the file could not downloaded
*/
public CompletableFuture execute() throws IOException {
return execute(true);
}
/**
* Starts downloading
* @param throwOnError {@code true} if the asynchronous worker action should
* throw if the download fails. {@code false} if only the returned
* {@link CompletableFuture} should complete exceptionally.
* @return a {@link CompletableFuture} that completes once the download
* has finished
* @throws IOException if the file could not downloaded
*/
public CompletableFuture execute(boolean throwOnError) throws IOException {
if (GradleVersion.current().compareTo(HARD_MIN_GRADLE_VERSION) < 0 && !quiet) {
throw new IllegalStateException("gradle-download-task requires " +
"Gradle 5.x or higher");
}
// if (GradleVersion.current().compareTo(SOFT_MIN_GRADLE_VERSION) < 0 && !quiet) {
// logger.warn("Support for running gradle-download-task "
// + "with Gradle 2.x, 3.x, and 4.x has been deprecated and will be removed in "
// + "gradle-download-task 5.0.0");
// }
if (JavaVersion.current().compareTo(JavaVersion.VERSION_1_8) < 0 && !quiet) {
throw new IllegalStateException("gradle-download-task requires " +
"Java 8 or higher");
}
// if (JavaVersion.current().compareTo(JavaVersion.VERSION_1_8) < 0 && !quiet) {
// logger.warn("Support for running gradle-download-task "
// + "using Java 7 has been deprecated and will be removed in "
// + "gradle-download-task 5.0.0");
// }
if (sourceObjects.isEmpty()) {
throw new IllegalArgumentException("Please provide a download source");
}
if (destObject == null) {
throw new IllegalArgumentException("Please provide a download destination");
}
List sources = getSources();
File dest = getDest();
if (dest.equals(projectLayout.getBuildDirectory().get().getAsFile())) {
//make sure build dir exists
dest.mkdirs();
}
if (sources.size() > 1 && !dest.isDirectory()) {
if (!dest.exists()) {
// create directory automatically
dest.mkdirs();
} else {
throw new IllegalArgumentException("If multiple sources are provided, "
+ "the destination has to be a directory.");
}
}
if (!eachFileActions.isEmpty() && sources.size() < 2) {
throw new IllegalArgumentException("An 'eachFile' action can only " +
"be added if multiple sources are provided.");
}
List destFiles = getOutputFiles();
// make sure parent directories of target files exist
ensureTargetDirectoriesExist(destFiles);
WorkerExecutorHelper workerExecutor = WorkerExecutorHelper.newInstance(objectFactory);
CachingHttpClientFactory clientFactory = new CachingHttpClientFactory();
CompletableFuture>[] futures = new CompletableFuture[sources.size()];
for (int i = 0; i < sources.size(); i++) {
URL src = sources.get(i);
File destFile = destFiles.get(i);
// submit download job for asynchronous execution
CompletableFuture f = new CompletableFuture<>();
futures[i] = f;
workerExecutor.submit(() -> {
// create progress logger
ProgressLoggerWrapper progressLogger = new ProgressLoggerWrapper(logger);
if (!quiet) {
try {
progressLogger.init(servicesOwner, src.toString());
} catch (Exception e) {
// unable to get progress logger
logger.error("Unable to get progress logger. Download "
+ "progress will not be displayed.");
}
}
try {
execute(src, destFile, clientFactory, progressLogger);
f.complete(null);
} catch (Throwable t) {
f.completeExceptionally(t);
if (throwOnError) {
throw t;
}
}
});
}
// wait for all downloads to finish (necessary if we're on an old
// Gradle version (< 5.6) without Worker API)
if (workerExecutor.needsAwait()) {
workerExecutor.await();
}
// Create a custom completable future that calls `workerExecutor.await()`
// on any `get` call. This is necessary so we can wait for all work
// items in the queue even if we only have one thread (i.e. if
// `max-workers` equals 1). See issue #205 for more details.
CompletableFuture rf = new WorkerExecutorFuture(workerExecutor);
CompletableFuture.allOf(futures).whenComplete((v, t) -> {
// always close HTTP client factory
try {
clientFactory.close();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
if (t == null) {
rf.complete(v);
} else {
rf.completeExceptionally(t);
}
});
return rf;
}
private void execute(URL src, File destFile, HttpClientFactory clientFactory,
ProgressLoggerWrapper progressLogger) throws IOException {
if (!overwrite && destFile.exists()) {
if (!quiet) {
logger.info("Destination file already exists. "
+ "Skipping '" + destFile.getName() + "'");
}
upToDate.incrementAndGet();
return;
}
progressLogger.setDestFileName(destFile.getName());
// in case offline mode is enabled don't try to download if
// destination already exists
if (isOffline) {
if (destFile.exists()) {
if (!quiet) {
logger.info("Skipping existing file '" +
destFile.getName() + "' in offline mode.");
}
return;
}
throw new IllegalStateException("Unable to download file '" + src +
"' in offline mode.");
}
final long timestamp = onlyIfModified && destFile.exists() ? destFile.lastModified() : 0;
if ("file".equals(src.getProtocol())) {
executeFileProtocol(src, timestamp, destFile, progressLogger);
} else {
executeHttpProtocol(src, clientFactory, timestamp, destFile, progressLogger);
}
}
private void executeFileProtocol(URL src, long timestamp, File destFile,
ProgressLoggerWrapper progressLogger) throws IOException {
File srcFile = null;
try {
srcFile = new File(src.toURI());
progressLogger.setSize(srcFile.length());
} catch (URISyntaxException e) {
logger.warn("Unable to determine file length.");
}
//check if file was modified
long lastModified = 0;
if (srcFile != null) {
lastModified = srcFile.lastModified();
if (lastModified != 0 && timestamp >= lastModified) {
if (!quiet) {
logger.info("Not modified. Skipping '" + src + "'");
}
upToDate.incrementAndGet();
return;
}
}
BufferedInputStream fileStream = new BufferedInputStream(src.openStream());
streamAndMove(fileStream, destFile, progressLogger);
//set last-modified time of destination file
if (onlyIfModified && lastModified > 0) {
destFile.setLastModified(lastModified);
}
}
private void executeHttpProtocol(URL src, HttpClientFactory clientFactory,
long timestamp, File destFile, ProgressLoggerWrapper progressLogger)
throws IOException {
// create HTTP host from URL
HttpHost httpHost = new HttpHost(src.getProtocol(), src.getHost(), src.getPort());
// create HTTP client
CloseableHttpClient client = clientFactory.createHttpClient(
httpHost, acceptAnyCertificate, retries, connectTimeoutMs,
headers, logger, quiet);
// get cached ETag if there is any
String etag = null;
if (onlyIfModified && useETag.enabled && destFile.exists()) {
etag = getCachedETag(httpHost, src.getFile());
if (!useETag.useWeakETags && isWeakETag(etag)) {
etag = null;
}
}
// open URL connection
openConnection(httpHost, src.getFile(), timestamp, etag, client, response -> {
// check if file on server was modified
long lastModified = parseLastModified(response);
int code = response.getCode();
if (code == HttpStatus.SC_NOT_MODIFIED ||
(lastModified != 0 && timestamp >= lastModified)) {
if (!quiet) {
logger.info("Not modified. Skipping '" + src + "'");
}
upToDate.incrementAndGet();
return null;
}
// perform the download
performDownload(response, destFile, progressLogger);
// set last-modified time of destination file
long newTimestamp = parseLastModified(response);
if (onlyIfModified && newTimestamp > 0) {
destFile.setLastModified(newTimestamp);
}
// store ETag
if (onlyIfModified && useETag.enabled) {
storeETag(httpHost, src.getFile(), response);
}
return null;
});
}
/**
* Save an HTTP response to a file
* @param response the response to save
* @param destFile the destination file
* @param progressLogger progress logger
* @throws IOException if the response could not be downloaded
*/
private void performDownload(ClassicHttpResponse response, File destFile,
ProgressLoggerWrapper progressLogger) throws IOException {
HttpEntity entity = response.getEntity();
if (entity == null) {
return;
}
// get content length
long contentLength = entity.getContentLength();
if (contentLength >= 0) {
progressLogger.setSize(contentLength);
}
// open stream and start downloading
InputStream is = entity.getContent();
streamAndMove(is, destFile, progressLogger);
}
/**
* Move a file by calling {@link File#renameTo(File)} first and, if this
* fails, by copying and deleting it.
* @param src the file to move
* @param dest the destination
* @param progressLogger progress logger
* @throws IOException if the file could not be moved
*/
private void moveFile(File src, File dest,
ProgressLoggerWrapper progressLogger) throws IOException {
if (src.renameTo(dest)) {
return;
}
// renameTo() failed. Try to copy the file and delete it afterwards.
// see issue #146
try (InputStream is = Files.newInputStream(src.toPath())) {
stream(is, dest, progressLogger);
}
if (!src.delete()) {
throw new IOException("Could not delete temporary file '" +
src.getAbsolutePath() + "' after copying it to '" +
dest.getAbsolutePath() + "'.");
}
}
/**
* If {@link #tempAndMove} is true
, copy bytes from an input
* stream to a temporary file and log progress. Upon successful
* completion, move the temporary file to the given destination. If
* {@link #tempAndMove} is false
, just forward to
* {@link #stream(InputStream, File, ProgressLoggerWrapper)}.
* @param is the input stream to read
* @param destFile the destination file
* @param progressLogger progress logger
* @throws IOException if an I/O error occurs
*/
private void streamAndMove(InputStream is, File destFile,
ProgressLoggerWrapper progressLogger) throws IOException {
if (!tempAndMove) {
stream(is, destFile, progressLogger);
} else {
//create parent directory
downloadTaskDir.mkdirs();
//create name of temporary file
File tempFile = File.createTempFile(destFile.getName(), ".part",
downloadTaskDir);
//stream and move
stream(is, tempFile, progressLogger);
if (destFile.exists()) {
//Delete destFile if it exists before renaming tempFile.
//Otherwise renaming might fail.
if (!destFile.delete()) {
throw new IOException("Could not delete old destination file '" +
destFile.getAbsolutePath() + "'.");
}
}
try {
moveFile(tempFile, destFile, progressLogger);
} catch (IOException e) {
throw new IOException("Failed to move temporary file '" +
tempFile.getAbsolutePath() + "' to destination file '" +
destFile.getAbsolutePath() + "'.", e);
}
}
}
/**
* Copy bytes from an input stream to a file and log progress
* @param is the input stream to read
* @param destFile the file to write to
* @param progressLogger progress logger
* @throws IOException if an I/O error occurs
*/
private void stream(InputStream is, File destFile,
ProgressLoggerWrapper progressLogger) throws IOException {
try {
progressLogger.started();
boolean finished = false;
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(
destFile.toPath(), StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.CREATE)) {
long pos = 0;
Future writeFuture = null;
byte[] buf1 = new byte[1024 * 10];
byte[] buf2 = new byte[1024 * 10];
ByteBuffer bb1 = ByteBuffer.wrap(buf1);
ByteBuffer bb2 = ByteBuffer.wrap(buf2);
int read;
while ((read = is.read(buf1)) >= 0) {
if (writeFuture != null) {
writeFuture.get();
}
bb1.position(0);
bb1.limit(read);
writeFuture = channel.write(bb1, pos);
pos += read;
progressLogger.incrementProgress(read);
// swap buffers for next asynchronous operation
byte[] tmpBuf = buf1;
buf1 = buf2;
buf2 = tmpBuf;
ByteBuffer tmpBB = bb1;
bb1 = bb2;
bb2 = tmpBB;
}
if (writeFuture != null) {
writeFuture.get();
}
finished = true;
} catch (InterruptedException e) {
throw new IOException("Writing to destination file was interrupted", e);
} catch (ExecutionException e) {
if (e.getCause() instanceof IOException) {
throw (IOException)e.getCause();
}
throw new IOException("Could not write to destination file", e);
} finally {
if (!finished) {
destFile.delete();
}
}
} finally {
is.close();
progressLogger.completed();
}
}
/**
* Make sure the parent directories of all given files exist and create
* them if necessary
* @param destFiles the files
*/
private void ensureTargetDirectoriesExist(List destFiles) {
Set parentDirs = new HashSet<>();
for (File o : destFiles) {
File parentDir = o.getParentFile();
if (parentDir != null && parentDirs.add(parentDir)) {
parentDir.mkdirs();
}
}
}
/**
* Reads the {@link #cachedETagsFile}
* @return a map containing the parsed contents of the cached etags file
* or an empty map if the file does not exist yet
*/
@SuppressWarnings("unchecked")
private Map readCachedETags() {
cachedETagsFileLock.lock();
try {
File cachedETagsFile = getCachedETagsFile();
Map cachedETags;
if (cachedETagsFile.exists()) {
JsonSlurper slurper = new JsonSlurper();
cachedETags = (Map)slurper.parse(cachedETagsFile, "UTF-8");
} else {
cachedETags = new LinkedHashMap<>();
}
return cachedETags;
} finally {
cachedETagsFileLock.unlock();
}
}
/**
* Get the cached ETag for the given host and file
* @param host the host
* @param file the file
* @return the cached ETag or null if there is no ETag in the cache
*/
private String getCachedETag(HttpHost host, String file) {
Map cachedETags = readCachedETags();
@SuppressWarnings("unchecked")
Map hostMap =
(Map)cachedETags.get(host.toURI());
if (hostMap == null) {
return null;
}
@SuppressWarnings("unchecked")
Map etagMap = (Map)hostMap.get(file);
if (etagMap == null) {
return null;
}
return etagMap.get("ETag");
}
/**
* Store the ETag header from the given response in the {@link #cachedETagsFile}
* @param host the queried host
* @param file the queried file
* @param response the HTTP response
* @throws IOException if the tag could not be written
*/
@SuppressWarnings("unchecked")
private void storeETag(HttpHost host, String file, HttpResponse response)
throws IOException {
//get ETag header
Header etagHdr = response.getFirstHeader("ETag");
if (etagHdr == null) {
if (!quiet) {
logger.warn("Server response does not include an "
+ "entity tag (ETag).");
}
return;
}
String etag = etagHdr.getValue();
//handle weak ETags
if (isWeakETag(etag)) {
if (useETag.displayWarningForWeak && !quiet) {
logger.warn("Weak entity tag (ETag) encountered. "
+ "Please make sure you want to compare resources based on "
+ "weak ETags. If yes, set the 'useETag' flag to \"all\", "
+ "otherwise set it to \"strongOnly\".");
}
if (!useETag.useWeakETags) {
//do not save weak etags
return;
}
}
cachedETagsFileLock.lock();
try {
// create directory for cached etags file
File parent = getCachedETagsFile().getParentFile();
if (parent != null) {
parent.mkdirs();
}
// read existing cached etags file
Map cachedETags = readCachedETags();
// create new entry in cached ETags file
Map etagMap = new LinkedHashMap<>();
etagMap.put("ETag", etag);
String uri = host.toURI();
Map hostMap = (Map)cachedETags.get(uri);
if (hostMap == null) {
hostMap = new LinkedHashMap<>();
cachedETags.put(uri, hostMap);
}
hostMap.put(file, etagMap);
// write cached ETags file
String cachedETagsContents = JsonOutput.toJson(cachedETags);
try (PrintWriter writer = new PrintWriter(getCachedETagsFile(), "UTF-8")) {
writer.write(cachedETagsContents);
writer.flush();
}
} finally {
cachedETagsFileLock.unlock();
}
}
/**
* Checks if the given ETag is a weak one
* @param etag the ETag
* @return true if etag
is weak
*/
private boolean isWeakETag(String etag) {
return etag != null && etag.startsWith("W/");
}
/**
* Generates the path to an output file for a given source URL
* @param src the source
* @param multipleSources {@code true} if multiple sources have been specified
* and the output file should be created inside a destination directory
* @return the path to the output file
*/
private File makeDestFile(URL src, boolean multipleSources) {
File destFile = getDest();
if (destFile == null) {
throw new IllegalArgumentException("Please provide a download destination");
}
boolean isDirectory = destFile.isDirectory() ||
destFile.equals(projectLayout.getBuildDirectory().get().getAsFile());
if (multipleSources || isDirectory) {
// guess name from URL
String name = src.toString();
if (name.endsWith("/")) {
name = name.substring(0, name.length() - 1);
}
name = name.substring(name.lastIndexOf('/') + 1);
// Call eachFile actions. We only need to do this if destFile is a
// directory because eachFile actions can only be specified if
// multiple sources are provided and this in turn requires destFile
// to be a directory.
RelativePath path = RelativePath.parse(true, name);
if (!eachFileActions.isEmpty()) {
DownloadDetails details = new DefaultDownloadDetails(path, src);
for (Action super DownloadDetails> a : eachFileActions) {
a.execute(details);
}
path = details.getRelativePath();
}
destFile = path.getFile(destFile);
}
return destFile;
}
/**
* Opens a connection to the given HTTP host and requests a file. Checks
* the last-modified header on the server if the given timestamp is
* greater than 0.
* @param httpHost the HTTP host to connect to
* @param file the file to request
* @param timestamp the timestamp of the destination file, in milliseconds
* @param etag the cached ETag for the requested host and file
* @param client the HTTP client to use to perform the request
* @param responseHandler a callback that handles the HTTP response
* @throws IOException if the connection could not be opened
*/
private void openConnection(HttpHost httpHost, String file,
long timestamp, String etag, CloseableHttpClient client,
HttpClientResponseHandler responseHandler) throws IOException {
// configure authentication
HttpClientContext context = null;
if (username != null && password != null) {
context = HttpClientContext.create();
Credentials c = new UsernamePasswordCredentials(username, password.toCharArray());
addAuthentication(httpHost, c, context, preemptiveAuth);
}
// create request
HttpUriRequestBase req = new HttpUriRequestBase(
this.method.toUpperCase(Locale.ROOT), URI.create(file));
if (body != null) {
req.setEntity(new StringEntity(body));
}
// configure timeouts
RequestConfig config = RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofMilliseconds(connectTimeoutMs))
.setResponseTimeout(Timeout.ofMilliseconds(readTimeoutMs))
.setContentCompressionEnabled(compress)
.build();
req.setConfig(config);
// add authentication information for proxy
String scheme = httpHost.getSchemeName();
String proxyHost = System.getProperty(scheme + ".proxyHost");
String proxyPort = System.getProperty(scheme + ".proxyPort");
String proxyUser = System.getProperty(scheme + ".proxyUser");
String proxyPassword = System.getProperty(scheme + ".proxyPassword");
if (proxyHost != null && proxyPort != null &&
proxyUser != null && proxyPassword != null) {
if (context == null) {
context = HttpClientContext.create();
}
int nProxyPort = Integer.parseInt(proxyPort);
HttpHost proxy = new HttpHost(scheme, proxyHost, nProxyPort);
Credentials credentials = new UsernamePasswordCredentials(
proxyUser, proxyPassword.toCharArray());
addAuthentication(proxy, credentials, context, false);
}
// set If-Modified-Since header
if (timestamp > 0) {
req.setHeader("If-Modified-Since", DateUtils.formatStandardDate(
Instant.ofEpochMilli(timestamp)));
}
// set If-None-Match header
if (etag != null) {
req.setHeader("If-None-Match", etag);
}
// set headers
if (headers != null) {
for (Map.Entry headerEntry : headers.entrySet()) {
req.addHeader(headerEntry.getKey(), headerEntry.getValue());
}
}
// execute request
client.execute(httpHost, req, context, response -> {
// handle response
int code = response.getCode();
boolean valid;
if (statusValidator != null) {
valid = Boolean.TRUE.equals(statusValidator.transform(code));
} else {
valid = (code >= 200 && code <= 299) || code == HttpStatus.SC_NOT_MODIFIED;
}
if (!valid) {
String url = httpHost + file;
String message = "HTTP status code: " + code + ", URL: " + url;
if (code == HttpStatus.SC_UNAUTHORIZED &&
!response.containsHeader(HttpHeaders.WWW_AUTHENTICATE)) {
message += ". Missing " + HttpHeaders.WWW_AUTHENTICATE +
" header in response; use the preemptiveAuth flag" +
" to send credentials in the first request.";
}
String phrase = response.getReasonPhrase();
if (phrase == null || phrase.isEmpty()) {
phrase = message;
} else {
phrase += " (" + message + ")";
}
throw new ClientProtocolException(phrase);
}
return responseHandler.handleResponse(response);
});
}
/**
* Add authentication information for the given host
* @param host the host
* @param credentials the credentials
* @param context the context in which the authentication information
* should be saved
*/
private void addAuthentication(HttpHost host, Credentials credentials,
HttpClientContext context, boolean preemptiveAuth) {
AuthCache authCache = context.getAuthCache();
if (authCache == null) {
authCache = new BasicAuthCache();
context.setAuthCache(authCache);
}
CredentialsProvider credsProvider = context.getCredentialsProvider();
if (credsProvider == null) {
credsProvider = new BasicCredentialsProvider();
context.setCredentialsProvider(credsProvider);
}
if (preemptiveAuth) {
BasicScheme basicAuth = new BasicScheme();
basicAuth.initPreemptive(credentials);
authCache.put(host, basicAuth);
}
((CredentialsStore)credsProvider).setCredentials(new AuthScope(host), credentials);
}
/**
* Parse the Last-Modified header of a {@link HttpResponse}
* @param response the {@link HttpResponse}
* @return the last-modified value or 0 if it is unknown
*/
private long parseLastModified(HttpResponse response) {
Header header = response.getLastHeader("Last-Modified");
if (header == null) {
return 0;
}
String value = header.getValue();
if (value == null || value.isEmpty()) {
return 0;
}
Instant date = DateUtils.parseStandardDate(value);
if (date == null) {
return 0;
}
return date.toEpochMilli();
}
/**
* @return true if the download destination is up to date
*/
public boolean isUpToDate() {
return upToDate.get() == getSources().size();
}
/**
* @return a list of files created by this action (i.e. the destination files)
* in the same order as the sources returned by {@link #getSources()}
*/
public List getOutputFiles() {
List sources = getSources();
cachedOutputFilesLock.lock();
try {
if (cachedOutputFiles != null && cachedOutputFiles.size() == sources.size()) {
return Collections.unmodifiableList(cachedOutputFiles);
}
// Create a new list if it does not exist yet or reset the list
// if it previously only contained one element. The latter is
// necessary, because 'multipleSources' below will become true and
// the output file will be different.
if (cachedOutputFiles == null || cachedOutputFiles.size() == 1) {
cachedOutputFiles = new ArrayList<>(sources.size());
}
// update cache
boolean multipleSources = sources.size() > 1;
Set distinctFiles = new HashSet<>(cachedOutputFiles);
for (int i = cachedOutputFiles.size(); i < sources.size(); ++i) {
File destFile = makeDestFile(sources.get(i), multipleSources);
cachedOutputFiles.add(destFile);
if (!distinctFiles.add(destFile)) {
throw new IllegalArgumentException("Duplicate destination " +
"file '" + destFile + "'");
}
}
return Collections.unmodifiableList(cachedOutputFiles);
} finally {
cachedOutputFilesLock.unlock();
}
}
@Override
public void src(Object src) {
sourceObjects.add(src);
}
@Override
public void dest(Object dest) {
destObject = dest;
}
@Override
public void quiet(boolean quiet) {
this.quiet = quiet;
}
@Override
public void overwrite(boolean overwrite) {
this.overwrite = overwrite;
}
@Override
public void onlyIfModified(boolean onlyIfModified) {
this.onlyIfModified = onlyIfModified;
}
@Override
public void onlyIfNewer(boolean onlyIfNewer) {
onlyIfModified(onlyIfNewer);
}
@Override
public void compress(boolean compress) {
this.compress = compress;
}
@Override
public void username(String username) {
this.username = username;
}
@Override
public void password(String password) {
this.password = password;
}
@Override
public void preemptiveAuth(boolean preemptiveAuth) {
this.preemptiveAuth = preemptiveAuth;
}
@Override
public void headers(Map headers) {
if (this.headers == null) {
this.headers = new LinkedHashMap<>();
} else {
this.headers.clear();
}
if (headers != null) {
this.headers.putAll(headers);
}
}
@Override
public void header(String name, String value) {
if (headers == null) {
headers = new LinkedHashMap<>();
}
headers.put(name, value);
}
@Override
public void acceptAnyCertificate(boolean accept) {
this.acceptAnyCertificate = accept;
}
@Override
public void connectTimeout(int milliseconds) {
this.connectTimeoutMs = milliseconds;
}
@Override
public void readTimeout(int milliseconds) {
this.readTimeoutMs = milliseconds;
}
@Override
public void retries(int retries) {
this.retries = retries;
}
/**
* Get a destination file from a property. This method accepts various
* input objects and tries to convert them to a {@link File} object
* @param dir the property
* @return the {@link File} object or {@code null} if the property was
* {@code null} or could not be converted
*/
private File getDestinationFromDirProperty(Object dir) {
if (dir instanceof Function0) {
// lazily evaluate Kotlin function
Function0> function = (Function0>)dir;
dir = function.invoke();
}
if (dir instanceof Closure) {
// lazily evaluate closure
Closure> closure = (Closure>)dir;
dir = closure.call();
}
if (dir instanceof Provider) {
dir = ((Provider>)dir).getOrNull();
}
if (dir instanceof CharSequence) {
return projectLayout.getProjectDirectory().file(dir.toString()).getAsFile();
} else if (dir instanceof Directory) {
File f = ((Directory)dir).getAsFile();
// Make sure the directory exists so we actually download to a file
// inside this directory. Otherwise, we will just create a file
// with the name of this directory.
f.mkdirs();
return f;
} else if (dir instanceof RegularFile) {
return ((RegularFile)dir).getAsFile();
} else if (dir instanceof File) {
return (File)dir;
}
return null;
}
@Override
public void downloadTaskDir(Object dir) {
downloadTaskDir = getDestinationFromDirProperty(dir);
if (downloadTaskDir == null) {
throw new IllegalArgumentException("download-task directory must " +
"either be a File or a CharSequence");
}
}
@Override
public void tempAndMove(boolean tempAndMove) {
this.tempAndMove = tempAndMove;
}
@Override
public void useETag(Object useETag) {
this.useETag = UseETag.fromValue(useETag);
}
@Override
public void cachedETagsFile(Object location) {
if (location instanceof Function0) {
// lazily evaluate Kotlin function
Function0> function = (Function0>)location;
location = function.invoke();
}
if (location instanceof Closure) {
// lazily evaluate closure
Closure> closure = (Closure>)location;
location = closure.call();
}
if (location instanceof Provider) {
location = ((Provider>)location).getOrNull();
}
if (location instanceof CharSequence) {
this.cachedETagsFile = projectLayout.getProjectDirectory()
.file(location.toString()).getAsFile();
} else if (location instanceof RegularFile) {
this.cachedETagsFile = ((RegularFile)location).getAsFile();
} else if (location instanceof File) {
this.cachedETagsFile = (File)location;
} else {
throw new IllegalArgumentException("Location for cached ETags must " +
"either be a File or a CharSequence");
}
}
@Override
public void eachFile(Action super DownloadDetails> action) {
eachFileActions.add(action);
}
@Override
public void method(String method) {
if (method == null) {
throw new IllegalArgumentException("HTTP method must not be null");
}
this.method = method;
}
@Override
public void body(String body) {
this.body = body;
}
@Override
public void validateStatus(Transformer validator) {
this.statusValidator = validator;
}
/**
* Recursively convert the given source to a list of URLs
* @param src the source to convert
* @return the list of URLs
*/
private List convertSource(Object src) {
List result = new ArrayList<>();
if (src instanceof Function0) {
// lazily evaluate Kotlin function
Function0> function = (Function0>)src;
src = function.invoke();
}
if (src instanceof Closure) {
// lazily evaluate closure
Closure> closure = (Closure>)src;
src = closure.call();
}
if (src instanceof Provider) {
src = ((Provider>)src).getOrNull();
}
if (src instanceof CharSequence) {
try {
result.add(new URL(src.toString()));
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid source URL", e);
}
} else if (src instanceof URL) {
result.add((URL)src);
} else if (src instanceof URI) {
try {
result.add(((URI)src).toURL());
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid source URL", e);
}
} else if (src instanceof Collection) {
Collection> sc = (Collection>)src;
for (Object sco : sc) {
result.addAll(convertSource(sco));
}
} else if (src != null && src.getClass().isArray()) {
int len = Array.getLength(src);
for (int i = 0; i < len; ++i) {
Object sco = Array.get(src, i);
result.addAll(convertSource(sco));
}
} else {
throw new IllegalArgumentException("Download source must either " +
"be a URL, a URI, a CharSequence, a Collection or an array.");
}
return result;
}
/**
* Evaluate {@link #sourceObjects} and return a list of source URLs.
* Cache the result in {@link #cachedSources}
* @return the list of URLs
*/
private List getSources() {
cachedSourcesLock.lock();
try {
if (cachedSources != null && sourceObjectsCached == sourceObjects.size()) {
return cachedSources;
}
if (cachedSources == null) {
cachedSources = new ArrayList<>(sourceObjects.size());
}
// update cache
for (int i = sourceObjectsCached; i < sourceObjects.size(); ++i) {
cachedSources.addAll(convertSource(sourceObjects.get(i)));
}
sourceObjectsCached = sourceObjects.size();
return cachedSources;
} finally {
cachedSourcesLock.unlock();
}
}
@Override
public Object getSrc() {
List sources = getSources();
if (sources.size() == 1) {
return sources.get(0);
}
return sources;
}
@Override
public File getDest() {
cachedDestLock.lock();
try {
if (cachedDest != null) {
return cachedDest;
}
cachedDest = getDestinationFromDirProperty(destObject);
if (cachedDest == null) {
throw new IllegalArgumentException("Download destination must " +
"be one of a File, Directory, RegularFile, or a CharSequence");
}
return cachedDest;
} finally {
cachedDestLock.unlock();
}
}
@Override
public boolean isQuiet() {
return quiet;
}
@Override
public boolean isOverwrite() {
return overwrite;
}
@Override
public boolean isOnlyIfModified() {
return onlyIfModified;
}
@Override
public boolean isOnlyIfNewer() {
return isOnlyIfModified();
}
@Override
public boolean isCompress() {
return compress;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public boolean isPreemptiveAuth() {
return preemptiveAuth;
}
@Override
public Map getHeaders() {
return headers;
}
@Override
public String getHeader(String name) {
if (headers == null) {
return null;
}
return headers.get(name);
}
@Override
public boolean isAcceptAnyCertificate() {
return acceptAnyCertificate;
}
@Override
public int getConnectTimeout() {
return connectTimeoutMs;
}
@Override
public int getReadTimeout() {
return readTimeoutMs;
}
@Override
public int getRetries() {
return retries;
}
@Override
public File getDownloadTaskDir() {
return downloadTaskDir;
}
@Override
public boolean isTempAndMove() {
return tempAndMove;
}
@Override
public Object getUseETag() {
return useETag.value;
}
@Override
public File getCachedETagsFile() {
cachedETagsFileLock.lock();
try {
if (cachedETagsFile == null) {
return new File(this.downloadTaskDir, "etags.json");
}
return cachedETagsFile;
} finally {
cachedETagsFileLock.unlock();
}
}
@Override
public String getMethod() {
return method;
}
@Override
public String getBody() {
return body;
}
/**
* In order to support Gradle's configuration cache, we need to make some
* fields transient. This method re-initializes these fields after the
* object has been read from the cache.
* @param in the input stream to read the object from the cache
* @throws IOException if the object could not be read
* @throws ClassNotFoundException if the object could not be deserialized
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// initialize transient fields
cachedSourcesLock = new ReentrantLock();
cachedDestLock = new ReentrantLock();
cachedOutputFilesLock = new ReentrantLock();
cachedETagsFileLock = new ReentrantLock();
}
/**
* Possible values for the "useETag" flag
*/
private enum UseETag {
/**
* Do not use ETags
*/
FALSE(Boolean.FALSE, false, false, false),
/**
* Use all ETags but display a warning for weak ones
*/
TRUE(Boolean.TRUE, true, true, true),
/**
* Use all ETags but do not display a warning for weak ones
*/
ALL("all", true, true, false),
/**
* Use only strong ETags
*/
STRONG_ONLY("strongOnly", true, false, false);
final Object value;
final boolean enabled;
final boolean useWeakETags;
final boolean displayWarningForWeak;
UseETag(Object value, boolean useAnyETag, boolean useWeakETags,
boolean displayWarningForWeak) {
this.value = value;
this.enabled = useAnyETag;
this.useWeakETags = useWeakETags;
this.displayWarningForWeak = displayWarningForWeak;
}
static UseETag fromValue(Object value) {
if (TRUE.value.equals(value)) {
return TRUE;
} else if (FALSE.value.equals(value)) {
return FALSE;
} else if (value instanceof String) {
String s = (String)value;
if (ALL.value.equals(s)) {
return ALL;
} else if (STRONG_ONLY.value.equals(s)) {
return STRONG_ONLY;
} else if ("true".equalsIgnoreCase(s)) {
return TRUE;
} else if ("false".equalsIgnoreCase(s)) {
return TRUE;
}
}
throw new IllegalArgumentException("Illegal value for 'useETag' flag");
}
}
}