Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ Since *GoodData Java SDK* version *2.32.0* API versioning is supported. The API
### Dependencies

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

### Running Acceptance Tests

To run acceptance tests against a real GoodData environment, use the following command:

```bash
# ⚠️ EXAMPLE ONLY - Replace with your actual credentials
host=your-instance.gooddata.com \
login=your.email@example.com \
password=YOUR_PASSWORD_HERE \
projectToken=YOUR_PROJECT_TOKEN \
warehouseToken=YOUR_WAREHOUSE_TOKEN \
mvn verify -P at
```

**Security Note:** Never commit real credentials to version control. Use environment variables or secure credential management systems in production.

For releasing see [Releasing How-To](https://github.com/gooddata/gooddata-java/wiki/Releasing).

## Contribute
Expand Down
24 changes: 23 additions & 1 deletion gooddata-java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,18 @@
<groupId>com.gooddata</groupId>
<artifactId>gooddata-http-client</artifactId>
</dependency>
<!-- HttpClient 4.x for backward compatibility -->

<!-- HttpClient 5.x - explicitly declared as we use it directly -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
</dependency>

<!-- HttpClient 4.x for Sardine library compatibility -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
Expand Down Expand Up @@ -97,6 +108,17 @@
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- JUnit Jupiter (JUnit 5) - explicitly declared as we use it in tests -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
package com.gooddata.sdk.common;


import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.*;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.client5.http.classic.HttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -29,19 +29,18 @@
import java.util.Map;

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

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

/**
* Create a factory with the given HttpClient 4.x instance.
* Create a factory with the given HttpClient 5.x instance.
*
* @param httpClient the HttpClient 4.x instance to use
* @param httpClient the HttpClient 5.x instance to use
*/
public HttpClient4ComponentsClientHttpRequestFactory(HttpClient httpClient) {
Assert.notNull(httpClient, "HttpClient must not be null");
Expand All @@ -50,50 +49,50 @@ public HttpClient4ComponentsClientHttpRequestFactory(HttpClient httpClient) {

@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
ClassicHttpRequest httpRequest = createHttpUriRequest(httpMethod, uri);
return new HttpClient4ComponentsClientHttpRequest(httpClient, httpRequest);
}

/**
* Create an Apache HttpComponents HttpUriRequest object for the given HTTP method and URI.
* Create an Apache HttpComponents ClassicHttpRequest object for the given HTTP method and URI.
*
* @param httpMethod the HTTP method
* @param uri the URI
* @return the HttpUriRequest
* @return the ClassicHttpRequest
*/
private HttpUriRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) {
private ClassicHttpRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) {
if (HttpMethod.GET.equals(httpMethod)) {
return new HttpGet(uri);
return ClassicRequestBuilder.get(uri).build();
} else if (HttpMethod.HEAD.equals(httpMethod)) {
return new HttpHead(uri);
return ClassicRequestBuilder.head(uri).build();
} else if (HttpMethod.POST.equals(httpMethod)) {
return new HttpPost(uri);
return ClassicRequestBuilder.post(uri).build();
} else if (HttpMethod.PUT.equals(httpMethod)) {
return new HttpPut(uri);
return ClassicRequestBuilder.put(uri).build();
} else if (HttpMethod.PATCH.equals(httpMethod)) {
return new HttpPatch(uri);
return ClassicRequestBuilder.patch(uri).build();
} else if (HttpMethod.DELETE.equals(httpMethod)) {
return new HttpDelete(uri);
return ClassicRequestBuilder.delete(uri).build();
} else if (HttpMethod.OPTIONS.equals(httpMethod)) {
return new HttpOptions(uri);
return ClassicRequestBuilder.options(uri).build();
} else if (HttpMethod.TRACE.equals(httpMethod)) {
return new HttpTrace(uri);
return ClassicRequestBuilder.trace(uri).build();
} else {
throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod);
}
}

