Skip to content

Commit 0b6dade

Browse files
Migrate CLI to Apache HttpClient 5
Closes gh-33534
1 parent 1588f9d commit 0b6dade

File tree

8 files changed

+85
-73
lines changed

8 files changed

+85
-73
lines changed

spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle

+1-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ dependencies {
2525
implementation("com.vaadin.external.google:android-json")
2626
implementation("jline:jline")
2727
implementation("net.sf.jopt-simple:jopt-simple")
28-
implementation("org.apache.httpcomponents:httpclient") {
29-
exclude group: "commons-logging", module: "commons-logging"
30-
}
28+
implementation("org.apache.httpcomponents.client5:httpclient5")
3129
implementation("org.slf4j:slf4j-simple")
3230
implementation("org.springframework:spring-core")
3331
implementation("org.springframework.security:spring-security-crypto")

spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/InitializrService.java

+35-30
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,18 @@
2121
import java.nio.charset.Charset;
2222
import java.nio.charset.StandardCharsets;
2323

24-
import org.apache.http.Header;
25-
import org.apache.http.HttpEntity;
26-
import org.apache.http.HttpHeaders;
27-
import org.apache.http.client.methods.CloseableHttpResponse;
28-
import org.apache.http.client.methods.HttpGet;
29-
import org.apache.http.client.methods.HttpUriRequest;
30-
import org.apache.http.entity.ContentType;
31-
import org.apache.http.impl.client.CloseableHttpClient;
32-
import org.apache.http.impl.client.HttpClientBuilder;
33-
import org.apache.http.message.BasicHeader;
24+
import org.apache.hc.client5.http.classic.HttpClient;
25+
import org.apache.hc.client5.http.classic.methods.HttpGet;
26+
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
27+
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
28+
import org.apache.hc.core5.http.ClassicHttpResponse;
29+
import org.apache.hc.core5.http.ContentType;
30+
import org.apache.hc.core5.http.Header;
31+
import org.apache.hc.core5.http.HttpEntity;
32+
import org.apache.hc.core5.http.HttpHeaders;
33+
import org.apache.hc.core5.http.HttpHost;
34+
import org.apache.hc.core5.http.message.BasicHeader;
35+
import org.apache.hc.core5.http.message.StatusLine;
3436
import org.json.JSONException;
3537
import org.json.JSONObject;
3638

@@ -62,16 +64,16 @@ class InitializrService {
6264
/**
6365
* Late binding HTTP client.
6466
*/
65-
private CloseableHttpClient http;
67+
private HttpClient http;
6668

6769
InitializrService() {
6870
}
6971

70-
InitializrService(CloseableHttpClient http) {
72+
InitializrService(HttpClient http) {
7173
this.http = http;
7274
}
7375

74-
protected CloseableHttpClient getHttp() {
76+
protected HttpClient getHttp() {
7577
if (this.http == null) {
7678
this.http = HttpClientBuilder.create().useSystemProperties().build();
7779
}
@@ -88,7 +90,7 @@ ProjectGenerationResponse generate(ProjectGenerationRequest request) throws IOEx
8890
Log.info("Using service at " + request.getServiceUrl());
8991
InitializrServiceMetadata metadata = loadMetadata(request.getServiceUrl());
9092
URI url = request.generateUrl(metadata);
91-
CloseableHttpResponse httpResponse = executeProjectGenerationRequest(url);
93+
ClassicHttpResponse httpResponse = executeProjectGenerationRequest(url);
9294
HttpEntity httpEntity = httpResponse.getEntity();
9395
validateResponse(httpResponse, request.getServiceUrl());
9496
return createResponse(httpResponse, httpEntity);
@@ -101,7 +103,7 @@ ProjectGenerationResponse generate(ProjectGenerationRequest request) throws IOEx
101103
* @throws IOException if the service's metadata cannot be loaded
102104
*/
103105
InitializrServiceMetadata loadMetadata(String serviceUrl) throws IOException {
104-
CloseableHttpResponse httpResponse = executeInitializrMetadataRetrieval(serviceUrl);
106+
ClassicHttpResponse httpResponse = executeInitializrMetadataRetrieval(serviceUrl);
105107
validateResponse(httpResponse, serviceUrl);
106108
return parseJsonMetadata(httpResponse.getEntity());
107109
}
@@ -118,10 +120,10 @@ InitializrServiceMetadata loadMetadata(String serviceUrl) throws IOException {
118120
Object loadServiceCapabilities(String serviceUrl) throws IOException {
119121
HttpGet request = new HttpGet(serviceUrl);
120122
request.setHeader(new BasicHeader(HttpHeaders.ACCEPT, ACCEPT_SERVICE_CAPABILITIES));
121-
CloseableHttpResponse httpResponse = execute(request, serviceUrl, "retrieve help");
123+
ClassicHttpResponse httpResponse = execute(request, URI.create(serviceUrl), "retrieve help");
122124
validateResponse(httpResponse, serviceUrl);
123125
HttpEntity httpEntity = httpResponse.getEntity();
124-
ContentType contentType = ContentType.getOrDefault(httpEntity);
126+
ContentType contentType = ContentType.create(httpEntity.getContentType());
125127
if (contentType.getMimeType().equals("text/plain")) {
126128
return getContent(httpEntity);
127129
}
@@ -137,18 +139,19 @@ private InitializrServiceMetadata parseJsonMetadata(HttpEntity httpEntity) throw
137139
}
138140
}
139141

140-
private void validateResponse(CloseableHttpResponse httpResponse, String serviceUrl) {
142+
private void validateResponse(ClassicHttpResponse httpResponse, String serviceUrl) {
141143
if (httpResponse.getEntity() == null) {
142144
throw new ReportableException("No content received from server '" + serviceUrl + "'");
143145
}
144-
if (httpResponse.getStatusLine().getStatusCode() != 200) {
146+
if (httpResponse.getCode() != 200) {
145147
throw createException(serviceUrl, httpResponse);
146148
}
147149
}
148150

149-
private ProjectGenerationResponse createResponse(CloseableHttpResponse httpResponse, HttpEntity httpEntity)
151+
private ProjectGenerationResponse createResponse(ClassicHttpResponse httpResponse, HttpEntity httpEntity)
150152
throws IOException {
151-
ProjectGenerationResponse response = new ProjectGenerationResponse(ContentType.getOrDefault(httpEntity));
153+
ProjectGenerationResponse response = new ProjectGenerationResponse(
154+
ContentType.create(httpEntity.getContentType()));
152155
response.setContent(FileCopyUtils.copyToByteArray(httpEntity.getContent()));
153156
String fileName = extractFileName(httpResponse.getFirstHeader("Content-Disposition"));
154157
if (fileName != null) {
@@ -162,7 +165,7 @@ private ProjectGenerationResponse createResponse(CloseableHttpResponse httpRespo
162165
* @param url the URL
163166
* @return the response
164167
*/
165-
private CloseableHttpResponse executeProjectGenerationRequest(URI url) {
168+
private ClassicHttpResponse executeProjectGenerationRequest(URI url) {
166169
return execute(new HttpGet(url), url, "generate project");
167170
}
168171

@@ -171,32 +174,34 @@ private CloseableHttpResponse executeProjectGenerationRequest(URI url) {
171174
* @param url the URL
172175
* @return the response
173176
*/
174-
private CloseableHttpResponse executeInitializrMetadataRetrieval(String url) {
177+
private ClassicHttpResponse executeInitializrMetadataRetrieval(String url) {
175178
HttpGet request = new HttpGet(url);
176179
request.setHeader(new BasicHeader(HttpHeaders.ACCEPT, ACCEPT_META_DATA));
177-
return execute(request, url, "retrieve metadata");
180+
return execute(request, URI.create(url), "retrieve metadata");
178181
}
179182

180-
private CloseableHttpResponse execute(HttpUriRequest request, Object url, String description) {
183+
private ClassicHttpResponse execute(HttpUriRequest request, URI url, String description) {
181184
try {
185+
HttpHost host = HttpHost.create(url);
182186
request.addHeader("User-Agent", "SpringBootCli/" + getClass().getPackage().getImplementationVersion());
183-
return getHttp().execute(request);
187+
return getHttp().execute(host, request);
184188
}
185189
catch (IOException ex) {
186190
throw new ReportableException(
187191
"Failed to " + description + " from service at '" + url + "' (" + ex.getMessage() + ")");
188192
}
189193
}
190194

191-
private ReportableException createException(String url, CloseableHttpResponse httpResponse) {
195+
private ReportableException createException(String url, ClassicHttpResponse httpResponse) {
196+
StatusLine statusLine = new StatusLine(httpResponse);
192197
String message = "Initializr service call failed using '" + url + "' - service returned "
193-
+ httpResponse.getStatusLine().getReasonPhrase();
198+
+ statusLine.getReasonPhrase();
194199
String error = extractMessage(httpResponse.getEntity());
195200
if (StringUtils.hasText(error)) {
196201
message += ": '" + error + "'";
197202
}
198203
else {
199-
int statusCode = httpResponse.getStatusLine().getStatusCode();
204+
int statusCode = statusLine.getStatusCode();
200205
message += " (unexpected " + statusCode + " error)";
201206
}
202207
throw new ReportableException(message);
@@ -222,7 +227,7 @@ private JSONObject getContentAsJson(HttpEntity entity) throws IOException, JSONE
222227
}
223228

224229
private String getContent(HttpEntity entity) throws IOException {
225-
ContentType contentType = ContentType.getOrDefault(entity);
230+
ContentType contentType = ContentType.create(entity.getContentType());
226231
Charset charset = contentType.getCharset();
227232
charset = (charset != null) ? charset : StandardCharsets.UTF_8;
228233
byte[] content = FileCopyUtils.copyToByteArray(entity.getContent());

spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerationRequest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import java.util.List;
2424
import java.util.Map;
2525

26-
import org.apache.http.client.utils.URIBuilder;
26+
import org.apache.hc.core5.net.URIBuilder;
2727

2828
import org.springframework.util.StringUtils;
2929

spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/init/ProjectGenerationResponse.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package org.springframework.boot.cli.command.init;
1818

19-
import org.apache.http.entity.ContentType;
19+
import org.apache.hc.core5.http.ContentType;
2020

2121
/**
2222
* Represent the response of a {@link ProjectGenerationRequest}.

spring-boot-project/spring-boot-tools/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/AbstractHttpClientMockTests.java

+27-26
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@
1919
import java.io.ByteArrayInputStream;
2020
import java.io.IOException;
2121

22-
import org.apache.http.Header;
23-
import org.apache.http.HttpEntity;
24-
import org.apache.http.HttpHeaders;
25-
import org.apache.http.StatusLine;
26-
import org.apache.http.client.methods.CloseableHttpResponse;
27-
import org.apache.http.client.methods.HttpGet;
28-
import org.apache.http.impl.client.CloseableHttpClient;
29-
import org.apache.http.message.BasicHeader;
22+
import org.apache.hc.client5.http.classic.HttpClient;
23+
import org.apache.hc.client5.http.classic.methods.HttpGet;
24+
import org.apache.hc.core5.http.ClassicHttpResponse;
25+
import org.apache.hc.core5.http.Header;
26+
import org.apache.hc.core5.http.HttpEntity;
27+
import org.apache.hc.core5.http.HttpHeaders;
28+
import org.apache.hc.core5.http.HttpHost;
29+
import org.apache.hc.core5.http.message.BasicHeader;
3030
import org.json.JSONException;
3131
import org.json.JSONObject;
3232
import org.mockito.ArgumentMatcher;
@@ -35,19 +35,20 @@
3535
import org.springframework.core.io.Resource;
3636
import org.springframework.util.StreamUtils;
3737

38+
import static org.mockito.ArgumentMatchers.any;
3839
import static org.mockito.ArgumentMatchers.argThat;
3940
import static org.mockito.ArgumentMatchers.isA;
4041
import static org.mockito.BDDMockito.given;
4142
import static org.mockito.Mockito.mock;
4243

4344
/**
44-
* Abstract base class for tests that use a mock {@link CloseableHttpClient}.
45+
* Abstract base class for tests that use a mock {@link HttpClient}.
4546
*
4647
* @author Stephane Nicoll
4748
*/
4849
public abstract class AbstractHttpClientMockTests {
4950

50-
protected final CloseableHttpClient http = mock(CloseableHttpClient.class);
51+
protected final HttpClient http = mock(HttpClient.class);
5152

5253
protected void mockSuccessfulMetadataTextGet() throws IOException {
5354
mockSuccessfulMetadataGet("metadata/service-metadata-2.1.0.txt", "text/plain", true);
@@ -65,11 +66,12 @@ protected void mockSuccessfulMetadataGetV2(boolean serviceCapabilities) throws I
6566

6667
protected void mockSuccessfulMetadataGet(String contentPath, String contentType, boolean serviceCapabilities)
6768
throws IOException {
68-
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
69+
ClassicHttpResponse response = mock(ClassicHttpResponse.class);
6970
byte[] content = readClasspathResource(contentPath);
7071
mockHttpEntity(response, content, contentType);
7172
mockStatus(response, 200);
72-
given(this.http.execute(argThat(getForMetadata(serviceCapabilities)))).willReturn(response);
73+
given(this.http.execute(any(HttpHost.class), argThat(getForMetadata(serviceCapabilities))))
74+
.willReturn(response);
7375
}
7476

7577
protected byte[] readClasspathResource(String contentPath) throws IOException {
@@ -80,36 +82,37 @@ protected byte[] readClasspathResource(String contentPath) throws IOException {
8082
protected void mockSuccessfulProjectGeneration(MockHttpProjectGenerationRequest request) throws IOException {
8183
// Required for project generation as the metadata is read first
8284
mockSuccessfulMetadataGet(false);
83-
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
85+
ClassicHttpResponse response = mock(ClassicHttpResponse.class);
8486
mockHttpEntity(response, request.content, request.contentType);
8587
mockStatus(response, 200);
8688
String header = (request.fileName != null) ? contentDispositionValue(request.fileName) : null;
8789
mockHttpHeader(response, "Content-Disposition", header);
88-
given(this.http.execute(argThat(getForNonMetadata()))).willReturn(response);
90+
given(this.http.execute(any(HttpHost.class), argThat(getForNonMetadata()))).willReturn(response);
8991
}
9092

9193
protected void mockProjectGenerationError(int status, String message) throws IOException, JSONException {
9294
// Required for project generation as the metadata is read first
9395
mockSuccessfulMetadataGet(false);
94-
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
96+
ClassicHttpResponse response = mock(ClassicHttpResponse.class);
9597
mockHttpEntity(response, createJsonError(status, message).getBytes(), "application/json");
9698
mockStatus(response, status);
97-
given(this.http.execute(isA(HttpGet.class))).willReturn(response);
99+
given(this.http.execute(any(HttpHost.class), isA(HttpGet.class))).willReturn(response);
98100
}
99101

100102
protected void mockMetadataGetError(int status, String message) throws IOException, JSONException {
101-
CloseableHttpResponse response = mock(CloseableHttpResponse.class);
103+
ClassicHttpResponse response = mock(ClassicHttpResponse.class);
102104
mockHttpEntity(response, createJsonError(status, message).getBytes(), "application/json");
103105
mockStatus(response, status);
104-
given(this.http.execute(isA(HttpGet.class))).willReturn(response);
106+
given(this.http.execute(any(HttpHost.class), isA(HttpGet.class))).willReturn(response);
105107
}
106108

107-
protected HttpEntity mockHttpEntity(CloseableHttpResponse response, byte[] content, String contentType) {
109+
protected HttpEntity mockHttpEntity(ClassicHttpResponse response, byte[] content, String contentType) {
108110
try {
109111
HttpEntity entity = mock(HttpEntity.class);
110112
given(entity.getContent()).willReturn(new ByteArrayInputStream(content));
111113
Header contentTypeHeader = (contentType != null) ? new BasicHeader("Content-Type", contentType) : null;
112-
given(entity.getContentType()).willReturn(contentTypeHeader);
114+
given(entity.getContentType())
115+
.willReturn((contentTypeHeader != null) ? contentTypeHeader.getValue() : null);
113116
given(response.getEntity()).willReturn(entity);
114117
return entity;
115118
}
@@ -118,13 +121,11 @@ protected HttpEntity mockHttpEntity(CloseableHttpResponse response, byte[] conte
118121
}
119122
}
120123

121-
protected void mockStatus(CloseableHttpResponse response, int status) {
122-
StatusLine statusLine = mock(StatusLine.class);
123-
given(statusLine.getStatusCode()).willReturn(status);
124-
given(response.getStatusLine()).willReturn(statusLine);
124+
protected void mockStatus(ClassicHttpResponse response, int status) {
125+
given(response.getCode()).willReturn(status);
125126
}
126127

127-
protected void mockHttpHeader(CloseableHttpResponse response, String headerName, String value) {
128+
protected void mockHttpHeader(ClassicHttpResponse response, String headerName, String value) {
128129
Header header = (value != null) ? new BasicHeader(headerName, value) : null;
129130
given(response.getFirstHeader(headerName)).willReturn(header);
130131
}
@@ -166,7 +167,7 @@ static class MockHttpProjectGenerationRequest {
166167
}
167168

168169
MockHttpProjectGenerationRequest(String contentType, String fileName, byte[] content) {
169-
this.contentType = contentType;
170+
this.contentType = (contentType != null) ? contentType : "application/text";
170171
this.fileName = fileName;
171172
this.content = content;
172173
}

spring-boot-project/spring-boot-tools/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/init/InitCommandTests.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
import java.util.zip.ZipOutputStream;
2626

2727
import joptsimple.OptionSet;
28-
import org.apache.http.Header;
29-
import org.apache.http.client.methods.HttpUriRequest;
28+
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
29+
import org.apache.hc.core5.http.Header;
30+
import org.apache.hc.core5.http.HttpHost;
3031
import org.junit.jupiter.api.Test;
3132
import org.junit.jupiter.api.extension.ExtendWith;
3233
import org.junit.jupiter.api.io.TempDir;
@@ -37,6 +38,8 @@
3738
import org.springframework.boot.cli.command.status.ExitStatus;
3839

3940
import static org.assertj.core.api.Assertions.assertThat;
41+
import static org.junit.jupiter.api.Assertions.fail;
42+
import static org.mockito.ArgumentMatchers.any;
4043
import static org.mockito.BDDMockito.then;
4144

4245
/**
@@ -205,6 +208,9 @@ void generateProjectAndExtractUnknownContentType(@TempDir File tempDir) throws E
205208
assertThat(this.command.run("--extract", tempDir.getAbsolutePath())).isEqualTo(ExitStatus.OK);
206209
assertThat(file).as("file should have been saved instead").exists();
207210
}
211+
catch (Exception ex) {
212+
fail(ex);
213+
}
208214
finally {
209215
assertThat(file.delete()).as("failed to delete test file").isTrue();
210216
}
@@ -393,7 +399,7 @@ void parseMoreThanOneArg() throws Exception {
393399
@Test
394400
void userAgent() throws Exception {
395401
this.command.run("--list", "--target=https://fake-service");
396-
then(this.http).should().execute(this.requestCaptor.capture());
402+
then(this.http).should().execute(any(HttpHost.class), this.requestCaptor.capture());
397403
Header agent = this.requestCaptor.getValue().getHeaders("User-Agent")[0];
398404
assertThat(agent.getValue()).startsWith("SpringBootCli/");
399405
}

0 commit comments

Comments
 (0)