diff --git a/README.md b/README.md
index f90e990b6..c883eaf33 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ The *GoodData Java SDK* is available in Maven Central Repository, to use it from
com.gooddata
gooddata-java
- 2.35.0+api1
+ 2.36.0+api1
```
See [releases page](https://github.com/gooddata/gooddata-java/releases) for information about versions and notable changes,
diff --git a/pom.xml b/pom.xml
index 075a62903..bfc410325 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
gooddata-java
- 2.35.1-SNAPSHOT
+ 2.36.1-SNAPSHOT
${project.artifactId}
GoodData Java SDK
https://github.com/gooddata/gooddata-java
diff --git a/src/main/java/com/gooddata/GoodData.java b/src/main/java/com/gooddata/GoodData.java
index df973e60b..63072686f 100644
--- a/src/main/java/com/gooddata/GoodData.java
+++ b/src/main/java/com/gooddata/GoodData.java
@@ -288,6 +288,8 @@ private HttpClientBuilder createHttpClientBuilder(final GoodDataSettings setting
return HttpClientBuilder.create()
.setUserAgent(StringUtils.isNotBlank(settings.getUserAgent()) ? String.format("%s %s", settings.getUserAgent(), getUserAgent()) : getUserAgent())
+ .addInterceptorFirst(new RequestIdInterceptor())
+ .addInterceptorFirst(new ResponseMissingRequestIdInterceptor())
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig.build());
}
diff --git a/src/main/java/com/gooddata/RequestIdInterceptor.java b/src/main/java/com/gooddata/RequestIdInterceptor.java
new file mode 100644
index 000000000..5d984cdc2
--- /dev/null
+++ b/src/main/java/com/gooddata/RequestIdInterceptor.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2004-2021, GoodData(R) Corporation. All rights reserved.
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+package com.gooddata;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.http.Header;
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.annotation.Contract;
+import org.apache.http.annotation.ThreadingBehavior;
+import org.apache.http.protocol.HttpContext;
+
+import java.io.IOException;
+
+import static com.gooddata.gdc.Header.GDC_REQUEST_ID;
+
+/**
+ * Intercepts the client-side requests on low-level in order to be able to catch requests also from the Sardine,
+ * that is working independently from Spring {@link org.springframework.web.client.RestTemplate} to set
+ * the X-GDC-REQUEST header to them.
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public class RequestIdInterceptor implements HttpRequestInterceptor {
+
+ @Override
+ public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
+ final StringBuilder requestIdBuilder = new StringBuilder();
+ final Header requestIdHeader = request.getFirstHeader(GDC_REQUEST_ID);
+ if (requestIdHeader != null) {
+ requestIdBuilder.append(requestIdHeader.getValue()).append(":");
+ }
+ final String requestId = requestIdBuilder.append(RandomStringUtils.randomAlphanumeric(16)).toString();
+ request.setHeader(GDC_REQUEST_ID, requestId);
+ }
+}
diff --git a/src/main/java/com/gooddata/ResponseMissingRequestIdInterceptor.java b/src/main/java/com/gooddata/ResponseMissingRequestIdInterceptor.java
new file mode 100644
index 000000000..8af1b4dc5
--- /dev/null
+++ b/src/main/java/com/gooddata/ResponseMissingRequestIdInterceptor.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2004-2021, GoodData(R) Corporation. All rights reserved.
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+package com.gooddata;
+
+import org.apache.http.Header;
+import org.apache.http.HttpException;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpResponseInterceptor;
+import org.apache.http.annotation.Contract;
+import org.apache.http.annotation.ThreadingBehavior;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpCoreContext;
+
+import java.io.IOException;
+
+import static com.gooddata.gdc.Header.GDC_REQUEST_ID;
+
+
+/**
+ * Intercepts responses to check if they have set the X-GDC-REQUEST header for easier debugging.
+ * If not, it takes this header from the request sent.
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public class ResponseMissingRequestIdInterceptor implements HttpResponseInterceptor {
+
+ @Override
+ public void process(final HttpResponse response, final HttpContext context) throws HttpException, IOException {
+
+ if (response.getFirstHeader(GDC_REQUEST_ID) == null) {
+ final HttpCoreContext coreContext = HttpCoreContext.adapt(context);
+ final Header requestIdHeader = coreContext.getRequest().getFirstHeader(GDC_REQUEST_ID);
+ response.setHeader(GDC_REQUEST_ID, requestIdHeader.getValue());
+ }
+ }
+}
diff --git a/src/main/java/com/gooddata/gdc/DataStoreService.java b/src/main/java/com/gooddata/gdc/DataStoreService.java
index 7f0064203..0ee8246ea 100644
--- a/src/main/java/com/gooddata/gdc/DataStoreService.java
+++ b/src/main/java/com/gooddata/gdc/DataStoreService.java
@@ -5,8 +5,8 @@
*/
package com.gooddata.gdc;
-import com.github.sardine.Sardine;
import com.github.sardine.impl.SardineException;
+import com.gooddata.GoodDataRestException;
import com.gooddata.UriPrefixer;
import org.apache.http.Header;
import org.apache.http.HeaderIterator;
@@ -24,19 +24,23 @@
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicHeader;
import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
+import java.util.Collections;
+import java.util.List;
import java.util.Locale;
import static com.gooddata.util.Validate.notEmpty;
@@ -47,7 +51,7 @@
*/
public class DataStoreService {
- private final Sardine sardine;
+ private final GdcSardine sardine;
private final GdcService gdcService;
private final URI gdcUri;
private final RestTemplate restTemplate;
@@ -102,7 +106,10 @@ public void upload(String path, InputStream stream) {
private void upload(URI url, InputStream stream) {
try {
- sardine.put(url.toString(), stream);
+ // We need to use it this way, if we want to track request_id in the stacktrace.
+ InputStreamEntity entity = new InputStreamEntity(stream);
+ List headers = Collections.singletonList(new BasicHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE));
+ sardine.put(url.toString(), entity, headers, new GdcSardineResponseHandler());
} catch (SardineException e) {
if (HttpStatus.INTERNAL_SERVER_ERROR.value() == e.getStatusCode()) {
// this error may occur when user issues request to WebDAV before SST and TT were obtained
@@ -144,7 +151,7 @@ public InputStream download(String path) {
notEmpty(path, "path");
final URI uri = getUri(path);
try {
- return sardine.get(uri.toString());
+ return sardine.get(uri.toString(), Collections.emptyList(), new GdcSardineResponseHandler());
} catch (IOException e) {
throw new DataStoreException("Unable to download from " + uri, e);
}
@@ -165,7 +172,7 @@ public void delete(String path) {
if (HttpStatus.MOVED_PERMANENTLY.equals(result.getStatusCode())) {
restTemplate.exchange(result.getHeaders().getLocation(), HttpMethod.DELETE, org.springframework.http.HttpEntity.EMPTY, Void.class);
}
- } catch (RestClientException e) {
+ } catch (GoodDataRestException e) {
throw new DataStoreException("Unable to delete " + uri, e);
}
}
diff --git a/src/main/java/com/gooddata/gdc/GdcSardine.java b/src/main/java/com/gooddata/gdc/GdcSardine.java
index 084b4d4ef..637692028 100644
--- a/src/main/java/com/gooddata/gdc/GdcSardine.java
+++ b/src/main/java/com/gooddata/gdc/GdcSardine.java
@@ -8,11 +8,18 @@
import static com.gooddata.util.Validate.notNull;
import com.github.sardine.impl.SardineImpl;
+import com.github.sardine.impl.io.ContentLengthInputStream;
+import com.github.sardine.impl.io.HttpMethodReleaseInputStream;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.impl.client.HttpClientBuilder;
import java.io.IOException;
+import java.util.List;
+import java.util.Map;
/**
* This class extends SardineImpl, connections were not correctly closed by parent
@@ -28,11 +35,41 @@ public GdcSardine(HttpClientBuilder builder) {
*/
@Override
protected T execute(HttpRequestBase request, ResponseHandler responseHandler) throws IOException {
- notNull(request,"request");
+ notNull(request, "request");
try {
return super.execute(request, responseHandler);
} finally {
request.releaseConnection();
}
}
+
+ /**
+ * The method body is retrieved from {@link SardineImpl#get(String, Map)} and extended about the response handler
+ * to be able to handle responses arbitrarily.
+ *
+ * @param url Path to the resource including protocol and hostname
+ * @param headers Additional HTTP headers to add to the request
+ * @param responseHandler Arbitrary response handler to manipulate with responses
+ * @return Data stream to read from
+ * @throws IOException I/O error or HTTP response validation failure
+ */
+ public ContentLengthInputStream get(final String url, final List headers, final
+ ResponseHandler responseHandler) throws IOException {
+
+ final HttpGet get = new HttpGet(url);
+ for (Header header : headers) {
+ get.addHeader(header);
+ }
+ // Must use #execute without handler, otherwise the entity is consumed
+ // already after the handler exits.
+ final HttpResponse response = this.execute(get);
+ try {
+ responseHandler.handleResponse(response);
+ // Will abort the read when closed before EOF.
+ return new ContentLengthInputStream(new HttpMethodReleaseInputStream(response), response.getEntity().getContentLength());
+ } catch (IOException ex) {
+ get.abort();
+ throw ex;
+ }
+ }
}
diff --git a/src/main/java/com/gooddata/gdc/GdcSardineException.java b/src/main/java/com/gooddata/gdc/GdcSardineException.java
new file mode 100644
index 000000000..0865818c4
--- /dev/null
+++ b/src/main/java/com/gooddata/gdc/GdcSardineException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007-2021, GoodData(R) Corporation. All rights reserved.
+ */
+package com.gooddata.gdc;
+
+import com.github.sardine.impl.SardineException;
+
+/**
+ * Extended Sardine exception about X-GDC-REQUEST header value.
+ */
+public class GdcSardineException extends SardineException {
+
+ private final String requestId;
+
+ /**
+ * @param msg Custom description of failure
+ * @param statusCode Error code returned by server
+ * @param responsePhrase Response phrase following the error code
+ * @param requestId The X-GDC-REQUEST identifier.
+ */
+ public GdcSardineException(String requestId, String msg, int statusCode, String responsePhrase) {
+ super(msg, statusCode, responsePhrase);
+ this.requestId = requestId;
+ }
+
+ @Override
+ public String getMessage() {
+ return String.format("[request_id=%s]: %s (%d %s)", this.requestId, super.getMessage(), this.getStatusCode(),
+ this.getResponsePhrase()
+ );
+ }
+
+}
diff --git a/src/main/java/com/gooddata/gdc/GdcSardineResponseHandler.java b/src/main/java/com/gooddata/gdc/GdcSardineResponseHandler.java
new file mode 100644
index 000000000..cbb1659c0
--- /dev/null
+++ b/src/main/java/com/gooddata/gdc/GdcSardineResponseHandler.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2004-2021, GoodData(R) Corporation. All rights reserved.
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+package com.gooddata.gdc;
+
+import com.github.sardine.impl.SardineException;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.StatusLine;
+import org.apache.http.client.ResponseHandler;
+
+import java.io.IOException;
+
+import static com.gooddata.gdc.Header.GDC_REQUEST_ID;
+
+
+/**
+ * A basic validation response handler that extends the functionality of {@link com.github.sardine.impl.handler.ValidatingResponseHandler}
+ * about the addition of X-GDC-REQUEST header to exception.
+ */
+class GdcSardineResponseHandler implements ResponseHandler {
+
+ @Override
+ public Void handleResponse(final HttpResponse response) throws IOException {
+ final StatusLine statusLine = response.getStatusLine();
+ final int statusCode = statusLine.getStatusCode();
+ if (statusCode >= HttpStatus.SC_OK && statusCode < HttpStatus.SC_MULTIPLE_CHOICES) {
+ return null;
+ }
+ final Header requestIdHeader = response.getFirstHeader(GDC_REQUEST_ID);
+ if (requestIdHeader != null) {
+ throw new GdcSardineException(requestIdHeader.getValue(), "Unexpected response", statusLine.getStatusCode(),
+ statusLine.getReasonPhrase()
+ );
+ } else {
+ throw new SardineException("Unexpected response", statusLine.getStatusCode(), statusLine.getReasonPhrase());
+ }
+ }
+}
diff --git a/src/test/java/com/gooddata/gdc/DatastoreServiceAT.java b/src/test/java/com/gooddata/gdc/DatastoreServiceAT.java
index 306584666..d65632662 100644
--- a/src/test/java/com/gooddata/gdc/DatastoreServiceAT.java
+++ b/src/test/java/com/gooddata/gdc/DatastoreServiceAT.java
@@ -18,10 +18,11 @@
import java.util.UUID;
import static com.gooddata.util.ResourceUtils.readFromResource;
-import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
-import static org.testng.Assert.assertEquals;
import static org.testng.Assert.fail;
/**
@@ -60,6 +61,17 @@ public void datastoreDownload() throws Exception {
}
@Test(groups = "datastore", dependsOnMethods = "datastoreDownload")
+ public void verifyRequestIdInException() throws Exception {
+ DataStoreService dataStoreService = AbstractGoodDataAT.gd.getDataStoreService();
+
+ try (InputStream ignored = dataStoreService.download(this.directory + "/none.txt")) {
+ fail("The exception should contain the request_id in its stacktrace.");
+ } catch (Exception e){
+ assertThat(e.getCause().toString(), containsString("request_id"));
+ }
+ }
+
+ @Test(groups = "datastore", dependsOnMethods = "verifyRequestIdInException")
public void datastoreDelete() throws Exception {
DataStoreService dataStoreService = gd.getDataStoreService();
dataStoreService.delete(this.file);
@@ -68,8 +80,10 @@ public void datastoreDelete() throws Exception {
try {
dataStoreService.delete(this.directory);
fail("Exception was expected, as there is nothing to delete");
- } catch (GoodDataRestException e) {
- assertEquals(404, e.getStatusCode());
+ } catch (DataStoreException e) {
+ assertThat(e.getCause().toString(), containsString("request_id"));
+ assertThat(e.getCause().toString(), containsString("404"));
+ assertThat(e.getCause(), instanceOf(GoodDataRestException.class));
}
}