/**
* {@link ClientHttpRequest} implementation based on Apache HttpComponents HttpClient 4.x.
* {@link ClientHttpRequest} implementation based on Apache HttpComponents HttpClient 5.x.
*/
private static class HttpClient4ComponentsClientHttpRequest implements ClientHttpRequest {

private final HttpClient httpClient;
private final HttpUriRequest httpRequest;
private final ClassicHttpRequest httpRequest;
private final HttpHeaders headers;
private ByteArrayOutputStream bufferedOutput = new ByteArrayOutputStream(1024);

public HttpClient4ComponentsClientHttpRequest(HttpClient httpClient, HttpUriRequest httpRequest) {
public HttpClient4ComponentsClientHttpRequest(HttpClient httpClient, ClassicHttpRequest httpRequest) {
this.httpClient = httpClient;
this.httpRequest = httpRequest;
this.headers = new HttpHeaders();
Expand All @@ -111,7 +110,11 @@ public String getMethodValue() {

@Override
public URI getURI() {
return httpRequest.getURI();
try {
return httpRequest.getUri();
} catch (Exception e) {
throw new RuntimeException("Failed to get URI", e);
}
}

@Override
Expand All @@ -129,84 +132,50 @@ public ClientHttpResponse execute() throws IOException {
// Create entity first (matching reference implementation exactly)
byte[] bytes = bufferedOutput.toByteArray();
if (bytes.length > 0) {
if (httpRequest instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) httpRequest;

// Ensure proper UTF-8 encoding before creating entity
// This is crucial for @JsonTypeInfo annotated classes like Execution
ByteArrayEntity requestEntity = new ByteArrayEntity(bytes);


if (logger.isDebugEnabled()) {
// Check if Content-Type is already set in headers
boolean hasContentType = false;
for (org.apache.http.Header header : httpRequest.getAllHeaders()) {
if ("Content-Type".equalsIgnoreCase(header.getName())) {
hasContentType = true;
// String contentType = header.getValue();
// logger.debug("Content-Type from headers: {}", contentType);
break;
}
}

if (!hasContentType) {
// logger.debug("Default Content-Type set: application/json; charset=UTF-8");
}
}

entityRequest.setEntity(requestEntity);

}
// HttpClient 5.x - set entity directly on the request
ByteArrayEntity requestEntity = new ByteArrayEntity(bytes, null);
httpRequest.setEntity(requestEntity);
}

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

// Handle both GoodDataHttpClient and standard HttpClient
org.apache.http.HttpResponse httpResponse;
if (httpClient.getClass().getName().contains("GoodDataHttpClient")) {
// Use reflection to call the execute method on GoodDataHttpClient
try {
// Try the single parameter execute method first
java.lang.reflect.Method executeMethod = httpClient.getClass().getMethod("execute",
org.apache.http.client.methods.HttpUriRequest.class);
httpResponse = (org.apache.http.HttpResponse) executeMethod.invoke(httpClient, httpRequest);
} catch (NoSuchMethodException e) {
// If that doesn't work, try the two parameter version with HttpContext
try {
java.lang.reflect.Method executeMethod = httpClient.getClass().getMethod("execute",
org.apache.http.client.methods.HttpUriRequest.class, org.apache.http.protocol.HttpContext.class);
httpResponse = (org.apache.http.HttpResponse) executeMethod.invoke(httpClient, httpRequest, null);
} catch (Exception e2) {
throw new IOException("Failed to execute request with GoodDataHttpClient", e2);
}
} catch (Exception e) {
throw new IOException("Failed to execute request with GoodDataHttpClient", e);
}
} else {
httpResponse = httpClient.execute(httpRequest);
// Extract HttpHost from the request URI for GoodDataHttpClient
// GoodDataHttpClient requires the target host to be explicitly provided
// to properly handle authentication and token management
try {
URI requestUri = httpRequest.getUri();
org.apache.hc.core5.http.HttpHost target = new org.apache.hc.core5.http.HttpHost(
requestUri.getScheme(),
requestUri.getHost(),
requestUri.getPort()
);

// CRITICAL: Call execute() WITHOUT ResponseHandler to ensure GoodDataHttpClient's
// authentication logic is invoked. The version with ResponseHandler bypasses auth!
// See: gooddata-http-client:2.0.0 GoodDataHttpClient.execute() implementation
ClassicHttpResponse response = httpClient.execute(target, httpRequest);

// We need to consume and store the response immediately since the connection may be closed
return new HttpClient4ComponentsClientHttpResponse(response);
} catch (java.net.URISyntaxException e) {
throw new IOException("Failed to extract target host from request URI", e);
}
return new HttpClient4ComponentsClientHttpResponse(httpResponse);
}

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

// First clear potentially problematic headers
if (httpRequest instanceof HttpUriRequest) {
HttpUriRequest uriRequest = (HttpUriRequest) httpRequest;
uriRequest.removeHeaders("Accept");
uriRequest.removeHeaders("X-GDC-Version");
uriRequest.removeHeaders("Content-Type");
}
httpRequest.removeHeaders("Accept");
httpRequest.removeHeaders("X-GDC-Version");
httpRequest.removeHeaders("Content-Type");

// 1. Accept header (first for checksum stability)
if (headers.containsKey("Accept")) {
Expand Down Expand Up @@ -243,8 +212,9 @@ private void addHeaders(HttpRequest httpRequest) {
// logger.debug("Using Spring Content-Type: {}", finalContentType);
// }
}
} else if (httpRequest instanceof HttpEntityEnclosingRequest) {
} else {
// Set default Content-Type for JSON requests with body
// In HttpClient 5.x, all requests can have entities, no need for instanceof check
finalContentType = "application/json; charset=UTF-8";
// if (logger.isDebugEnabled()) {
// logger.debug("Default Content-Type for JSON requests: {}", finalContentType);
Expand Down
Loading
Loading