Fix Request/Response attributes, add more assertions in tests

This commit is contained in:
Oleg Zhurakousky
2023-03-07 14:30:00 +01:00
parent 0c82bca08a
commit 017093a8b5
14 changed files with 282 additions and 597 deletions

View File

@@ -10,7 +10,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-parent</artifactId>
<version>3.2.9-SNAPSHOT</version>
<version>3.2.10-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -25,7 +25,6 @@
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-serverless-web</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>

View File

@@ -19,31 +19,25 @@ package org.springframework.cloud.function.adapter.aws.web;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.Filter;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.function.serverless.web.ProxyHttpServletRequest;
import org.springframework.cloud.function.serverless.web.ProxyHttpServletResponse;
import org.springframework.cloud.function.serverless.web.ProxyMvc;
import org.springframework.cloud.function.serverless.web.ProxyServletContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
/**
*
@@ -74,10 +68,9 @@ public class WebProxyInvoker {
*/
@SuppressWarnings("unchecked")
private HttpServletRequest prepareRequest(InputStream input) throws IOException {
Map<String, Object> request = mapper.readValue(input, Map.class);
if (logger.isDebugEnabled()) {
logger.debug("Request: " + request);
if (logger.isInfoEnabled()) {
logger.info("Request: " + request);
}
String httpMethod = (String) request.get("httpMethod");
String path = (String) request.get("path");
@@ -86,18 +79,29 @@ public class WebProxyInvoker {
logger.debug("path: " + path);
}
ProxyHttpServletRequest httpRequest = new ProxyHttpServletRequest(null, httpMethod, path);
// CONTENT
if (StringUtils.hasText((String) request.get("body"))) {
httpRequest.setContent(((String) request.get("body")).getBytes());
}
if (request.get("queryStringParameters") != null) {
httpRequest.setParameters((Map<String, ?>) request.get("queryStringParameters"));
// REQUEST PARAM
if (request.get("multiValueQueryStringParameters") != null) {
Map<String, List<String>> parameters = (Map<String, List<String>>) request.get("multiValueQueryStringParameters");
for (Entry<String, List<String>> parameter : parameters.entrySet()) {
httpRequest.setParameter(parameter.getKey(), parameter.getValue().toArray(new String[] {}));
}
}
Map<String, Object> headers = (Map<String, Object>) request.get("headers");
headers.putAll((Map<String, Object>) request.get("multiValueHeaders"));
for (Entry<String, Object> entry : headers.entrySet()) {
httpRequest.addHeader(entry.getKey(), entry.getValue());
// HEADERS
Map<String, List<String>> headers = (Map<String, List<String>>) request.get("multiValueHeaders");
HttpHeaders httpHeaders = new HttpHeaders();
for (Entry<String, List<String>> entry : headers.entrySet()) {
// TODO may need to do some header formatting
httpHeaders.addAll(entry.getKey(), entry.getValue());
}
httpRequest.setHeaders(httpHeaders);
return httpRequest;
}
@@ -121,47 +125,20 @@ public class WebProxyInvoker {
}
Map<String, Object> apiGatewayResponseStructure = new HashMap<String, Object>();
apiGatewayResponseStructure.put("isBase64Encoded", false);
apiGatewayResponseStructure.put("statusCode", 200);
apiGatewayResponseStructure.put("statusCode", HttpStatus.OK.value());
apiGatewayResponseStructure.put("body", responseString);
Map<String, List<String>> multiValueHeaders = new HashMap<>();
Map<String, String> headers = new HashMap<>();
for (String headerName : httpResponse.getHeaderNames()) {
multiValueHeaders.put(headerName, httpResponse.getHeaders(headerName));
headers.put(headerName, httpResponse.getHeaders(headerName).toString());
}
// TODO investigate why AWS doesn't like List as value
// apiGatewayResponseStructure.put("headers", multiValueHeaders);
apiGatewayResponseStructure.put("multiValueHeaders", multiValueHeaders);
apiGatewayResponseStructure.put("headers", headers);
byte[] apiGatewayResponseBytes = mapper.writeValueAsBytes(apiGatewayResponseStructure);
StreamUtils.copy(apiGatewayResponseBytes, output);
}
}
private static class ProxyServletConfig implements ServletConfig {
private final ServletContext servletContext;
ProxyServletConfig(ServletContext servletContext) {
this.servletContext = servletContext;
}
@Override
public String getServletName() {
return "serverless-proxy";
}
@Override
public ServletContext getServletContext() {
return this.servletContext;
}
@Override
public Enumeration<String> getInitParameterNames() {
return Collections.enumeration(new ArrayList<String>());
}
@Override
public String getInitParameter(String name) {
return null;
}
}
}

View File

