Skip to content

Commit b1e32be

Browse files
committed
✨ Added RestClient support for Response HttpHeaders and Response Cookies
1 parent 53efc7d commit b1e32be

5 files changed

Lines changed: 374 additions & 2 deletions

File tree

rest-services/client/src/main/java/com/_4point/aem/docservices/rest_services/client/RestClient.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ public interface Builder {
209209
*/
210210
public GetRequest.Builder getRequestBuilder(String additionalPath);
211211

212+
public interface Cookies {
213+
public boolean isEmpty();
214+
public boolean isPresent();
215+
};
216+
212217
/**
213218
* A response from the AEM Rest Service
214219
*/
@@ -235,9 +240,44 @@ public interface Response {
235240
* @return
236241
*/
237242
public Optional<String> retrieveHeader(String header);
238-
243+
244+
public HttpHeaders headers();
245+
246+
public Cookies getCookies();
239247
}
240248

249+
/**
250+
* Represents an HTTP header as a name / value pair.
251+
*
252+
* Note that there may be multiple headers with the same name, so HttpHeaders.getHeaders() returns a
253+
* List<HttpHeader> rather than a single HttpHeader.
254+
*/
255+
public record HttpHeader(String name, String value) {};
256+
257+
public interface HttpHeaders {
258+
public enum CaseHandling {
259+
PRESERVES_CASE, DOWNSHIFTS, UPSHIFTS;
260+
}
261+
262+
/**
263+
*
264+
* Indicates how this implementation handles the case of HTTP header names.
265+
* This is important to know when calling getHeaders() since HTTP header names are case-insensitive,
266+
* but the actual case used may vary based on the implementation.
267+
*
268+
* PRESERVES_CASE means that the header names are returned in the same case as they were sent by the server.
269+
* DOWNSHIFTS means that the header names are returned in all lower case.
270+
* UPSHIFTS means that the header names are returned in all upper case.
271+
*
272+
* @return How this implementation handles the case of HTTP header names.
273+
*
274+
*/
275+
public CaseHandling caseHandling();
276+
277+
278+
public List<HttpHeader> getHeaders(String headerName);
279+
}
280+
241281
@SuppressWarnings("serial")
242282
public static class RestClientException extends Exception {
243283

rest-services/jersey-client/src/main/java/com/_4point/aem/docservices/rest_services/client/jersey/JerseyRestClient.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import java.util.ArrayList;
77
import java.util.Collections;
88
import java.util.List;
9+
import java.util.Map;
910
import java.util.Optional;
11+
import java.util.function.Consumer;
1012
import java.util.function.Supplier;
1113

1214
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
@@ -23,6 +25,7 @@
2325
import jakarta.ws.rs.client.Entity;
2426
import jakarta.ws.rs.client.WebTarget;
2527
import jakarta.ws.rs.core.MediaType;
28+
import jakarta.ws.rs.core.NewCookie;
2629
import jakarta.ws.rs.core.Response.Status;
2730
import jakarta.ws.rs.core.Response.Status.Family;
2831
import jakarta.ws.rs.core.Response.StatusType;
@@ -100,9 +103,19 @@ public InputStream data() {
100103

101104
@Override
102105
public Optional<String> retrieveHeader(String header) {
103-
return Optional.ofNullable(response.getHeaderString(header));
106+
return Optional.ofNullable(response.getStringHeaders().getFirst(header));
107+
}
108+
109+
@Override
110+
public HttpHeaders headers() {
111+
return new JerseyHttpHeaders(response.getStringHeaders());
104112
}
105113

114+
@Override
115+
public Cookies getCookies() {
116+
return new JerseyResponseCookies(response.getCookies());
117+
}
118+
106119
private static Optional<Response> processResponse(jakarta.ws.rs.core.Response response, MediaType expectedMediaType) throws RestClientException {
107120
try {
108121
StatusType resultStatus = response.getStatusInfo();
@@ -133,6 +146,24 @@ private static Optional<Response> processResponse(jakarta.ws.rs.core.Response re
133146
throw new RestClientException("IO Error while reading AEM response.", e);
134147
}
135148
}
149+
150+
private static class JerseyResponseCookies implements Cookies {
151+
private final Map<String, NewCookie> cookies;
152+
153+
private JerseyResponseCookies(Map<String, NewCookie> cookies) {
154+
this.cookies = cookies;
155+
}
156+
157+
@Override
158+
public boolean isEmpty() {
159+
return cookies.isEmpty();
160+
}
161+
162+
@Override
163+
public boolean isPresent() {
164+
return !isEmpty();
165+
}
166+
}
136167
}
137168

138169
private static String inputStreamtoString(InputStream inputStream) throws IOException {
@@ -330,6 +361,35 @@ public PayloadBuilder addHeader(String name, String value) {
330361
requestHeaders.add(new NameValuePair(name, value));
331362
return this;
332363
}
364+
}
365+
366+
private static class JerseyHttpHeaders implements HttpHeaders {
367+
private final jakarta.ws.rs.core.MultivaluedMap<String, String> headers;
368+
369+
private JerseyHttpHeaders(jakarta.ws.rs.core.MultivaluedMap<String, String> headers) {
370+
this.headers = headers;
371+
}
372+
373+
@Override
374+
public CaseHandling caseHandling() {
375+
return CaseHandling.UPSHIFTS;
376+
}
377+
378+
@Override
379+
public List<HttpHeader> getHeaders(String headerName) {
380+
return headers.entrySet()
381+
.stream()
382+
.filter(header -> header.getKey().equalsIgnoreCase(headerName))
383+
.mapMulti(JerseyHttpHeaders::mapHeaderValues)
384+
.toList();
385+
}
386+
387+
private static void mapHeaderValues(Map.Entry<String, List<String>> headerEntry, Consumer<HttpHeader> valueConsumer) {
388+
String headerName = headerEntry.getKey();
389+
for(String headerValue : headerEntry.getValue()) {
390+
valueConsumer.accept(new HttpHeader(headerName, headerValue));
391+
}
392+
}
333393

334394
}
335395
}

rest-services/jersey-client/src/test/java/com/_4point/aem/docservices/rest_services/client/jersey/AbstractRestClientTest.java

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
import static org.hamcrest.MatcherAssert.assertThat;
77

88
import java.io.InputStream;
9+
import java.util.List;
910
import java.util.Map;
1011
import java.util.Optional;
1112
import java.util.function.Supplier;
1213
import java.io.ByteArrayInputStream;
1314

15+
import org.hamcrest.Matchers;
1416
import org.junit.jupiter.api.BeforeEach;
1517
import org.junit.jupiter.api.DisplayName;
1618
import org.junit.jupiter.api.Test;
@@ -20,6 +22,9 @@
2022
import com._4point.aem.docservices.rest_services.client.RestClient;
2123
import com._4point.aem.docservices.rest_services.client.RestClient.ContentType;
2224
import com._4point.aem.docservices.rest_services.client.RestClient.GetRequest;
25+
import com._4point.aem.docservices.rest_services.client.RestClient.HttpHeaders;
26+
import com._4point.aem.docservices.rest_services.client.RestClient.Cookies;
27+
import com._4point.aem.docservices.rest_services.client.RestClient.HttpHeader;
2328
import com._4point.aem.docservices.rest_services.client.RestClient.MultipartPayload;
2429
import com._4point.aem.docservices.rest_services.client.RestClient.Response;
2530
import com._4point.aem.docservices.rest_services.client.RestClient.RestClientException;
@@ -141,6 +146,107 @@ void testPostToServer_DocumentResponseWithHeader() throws Exception {
141146
);
142147
}
143148

149+
@DisplayName("PostToServer with 1 part and return 1 header with 3 values in the response")
150+
@Test
151+
void testPostToServer_DocumentResponseWithMultipleHeaders() throws Exception {
152+
// Given
153+
stubFor(post(ENDPOINT).willReturn(okForContentType(ContentType.APPLICATION_PDF.contentType(), MOCK_PDF_BYTES)
154+
.withHeader(SAMPLE_HEADER.toUpperCase(), SAMPLE_HEADER_VALUE + "_3")
155+
.withHeader(SAMPLE_HEADER, SAMPLE_HEADER_VALUE + "_2")
156+
.withHeader(SAMPLE_HEADER, SAMPLE_HEADER_VALUE)
157+
));
158+
159+
// When
160+
Response response = postToServerBuilder().performPostToServer(FIELD1_NAME, FIELD1_DATA).orElseThrow();
161+
162+
// Then
163+
assertEquals(ContentType.APPLICATION_PDF, response.contentType());
164+
assertEquals(MOCK_PDF_BYTES, new String(response.data().readAllBytes()));
165+
assertThat(response.retrieveHeader(SAMPLE_HEADER).orElseThrow(), anyOf(Matchers.equalTo(SAMPLE_HEADER_VALUE), Matchers.equalTo(SAMPLE_HEADER_VALUE + "_3"))); // Should retrieve the first header value or 3rd, not the 2nd
166+
assertThat(response.retrieveHeader(SAMPLE_HEADER.toUpperCase()).orElseThrow(), anyOf(Matchers.equalTo(SAMPLE_HEADER_VALUE), Matchers.equalTo(SAMPLE_HEADER_VALUE + "_3"))); // Uppercase version should also retrieve the first header value or 3rd, not the 2nd
167+
168+
HttpHeaders headers = response.headers();
169+
List<HttpHeader> headersList = headers.getHeaders(SAMPLE_HEADER);
170+
switch (headers.caseHandling()) {
171+
case DOWNSHIFTS -> assertThat(headersList, containsInAnyOrder(equalTo(new HttpHeader(SAMPLE_HEADER.toLowerCase(), SAMPLE_HEADER_VALUE)),
172+
equalTo(new HttpHeader(SAMPLE_HEADER.toLowerCase(), SAMPLE_HEADER_VALUE + "_2")),
173+
equalTo(new HttpHeader(SAMPLE_HEADER.toLowerCase(), SAMPLE_HEADER_VALUE + "_3"))
174+
)
175+
);
176+
case UPSHIFTS -> assertThat(headersList, containsInAnyOrder(equalTo(new HttpHeader(SAMPLE_HEADER.toUpperCase(), SAMPLE_HEADER_VALUE)),
177+
equalTo(new HttpHeader(SAMPLE_HEADER.toUpperCase(), SAMPLE_HEADER_VALUE + "_2")),
178+
equalTo(new HttpHeader(SAMPLE_HEADER.toUpperCase(), SAMPLE_HEADER_VALUE + "_3"))
179+
)
180+
);
181+
case PRESERVES_CASE -> assertThat(headersList, containsInAnyOrder(equalTo(new HttpHeader(SAMPLE_HEADER, SAMPLE_HEADER_VALUE)),
182+
equalTo(new HttpHeader(SAMPLE_HEADER, SAMPLE_HEADER_VALUE + "_2")),
183+
equalTo(new HttpHeader(SAMPLE_HEADER.toUpperCase(), SAMPLE_HEADER_VALUE + "_3"))
184+
)
185+
);
186+
}
187+
188+
verify(postRequestedFor(urlEqualTo(ENDPOINT))
189+
.withAllRequestBodyParts(aMultipart(FIELD1_NAME).withBody(equalTo(FIELD1_DATA)))
190+
.withHeader(RestClient.CORRELATION_ID_HTTP_HDR, equalTo(CORRELATION_ID_TEXT))
191+
);
192+
}
193+
194+
@DisplayName("PostToServer with 1 part and return no cookies in the response")
195+
@Test
196+
void testPostToServer_DocumentResponseWithNoCookies() throws Exception {
197+
// Given
198+
final String COOKIES_KEY = "Set-Cookie";
199+
stubFor(post(ENDPOINT).willReturn(okForContentType(ContentType.APPLICATION_PDF.contentType(), MOCK_PDF_BYTES)
200+
));
201+
202+
// When
203+
Response response = postToServerBuilder().performPostToServer(FIELD1_NAME, FIELD1_DATA).orElseThrow();
204+
205+
// Then
206+
assertEquals(ContentType.APPLICATION_PDF, response.contentType());
207+
assertEquals(MOCK_PDF_BYTES, new String(response.data().readAllBytes()));
208+
assertTrue(response.retrieveHeader(COOKIES_KEY).isEmpty()); // Should not be present
209+
assertTrue(response.retrieveHeader(COOKIES_KEY.toUpperCase()).isEmpty()); // Should not be present
210+
211+
Cookies cookies = response.getCookies();
212+
assertFalse(cookies.isPresent());
213+
assertTrue(cookies.isEmpty());
214+
215+
verify(postRequestedFor(urlEqualTo(ENDPOINT))
216+
.withAllRequestBodyParts(aMultipart(FIELD1_NAME).withBody(equalTo(FIELD1_DATA)))
217+
.withHeader(RestClient.CORRELATION_ID_HTTP_HDR, equalTo(CORRELATION_ID_TEXT))
218+
);
219+
}
220+
221+
@DisplayName("PostToServer with 1 part and return 1 set-cookie in the response")
222+
@Test
223+
void testPostToServer_DocumentResponseWithCookie() throws Exception {
224+
// Given
225+
final String COOKIES_KEY = "Set-Cookie";
226+
final String COOKIES_VALUE = "cookie1=value1; cookie2=value2; HttpOnly";
227+
stubFor(post(ENDPOINT).willReturn(okForContentType(ContentType.APPLICATION_PDF.contentType(), MOCK_PDF_BYTES)
228+
.withHeader(COOKIES_KEY, COOKIES_VALUE)
229+
));
230+
231+
// When
232+
Response response = postToServerBuilder().performPostToServer(FIELD1_NAME, FIELD1_DATA).orElseThrow();
233+
234+
// Then
235+
assertEquals(ContentType.APPLICATION_PDF, response.contentType());
236+
assertEquals(MOCK_PDF_BYTES, new String(response.data().readAllBytes()));
237+
assertEquals(COOKIES_VALUE, response.retrieveHeader(COOKIES_KEY).orElseThrow()); // Should retrieve the first header value, not the 2nd or 3rd one
238+
assertEquals(COOKIES_VALUE, response.retrieveHeader(COOKIES_KEY.toUpperCase()).orElseThrow()); // Should retrieve the first header value, not the 3rd one
239+
240+
Cookies cookies = response.getCookies();
241+
assertTrue(cookies.isPresent());
242+
assertFalse(cookies.isEmpty());
243+
244+
verify(postRequestedFor(urlEqualTo(ENDPOINT))
245+
.withAllRequestBodyParts(aMultipart(FIELD1_NAME).withBody(equalTo(FIELD1_DATA)))
246+
.withHeader(RestClient.CORRELATION_ID_HTTP_HDR, equalTo(CORRELATION_ID_TEXT))
247+
);
248+
}
249+
144250
@DisplayName("PostToServer with 1 header and 1 part using byte array data")
145251
@Test
146252
void testPostToServer_DocumentResponseFromByteArray() throws Exception {

spring/fluentforms-spring-boot-autoconfigure/src/main/java/com/_4point/aem/fluentforms/spring/rest_services/client/SpringRestClientRestClient.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
import java.io.InputStream;
66
import java.net.URI;
77
import java.util.List;
8+
import java.util.Map;
89
import java.util.Optional;
910
import java.util.function.Consumer;
1011
import java.util.function.Function;
1112
import java.util.function.Supplier;
1213

14+
import org.jspecify.annotations.Nullable;
1315
import org.springframework.core.io.ByteArrayResource;
1416
import org.springframework.core.io.InputStreamResource;
1517
import org.springframework.http.HttpEntity;
@@ -163,6 +165,34 @@ public ContentType contentType() {
163165
return toContentType(headers.getContentType());
164166
}
165167

168+
@Override
169+
public HttpHeaders headers() {
170+
return new SpringRestClientHttpHeaders(headers);
171+
}
172+
173+
@Override
174+
public Cookies getCookies() {
175+
return new SpringRestClientResponseCookies(headers.getFirst(org.springframework.http.HttpHeaders.SET_COOKIE));
176+
}
177+
178+
private static class SpringRestClientResponseCookies implements Cookies {
179+
private final @Nullable String cookieHeaderValue;
180+
181+
private SpringRestClientResponseCookies(@Nullable String cookieHeaderValue) {
182+
this.cookieHeaderValue = cookieHeaderValue;
183+
}
184+
185+
@Override
186+
public boolean isEmpty() {
187+
return cookieHeaderValue == null || cookieHeaderValue.isEmpty();
188+
}
189+
190+
@Override
191+
public boolean isPresent() {
192+
return !isEmpty();
193+
}
194+
195+
}
166196
}
167197

168198
private static ContentType toContentType(MediaType mediaType) {
@@ -294,4 +324,34 @@ public SpringClientGetRequestBuilder addHeader(String name, String value) {
294324
return this;
295325
}
296326
}
327+
328+
private static class SpringRestClientHttpHeaders implements RestClient.HttpHeaders {
329+
private final org.springframework.http.HttpHeaders headers;
330+
331+
private SpringRestClientHttpHeaders(org.springframework.http.HttpHeaders headers) {
332+
this.headers = headers;
333+
}
334+
335+
@Override
336+
public CaseHandling caseHandling() {
337+
return CaseHandling.UPSHIFTS;
338+
}
339+
340+
@Override
341+
public List<HttpHeader> getHeaders(String headerName) {
342+
return headers.headerSet().stream()
343+
.filter(e->e.getKey().equalsIgnoreCase(headerName))
344+
.mapMulti(SpringRestClientHttpHeaders::mapHeaderValues)
345+
.toList();
346+
}
347+
348+
private static void mapHeaderValues(Map.Entry<String, List<String>> headerEntry, Consumer<HttpHeader> valueConsumer) {
349+
String headerName = headerEntry.getKey();
350+
for(String headerValue : headerEntry.getValue()) {
351+
valueConsumer.accept(new HttpHeader(headerName, headerValue));
352+
}
353+
}
354+
355+
}
356+
297357
}

0 commit comments

Comments
 (0)