Skip to content

Commit 466aa0b

Browse files
authored
add interceptors for httpclient5 (via #935)
1 parent 10b1bcc commit 466aa0b

File tree

12 files changed

+888
-0
lines changed

12 files changed

+888
-0
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,25 @@ Usage example:
176176
.addInterceptorLast(new AllureHttpClientResponse());
177177
```
178178

179+
## Http client 5
180+
Interceptors for Apache [httpclient5](https://hc.apache.org/httpcomponents-client-5.2.x/index.html).
181+
Additional info can be found in module `allure-httpclient5`
182+
183+
```xml
184+
<dependency>
185+
<groupId>io.qameta.allure</groupId>
186+
<artifactId>allure-httpclient5</artifactId>
187+
<version>$LATEST_VERSION</version>
188+
</dependency>
189+
```
190+
191+
Usage example:
192+
```java
193+
final HttpClientBuilder builder = HttpClientBuilder.create()
194+
.addRequestInterceptorFirst(new AllureHttpClient5Request("your-request-template-attachment.ftl"))
195+
.addResponseInterceptorLast(new AllureHttpClient5Response("your-response-template-attachment.ftl"));
196+
```
197+
179198
## JAX-RS Filter
180199

181200
Filter that can be used with JAX-RS compliant clients such as RESTeasy and Jersey
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
description = "Allure Apache HttpClient5 Integration"
2+
3+
dependencies {
4+
api(project(":allure-attachments"))
5+
implementation("org.apache.httpcomponents.client5:httpclient5")
6+
testImplementation("com.github.tomakehurst:wiremock")
7+
testImplementation("io.github.glytching:junit-extensions")
8+
testImplementation("org.assertj:assertj-core")
9+
testImplementation("org.junit.jupiter:junit-jupiter-api")
10+
testImplementation("org.mockito:mockito-core")
11+
testImplementation("org.slf4j:slf4j-simple")
12+
testImplementation(project(":allure-java-commons-test"))
13+
testImplementation(project(":allure-junit-platform"))
14+
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
15+
}
16+
17+
tasks.jar {
18+
manifest {
19+
attributes(mapOf(
20+
"Automatic-Module-Name" to "io.qameta.allure.httpclient5"
21+
))
22+
}
23+
}
24+
25+
tasks.test {
26+
useJUnitPlatform()
27+
}

allure-httpclient5/readme.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
## Allure-httpclient5
2+
Extended logging for requests and responses with [httpclient5](https://mvnrepository.com/artifact/org.apache.httpcomponents.client5/httpclient5)
3+
This library does not support `httpclient` due to package and API changes between `httpclient` and `httpclient5`.
4+
To work with `httpclient`, it is recommended to use the `allure-httpclient` library.
5+
6+
## Wiki
7+
https://hc.apache.org/httpcomponents-client-5.2.x/
8+
https://hc.apache.org/httpcomponents-client-5.2.x/quickstart.html
9+
https://hc.apache.org/httpcomponents-client-5.2.x/migration-guide/index.html
10+
https://hc.apache.org/httpcomponents-client-5.2.x/examples.html
11+
12+
## Additional features
13+
Implemented:
14+
- The `httpclient5` library uses `gzip` compression by default. Interceptors attach message bodies in decompressed form
15+
- `HttpEntityEnclosingRequest` is removed from `httpclient5`. Request interceptor works wo `HttpEntityEnclosingRequest`
16+
17+
Not tested:
18+
- The httpclient5 library support Async interactions (Not tested)
19+
20+
## Examples
21+
22+
```java
23+
import org.apache.hc.client5.http.classic.methods.HttpGet;
24+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
25+
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
26+
import org.apache.hc.core5.http.io.entity.EntityUtils;
27+
import io.qameta.allure.httpclient5.AllureHttpClient5Request;
28+
import io.qameta.allure.httpclient5.AllureHttpClient5Response;
29+
30+
class Test {
31+
32+
@Test
33+
void smokeGetShouldNotThrowThenReturnCorrectResponseMessage() throws IOException {
34+
final HttpClientBuilder builder = HttpClientBuilder.create()
35+
.addRequestInterceptorFirst(new AllureHttpClient5Request())
36+
.addResponseInterceptorLast(new AllureHttpClient5Response());
37+
38+
try (CloseableHttpClient httpClient = builder.build()) {
39+
final HttpGet httpGet = new HttpGet("/hello");
40+
httpClient.execute(httpGet, response -> {
41+
assertThat(EntityUtils.toString(response.getEntity())).isEqualTo(BODY_STRING);
42+
return response;
43+
});
44+
}
45+
}
46+
}
47+
```
48+
49+
In addition to using standard templates for formatting, you can use your custom `ftl` templates along the path
50+
`/resources/tpl/...`. For examples, you can use templates from the `allure-attachments` module.
51+
52+
```java
53+
final HttpClientBuilder builder = HttpClientBuilder.create()
54+
.addRequestInterceptorFirst(new AllureHttpClient5Request("your-request-template-attachment.ftl"))
55+
.addResponseInterceptorLast(new AllureHttpClient5Response("your-response-template-attachment.ftl"));
56+
```
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2019 Qameta Software OÜ
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.qameta.allure.httpclient5;
17+
18+
import io.qameta.allure.attachment.AttachmentData;
19+
import io.qameta.allure.attachment.AttachmentProcessor;
20+
import io.qameta.allure.attachment.AttachmentRenderer;
21+
import io.qameta.allure.attachment.DefaultAttachmentProcessor;
22+
import io.qameta.allure.attachment.FreemarkerAttachmentRenderer;
23+
import io.qameta.allure.attachment.http.HttpRequestAttachment;
24+
import org.apache.hc.core5.http.EntityDetails;
25+
import org.apache.hc.core5.http.HttpEntity;
26+
import org.apache.hc.core5.http.HttpRequest;
27+
import org.apache.hc.core5.http.HttpRequestInterceptor;
28+
import org.apache.hc.core5.http.protocol.HttpContext;
29+
30+
import java.util.stream.Stream;
31+
32+
import static io.qameta.allure.attachment.http.HttpRequestAttachment.Builder.create;
33+
34+
/**
35+
* @author a-simeshin (Simeshin Artem)
36+
*/
37+
@SuppressWarnings("PMD.MethodArgumentCouldBeFinal")
38+
public class AllureHttpClient5Request implements HttpRequestInterceptor {
39+
40+
private final AttachmentRenderer<AttachmentData> renderer;
41+
private final AttachmentProcessor<AttachmentData> processor;
42+
43+
public AllureHttpClient5Request() {
44+
this("http-request.ftl");
45+
}
46+
47+
public AllureHttpClient5Request(final String templateName) {
48+
this(new FreemarkerAttachmentRenderer(templateName), new DefaultAttachmentProcessor());
49+
}
50+
51+
public AllureHttpClient5Request(final AttachmentRenderer<AttachmentData> renderer,
52+
final AttachmentProcessor<AttachmentData> processor) {
53+
this.renderer = renderer;
54+
this.processor = processor;
55+
}
56+
57+
/**
58+
* Processes the HTTP request and adds an attachment to the Allure Attachment processor.
59+
*
60+
* @param request the HTTP request
61+
* @param entity the entity details
62+
* @param context the HTTP context
63+
*/
64+
@Override
65+
public void process(HttpRequest request, EntityDetails entity, HttpContext context) {
66+
final String attachmentName = getAttachmentName(request);
67+
final HttpRequestAttachment.Builder builder = create(attachmentName, request.getRequestUri());
68+
builder.setMethod(request.getMethod());
69+
70+
Stream.of(request.getHeaders()).forEach(header -> builder.setHeader(header.getName(), header.getValue()));
71+
72+
if (entity instanceof HttpEntity && ((HttpEntity) entity).isRepeatable() && entity.getContentLength() != 0) {
73+
builder.setBody(AllureHttpEntityUtils.getBody((HttpEntity) entity));
74+
}
75+
76+
processor.addAttachment(builder.build(), renderer);
77+
}
78+
79+
private String getAttachmentName(final HttpRequest request) {
80+
return String.format("Request_%s_%s", request.getMethod(), request.getRequestUri());
81+
}
82+
83+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2019 Qameta Software OÜ
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.qameta.allure.httpclient5;
17+
18+
import io.qameta.allure.attachment.AttachmentData;
19+
import io.qameta.allure.attachment.AttachmentProcessor;
20+
import io.qameta.allure.attachment.AttachmentRenderer;
21+
import io.qameta.allure.attachment.DefaultAttachmentProcessor;
22+
import io.qameta.allure.attachment.FreemarkerAttachmentRenderer;
23+
import io.qameta.allure.attachment.http.HttpResponseAttachment;
24+
import org.apache.hc.core5.http.EntityDetails;
25+
import org.apache.hc.core5.http.HttpEntity;
26+
import org.apache.hc.core5.http.HttpResponse;
27+
import org.apache.hc.core5.http.HttpResponseInterceptor;
28+
import org.apache.hc.core5.http.io.entity.BufferedHttpEntity;
29+
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
30+
import org.apache.hc.core5.http.protocol.HttpContext;
31+
32+
import java.io.IOException;
33+
import java.util.stream.Stream;
34+
35+
import static io.qameta.allure.attachment.http.HttpResponseAttachment.Builder.create;
36+
37+
/**
38+
* @author a-simeshin (Simeshin Artem)
39+
*/
40+
@SuppressWarnings({
41+
"checkstyle:ParameterAssignment",
42+
"PMD.MethodArgumentCouldBeFinal",
43+
"PMD.AvoidReassigningParameters"})
44+
public class AllureHttpClient5Response implements HttpResponseInterceptor {
45+
private final AttachmentRenderer<AttachmentData> renderer;
46+
private final AttachmentProcessor<AttachmentData> processor;
47+
private static final String NO_BODY = "No body present";
48+
49+
public AllureHttpClient5Response() {
50+
this("http-response.ftl");
51+
}
52+
53+
public AllureHttpClient5Response(final String templateName) {
54+
this(new FreemarkerAttachmentRenderer(templateName), new DefaultAttachmentProcessor());
55+
}
56+
57+
public AllureHttpClient5Response(final AttachmentRenderer<AttachmentData> renderer,
58+
final AttachmentProcessor<AttachmentData> processor) {
59+
this.renderer = renderer;
60+
this.processor = processor;
61+
}
62+
63+
/**
64+
* Processes the HTTP response and adds an attachment to the Allure Attachment processor.
65+
*
66+
* @param response the HTTP response
67+
* @param entity the entity details, may be null for no response body responses
68+
* @param context the HTTP context
69+
* @throws IOException if an I/O error occurs
70+
*/
71+
@Override
72+
public void process(HttpResponse response, EntityDetails entity, HttpContext context) throws IOException {
73+
final HttpResponseAttachment.Builder builder = create("Response");
74+
builder.setResponseCode(response.getCode());
75+
76+
Stream.of(response.getHeaders()).forEach(header -> builder.setHeader(header.getName(), header.getValue()));
77+
78+
final HttpEntity originalHttpEntity = (HttpEntity) entity;
79+
if (originalHttpEntity != null && !originalHttpEntity.isRepeatable()) {
80+
// Looks like a bug or completely new logic. It's not enough to replace chaining EntityDetails entity.
81+
// To read the response body twice, It needs to put in the context also
82+
entity = new BufferedHttpEntity(originalHttpEntity);
83+
final BasicClassicHttpResponse responseEntity =
84+
(BasicClassicHttpResponse) context.getAttribute("http.response");
85+
responseEntity.setEntity((HttpEntity) entity);
86+
87+
final String responseBody = AllureHttpEntityUtils.getBody((HttpEntity) entity);
88+
if (responseBody == null || responseBody.isEmpty()) {
89+
builder.setBody(NO_BODY);
90+
} else {
91+
builder.setBody(responseBody);
92+
}
93+
} else {
94+
builder.setBody(NO_BODY);
95+
}
96+
97+
processor.addAttachment(builder.build(), renderer);
98+
}
99+
100+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2023 Qameta Software OÜ
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.qameta.allure.httpclient5;
17+
18+
import io.qameta.allure.AllureResultsWriteException;
19+
import org.apache.hc.core5.http.HttpEntity;
20+
import org.apache.hc.core5.http.ParseException;
21+
import org.apache.hc.core5.http.io.entity.EntityUtils;
22+
23+
import java.io.BufferedReader;
24+
import java.io.IOException;
25+
import java.io.InputStreamReader;
26+
import java.nio.charset.Charset;
27+
import java.nio.charset.StandardCharsets;
28+
import java.util.zip.GZIPInputStream;
29+
30+
/**
31+
* Utility class for working with HTTP entity in Allure framework.
32+
*/
33+
@SuppressWarnings({"checkstyle:ParameterAssignment", "PMD.AssignmentInOperand"})
34+
public final class AllureHttpEntityUtils {
35+
36+
private AllureHttpEntityUtils() {
37+
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
38+
}
39+
40+
/**
41+
* Retrieves the body of the HTTP entity as a string.
42+
*
43+
* @param httpEntity the HTTP entity
44+
* @return the body of the HTTP entity as a string
45+
* @throws AllureResultsWriteException if an error occurs while reading the entity body
46+
*/
47+
static String getBody(final HttpEntity httpEntity) {
48+
try {
49+
final String contentEncoding = httpEntity.getContentEncoding();
50+
if (contentEncoding != null && contentEncoding.contains("gzip")) {
51+
return unpackGzipEntityString(httpEntity);
52+
} else {
53+
return EntityUtils.toString(httpEntity, getContentEncoding(httpEntity.getContentEncoding()));
54+
}
55+
} catch (IOException | ParseException e) {
56+
throw new AllureResultsWriteException("Can't read request message body to String", e);
57+
}
58+
}
59+
60+
/**
61+
* Retrieves the content encoding of the HTTP entity.
62+
*
63+
* @param contentEncoding the content encoding value
64+
* @return the charset corresponding to the content encoding, or UTF-8 if the encoding is invalid
65+
*/
66+
static Charset getContentEncoding(final String contentEncoding) {
67+
try {
68+
return Charset.forName(contentEncoding);
69+
} catch (IllegalArgumentException ignored) {
70+
return StandardCharsets.UTF_8;
71+
}
72+
}
73+
74+
/**
75+
* Unpacks the GZIP-encoded entity string.
76+
*
77+
* @param entity the GZIP-encoded HTTP entity
78+
* @return the unpacked entity string
79+
* @throws IOException if an error occurs while unpacking the entity
80+
*/
81+
static String unpackGzipEntityString(final HttpEntity entity) throws IOException {
82+
final GZIPInputStream gis = new GZIPInputStream(entity.getContent());
83+
final Charset contentEncoding = getContentEncoding(entity.getContentEncoding());
84+
try (InputStreamReader inputStreamReader = new InputStreamReader(gis, contentEncoding)) {
85+
try (BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
86+
final StringBuilder outStr = new StringBuilder();
87+
String line;
88+
while ((line = bufferedReader.readLine()) != null) {
89+
outStr.append(line);
90+
}
91+
return outStr.toString();
92+
}
93+
}
94+
}
95+
96+
}

0 commit comments

Comments
 (0)