@@ -19,106 +19,157 @@ package org.springframework.cloud.function.adapter.aws.web;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.function.serverless.web.ProxyHttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Oleg Zhurakousky
*/
public class WebProxyInvokerTests {
static String apiGatewayEvent = "{\n" +
" \"resource\": \"/pets\",\n" +
" \"path\": \"/pets/64f56d94-a059-4111-9eeb-ee0c994b1ba8?foo=bar\",\n" +
" \"httpMethod\": \"GET\",\n" +
" \"headers\": {\n" +
" \"accept\": \"*/*\",\n" +
" \"content-type\": \"application/json\",\n" +
" \"Host\": \"fhul32ccy2.execute-api.eu-west-3.amazonaws.com\",\n" +
" \"User-Agent\": \"curl/7.54.0\",\n" +
" \"X-Amzn-Trace-Id\": \"Root=1-5ece339e-e0595766066d703ec70f1522\",\n" +
" \"X-Forwarded-For\": \"90.37.8.133\",\n" +
" \"X-Forwarded-Port\": \"443\",\n" +
" \"X-Forwarded-Proto\": \"https\"\n" +
" },\n" +
" \"multiValueHeaders\": {\n" +
" \"accept\": [\n" +
" \"*/*\"\n" +
" ],\n" +
" \"content-type\": [\n" +
" \"application/json\"\n" +
" ],\n" +
" \"Host\": [\n" +
" \"fhul32ccy2.execute-api.eu-west-3.amazonaws.com\"\n" +
" ],\n" +
" \"User-Agent\": [\n" +
" \"curl/7.54.0\"\n" +
" ],\n" +
" \"X-Amzn-Trace-Id\": [\n" +
" \"Root=1-5ece339e-e0595766066d703ec70f1522\"\n" +
" ],\n" +
" \"X-Forwarded-For\": [\n" +
" \"90.37.8.133\"\n" +
" ],\n" +
" \"X-Forwarded-Port\": [\n" +
" \"443\"\n" +
" ],\n" +
" \"X-Forwarded-Proto\": [\n" +
" \"https\"\n" +
" ]\n" +
" },\n" +
" \"queryStringParameters\": null,\n" +
" \"multiValueQueryStringParameters\": null,\n" +
" \"pathParameters\": null,\n" +
" \"stageVariables\": null,\n" +
" \"requestContext\": {\n" +
" \"resourceId\": \"qf0io6\",\n" +
" \"resourcePath\": \"/pets\",\n" +
" \"httpMethod\": \"GET\",\n" +
" \"extendedRequestId\": \"NL0A1EokCGYFZOA=\",\n" +
" \"requestTime\": \"27/May/2020:09:32:14 +0000\",\n" +
" \"path\": \"/test/uppercase2\",\n" +
" \"accountId\": \"123456789098\",\n" +
" \"protocol\": \"HTTP/1.1\",\n" +
" \"stage\": \"test\",\n" +
" \"domainPrefix\": \"fhul32ccy2\",\n" +
" \"requestTimeEpoch\": 1590571934872,\n" +
" \"requestId\": \"b96500aa-f92a-43c3-9360-868ba4053a00\",\n" +
" \"identity\": {\n" +
" \"cognitoIdentityPoolId\": null,\n" +
" \"accountId\": null,\n" +
" \"cognitoIdentityId\": null,\n" +
" \"caller\": null,\n" +
" \"sourceIp\": \"90.37.8.133\",\n" +
" \"principalOrgId\": null,\n" +
" \"accessKey\": null,\n" +
" \"cognitoAuthenticationType\": null,\n" +
" \"cognitoAuthenticationProvider\": null,\n" +
" \"userArn\": null,\n" +
" \"userAgent\": \"curl/7.54.0\",\n" +
" \"user\": null\n" +
" },\n" +
" \"domainName\": \"fhul32ccy2.execute-api.eu-west-3.amazonaws.com\",\n" +
" \"apiId\": \"fhul32ccy2\"\n" +
" },\n" +
" \"body\":\"\",\n" +
" \"isBase64Encoded\": false\n" +
"}";
static String API_GATEWAY_EVENT = "{\n"
+ " \"version\": \"1.0\",\n"
+ " \"resource\": \"$default\",\n"
+ " \"path\": \"/pets\",\n"
+ " \"httpMethod\": \"POST\",\n"
+ " \"headers\": {\n"
+ " \"Content-Length\": \"45\",\n"
+ " \"Content-Type\": \"application/json\",\n"
+ " \"Host\": \"i76bfhczs0.execute-api.eu-west-3.amazonaws.com\",\n"
+ " \"User-Agent\": \"curl/7.79.1\",\n"
+ " \"X-Amzn-Trace-Id\": \"Root=1-64087690-2151375b219d3ba3389ea84e\",\n"
+ " \"X-Forwarded-For\": \"109.210.252.44\",\n"
+ " \"X-Forwarded-Port\": \"443\",\n"
+ " \"X-Forwarded-Proto\": \"https\",\n"
+ " \"accept\": \"*/*\"\n"
+ " },\n"
+ " \"multiValueHeaders\": {\n"
+ " \"Content-Length\": [\n"
+ " \"45\"\n"
+ " ],\n"
+ " \"Content-Type\": [\n"
+ " \"application/json\"\n"
+ " ],\n"
+ " \"Host\": [\n"
+ " \"i76bfhczs0.execute-api.eu-west-3.amazonaws.com\"\n"
+ " ],\n"
+ " \"User-Agent\": [\n"
+ " \"curl/7.79.1\"\n"
+ " ],\n"
+ " \"X-Amzn-Trace-Id\": [\n"
+ " \"Root=1-64087690-2151375b219d3ba3389ea84e\"\n"
+ " ],\n"
+ " \"X-Forwarded-For\": [\n"
+ " \"109.210.252.44\"\n"
+ " ],\n"
+ " \"X-Forwarded-Port\": [\n"
+ " \"443\"\n"
+ " ],\n"
+ " \"X-Forwarded-Proto\": [\n"
+ " \"https\"\n"
+ " ],\n"
+ " \"accept\": [\n"
+ " \"*/*\"\n"
+ " ]\n"
+ " },\n"
+ " \"queryStringParameters\": {\n"
+ " \"abc\": \"xyz\",\n"
+ " \"foo\": \"baz\"\n"
+ " },\n"
+ " \"multiValueQueryStringParameters\": {\n"
+ " \"abc\": [\n"
+ " \"xyz\"\n"
+ " ],\n"
+ " \"foo\": [\n"
+ " \"bar\",\n"
+ " \"baz\"\n"
+ " ]\n"
+ " },\n"
+ " \"requestContext\": {\n"
+ " \"accountId\": \"123456789098\",\n"
+ " \"apiId\": \"i76bfhczs0\",\n"
+ " \"domainName\": \"i76bfhczs0.execute-api.eu-west-3.amazonaws.com\",\n"
+ " \"domainPrefix\": \"i76bfhczs0\",\n"
+ " \"extendedRequestId\": \"Bdd2ngt5iGYEMIg=\",\n"
+ " \"httpMethod\": \"POST\",\n"
+ " \"identity\": {\n"
+ " \"accessKey\": null,\n"
+ " \"accountId\": null,\n"
+ " \"caller\": null,\n"
+ " \"cognitoAmr\": null,\n"
+ " \"cognitoAuthenticationProvider\": null,\n"
+ " \"cognitoAuthenticationType\": null,\n"
+ " \"cognitoIdentityId\": null,\n"
+ " \"cognitoIdentityPoolId\": null,\n"
+ " \"principalOrgId\": null,\n"
+ " \"sourceIp\": \"109.210.252.44\",\n"
+ " \"user\": null,\n"
+ " \"userAgent\": \"curl/7.79.1\",\n"
+ " \"userArn\": null\n"
+ " },\n"
+ " \"path\": \"/pets\",\n"
+ " \"protocol\": \"HTTP/1.1\",\n"
+ " \"requestId\": \"Bdd2ngt5iGYEMIg=\",\n"
+ " \"requestTime\": \"08/Mar/2023:11:50:40 +0000\",\n"
+ " \"requestTimeEpoch\": 1678276240455,\n"
+ " \"resourceId\": \"$default\",\n"
+ " \"resourcePath\": \"$default\",\n"
+ " \"stage\": \"$default\"\n"
+ " },\n"
+ " \"pathParameters\": null,\n"
+ " \"stageVariables\": null,\n"
+ " \"body\": \"{\\\"id\\\":\\\"123\\\",\\\"breed\\\":\\\"Datsun\\\",\\\"name\\\":\\\"Donald\\\"}\",\n"
+ " \"isBase64Encoded\": false\n"
+ "}";
@Test
public void validateHttpServletRequestConstruction() throws Exception {
System.setProperty("MAIN_CLASS", PetStoreSpringAppConfig.class.getName());
WebProxyInvoker invoker = new WebProxyInvoker();
InputStream targetStream = new ByteArrayInputStream(API_GATEWAY_EVENT.getBytes());
Method prepareRequest = ReflectionUtils.findMethod(WebProxyInvoker.class, "prepareRequest", InputStream.class);
prepareRequest.setAccessible(true);
ProxyHttpServletRequest request = (ProxyHttpServletRequest) prepareRequest.invoke(invoker, targetStream);
assertThat(request.getContentType()).isEqualTo("application/json");
assertThat(request.getParameterValues("foo").length).isEqualTo(2);
assertThat(request.getParameterValues("foo")[0]).isEqualTo("bar");
assertThat(request.getParameterValues("abc").length).isEqualTo(1);
assertThat(request.getParameterValues("abc")[0]).isEqualTo("xyz");
assertThat(request.getHeaders(HttpHeaders.CONTENT_TYPE).nextElement()).isEqualTo("application/json");
assertThat(request.getContentAsString()).isEqualTo("{\"id\":\"123\",\"breed\":\"Datsun\",\"name\":\"Donald\"}");
assertThat(request.getContentLength()).isEqualTo(45);
assertThat(request.getMethod()).isEqualTo("POST");
}
@Test
public void testApiGatewayProxy() throws Exception {
System.setProperty("MAIN_CLASS", PetStoreSpringAppConfig.class.getName());
WebProxyInvoker invoker = new WebProxyInvoker();
InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes());
InputStream targetStream = new ByteArrayInputStream(API_GATEWAY_EVENT.getBytes());
ByteArrayOutputStream output = new ByteArrayOutputStream();
invoker.handleRequest(targetStream, output);
ObjectMapper mapper = new ObjectMapper();
System.out.println("RESULT: =======> " + new String(output.toByteArray()));
Map result = mapper.readValue(output.toByteArray(), Map.class);
System.out.println(result);
assertThat((boolean) result.get("isBase64Encoded")).isFalse();
assertThat(((Map<String, List<String>>) result.get("multiValueHeaders")).get("Content-Type").get(0)).isEqualTo("application/json");
assertThat(result.get("statusCode")).isEqualTo(200);
Pet pet = mapper.readValue((String) result.get("body"), Pet.class);
assertThat(pet.getName()).isEqualTo("Donald");
}
}

