diff --git a/README.md b/README.md index bb6e1c9c..4d3dee69 100644 --- a/README.md +++ b/README.md @@ -327,6 +327,12 @@ ETags. Possible values are:
cachedETagsFile
The location of the file that keeps entity tags (ETags) received from the server. (default: ${downloadTaskDir}/etags.json)
+
method
+
The HTTP method to use (default: GET)
+
body
+
An optional request body. As gradle-download-task is meant for downloading +and not for uploading, only simple strings are supported. +(optional)
Verify task diff --git a/src/main/java/de/undercouch/gradle/tasks/download/Download.java b/src/main/java/de/undercouch/gradle/tasks/download/Download.java index 947ddc6a..d9b7cbd3 100644 --- a/src/main/java/de/undercouch/gradle/tasks/download/Download.java +++ b/src/main/java/de/undercouch/gradle/tasks/download/Download.java @@ -200,6 +200,16 @@ public void eachFile(Action action) { this.action.eachFile(action); } + @Override + public void method(String method) { + action.method(method); + } + + @Override + public void body(String body) { + action.body(body); + } + @Input @Override public Object getSrc() { @@ -322,4 +332,18 @@ public Object getUseETag() { public File getCachedETagsFile() { return action.getCachedETagsFile(); } + + @Input + @Optional + @Override + public String getMethod() { + return this.action.getMethod(); + } + + @Input + @Optional + @Override + public String getBody() { + return this.action.getBody(); + } } diff --git a/src/main/java/de/undercouch/gradle/tasks/download/DownloadAction.java b/src/main/java/de/undercouch/gradle/tasks/download/DownloadAction.java index f0977821..5bf562fc 100644 --- a/src/main/java/de/undercouch/gradle/tasks/download/DownloadAction.java +++ b/src/main/java/de/undercouch/gradle/tasks/download/DownloadAction.java @@ -17,7 +17,7 @@ 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.HttpGet; +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; @@ -33,6 +33,7 @@ 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; @@ -58,6 +59,7 @@ 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; @@ -71,6 +73,7 @@ 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; @@ -121,6 +124,8 @@ public class DownloadAction implements DownloadSpec, Serializable { private File downloadTaskDir; private boolean tempAndMove = false; private UseETag useETag = UseETag.FALSE; + private String method = "GET"; + private String body; private File cachedETagsFile; private transient Lock cachedETagsFileLock = new ReentrantLock(); private final List> eachFileActions = new ArrayList<>(); @@ -794,8 +799,12 @@ private void openConnection(HttpHost httpHost, String file, addAuthentication(httpHost, c, context); } - //c reate request - HttpGet get = new HttpGet(file); + // 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() @@ -803,7 +812,7 @@ private void openConnection(HttpHost httpHost, String file, .setResponseTimeout(Timeout.ofMilliseconds(readTimeoutMs)) .setContentCompressionEnabled(compress) .build(); - get.setConfig(config); + req.setConfig(config); // add authentication information for proxy String scheme = httpHost.getSchemeName(); @@ -825,24 +834,24 @@ private void openConnection(HttpHost httpHost, String file, // set If-Modified-Since header if (timestamp > 0) { - get.setHeader("If-Modified-Since", DateUtils.formatStandardDate( + req.setHeader("If-Modified-Since", DateUtils.formatStandardDate( Instant.ofEpochMilli(timestamp))); } // set If-None-Match header if (etag != null) { - get.setHeader("If-None-Match", etag); + req.setHeader("If-None-Match", etag); } // set headers if (headers != null) { for (Map.Entry headerEntry : headers.entrySet()) { - get.addHeader(headerEntry.getKey(), headerEntry.getValue()); + req.addHeader(headerEntry.getKey(), headerEntry.getValue()); } } // execute request - client.execute(httpHost, get, context, response -> { + client.execute(httpHost, req, context, response -> { // handle response int code = response.getCode(); if ((code < 200 || code > 299) && code != HttpStatus.SC_NOT_MODIFIED) { @@ -1147,6 +1156,19 @@ public void eachFile(Action 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; + } + /** * Recursively convert the given source to a list of URLs * @param src the source to convert @@ -1353,6 +1375,16 @@ public File getCachedETagsFile() { } } + @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 diff --git a/src/main/java/de/undercouch/gradle/tasks/download/DownloadSpec.java b/src/main/java/de/undercouch/gradle/tasks/download/DownloadSpec.java index c3cdec48..80f757cc 100644 --- a/src/main/java/de/undercouch/gradle/tasks/download/DownloadSpec.java +++ b/src/main/java/de/undercouch/gradle/tasks/download/DownloadSpec.java @@ -200,6 +200,19 @@ public interface DownloadSpec { */ void eachFile(Action action); + /** + * Sets the HTTP method to use + * @param method the HTTP method (default: {@code GET}) + */ + void method(String method); + + /** + * Sets an optional request body to send to the server before the download. + * By default, the request will not have a body. + * @param body the request body ({@code null} to send no body) + */ + void body(String body); + /** * @return the download source(s), either a URL or a list of URLs */ @@ -311,4 +324,15 @@ public interface DownloadSpec { * from the server */ File getCachedETagsFile(); + + /** + * @return the HTTP method to use (default: {@code GET}) + */ + String getMethod(); + + /** + * @return an optional request body to send to the server before + * downloading (default: {@code null}) + */ + String getBody(); } diff --git a/src/test/java/de/undercouch/gradle/tasks/download/MethodTest.java b/src/test/java/de/undercouch/gradle/tasks/download/MethodTest.java new file mode 100644 index 00000000..5523e535 --- /dev/null +++ b/src/test/java/de/undercouch/gradle/tasks/download/MethodTest.java @@ -0,0 +1,113 @@ +// Copyright 2013-2023 Michel Kraemer +// +// 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 de.undercouch.gradle.tasks.download; + +import com.github.tomakehurst.wiremock.matching.MatchResult; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.charset.StandardCharsets; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.put; +import static com.github.tomakehurst.wiremock.client.WireMock.requestMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests if another HTTP method can be used + * @author Michel Kraemer + */ +public class MethodTest extends TestBaseWithMockServer { + private void testWithMethod(String method) throws Exception { + Download t = makeProjectAndTask(); + t.src(wireMock.url(TEST_FILE_NAME)); + File dst = newTempFile(); + t.dest(dst); + t.method(method); + execute(t); + + assertThat(dst).usingCharset(StandardCharsets.UTF_8).hasContent(CONTENTS); + } + + /** + * Tests that we can download a file using HTTP POST + * @throws Exception if anything goes wrong + */ + @Test + public void postMethod() throws Exception { + stubFor(post(urlEqualTo("/" + TEST_FILE_NAME)) + .willReturn(aResponse() + .withBody(CONTENTS))); + testWithMethod("POST"); + } + + /** + * Tests that we can download a file using HTTP PUT + * @throws Exception if anything goes wrong + */ + @Test + public void putMethod() throws Exception { + stubFor(put(urlEqualTo("/" + TEST_FILE_NAME)) + .willReturn(aResponse() + .withBody(CONTENTS))); + testWithMethod("PUT"); + } + + /** + * Tests that we can download a file using HTTP POST even if we specify + * the method in mixed case + * @throws Exception if anything goes wrong + */ + @Test + public void caseInsensitive() throws Exception { + stubFor(post(urlEqualTo("/" + TEST_FILE_NAME)) + .willReturn(aResponse() + .withBody(CONTENTS))); + testWithMethod("poSt"); + } + + /** + * Tests that we can download a file using a custom HTTP method + * the method in mixed case + * @throws Exception if anything goes wrong + */ + @Test + public void customMethod() throws Exception { + stubFor(requestMatching(r -> { + if (r.getMethod().getName().equals("CUSTOM") && + r.getUrl().equals("/" + TEST_FILE_NAME)) { + return MatchResult.exactMatch(); + } + return MatchResult.noMatch(); + }).willReturn(aResponse().withBody(CONTENTS))); + testWithMethod("custom"); + } + + /** + * Makes sure {@code null} cannot be used as method + * @throws Exception if anything goes wrong + */ + @Test + public void nullMethod() throws Exception { + Download t = makeProjectAndTask(); + assertThat(t.getMethod()).isEqualTo("GET"); + assertThatThrownBy(() -> t.method(null)).isInstanceOf( + IllegalArgumentException.class); + } +} diff --git a/src/test/java/de/undercouch/gradle/tasks/download/PostWithBodyTest.java b/src/test/java/de/undercouch/gradle/tasks/download/PostWithBodyTest.java new file mode 100644 index 00000000..f54865b9 --- /dev/null +++ b/src/test/java/de/undercouch/gradle/tasks/download/PostWithBodyTest.java @@ -0,0 +1,57 @@ +// Copyright 2013-2023 Michel Kraemer +// +// 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 de.undercouch.gradle.tasks.download; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.charset.StandardCharsets; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests if another we can POST a request body + * @author Michel Kraemer + */ +public class PostWithBodyTest extends TestBaseWithMockServer { + /** + * Tests that we can download a file using HTTP POST and a body + * @throws Exception if anything goes wrong + */ + @Test + public void postWithBody() throws Exception { + String body = "This is a body"; + + stubFor(post(urlEqualTo("/" + TEST_FILE_NAME)) + .withRequestBody(equalTo(body)) + .willReturn(aResponse() + .withBody(CONTENTS))); + + Download t = makeProjectAndTask(); + t.src(wireMock.url(TEST_FILE_NAME)); + File dst = newTempFile(); + t.dest(dst); + t.method("POST"); + t.body(body); + execute(t); + + assertThat(dst).usingCharset(StandardCharsets.UTF_8).hasContent(CONTENTS); + } +}