Skip to content

Commit 591d415

Browse files
committed
JIRA:GRIF-315 upgrade to httpclient5
1 parent cec0f54 commit 591d415

21 files changed

Lines changed: 601 additions & 443 deletions

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ Since *GoodData Java SDK* version *2.32.0* API versioning is supported. The API
6565
### Dependencies
6666

6767
The *GoodData Java SDK* uses:
68-
* the [GoodData HTTP client](https://github.com/gooddata/gooddata-http-client) version 0.9.3 or later
69-
* the *Apache HTTP Client* version 4.5 or later (for white-labeled domains at least version 4.3.2 is required)
68+
* the [GoodData HTTP client](https://github.com/gooddata/gooddata-http-client) version 2.0.0 or later
69+
* the *Apache HTTP Client* version 5.2.x (for compatibility with older code, 4.5.x is also included for Sardine WebDAV library)
7070
* the *Spring Framework* version 6.x (compatible with Spring Boot 3.x)
7171
* the *Jackson JSON Processor* version 2.*
7272
* the *Slf4j API* version 2.0.*
@@ -106,6 +106,22 @@ Good SO thread about differences between various types in Java Date/Time API: ht
106106
Build the library with `mvn package`, see the
107107
[Testing](https://github.com/gooddata/gooddata-java/wiki/Testing) page for different testing methods.
108108

109+
### Running Acceptance Tests
110+
111+
To run acceptance tests against a real GoodData environment, use the following command:
112+
113+
```bash
114+
# ⚠️ EXAMPLE ONLY - Replace with your actual credentials
115+
host=your-instance.gooddata.com \
116+
login=your.email@example.com \
117+
password=YOUR_PASSWORD_HERE \
118+
projectToken=YOUR_PROJECT_TOKEN \
119+
warehouseToken=YOUR_WAREHOUSE_TOKEN \
120+
mvn verify -P at
121+
```
122+
123+
**Security Note:** Never commit real credentials to version control. Use environment variables or secure credential management systems in production.
124+
109125
For releasing see [Releasing How-To](https://github.com/gooddata/gooddata-java/wiki/Releasing).
110126

111127
## Contribute

gooddata-java/src/main/java/com/gooddata/sdk/common/HttpClient4ComponentsClientHttpRequestFactory.java

Lines changed: 44 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
package com.gooddata.sdk.common;
77

88

9-
import org.apache.http.HttpEntityEnclosingRequest;
10-
import org.apache.http.HttpRequest;
11-
import org.apache.http.client.HttpClient;
12-
import org.apache.http.client.methods.*;
13-
import org.apache.http.entity.ByteArrayEntity;
9+
import org.apache.hc.core5.http.ClassicHttpRequest;
10+
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
11+
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
12+
import org.apache.hc.client5.http.classic.HttpClient;
1413
import org.slf4j.Logger;
1514
import org.slf4j.LoggerFactory;
1615

@@ -29,19 +28,18 @@
2928
import java.util.Map;
3029

3130
/**
32-
* Spring 6 compatible {@link ClientHttpRequestFactory} implementation that uses Apache HttpComponents HttpClient 4.x.
33-
* This is a custom implementation to bridge the gap between Spring 6 (which expects HttpClient 5.x)
34-
* and our requirement to use HttpClient 4.x for compatibility.
31+
* Spring 6 compatible {@link ClientHttpRequestFactory} implementation that uses Apache HttpComponents HttpClient 5.x.
32+
* This is a custom implementation to bridge the gap between Spring 6 and HttpClient 5.x.
3533
*/
3634
public class HttpClient4ComponentsClientHttpRequestFactory implements ClientHttpRequestFactory {
3735

3836
private static final Logger logger = LoggerFactory.getLogger(HttpClient4ComponentsClientHttpRequestFactory.class);
3937
private final HttpClient httpClient;
4038

4139
/**
42-
* Create a factory with the given HttpClient 4.x instance.
40+
* Create a factory with the given HttpClient 5.x instance.
4341
*
44-
* @param httpClient the HttpClient 4.x instance to use
42+
* @param httpClient the HttpClient 5.x instance to use
4543
*/
4644
public HttpClient4ComponentsClientHttpRequestFactory(HttpClient httpClient) {
4745
Assert.notNull(httpClient, "HttpClient must not be null");
@@ -50,50 +48,50 @@ public HttpClient4ComponentsClientHttpRequestFactory(HttpClient httpClient) {
5048

5149
@Override
5250
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
53-
HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
51+
ClassicHttpRequest httpRequest = createHttpUriRequest(httpMethod, uri);
5452
return new HttpClient4ComponentsClientHttpRequest(httpClient, httpRequest);
5553
}
5654

5755
/**
58-
* Create an Apache HttpComponents HttpUriRequest object for the given HTTP method and URI.
56+
* Create an Apache HttpComponents ClassicHttpRequest object for the given HTTP method and URI.
5957
*
6058
* @param httpMethod the HTTP method
6159
* @param uri the URI
62-
* @return the HttpUriRequest
60+
* @return the ClassicHttpRequest
6361
*/
64-
private HttpUriRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) {
62+
private ClassicHttpRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) {
6563
if (HttpMethod.GET.equals(httpMethod)) {
66-
return new HttpGet(uri);
64+
return ClassicRequestBuilder.get(uri).build();
6765
} else if (HttpMethod.HEAD.equals(httpMethod)) {
68-
return new HttpHead(uri);
66+
return ClassicRequestBuilder.head(uri).build();
6967
} else if (HttpMethod.POST.equals(httpMethod)) {
70-
return new HttpPost(uri);
68+
return ClassicRequestBuilder.post(uri).build();
7169
} else if (HttpMethod.PUT.equals(httpMethod)) {
72-
return new HttpPut(uri);
70+
return ClassicRequestBuilder.put(uri).build();
7371
} else if (HttpMethod.PATCH.equals(httpMethod)) {
74-
return new HttpPatch(uri);
72+
return ClassicRequestBuilder.patch(uri).build();
7573
} else if (HttpMethod.DELETE.equals(httpMethod)) {
76-
return new HttpDelete(uri);
74+
return ClassicRequestBuilder.delete(uri).build();
7775
} else if (HttpMethod.OPTIONS.equals(httpMethod)) {
78-
return new HttpOptions(uri);
76+
return ClassicRequestBuilder.options(uri).build();
7977
} else if (HttpMethod.TRACE.equals(httpMethod)) {
80-
return new HttpTrace(uri);
78+
return ClassicRequestBuilder.trace(uri).build();
8179
} else {
8280
throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod);
8381
}
8482
}
8583

8684
/**
87-
* {@link ClientHttpRequest} implementation based on Apache HttpComponents HttpClient 4.x.
85+
* {@link ClientHttpRequest} implementation based on Apache HttpComponents HttpClient 5.x.
8886
*/
8987
private static class HttpClient4ComponentsClientHttpRequest implements ClientHttpRequest {
9088

9189
private final HttpClient httpClient;
92-
private final HttpUriRequest httpRequest;
90+
private final ClassicHttpRequest httpRequest;
9391
private final HttpHeaders headers;
9492
private ByteArrayOutputStream bufferedOutput = new ByteArrayOutputStream(1024);
9593

96-
public HttpClient4ComponentsClientHttpRequest(HttpClient httpClient, HttpUriRequest httpRequest) {
94+
public HttpClient4ComponentsClientHttpRequest(HttpClient httpClient, ClassicHttpRequest httpRequest) {
9795
this.httpClient = httpClient;
9896
this.httpRequest = httpRequest;
9997
this.headers = new HttpHeaders();
@@ -111,7 +109,11 @@ public String getMethodValue() {
111109

112110
@Override
113111
public URI getURI() {
114-
return httpRequest.getURI();
112+
try {
113+
return httpRequest.getUri();
114+
} catch (Exception e) {
115+
throw new RuntimeException("Failed to get URI", e);
116+
}
115117
}
116118

117119
@Override
@@ -129,84 +131,35 @@ public ClientHttpResponse execute() throws IOException {
129131
// Create entity first (matching reference implementation exactly)
130132
byte[] bytes = bufferedOutput.toByteArray();
131133
if (bytes.length > 0) {
132-
if (httpRequest instanceof HttpEntityEnclosingRequest) {
133-
HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) httpRequest;
134-
135-
// Ensure proper UTF-8 encoding before creating entity
136-
// This is crucial for @JsonTypeInfo annotated classes like Execution
137-
ByteArrayEntity requestEntity = new ByteArrayEntity(bytes);
138-
139-
140-
if (logger.isDebugEnabled()) {
141-
// Check if Content-Type is already set in headers
142-
boolean hasContentType = false;
143-
for (org.apache.http.Header header : httpRequest.getAllHeaders()) {
144-
if ("Content-Type".equalsIgnoreCase(header.getName())) {
145-
hasContentType = true;
146-
// String contentType = header.getValue();
147-
// logger.debug("Content-Type from headers: {}", contentType);
148-
break;
149-
}
150-
}
151-
152-
if (!hasContentType) {
153-
// logger.debug("Default Content-Type set: application/json; charset=UTF-8");
154-
}
155-
}
156-
157-
entityRequest.setEntity(requestEntity);
158-
159-
}
134+
// HttpClient 5.x - set entity directly on the request
135+
ByteArrayEntity requestEntity = new ByteArrayEntity(bytes, null);
136+
httpRequest.setEntity(requestEntity);
160137
}
161138

162139
// Set headers exactly like reference implementation
163-
// (no additional headers parameter in our case, but same logic)
164140
addHeaders(httpRequest);
165141

166-
// Handle both GoodDataHttpClient and standard HttpClient
167-
org.apache.http.HttpResponse httpResponse;
168-
if (httpClient.getClass().getName().contains("GoodDataHttpClient")) {
169-
// Use reflection to call the execute method on GoodDataHttpClient
170-
try {
171-
// Try the single parameter execute method first
172-
java.lang.reflect.Method executeMethod = httpClient.getClass().getMethod("execute",
173-
org.apache.http.client.methods.HttpUriRequest.class);
174-
httpResponse = (org.apache.http.HttpResponse) executeMethod.invoke(httpClient, httpRequest);
175-
} catch (NoSuchMethodException e) {
176-
// If that doesn't work, try the two parameter version with HttpContext
177-
try {
178-
java.lang.reflect.Method executeMethod = httpClient.getClass().getMethod("execute",
179-
org.apache.http.client.methods.HttpUriRequest.class, org.apache.http.protocol.HttpContext.class);
180-
httpResponse = (org.apache.http.HttpResponse) executeMethod.invoke(httpClient, httpRequest, null);
181-
} catch (Exception e2) {
182-
throw new IOException("Failed to execute request with GoodDataHttpClient", e2);
183-
}
184-
} catch (Exception e) {
185-
throw new IOException("Failed to execute request with GoodDataHttpClient", e);
186-
}
187-
} else {
188-
httpResponse = httpClient.execute(httpRequest);
189-
}
190-
return new HttpClient4ComponentsClientHttpResponse(httpResponse);
142+
// Execute the request using HttpClient 5.x API
143+
// The execute method with ResponseHandler automatically handles the response
144+
return httpClient.execute(httpRequest, response -> {
145+
// We need to consume and store the response since ResponseHandler closes it
146+
return new HttpClient4ComponentsClientHttpResponse(response);
147+
});
191148
}
192149

193150
/**
194151
* Add the headers from the HttpHeaders to the HttpRequest.
195-
* Excludes Content-Length headers to avoid conflicts with HttpClient 4.x internal management.
152+
* Excludes Content-Length headers to avoid conflicts with HttpClient 5.x internal management.
196153
* Uses setHeader instead of addHeader to match the reference implementation.
197-
* Follows HttpClient4ClientHttpRequest.executeInternal implementation pattern.
198154
*/
199-
private void addHeaders(HttpRequest httpRequest) {
155+
private void addHeaders(ClassicHttpRequest httpRequest) {
200156
// CRITICAL for GoodData API: set headers in fixed order
201157
// for stable checksum. Order: Accept, X-GDC-Version, Content-Type, others
202158

203159
// First clear potentially problematic headers
204-
if (httpRequest instanceof HttpUriRequest) {
205-
HttpUriRequest uriRequest = (HttpUriRequest) httpRequest;
206-
uriRequest.removeHeaders("Accept");
207-
uriRequest.removeHeaders("X-GDC-Version");
208-
uriRequest.removeHeaders("Content-Type");
209-
}
160+
httpRequest.removeHeaders("Accept");
161+
httpRequest.removeHeaders("X-GDC-Version");
162+
httpRequest.removeHeaders("Content-Type");
210163

211164
// 1. Accept header (first for checksum stability)
212165
if (headers.containsKey("Accept")) {
@@ -243,8 +196,9 @@ private void addHeaders(HttpRequest httpRequest) {
243196
// logger.debug("Using Spring Content-Type: {}", finalContentType);
244197
// }
245198
}
246-
} else if (httpRequest instanceof HttpEntityEnclosingRequest) {
199+
} else {
247200
// Set default Content-Type for JSON requests with body
201+
// In HttpClient 5.x, all requests can have entities, no need for instanceof check
248202
finalContentType = "application/json; charset=UTF-8";
249203
// if (logger.isDebugEnabled()) {
250204
// logger.debug("Default Content-Type for JSON requests: {}", finalContentType);

gooddata-java/src/main/java/com/gooddata/sdk/common/HttpClient4ComponentsClientHttpResponse.java

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
*/
66
package com.gooddata.sdk.common;
77

8-
import org.apache.http.Header;
9-
import org.apache.http.HttpEntity;
8+
import org.apache.hc.core5.http.Header;
9+
import org.apache.hc.core5.http.HttpEntity;
10+
import org.apache.hc.core5.http.ClassicHttpResponse;
1011
import org.springframework.http.HttpHeaders;
1112
import org.springframework.http.HttpStatus;
1213
import org.springframework.http.HttpStatusCode;
@@ -17,39 +18,39 @@
1718
import java.io.InputStream;
1819

1920
/**
20-
* Spring 6 compatible {@link ClientHttpResponse} implementation that wraps Apache HttpComponents HttpClient 4.x response.
21-
* This bridges HttpClient 4.x responses with Spring 6's ClientHttpResponse interface.
21+
* Spring 6 compatible {@link ClientHttpResponse} implementation that wraps Apache HttpComponents HttpClient 5.x response.
22+
* This bridges HttpClient 5.x responses with Spring 6's ClientHttpResponse interface.
2223
* Package-private as it's only used internally within the common package.
2324
*/
2425
class HttpClient4ComponentsClientHttpResponse implements ClientHttpResponse {
2526

26-
private final org.apache.http.HttpResponse httpResponse;
27+
private final ClassicHttpResponse httpResponse;
2728
private HttpHeaders headers;
2829

29-
public HttpClient4ComponentsClientHttpResponse(org.apache.http.HttpResponse httpResponse) {
30+
public HttpClient4ComponentsClientHttpResponse(ClassicHttpResponse httpResponse) {
3031
this.httpResponse = httpResponse;
3132
}
3233

3334
@Override
3435
public HttpStatusCode getStatusCode() throws IOException {
35-
return HttpStatusCode.valueOf(httpResponse.getStatusLine().getStatusCode());
36+
return HttpStatusCode.valueOf(httpResponse.getCode());
3637
}
3738

3839
@Override
3940
public int getRawStatusCode() throws IOException {
40-
return httpResponse.getStatusLine().getStatusCode();
41+
return httpResponse.getCode();
4142
}
4243

4344
@Override
4445
public String getStatusText() throws IOException {
45-
return httpResponse.getStatusLine().getReasonPhrase();
46+
return httpResponse.getReasonPhrase();
4647
}
4748

4849
@Override
4950
public HttpHeaders getHeaders() {
5051
if (headers == null) {
5152
headers = new HttpHeaders();
52-
for (Header header : httpResponse.getAllHeaders()) {
53+
for (Header header : httpResponse.getHeaders()) {
5354
headers.add(header.getName(), header.getValue());
5455
}
5556
}
@@ -64,13 +65,9 @@ public InputStream getBody() throws IOException {
6465

6566
@Override
6667
public void close() {
67-
// HttpClient 4.x doesn't require explicit connection closing in most cases
68-
// The connection is managed by the connection manager
68+
// HttpClient 5.x - close the response
6969
try {
70-
HttpEntity entity = httpResponse.getEntity();
71-
if (entity != null && entity.getContent() != null) {
72-
entity.getContent().close();
73-
}
70+
httpResponse.close();
7471
} catch (IOException e) {
7572
// Ignore close exceptions
7673
}

gooddata-java/src/main/java/com/gooddata/sdk/service/GoodDataEndpoint.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66
package com.gooddata.sdk.service;
77

8-
import org.apache.http.HttpHost;
8+
import org.apache.hc.core5.http.HttpHost;
99

1010
import static com.gooddata.sdk.common.util.Validate.notEmpty;
1111

@@ -62,7 +62,7 @@ public GoodDataEndpoint() {
6262
* @return the host URI, as a string.
6363
*/
6464
public String toUri() {
65-
return new HttpHost(hostname, port, protocol).toURI();
65+
return new HttpHost(protocol, hostname, port).toURI();
6666
}
6767

6868
/**

gooddata-java/src/main/java/com/gooddata/sdk/service/GoodDataSettings.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
import com.gooddata.sdk.service.retry.RetrySettings;
1010
import com.gooddata.sdk.common.util.GoodDataToStringBuilder;
1111
import org.apache.commons.lang3.StringUtils;
12-
import org.apache.http.impl.client.HttpClientBuilder;
13-
import org.apache.http.util.VersionInfo;
12+
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
1413
import org.springframework.http.MediaType;
1514
import org.springframework.util.StreamUtils;
1615

@@ -22,7 +21,6 @@
2221
import java.util.concurrent.TimeUnit;
2322

2423
import static com.gooddata.sdk.common.util.Validate.notNull;
25-
import static org.apache.http.util.VersionInfo.loadVersionInfo;
2624
import static org.springframework.util.Assert.isTrue;
2725

2826
/**
@@ -299,8 +297,10 @@ private String getDefaultUserAgent() {
299297
final String clientVersion = pkg != null && pkg.getImplementationVersion() != null
300298
? pkg.getImplementationVersion() : UNKNOWN_VERSION;
301299

302-
final VersionInfo vi = loadVersionInfo("org.apache.http.client", HttpClientBuilder.class.getClassLoader());
303-
final String apacheVersion = vi != null ? vi.getRelease() : UNKNOWN_VERSION;
300+
// Get HttpClient 5.x version from package
301+
final Package httpClientPkg = HttpClientBuilder.class.getPackage();
302+
final String apacheVersion = httpClientPkg != null && httpClientPkg.getImplementationVersion() != null
303+
? httpClientPkg.getImplementationVersion() : UNKNOWN_VERSION;
304304

305305
return String.format("%s/%s (%s; %s) %s/%s", "GoodData-Java-SDK", clientVersion,
306306
System.getProperty("os.name"), System.getProperty("java.specification.version"),

0 commit comments

Comments
 (0)