Skip to content

Commit 89bcec0

Browse files
committed
JIRA:GRIF-316 upgrade 1
1 parent d1b8aaf commit 89bcec0

File tree

4 files changed

+268
-44
lines changed

4 files changed

+268
-44
lines changed

README.md

Lines changed: 179 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
11
# GoodData HTTP Client
22
[![Build Status](https://github.com/gooddata/gooddata-http-client/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/gooddata/gooddata-http-client/actions/workflows/build.yml) [![Javadocs](http://javadoc.io/badge/com.gooddata/gooddata-http-client.svg)](http://javadoc.io/doc/com.gooddata/gooddata-http-client) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gooddata/gooddata-http-client/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.gooddata/gooddata-http-client) [![Release](https://img.shields.io/github/v/release/gooddata/gooddata-http-client.svg)](https://search.maven.org/artifact/com.gooddata/gooddata-http-client)
33

4-
GoodData HTTP Client is an extension of [Apache HTTP Client](http://hc.apache.org/httpcomponents-client-4.3.x/index.html) (former Jakarta Commons).
4+
GoodData HTTP Client is an extension of [Apache HTTP Client 5.x](https://hc.apache.org/httpcomponents-client-5.3.x/index.html).
55
This specialized Java client transparently handles [GoodData authentication](https://help.gooddata.com/display/doc/API+Reference#/reference/authentication/log-in)
66
so you can focus on writing logic on top of [GoodData API](https://help.gooddata.com/display/doc/API+Reference).
77

8+
## ⚠️ Version 2.0+ Breaking Changes
9+
10+
**Version 2.0.0** introduces a major update migrating from Apache HttpClient 4.x to 5.x. See the [Migration Guide](#migration-guide) below for upgrade instructions.
11+
12+
## Requirements
13+
14+
- **Java 17+** (updated from Java 8)
15+
- **Apache HttpClient 5.5+** (migrated from 4.x)
16+
- **Maven 3.6+** (for building)
17+
818
## Design
919

10-
```com.gooddata.http.client.GoodDataHttpClient``` central class implements [org.apache.http.client.HttpClient interface](http://hc.apache.org/httpcomponents-client-4.2.x/httpclient/apidocs/org/apache/http/client/HttpClient.html)
11-
It allows seamless switch for existing code base currently using ```org.apache.http.client.HttpClient```. Business logic encapsulating
12-
access to [GoodData API](https://help.gooddata.com/display/doc/API+Reference) should use ```org.apache.http.client.HttpClient``` interface
13-
and keep ```com.gooddata.http.client.GoodDataHttpClient``` inside a factory class. ```com.gooddata.http.client.GoodDataHttpClient``` uses underlying ```org.apache.http.client.HttpClient```. A HTTP client
14-
instance can be passed via the constructor.
20+
`com.gooddata.http.client.GoodDataHttpClient` is a thread-safe HTTP client that wraps Apache HttpClient 5.x and provides transparent GoodData authentication handling. The client automatically manages SST (Super Secure Token) and TT (Temporary Token) lifecycle, including:
21+
22+
- Automatic token refresh on expiration
23+
- Retry logic for authentication failures
24+
- Thread-safe token management
25+
- Support for all HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
26+
27+
Business logic should use the `GoodDataHttpClient` class directly, which handles all authentication concerns internally.
1528

1629
## Usage
1730

@@ -32,52 +45,190 @@ If your project is managed by Maven you can add GoodData HTTP client as a new de
3245

3346
### <a name="credentials"/>Authentication using credentials</a>
3447

35-
```Java
36-
import com.gooddata.http.client.*
37-
import java.io.IOException;
38-
import org.apache.http.*;
48+
```java
49+
import com.gooddata.http.client.*;
50+
import org.apache.hc.client5.http.impl.classic.HttpClients;
51+
import org.apache.hc.core5.http.*;
52+
import org.apache.hc.core5.http.io.entity.EntityUtils;
53+
import org.apache.hc.client5.http.classic.methods.HttpGet;
3954

40-
HttpHost hostGoodData = new HttpHost("secure.gooddata.com", 443, "https");
55+
HttpHost hostGoodData = new HttpHost("https", "secure.gooddata.com", 443);
4156

42-
// create login strategy, which will obtain SST via credentials
57+
// Create login strategy, which will obtain SST via credentials
4358
SSTRetrievalStrategy sstStrategy = new LoginSSTRetrievalStrategy(login, password);
4459

45-
HttpClient client = new GoodDataHttpClient(HttpClientBuilder.create().build(), hostGoodData, sstStrategy);
60+
// Create GoodData HTTP client
61+
GoodDataHttpClient client = new GoodDataHttpClient(
62+
HttpClients.createDefault(),
63+
hostGoodData,
64+
sstStrategy
65+
);
4666

47-
// use HTTP client with transparent GoodData authentication
67+
// Use HTTP client with transparent GoodData authentication
4868
HttpGet getProject = new HttpGet("/gdc/projects");
4969
getProject.addHeader("Accept", ContentType.APPLICATION_JSON.getMimeType());
50-
HttpResponse getProjectResponse = client.execute(hostGoodData, getProject);
70+
ClassicHttpResponse getProjectResponse = client.execute(hostGoodData, getProject);
5171

5272
System.out.println(EntityUtils.toString(getProjectResponse.getEntity()));
5373
```
5474

5575
### <a name="sst"/>Authentication using super-secure token (SST)</a>
5676

57-
```Java
58-
import com.gooddata.http.client.*
59-
import java.io.IOException;
60-
import org.apache.http.*;
61-
62-
// create HTTP client
63-
HttpClient httpClient = HttpClientBuilder.create().build();
77+
```java
78+
import com.gooddata.http.client.*;
79+
import org.apache.hc.client5.http.impl.classic.HttpClients;
80+
import org.apache.hc.core5.http.*;
81+
import org.apache.hc.core5.http.io.entity.EntityUtils;
82+
import org.apache.hc.client5.http.classic.methods.HttpGet;
6483

65-
HttpHost hostGoodData = new HttpHost("secure.gooddata.com", 443, "https");
84+
HttpHost hostGoodData = new HttpHost("https", "secure.gooddata.com", 443);
6685

67-
// create login strategy (you must somehow obtain SST)
86+
// Create login strategy (you must somehow obtain SST)
6887
SSTRetrievalStrategy sstStrategy = new SimpleSSTRetrievalStrategy("my super-secure token");
6988

70-
// wrap your HTTP client into GoodData HTTP client
71-
HttpClient client = new GoodDataHttpClient(httpClient, hostGoodData, sstStrategy);
89+
// Create GoodData HTTP client
90+
GoodDataHttpClient client = new GoodDataHttpClient(
91+
HttpClients.createDefault(),
92+
hostGoodData,
93+
sstStrategy
94+
);
7295

73-
// use GoodData HTTP client
96+
// Use GoodData HTTP client
7497
HttpGet getProject = new HttpGet("/gdc/projects");
7598
getProject.addHeader("Accept", ContentType.APPLICATION_JSON.getMimeType());
76-
HttpResponse getProjectResponse = client.execute(hostGoodData, getProject);
99+
ClassicHttpResponse getProjectResponse = client.execute(hostGoodData, getProject);
77100

78101
System.out.println(EntityUtils.toString(getProjectResponse.getEntity()));
79102
```
80103

104+
## Migration Guide
105+
106+
### Migrating from 1.x to 2.0+ (Apache HttpClient 4.x to 5.x)
107+
108+
Version 2.0.0 introduces breaking changes due to the Apache HttpClient 5.x migration. Follow these steps to upgrade:
109+
110+
#### 1. Update Dependencies
111+
112+
**Maven:**
113+
```xml
114+
<dependency>
115+
<groupId>com.gooddata</groupId>
116+
<artifactId>gooddata-http-client</artifactId>
117+
<version>2.0.0</version> <!-- Updated from 1.x -->
118+
</dependency>
119+
```
120+
121+
#### 2. Update Java Version
122+
123+
Ensure your project uses **Java 17 or higher**:
124+
```xml
125+
<maven.compiler.source>17</maven.compiler.source>
126+
<maven.compiler.target>17</maven.compiler.target>
127+
```
128+
129+
#### 3. Update Imports
130+
131+
Replace Apache HttpClient 4.x imports with 5.x equivalents:
132+
133+
**Before (1.x):**
134+
```java
135+
import org.apache.http.HttpHost;
136+
import org.apache.http.HttpResponse;
137+
import org.apache.http.client.HttpClient;
138+
import org.apache.http.client.methods.HttpGet;
139+
import org.apache.http.impl.client.HttpClientBuilder;
140+
import org.apache.http.util.EntityUtils;
141+
```
142+
143+
**After (2.0+):**
144+
```java
145+
import org.apache.hc.core5.http.HttpHost;
146+
import org.apache.hc.core5.http.ClassicHttpResponse;
147+
import org.apache.hc.client5.http.classic.HttpClient;
148+
import org.apache.hc.client5.http.classic.methods.HttpGet;
149+
import org.apache.hc.client5.http.impl.classic.HttpClients;
150+
import org.apache.hc.core5.http.io.entity.EntityUtils;
151+
```
152+
153+
#### 4. Update HttpHost Construction
154+
155+
**Before (1.x):**
156+
```java
157+
HttpHost host = new HttpHost("secure.gooddata.com", 443, "https");
158+
```
159+
160+
**After (2.0+):**
161+
```java
162+
HttpHost host = new HttpHost("https", "secure.gooddata.com", 443);
163+
// Note: scheme is now the first parameter
164+
```
165+
166+
#### 5. Update Response Handling
167+
168+
**Before (1.x):**
169+
```java
170+
HttpResponse response = client.execute(host, request);
171+
```
172+
173+
**After (2.0+):**
174+
```java
175+
ClassicHttpResponse response = client.execute(host, request);
176+
```
177+
178+
#### 6. Update HttpClient Creation
179+
180+
**Before (1.x):**
181+
```java
182+
HttpClient httpClient = HttpClientBuilder.create().build();
183+
```
184+
185+
**After (2.0+):**
186+
```java
187+
HttpClient httpClient = HttpClients.createDefault();
188+
// Or with custom configuration:
189+
HttpClient httpClient = HttpClients.custom()
190+
.setDefaultRequestConfig(RequestConfig.custom()
191+
.setConnectionRequestTimeout(Timeout.ofSeconds(30))
192+
.build())
193+
.build();
194+
```
195+
196+
#### 7. Key Behavioral Changes
197+
198+
- **Thread Safety**: All requests now use write locks for consistency. This may reduce throughput under high concurrency but ensures reliable token management.
199+
- **Entity Handling**: Non-repeatable request entities are automatically buffered for retry scenarios.
200+
- **Error Handling**: More specific exceptions for authentication failures.
201+
- **HTTP Methods**: Full support for POST, PUT, PATCH in addition to GET and DELETE.
202+
203+
#### 8. Testing Your Migration
204+
205+
After updating your code:
206+
207+
1. **Compile**: Ensure no compilation errors
208+
2. **Test**: Run your existing test suite
209+
3. **Integration Test**: Test against GoodData API with real credentials
210+
4. **Monitor**: Watch for authentication issues or performance changes
211+
212+
#### Common Migration Issues
213+
214+
**Issue: NoClassDefFoundError**
215+
- **Cause**: Conflicting HttpClient versions in classpath
216+
- **Fix**: Use `mvn dependency:tree` to identify conflicts and exclude old HttpClient 4.x dependencies
217+
218+
**Issue: Method not found errors**
219+
- **Cause**: Using old HttpClient 4.x APIs
220+
- **Fix**: Update all imports and method calls to HttpClient 5.x equivalents
221+
222+
**Issue: Authentication failures**
223+
- **Cause**: Token handling differences
224+
- **Fix**: Ensure SST/TT tokens are being passed correctly; check logs for authentication errors
225+
226+
### Need Help?
227+
228+
- Review the [complete API documentation](http://javadoc.io/doc/com.gooddata/gooddata-http-client)
229+
- Check [Apache HttpClient 5.x migration guide](https://hc.apache.org/httpcomponents-client-5.3.x/migration-guide/index.html)
230+
- Report issues on [GitHub](https://github.com/gooddata/gooddata-http-client/issues)
231+
81232
## Build
82233

83234
```

pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@
164164
<version>3.2.5</version>
165165
<configuration>
166166
<useModulePath>false</useModulePath>
167-
<!-- <skip>true</skip> -->
168167
</configuration>
169168
</plugin>
170169
</plugins>

src/main/java/com/gooddata/http/client/GoodDataHttpClient.java

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ private ClassicHttpResponse handleResponse(
116116
throw new IOException("Interrupted while waiting for token refresh", e);
117117
}
118118
}
119+
// After waiting, verify that tt was successfully obtained
120+
if (tt == null) {
121+
throw new GoodDataAuthException("Token refresh completed but TT is still null");
122+
}
119123
final ClassicHttpRequest retryRequest = cloneRequestWithNewTT(originalRequest, tt);
120124
return this.httpClient.execute(httpHost, retryRequest, context, response -> copyResponseEntity(response));
121125
} else {
@@ -160,28 +164,93 @@ private ClassicHttpResponse handleResponse(
160164
return retryResponse;
161165
}
162166

163-
private ClassicHttpRequest cloneRequestWithNewTT(ClassicHttpRequest original, String newTT) {
167+
private ClassicHttpRequest cloneRequestWithNewTT(ClassicHttpRequest original, String newTT) throws IOException {
164168
ClassicHttpRequest copy;
165-
// Clone basic types (extend if needed)
169+
170+
// Clone request based on method type
166171
switch (original.getMethod()) {
167172
case "GET":
168173
copy = new HttpGet(original.getRequestUri());
169174
break;
175+
case "POST":
176+
copy = cloneRequestWithEntity(
177+
new org.apache.hc.client5.http.classic.methods.HttpPost(original.getRequestUri()),
178+
original
179+
);
180+
break;
181+
case "PUT":
182+
copy = cloneRequestWithEntity(
183+
new org.apache.hc.client5.http.classic.methods.HttpPut(original.getRequestUri()),
184+
original
185+
);
186+
break;
187+
case "PATCH":
188+
copy = cloneRequestWithEntity(
189+
new org.apache.hc.client5.http.classic.methods.HttpPatch(original.getRequestUri()),
190+
original
191+
);
192+
break;
170193
case "DELETE":
171194
copy = new org.apache.hc.client5.http.classic.methods.HttpDelete(original.getRequestUri());
172195
break;
196+
case "HEAD":
197+
copy = new org.apache.hc.client5.http.classic.methods.HttpHead(original.getRequestUri());
198+
break;
199+
case "OPTIONS":
200+
copy = new org.apache.hc.client5.http.classic.methods.HttpOptions(original.getRequestUri());
201+
break;
173202
default:
174203
throw new UnsupportedOperationException("Unsupported HTTP method: " + original.getMethod());
175204
}
205+
176206
// Copy original headers
177207
for (Header header : original.getHeaders()) {
178208
copy.addHeader(header.getName(), header.getValue());
179209
}
210+
180211
// Set the new TT
181212
copy.addHeader(TT_HEADER, newTT);
182213
return copy;
183214
}
184215

216+
/**
217+
* Helper method to clone request entity safely, handling both repeatable and non-repeatable entities.
218+
* For non-repeatable entities, buffers the content to allow reuse.
219+
*/
220+
private <T extends ClassicHttpRequest> T cloneRequestWithEntity(T target, ClassicHttpRequest source) throws IOException {
221+
if (!(source instanceof org.apache.hc.core5.http.HttpEntityContainer)) {
222+
return target;
223+
}
224+
225+
org.apache.hc.core5.http.HttpEntity entity =
226+
((org.apache.hc.core5.http.HttpEntityContainer) source).getEntity();
227+
228+
if (entity == null) {
229+
return target;
230+
}
231+
232+
// Check if entity is repeatable - if so, we can reuse it directly
233+
if (entity.isRepeatable()) {
234+
if (target instanceof org.apache.hc.core5.http.HttpEntityContainer) {
235+
((org.apache.hc.core5.http.HttpEntityContainer) target).setEntity(entity);
236+
}
237+
} else {
238+
// Entity is not repeatable - buffer it for reuse
239+
log.debug("Buffering non-repeatable entity for retry");
240+
byte[] content = EntityUtils.toByteArray(entity);
241+
String contentTypeStr = entity.getContentType();
242+
ContentType contentType = contentTypeStr != null ?
243+
ContentType.parseLenient(contentTypeStr) : ContentType.DEFAULT_BINARY;
244+
245+
ByteArrayEntity bufferedEntity = new ByteArrayEntity(content, contentType);
246+
if (target instanceof org.apache.hc.core5.http.HttpEntityContainer) {
247+
((org.apache.hc.core5.http.HttpEntityContainer) target).setEntity(bufferedEntity);
248+
}
249+
}
250+
251+
return target;
252+
}
253+
185254
private boolean refreshTt() throws IOException {
186255
log.debug("Obtaining TT");
187256
final HttpGet request = new HttpGet(TOKEN_URL);
@@ -212,7 +281,11 @@ private boolean refreshTt() throws IOException {
212281
*/
213282
public ClassicHttpResponse execute(HttpHost target, ClassicHttpRequest request, HttpContext context) throws IOException {
214283
notNull(request, "Request can't be null");
215-
// FIX: use only writeLock to avoid deadlock during handleResponse
284+
// Using write lock for all requests to prevent deadlock scenarios where:
285+
// 1. Thread A holds read lock and calls handleResponse (needs write lock)
286+
// 2. Thread B waits for write lock to refresh tokens
287+
// 3. Deadlock occurs as Thread A can't upgrade from read to write lock
288+
// Trade-off: Serializes all requests but ensures thread safety during token refresh
216289
final Lock lock = rwLock.writeLock();
217290
lock.lock();
218291
try {
@@ -256,8 +329,7 @@ public ClassicHttpResponse execute(HttpHost target, ClassicHttpRequest request,
256329
}
257330

258331
public ClassicHttpResponse execute(HttpHost target, ClassicHttpRequest request) throws IOException {
259-
// return httpClient.execute(target, request, (HttpContext) null, response -> response);
260-
return execute(target, request, null);
332+
return execute(target, request, null);
261333
}
262334

263335
public <T> T execute(HttpHost target, ClassicHttpRequest request, HttpContext context,
@@ -284,7 +356,9 @@ private ClassicHttpResponse copyResponseEntity(ClassicHttpResponse response) thr
284356

285357
// Copy the entity content
286358
byte[] content = EntityUtils.toByteArray(response.getEntity());
287-
ContentType contentType = ContentType.parseLenient(response.getEntity().getContentType());
359+
String contentTypeStr = response.getEntity().getContentType();
360+
ContentType contentType = contentTypeStr != null ?
361+
ContentType.parseLenient(contentTypeStr) : ContentType.DEFAULT_BINARY;
288362

289363
// Create a new response with copied entity
290364
BasicClassicHttpResponse newResponse = new BasicClassicHttpResponse(response.getCode(), response.getReasonPhrase());

0 commit comments

Comments
 (0)