View File

@@ -10,7 +10,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-parent</artifactId>
<version>3.2.9-SNAPSHOT</version>
<version>3.2.10-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@@ -1,78 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.serverless.web;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
class HeaderValueHolder {
private final List<Object> values = new LinkedList<>();
void setValue(@Nullable Object value) {
this.values.clear();
if (value != null) {
this.values.add(value);
}
}
void addValue(Object value) {
this.values.add(value);
}
void addValues(Collection<?> values) {
this.values.addAll(values);
}
void addValueArray(Object values) {
CollectionUtils.mergeArrayIntoCollection(values, this.values);
}
List<Object> getValues() {
return Collections.unmodifiableList(this.values);
}
List<String> getStringValues() {
List<String> stringList = new ArrayList<>(this.values.size());
for (Object value : this.values) {
stringList.add(value.toString());
}
return Collections.unmodifiableList(stringList);
}
@Nullable
Object getValue() {
return (!this.values.isEmpty() ? this.values.get(0) : null);
}
@Nullable
String getStringValue() {
return (!this.values.isEmpty() ? String.valueOf(this.values.get(0)) : null);
}
@Override
public String toString() {
return this.values.toString();
}
}

View File

@@ -24,6 +24,7 @@ import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
@@ -58,18 +59,19 @@ import javax.servlet.http.HttpUpgradeHandler;
import javax.servlet.http.Part;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
*
* @author Oleg Zhurakousky
*
*/
public class ProxyHttpServletRequest implements HttpServletRequest {
private static final String CHARSET_PREFIX = "charset=";
private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
private static final BufferedReader EMPTY_BUFFERED_READER = new BufferedReader(new StringReader(""));
@@ -97,9 +99,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
@Nullable
private byte[] content;
@Nullable
private String contentType;
@Nullable
private ServletInputStream inputStream;
@@ -117,17 +116,13 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
private DispatcherType dispatcherType = DispatcherType.REQUEST;
// ---------------------------------------------------------------------
// HttpServletRequest properties
// ---------------------------------------------------------------------
@Nullable
private String authType;
@Nullable
private Cookie[] cookies;
private final Map<String, HeaderValueHolder> headers = new LinkedCaseInsensitiveMap<>();
private final HttpHeaders headers = new HttpHeaders();
@Nullable
private String method;
@@ -202,18 +197,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
@Override
public void setCharacterEncoding(@Nullable String characterEncoding) {
this.characterEncoding = characterEncoding;
updateContentTypeHeader();
}
private void updateContentTypeHeader() {
if (StringUtils.hasLength(this.contentType)) {
String value = this.contentType;
if (StringUtils.hasLength(this.characterEncoding)
&& !this.contentType.toLowerCase().contains(CHARSET_PREFIX)) {
value += ';' + CHARSET_PREFIX + this.characterEncoding;
}
doAddHeaderValue(HttpHeaders.CONTENT_TYPE, value, true);
}
}
/**
@@ -262,13 +245,13 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
*/
@Nullable
public String getContentAsString() throws IllegalStateException, UnsupportedEncodingException {
Assert.state(this.characterEncoding != null, "Cannot get content as a String for a null character encoding. "
+ "Consider setting the characterEncoding in the request.");
// Assert.state(this.characterEncoding != null, "Cannot get content as a String for a null character encoding. "
// + "Consider setting the characterEncoding in the request.");
if (this.content == null) {
return null;
}
return new String(this.content, this.characterEncoding);
return new String(this.content, StandardCharsets.UTF_8);
}
@Override
@@ -282,29 +265,13 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
}
public void setContentType(@Nullable String contentType) {
this.contentType = contentType;
if (contentType != null) {
try {
MediaType mediaType = MediaType.parseMediaType(contentType);
if (mediaType.getCharset() != null) {
this.characterEncoding = mediaType.getCharset().name();
}
}
catch (IllegalArgumentException ex) {
// Try to get charset value anyway
int charsetIndex = contentType.toLowerCase().indexOf(CHARSET_PREFIX);
if (charsetIndex != -1) {
this.characterEncoding = contentType.substring(charsetIndex + CHARSET_PREFIX.length());
}
}
updateContentTypeHeader();
}
this.headers.set(HttpHeaders.CONTENT_TYPE, contentType);
}
@Override
@Nullable
public String getContentType() {
return this.contentType;
return this.headers.containsKey(HttpHeaders.CONTENT_TYPE) ? this.headers.get(HttpHeaders.CONTENT_TYPE).get(0) : null;
}
@Override
@@ -564,37 +531,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
this.attributes.clear();
}
/**
* Add a new preferred locale, before any existing locales.
*
* @see #setPreferredLocales
*/
public void addPreferredLocale(Locale locale) {
Assert.notNull(locale, "Locale must not be null");
this.locales.addFirst(locale);
updateAcceptLanguageHeader();
}
/**
* Set the list of preferred locales, in descending order, effectively replacing
* any existing locales.
*
* @since 3.2
* @see #addPreferredLocale
*/
public void setPreferredLocales(List<Locale> locales) {
Assert.notEmpty(locales, "Locale list must not be empty");
this.locales.clear();
this.locales.addAll(locales);
updateAcceptLanguageHeader();
}
private void updateAcceptLanguageHeader() {
HttpHeaders headers = new HttpHeaders();
headers.setAcceptLanguageAsLocales(this.locales);
doAddHeaderValue(HttpHeaders.ACCEPT_LANGUAGE, headers.getFirst(HttpHeaders.ACCEPT_LANGUAGE), true);
}
/**
* Return the first preferred {@linkplain Locale locale} configured in this mock
* request.
@@ -758,104 +694,86 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
return this.cookies;
}
/**
* Add an HTTP header entry for the given name.
* <p>
* While this method can take any {@code Object} as a parameter, it is
* recommended to use the following types:
* <ul>
* <li>String or any Object to be converted using {@code toString()}; see
* {@link #getHeader}.</li>
* <li>String, Number, or Date for date headers; see
* {@link #getDateHeader}.</li>
* <li>String or Number for integer headers; see {@link #getIntHeader}.</li>
* <li>{@code String[]} or {@code Collection<String>} for multiple values; see
* {@link #getHeaders}.</li>
* </ul>
*
* @see #getHeaderNames
* @see #getHeaders
* @see #getHeader
* @see #getDateHeader
*/
public void addHeader(String name, Object value) {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name) && !this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
setContentType(value.toString());
}
else if (HttpHeaders.ACCEPT_LANGUAGE.equalsIgnoreCase(name)
&& !this.headers.containsKey(HttpHeaders.ACCEPT_LANGUAGE)) {
try {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.ACCEPT_LANGUAGE, value.toString());
List<Locale> locales = headers.getAcceptLanguageAsLocales();
this.locales.clear();
this.locales.addAll(locales);
if (this.locales.isEmpty()) {
this.locales.add(Locale.ENGLISH);
}
@Override
@Nullable
public String getHeader(String name) {
return this.headers.containsKey(name) ? this.headers.get(name).toString() : null;
}
@Override
public Enumeration<String> getHeaders(String name) {
return Collections.enumeration(this.headers.containsKey(name) ? this.headers.get(name) : new LinkedList<>());
}
@Override
public Enumeration<String> getHeaderNames() {
return Collections.enumeration(this.headers.keySet());
}
public void setHeader(String name, @Nullable String value) {
this.headers.set(name, value);
}
public void addHeader(String name, @Nullable String value) {
this.headers.add(name, value);
}
public void addHeaders(MultiValueMap<String, String> headers) {
this.headers.addAll(headers);
}
public void setHeaders(MultiValueMap<String, String> headers) {
this.headers.clear();
this.addHeaders(headers);
}
@Override
public int getIntHeader(String name) {
List<String> header = this.headers.get(name);
if (!CollectionUtils.isEmpty(header) && header.size() == 1) {
Object value = header.get(0);
if (value instanceof Number) {
return ((Number) value).intValue();
}
catch (IllegalArgumentException ex) {
// Invalid Accept-Language format -> just store plain header
else if (value instanceof String) {
return Integer.parseInt((String) value);
}
else if (value != null) {
throw new NumberFormatException("Value for header '" + name + "' is not a Number: " + value);
}
else {
return -1;
}
doAddHeaderValue(name, value, true);
}
else {
doAddHeaderValue(name, value, false);
return -1;
}
}
private void doAddHeaderValue(String name, @Nullable Object value, boolean replace) {
HeaderValueHolder header = this.headers.get(name);
Assert.notNull(value, "Header value must not be null");
if (header == null || replace) {
header = new HeaderValueHolder();
this.headers.put(name, header);
}
if (value instanceof Collection) {
header.addValues((Collection<?>) value);
}
else if (value.getClass().isArray()) {
header.addValueArray(value);
}
else {
header.addValue(value);
}
}
/**
* Return the long timestamp for the date header with the given {@code name}.
* <p>
* If the internal value representation is a String, this method will try to
* parse it as a date using the supported date formats:
* <ul>
* <li>"EEE, dd MMM yyyy HH:mm:ss zzz"</li>
* <li>"EEE, dd-MMM-yy HH:mm:ss zzz"</li>
* <li>"EEE MMM dd HH:mm:ss yyyy"</li>
* </ul>
*
* @param name the header name
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section
* 7.1.1.1 of RFC 7231</a>
*/
@Override
public long getDateHeader(String name) {
HeaderValueHolder header = this.headers.get(name);
Object value = (header != null ? header.getValue() : null);
if (value instanceof Date) {
return ((Date) value).getTime();
}
else if (value instanceof Number) {
return ((Number) value).longValue();
}
else if (value instanceof String) {
return parseDateHeader(name, (String) value);
}
else if (value != null) {
throw new IllegalArgumentException(
"Value for header '" + name + "' is not a Date, Number, or String: " + value);
List<String> header = this.headers.get(name);
if (!CollectionUtils.isEmpty(header) && header.size() == 1) {
Object value = header.get(0);
if (value instanceof Date) {
return ((Date) value).getTime();
}
else if (value instanceof Number) {
return ((Number) value).longValue();
}
else if (value instanceof String) {
return parseDateHeader(name, (String) value);
}
else if (value != null) {
throw new IllegalArgumentException(
"Value for header '" + name + "' is not a Date, Number, or String: " + value);
}
else {
return -1L;
}
}
else {
return -1L;
return -1;
}
}
@@ -873,42 +791,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest {
throw new IllegalArgumentException("Cannot parse date value '" + value + "' for '" + name + "' header");
}
@Override
@Nullable
public String getHeader(String name) {
HeaderValueHolder header = this.headers.get(name);
return (header != null ? header.getStringValue() : null);
}
@Override
public Enumeration<String> getHeaders(String name) {
HeaderValueHolder header = this.headers.get(name);
return Collections.enumeration(header != null ? header.getStringValues() : new LinkedList<>());
}
@Override
public Enumeration<String> getHeaderNames() {
return Collections.enumeration(this.headers.keySet());
}
@Override
public int getIntHeader(String name) {
HeaderValueHolder header = this.headers.get(name);
Object value = (header != null ? header.getValue() : null);
if (value instanceof Number) {
return ((Number) value).intValue();
}
else if (value instanceof String) {
return Integer.parseInt((String) value);
}
else if (value != null) {
throw new NumberFormatException("Value for header '" + name + "' is not a Number: " + value);
}
else {
return -1;
}
}
public void setMethod(@Nullable String method) {
this.method = method;
}

View File

@@ -22,16 +22,12 @@ import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
@@ -41,86 +37,43 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.web.util.WebUtils;
/**
*
* @author Oleg Zhurakousky
*
*/
public class ProxyHttpServletResponse implements HttpServletResponse {
private static final String CHARSET_PREFIX = "charset=";
private static final String DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
// ---------------------------------------------------------------------
// ServletResponse properties
// ---------------------------------------------------------------------
private boolean outputStreamAccessAllowed = true;
private String defaultCharacterEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING;
private String characterEncoding = this.defaultCharacterEncoding;
/**
* {@code true} if the character encoding has been explicitly set through
* {@link HttpServletResponse} methods or through a {@code charset} parameter on
* the {@code Content-Type}.
*/
private boolean characterEncodingSet = false;
private final ByteArrayOutputStream content = new ByteArrayOutputStream(1024);
private final ServletOutputStream outputStream = new ResponseServletOutputStream();
private long contentLength = 0;
private String contentType;
private int bufferSize = 4096;
private boolean committed;
private Locale locale = Locale.getDefault();
// ---------------------------------------------------------------------
// HttpServletResponse properties
// ---------------------------------------------------------------------
private final List<Cookie> cookies = new ArrayList<>();
private final Map<String, HeaderValueHolder> headers = new LinkedCaseInsensitiveMap<>();
private final HttpHeaders headers = new HttpHeaders();
private int status = HttpServletResponse.SC_OK;
@Nullable
private String errorMessage;
// ---------------------------------------------------------------------
// ServletResponse interface
// ---------------------------------------------------------------------
@Override
public void setCharacterEncoding(String characterEncoding) {
setExplicitCharacterEncoding(characterEncoding);
updateContentTypePropertyAndHeader();
}
private void setExplicitCharacterEncoding(String characterEncoding) {
Assert.notNull(characterEncoding, "'characterEncoding' must not be null");
this.characterEncoding = characterEncoding;
this.characterEncodingSet = true;
}
private void updateContentTypePropertyAndHeader() {
if (this.contentType != null) {
String value = this.contentType;
if (this.characterEncodingSet && !value.toLowerCase().contains(CHARSET_PREFIX)) {
value += ';' + CHARSET_PREFIX + getCharacterEncoding();
this.contentType = value;
}
doAddHeaderValue(HttpHeaders.CONTENT_TYPE, value, true);
}
}
@Override
@@ -130,7 +83,6 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
@Override
public ServletOutputStream getOutputStream() {
Assert.state(this.outputStreamAccessAllowed, "OutputStream access not allowed");
return this.outputStream;
}
@@ -162,24 +114,8 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
return this.content.toString(getCharacterEncoding());
}
/**
* Get the content of the response body as a {@code String}, using the provided
* {@code fallbackCharset} if no charset has been explicitly defined and
* otherwise using the charset specified for the response by the application,
* either through {@link HttpServletResponse} methods or through a charset
* parameter on the {@code Content-Type}.
*
* @return the content as a {@code String}
* @throws UnsupportedEncodingException if the character encoding is not
* supported
* @since 5.2
* @see #getContentAsString()
* @see #setCharacterEncoding(String)
* @see #setContentType(String)
*/
public String getContentAsString(Charset fallbackCharset) throws UnsupportedEncodingException {
String charsetName = (this.characterEncodingSet ? getCharacterEncoding() : fallbackCharset.name());
return this.content.toString(charsetName);
return this.content.toString(getCharacterEncoding());
}
@Override
@@ -224,21 +160,15 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
this.content.reset();
}
public void setCommitted(boolean committed) {
this.committed = committed;
}
@Override
public boolean isCommitted() {
return this.committed;
return true;
}
@Override
public void reset() {
resetBuffer();
this.characterEncoding = this.defaultCharacterEncoding;
this.characterEncodingSet = false;
this.contentLength = 0;
this.contentType = null;
this.locale = Locale.getDefault();
this.cookies.clear();
@@ -249,16 +179,11 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
@Override
public void setLocale(@Nullable Locale locale) {
// Although the Javadoc for javax.servlet.ServletResponse.setLocale(Locale) does
// not
// state how a null value for the supplied Locale should be handled, both Tomcat
// and
// Jetty simply ignore a null value. So we do the same here.
if (locale == null) {
return;
}
this.locale = locale;
doAddHeaderValue(HttpHeaders.CONTENT_LANGUAGE, locale.toLanguageTag(), true);
this.headers.add(HttpHeaders.CONTENT_LANGUAGE, locale.toLanguageTag());
}
@Override
@@ -314,8 +239,7 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
@Override
@Nullable
public String getHeader(String name) {
HeaderValueHolder header = this.headers.get(name);
return (header != null ? header.getStringValue() : null);
return this.headers.containsKey(name) ? this.headers.get(name).toString() : null;
}
/**
@@ -331,13 +255,7 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
*/
@Override
public List<String> getHeaders(String name) {
HeaderValueHolder header = this.headers.get(name);
if (header != null) {
return header.getStringValues();
}
else {
return Collections.emptyList();
}
return this.headers.get(name);
}
/**
@@ -350,24 +268,7 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
*/
@Nullable
public Object getHeaderValue(String name) {
HeaderValueHolder header = this.headers.get(name);
return (header != null ? header.getValue() : null);
}
/**
* Return all values for the given header as a List of value objects.
*
* @param name the name of the header
* @return the associated header values, or an empty List if none
*/
public List<Object> getHeaderValues(String name) {
HeaderValueHolder header = this.headers.get(name);
if (header != null) {
return header.getValues();
}
else {
return Collections.emptyList();
}
return this.headers.containsKey(name) ? this.headers.get(name).get(0) : null;
}
/**
@@ -411,14 +312,12 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
Assert.state(!isCommitted(), "Cannot set error status - response is already committed");
this.status = status;
this.errorMessage = errorMessage;
setCommitted(true);
}
@Override
public void sendError(int status) throws IOException {
Assert.state(!isCommitted(), "Cannot set error status - response is already committed");
this.status = status;
setCommitted(true);
}
@Override
@@ -427,7 +326,6 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
Assert.notNull(url, "Redirect URL must not be null");
setHeader(HttpHeaders.LOCATION, url);
setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
setCommitted(true);
}
@Nullable
@@ -437,25 +335,12 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
@Override
public void setDateHeader(String name, long value) {
setHeaderValue(name, formatDate(value));
this.headers.set(name, formatDate(value));
}
@Override
public void addDateHeader(String name, long value) {
addHeaderValue(name, formatDate(value));
}
public long getDateHeader(String name) {
String headerValue = getHeader(name);
if (headerValue == null) {
return -1;
}
try {
return newDateFormat().parse(getHeader(name)).getTime();
}
catch (ParseException ex) {
throw new IllegalArgumentException("Value for header '" + name + "' is not a valid Date: " + headerValue);
}
this.headers.add(name, formatDate(value));
}
private String formatDate(long date) {
@@ -464,55 +349,27 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
private DateFormat newDateFormat() {
SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US);
dateFormat.setTimeZone(GMT);
return dateFormat;
}
@Override
public void setHeader(String name, @Nullable String value) {
setHeaderValue(name, value);
this.headers.set(name, value);
}
@Override
public void addHeader(String name, @Nullable String value) {
addHeaderValue(name, value);
this.headers.add(name, value);
}
@Override
public void setIntHeader(String name, int value) {
setHeaderValue(name, value);
this.headers.set(name, String.valueOf(value));
}
@Override
public void addIntHeader(String name, int value) {
addHeaderValue(name, value);
}
private void setHeaderValue(String name, @Nullable Object value) {
if (value == null) {
return;
}
boolean replaceHeader = true;
doAddHeaderValue(name, value, replaceHeader);
}
private void addHeaderValue(String name, @Nullable Object value) {
if (value == null) {
return;
}
boolean replaceHeader = false;
doAddHeaderValue(name, value, replaceHeader);
}
private void doAddHeaderValue(String name, Object value, boolean replace) {
Assert.notNull(value, "Header value must not be null");
HeaderValueHolder header = this.headers.computeIfAbsent(name, key -> new HeaderValueHolder());
if (replace) {
header.setValue(value);
}
else {
header.addValue(value);
}
this.headers.add(name, String.valueOf(value));
}
@Override
@@ -538,20 +395,6 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
return this.errorMessage;
}
// ---------------------------------------------------------------------
// Methods for MockRequestDispatcher
// ---------------------------------------------------------------------
@Nullable
public String getForwardedUrl() {
throw new UnsupportedOperationException();
}
@Nullable
public String getIncludedUrl() {
throw new UnsupportedOperationException();
}
/**
* Inner class that adapts the ServletOutputStream to mark the response as
* committed once the buffer size is exceeded.

View File

@@ -44,6 +44,15 @@ import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
/**
* Represents the main entry point into interaction with web application over light-weight proxy.
* After creating an instance via {@link #INSTANCE(Class...)} operation which will initialize the provided component
* classes of your web application (effectively starting your web application less web server),
* you use {@link #service(HttpServletRequest, HttpServletResponse)} operation to send request and receive a response.
*
* @author Oleg Zhurakousky
*
*/
public class ProxyMvc {
static final String MVC_RESULT_ATTRIBUTE = ProxyMvc.class.getName().concat(".MVC_RESULT_ATTRIBUTE");

View File

@@ -14,9 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.adapter.aws.web;
package org.springframework.cloud.function.serverless.web;
import java.util.List;
@@ -28,9 +26,8 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.function.serverless.web.ProxyHttpServletRequest;
import org.springframework.cloud.function.serverless.web.ProxyHttpServletResponse;
import org.springframework.cloud.function.serverless.web.ProxyMvc;
import org.springframework.cloud.function.test.app.Pet;
import org.springframework.cloud.function.test.app.PetStoreSpringAppConfig;
import static org.assertj.core.api.Assertions.assertThat;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.adapter.aws.web;
package org.springframework.cloud.function.test.app;
import java.util.Date;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.adapter.aws.web;
package org.springframework.cloud.function.test.app;
import java.util.ArrayList;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.adapter.aws.web;
package org.springframework.cloud.function.test.app;
import java.security.Principal;
import java.util.Optional;