From 145335bcb246aec36509ba5ff4e29c69c5fc9913 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Tue, 21 Feb 2023 13:05:43 +0100 Subject: [PATCH] working on AWS --- .../adapter/aws/web/WebProxyInvoker.java | 104 ++ .../web/client/HeaderValueHolder.java | 78 + .../web/client/ProxyHttpServletRequest.java | 1416 +++++++++++++++++ .../web/client/ProxyHttpServletResponse.java | 601 +++++++ .../springframework/web/client/ProxyMvc.java | 214 +++ .../web/client/ProxyServletConfig.java | 53 + .../web/client/ProxyServletContext.java | 397 +++++ .../cloud/function/adapter/aws/web/Pet.java | 58 + .../function/adapter/aws/web/PetData.java | 118 ++ .../aws/web/PetStoreSpringAppConfig.java | 71 + .../adapter/aws/web/PetsController.java | 76 + .../adapter/aws/web/WebProxyInvokerTests.java | 126 ++ 12 files changed, 3312 insertions(+) create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvoker.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/HeaderValueHolder.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyHttpServletRequest.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyHttpServletResponse.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyMvc.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyServletConfig.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyServletContext.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/Pet.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetData.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetStoreSpringAppConfig.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetsController.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvokerTests.java diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvoker.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvoker.java new file mode 100644 index 000000000..9b5bc44de --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvoker.java @@ -0,0 +1,104 @@ +/* + * 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.adapter.aws.web; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.Filter; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.cloud.function.json.JacksonMapper; +import org.springframework.cloud.function.json.JsonMapper; +import org.springframework.cloud.function.utils.FunctionClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StreamUtils; +import org.springframework.web.client.ProxyDispatcherServlet; +import org.springframework.web.client.ProxyHttpServletRequest; +import org.springframework.web.client.ProxyHttpServletResponse; +import org.springframework.web.client.ProxyMvc; +import org.springframework.web.client.ProxyServletConfig; +import org.springframework.web.client.ProxyServletContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + + + +public class WebProxyInvoker implements RequestStreamHandler { + + private final ProxyMvc mvc; + + public WebProxyInvoker() throws ServletException { + Class startClass = FunctionClassUtils.getStartClass(); + AnnotationConfigWebApplicationContext applpicationContext = new AnnotationConfigWebApplicationContext(); + applpicationContext.register(startClass); + ServletContext servletContext = new ProxyServletContext(); + ServletConfig servletConfig = new ProxyServletConfig(servletContext); + applpicationContext.setServletConfig(servletConfig); + + DispatcherServlet servlet = new DispatcherServlet(applpicationContext); + servlet.init(servletConfig); + this.mvc = new ProxyMvc(servlet, applpicationContext.getBeansOfType(Filter.class).values().toArray(new Filter[0])); + } + + @Override + public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { + ProxyServletContext servletContext = new ProxyServletContext(); + JsonMapper mapper = new JacksonMapper(new ObjectMapper()); + Map request = mapper.fromJson(StreamUtils.copyToByteArray(input), Map.class); + System.out.println("!!!==> REQUEST: " + request); + String httpMethod = (String) request.get("httpMethod"); + String path = (String) request.get("path"); + System.out.println("!!!==> httpMethod: " + httpMethod); + System.out.println("!!!==> path: " + path); + HttpServletRequest resquest = new ProxyHttpServletRequest(null, httpMethod, path); + ProxyHttpServletResponse response = new ProxyHttpServletResponse(); + try { + this.mvc.perform(resquest, response); + } + catch (Exception e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } + byte[] responseBytes = response.getContentAsByteArray(); + if (!ObjectUtils.isEmpty(responseBytes)) { + System.out.println("!!!==> responseBytes: " + response.getContentAsString()); + + Map apiGatewayResponseStructure = new HashMap(); + apiGatewayResponseStructure.put("isBase64Encoded", false); + apiGatewayResponseStructure.put("statusCode", 200); + apiGatewayResponseStructure.put("body", response.getContentAsString()); + apiGatewayResponseStructure.put("headers", Collections.singletonMap("foo", "bar")); + + byte[] apiGatewayResponseBytes = mapper.toJson(apiGatewayResponseStructure); + System.out.println("!!!==> apiGatewayResponseStructure: " + apiGatewayResponseStructure); + StreamUtils.copy(apiGatewayResponseBytes, output); + System.out.println("!!!==> COPIED RESPONSE"); + } + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/HeaderValueHolder.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/HeaderValueHolder.java new file mode 100644 index 000000000..74cef84db --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/HeaderValueHolder.java @@ -0,0 +1,78 @@ +/* + * 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.web.client; + +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 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 getValues() { + return Collections.unmodifiableList(this.values); + } + + List getStringValues() { + List 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(); + } + +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyHttpServletRequest.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyHttpServletRequest.java new file mode 100644 index 000000000..203f1e031 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyHttpServletRequest.java @@ -0,0 +1,1416 @@ +/* + * 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.web.client; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.stream.Collectors; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpUpgradeHandler; +import javax.servlet.http.Part; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +public class ProxyHttpServletRequest implements HttpServletRequest { + + private static final String HTTP = "http"; + + private static final String HTTPS = "https"; + + 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("")); + + /** + * Date formats as specified in the HTTP RFC. + * + * @see Section + * 7.1.1.1 of RFC 7231 + */ + private static final String[] DATE_FORMATS = new String[] { "EEE, dd MMM yyyy HH:mm:ss zzz", + "EEE, dd-MMM-yy HH:mm:ss zzz", "EEE MMM dd HH:mm:ss yyyy" }; + + // --------------------------------------------------------------------- + // Public constants + // --------------------------------------------------------------------- + + /** + * The default protocol: 'HTTP/1.1'. + * + * @since 4.3.7 + */ + public static final String DEFAULT_PROTOCOL = "HTTP/1.1"; + + /** + * The default scheme: 'http'. + * + * @since 4.3.7 + */ + public static final String DEFAULT_SCHEME = HTTP; + + /** + * The default server address: '127.0.0.1'. + */ + public static final String DEFAULT_SERVER_ADDR = "127.0.0.1"; + + /** + * The default server name: 'localhost'. + */ + public static final String DEFAULT_SERVER_NAME = "localhost"; + + /** + * The default server port: '80'. + */ + public static final int DEFAULT_SERVER_PORT = 80; + + /** + * The default remote address: '127.0.0.1'. + */ + public static final String DEFAULT_REMOTE_ADDR = "127.0.0.1"; + + /** + * The default remote host: 'localhost'. + */ + public static final String DEFAULT_REMOTE_HOST = "localhost"; + + // --------------------------------------------------------------------- + // Lifecycle properties + // --------------------------------------------------------------------- + + private final ServletContext servletContext; + + private boolean active = true; + + // --------------------------------------------------------------------- + // ServletRequest properties + // --------------------------------------------------------------------- + + private final Map attributes = new LinkedHashMap<>(); + + @Nullable + private String characterEncoding; + + @Nullable + private byte[] content; + + @Nullable + private String contentType; + + @Nullable + private ServletInputStream inputStream; + + @Nullable + private BufferedReader reader; + + private final Map parameters = new LinkedHashMap<>(16); + +// private String protocol = DEFAULT_PROTOCOL; +// +// private String scheme = DEFAULT_SCHEME; +// +// private String serverName = DEFAULT_SERVER_NAME; +// +// private int serverPort = DEFAULT_SERVER_PORT; +// +// private String remoteAddr = DEFAULT_REMOTE_ADDR; +// +// private String remoteHost = DEFAULT_REMOTE_HOST; + + /** List of locales in descending order. */ + private final LinkedList locales = new LinkedList<>(); + + private boolean secure = false; + +// private int remotePort = DEFAULT_SERVER_PORT; +// +// private String localName = DEFAULT_SERVER_NAME; +// +// private String localAddr = DEFAULT_SERVER_ADDR; +// +// private int localPort = DEFAULT_SERVER_PORT; + + private boolean asyncStarted = false; + + private boolean asyncSupported = false; + + private DispatcherType dispatcherType = DispatcherType.REQUEST; + + // --------------------------------------------------------------------- + // HttpServletRequest properties + // --------------------------------------------------------------------- + + @Nullable + private String authType; + + @Nullable + private Cookie[] cookies; + + private final Map headers = new LinkedCaseInsensitiveMap<>(); + + @Nullable + private String method; + + @Nullable + private String pathInfo; + + private String contextPath = ""; + + @Nullable + private String queryString; + + @Nullable + private String remoteUser; + + private final Set userRoles = new HashSet<>(); + + @Nullable + private Principal userPrincipal; + + @Nullable + private String requestedSessionId; + + @Nullable + private String requestURI; + + private String servletPath = ""; + + @Nullable + private HttpSession session; + + private boolean requestedSessionIdValid = true; + + private boolean requestedSessionIdFromCookie = true; + + private boolean requestedSessionIdFromURL = false; + + private final MultiValueMap parts = new LinkedMultiValueMap<>(); + + // --------------------------------------------------------------------- + // Constructors + // --------------------------------------------------------------------- + +// /** +// * Create a new {@code MockHttpServletRequest} with a default +// * {@link MockServletContext}. +// * @param method the request method (may be {@code null}) +// * @param requestURI the request URI (may be {@code null}) +// * @see #setMethod +// * @see #setRequestURI +// * @see #MockHttpServletRequest(ServletContext, String, String) +// */ +// public ServerlessHttpServletRequest(@Nullable String method, @Nullable String requestURI) { +// this(null, method, requestURI); +// } +// +// /** +// * Create a new {@code MockHttpServletRequest} with the supplied {@link ServletContext}. +// * @param servletContext the ServletContext that the request runs in +// * (may be {@code null} to use a default {@link MockServletContext}) +// * @see #MockHttpServletRequest(ServletContext, String, String) +// */ +// public ServerlessHttpServletRequest(@Nullable ServletContext servletContext) { +// this(servletContext, "", ""); +// } + + /** + * Create a new {@code MockHttpServletRequest} with the supplied + * {@link ServletContext}, {@code method}, and {@code requestURI}. + *

+ * The preferred locale will be set to {@link Locale#ENGLISH}. + * + * @param servletContext the ServletContext that the request runs in (may be + * {@code null} to use a default + * {@link MockServletContext}) + * @param method the request method (may be {@code null}) + * @param requestURI the request URI (may be {@code null}) + * @see #setMethod + * @see #setRequestURI + * @see #setPreferredLocales + * @see MockServletContext + */ + public ProxyHttpServletRequest(ServletContext servletContext, String method, String requestURI) { + this.servletContext = servletContext; + this.method = method; + this.requestURI = requestURI; + this.locales.add(Locale.ENGLISH); + } + + // --------------------------------------------------------------------- + // Lifecycle methods + // --------------------------------------------------------------------- + + /** + * Return the ServletContext that this request is associated with. (Not + * available in the standard HttpServletRequest interface for some reason.) + */ + @Override + public ServletContext getServletContext() { + return this.servletContext; + } + + /** + * Return whether this request is still active (that is, not completed yet). + */ + public boolean isActive() { + return this.active; + } + + /** + * Mark this request as completed, keeping its state. + */ + public void close() { + this.active = false; + } + + /** + * Invalidate this request, clearing its state. + */ + public void invalidate() { + close(); + clearAttributes(); + } + + /** + * Check whether this request is still active (that is, not completed yet), + * throwing an IllegalStateException if not active anymore. + */ + protected void checkActive() throws IllegalStateException { + Assert.state(this.active, "Request is not active anymore"); + } + + // --------------------------------------------------------------------- + // ServletRequest interface + // --------------------------------------------------------------------- + + @Override + public Object getAttribute(String name) { + checkActive(); + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + checkActive(); + return Collections.enumeration(new LinkedHashSet<>(this.attributes.keySet())); + } + + @Override + @Nullable + public String getCharacterEncoding() { + return this.characterEncoding; + } + + @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); + } + } + + /** + * Set the content of the request body as a byte array. + *

+ * If the supplied byte array represents text such as XML or JSON, the + * {@link #setCharacterEncoding character encoding} should typically be set as + * well. + * + * @see #setCharacterEncoding(String) + * @see #getContentAsByteArray() + * @see #getContentAsString() + */ + public void setContent(@Nullable byte[] content) { + this.content = content; + this.inputStream = null; + this.reader = null; + } + + /** + * Get the content of the request body as a byte array. + * + * @return the content as a byte array (potentially {@code null}) + * @since 5.0 + * @see #setContent(byte[]) + * @see #getContentAsString() + */ + @Nullable + public byte[] getContentAsByteArray() { + return this.content; + } + + /** + * Get the content of the request body as a {@code String}, using the configured + * {@linkplain #getCharacterEncoding character encoding}. + * + * @return the content as a {@code String}, potentially {@code null} + * @throws IllegalStateException if the character encoding has not been + * set + * @throws UnsupportedEncodingException if the character encoding is not + * supported + * @since 5.0 + * @see #setContent(byte[]) + * @see #setCharacterEncoding(String) + * @see #getContentAsByteArray() + */ + @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."); + + if (this.content == null) { + return null; + } + return new String(this.content, this.characterEncoding); + } + + @Override + public int getContentLength() { + return (this.content != null ? this.content.length : -1); + } + + @Override + public long getContentLengthLong() { + return getContentLength(); + } + + 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(); + } + } + + @Override + @Nullable + public String getContentType() { + return this.contentType; + } + + @Override + public ServletInputStream getInputStream() { +// if (this.inputStream != null) { +// return this.inputStream; +// } +// else if (this.reader != null) { +// throw new IllegalStateException( +// "Cannot call getInputStream() after getReader() has already been called for the current request") ; +// } +// +// this.inputStream = (this.content != null ? +// new DelegatingServletInputStream(new ByteArrayInputStream(this.content)) : +// EMPTY_SERVLET_INPUT_STREAM); +// return this.inputStream; + throw new UnsupportedOperationException(); + } + + /** + * Set a single value for the specified HTTP parameter. + *

+ * If there are already one or more values registered for the given parameter + * name, they will be replaced. + */ + public void setParameter(String name, String value) { + setParameter(name, new String[] { value }); + } + + /** + * Set an array of values for the specified HTTP parameter. + *

+ * If there are already one or more values registered for the given parameter + * name, they will be replaced. + */ + public void setParameter(String name, String... values) { + Assert.notNull(name, "Parameter name must not be null"); + this.parameters.put(name, values); + } + + /** + * Set all provided parameters replacing any existing values + * for the provided parameter names. To add without replacing existing values, + * use {@link #addParameters(java.util.Map)}. + */ + public void setParameters(Map params) { + Assert.notNull(params, "Parameter map must not be null"); + params.forEach((key, value) -> { + if (value instanceof String) { + setParameter(key, (String) value); + } + else if (value instanceof String[]) { + setParameter(key, (String[]) value); + } + else { + throw new IllegalArgumentException("Parameter map value must be single value " + " or array of type [" + + String.class.getName() + "]"); + } + }); + } + + /** + * Add a single value for the specified HTTP parameter. + *

+ * If there are already one or more values registered for the given parameter + * name, the given value will be added to the end of the list. + */ + public void addParameter(String name, @Nullable String value) { + addParameter(name, new String[] { value }); + } + + /** + * Add an array of values for the specified HTTP parameter. + *

+ * If there are already one or more values registered for the given parameter + * name, the given values will be added to the end of the list. + */ + public void addParameter(String name, String... values) { + Assert.notNull(name, "Parameter name must not be null"); + String[] oldArr = this.parameters.get(name); + if (oldArr != null) { + String[] newArr = new String[oldArr.length + values.length]; + System.arraycopy(oldArr, 0, newArr, 0, oldArr.length); + System.arraycopy(values, 0, newArr, oldArr.length, values.length); + this.parameters.put(name, newArr); + } + else { + this.parameters.put(name, values); + } + } + + /** + * Add all provided parameters without replacing any existing + * values. To replace existing values, use + * {@link #setParameters(java.util.Map)}. + */ + public void addParameters(Map params) { + Assert.notNull(params, "Parameter map must not be null"); + params.forEach((key, value) -> { + if (value instanceof String) { + addParameter(key, (String) value); + } + else if (value instanceof String[]) { + addParameter(key, (String[]) value); + } + else { + throw new IllegalArgumentException("Parameter map value must be single value " + " or array of type [" + + String.class.getName() + "]"); + } + }); + } + + /** + * Remove already registered values for the specified HTTP parameter, if any. + */ + public void removeParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + this.parameters.remove(name); + } + + /** + * Remove all existing parameters. + */ + public void removeAllParameters() { + this.parameters.clear(); + } + + @Override + @Nullable + public String getParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + String[] arr = this.parameters.get(name); + return (arr != null && arr.length > 0 ? arr[0] : null); + } + + @Override + public Enumeration getParameterNames() { + return Collections.enumeration(this.parameters.keySet()); + } + + @Override + public String[] getParameterValues(String name) { + Assert.notNull(name, "Parameter name must not be null"); + return this.parameters.get(name); + } + + @Override + public Map getParameterMap() { + return Collections.unmodifiableMap(this.parameters); + } + + public void setProtocol(String protocol) { + // this.protocol = protocol; + throw new UnsupportedOperationException(); + } + + @Override + public String getProtocol() { + // return this.protocol; + throw new UnsupportedOperationException(); + } + + public void setScheme(String scheme) { +// this.scheme = scheme; + throw new UnsupportedOperationException(); + } + + @Override + public String getScheme() { +// return this.scheme; + throw new UnsupportedOperationException(); + } + + public void setServerName(String serverName) { +// this.serverName = serverName; + throw new UnsupportedOperationException(); + } + + @Override + public String getServerName() { +// String rawHostHeader = getHeader(HttpHeaders.HOST); +// String host = rawHostHeader; +// if (host != null) { +// host = host.trim(); +// if (host.startsWith("[")) { +// int indexOfClosingBracket = host.indexOf(']'); +// Assert.state(indexOfClosingBracket > -1, () -> "Invalid Host header: " + rawHostHeader); +// host = host.substring(0, indexOfClosingBracket + 1); +// } +// else if (host.contains(":")) { +// host = host.substring(0, host.indexOf(':')); +// } +// return host; +// } +// +// // else +// return this.serverName; + throw new UnsupportedOperationException(); + } + + public void setServerPort(int serverPort) { +// this.serverPort = serverPort; + throw new UnsupportedOperationException(); + } + + @Override + public int getServerPort() { +// String rawHostHeader = getHeader(HttpHeaders.HOST); +// String host = rawHostHeader; +// if (host != null) { +// host = host.trim(); +// int idx; +// if (host.startsWith("[")) { +// int indexOfClosingBracket = host.indexOf(']'); +// Assert.state(indexOfClosingBracket > -1, () -> "Invalid Host header: " + rawHostHeader); +// idx = host.indexOf(':', indexOfClosingBracket); +// } +// else { +// idx = host.indexOf(':'); +// } +// if (idx != -1) { +// return Integer.parseInt(host.substring(idx + 1)); +// } +// } +// +// // else +// return this.serverPort; + throw new UnsupportedOperationException(); + } + + @Override + public BufferedReader getReader() throws UnsupportedEncodingException { + if (this.reader != null) { + return this.reader; + } + else if (this.inputStream != null) { + throw new IllegalStateException( + "Cannot call getReader() after getInputStream() has already been called for the current request"); + } + + if (this.content != null) { + InputStream sourceStream = new ByteArrayInputStream(this.content); + Reader sourceReader = (this.characterEncoding != null) + ? new InputStreamReader(sourceStream, this.characterEncoding) + : new InputStreamReader(sourceStream); + this.reader = new BufferedReader(sourceReader); + } + else { + this.reader = EMPTY_BUFFERED_READER; + } + return this.reader; + } + + public void setRemoteAddr(String remoteAddr) { +// this.remoteAddr = remoteAddr; + throw new UnsupportedOperationException(); + } + + @Override + public String getRemoteAddr() { + return "proxy"; +// throw new UnsupportedOperationException(); + } + + public void setRemoteHost(String remoteHost) { +// this.remoteHost = remoteHost; + throw new UnsupportedOperationException(); + } + + @Override + public String getRemoteHost() { +// return this.remoteHost; + throw new UnsupportedOperationException(); + } + + @Override + public void setAttribute(String name, @Nullable Object value) { + checkActive(); + Assert.notNull(name, "Attribute name must not be null"); + if (value != null) { + this.attributes.put(name, value); + } + else { + this.attributes.remove(name); + } + } + + @Override + public void removeAttribute(String name) { + checkActive(); + Assert.notNull(name, "Attribute name must not be null"); + this.attributes.remove(name); + } + + /** + * Clear all of this request's attributes. + */ + public void clearAttributes() { + 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 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. + *

+ * If no locales have been explicitly configured, the default, preferred + * {@link Locale} for the server mocked by this request is + * {@link Locale#ENGLISH}. + *

+ * In contrast to the Servlet specification, this mock implementation does + * not take into consideration any locales specified via the + * {@code Accept-Language} header. + * + * @see javax.servlet.ServletRequest#getLocale() + * @see #addPreferredLocale(Locale) + * @see #setPreferredLocales(List) + */ + @Override + public Locale getLocale() { + return this.locales.getFirst(); + } + + /** + * Return an {@linkplain Enumeration enumeration} of the preferred + * {@linkplain Locale locales} configured in this mock request. + *

+ * If no locales have been explicitly configured, the default, preferred + * {@link Locale} for the server mocked by this request is + * {@link Locale#ENGLISH}. + *

+ * In contrast to the Servlet specification, this mock implementation does + * not take into consideration any locales specified via the + * {@code Accept-Language} header. + * + * @see javax.servlet.ServletRequest#getLocales() + * @see #addPreferredLocale(Locale) + * @see #setPreferredLocales(List) + */ + @Override + public Enumeration getLocales() { + return Collections.enumeration(this.locales); + } + + /** + * Set the boolean {@code secure} flag indicating whether the mock request was + * made using a secure channel, such as HTTPS. + * + * @see #isSecure() + * @see #getScheme() + * @see #setScheme(String) + */ + public void setSecure(boolean secure) { + this.secure = secure; + } + + /** + * Return {@code true} if the {@link #setSecure secure} flag has been set to + * {@code true} or if the {@link #getScheme scheme} is {@code https}. + * + * @see javax.servlet.ServletRequest#isSecure() + */ + @Override + public boolean isSecure() { +// return (this.secure || HTTPS.equalsIgnoreCase(this.scheme)); + throw new UnsupportedOperationException(); + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { +// return new MockRequestDispatcher(path); + throw new UnsupportedOperationException(); + } + + @Override + @Deprecated + public String getRealPath(String path) { + return this.servletContext.getRealPath(path); + } + + public void setRemotePort(int remotePort) { +// this.remotePort = remotePort; + throw new UnsupportedOperationException(); + } + + @Override + public int getRemotePort() { +// return this.remotePort; + throw new UnsupportedOperationException(); + } + + public void setLocalName(String localName) { +// this.localName = localName; + throw new UnsupportedOperationException(); + } + + @Override + public String getLocalName() { +// return this.localName; + throw new UnsupportedOperationException(); + } + + public void setLocalAddr(String localAddr) { +// this.localAddr = localAddr; + throw new UnsupportedOperationException(); + } + + @Override + public String getLocalAddr() { + return "proxy"; + } + + public void setLocalPort(int localPort) { +// this.localPort = localPort; + throw new UnsupportedOperationException(); + } + + @Override + public int getLocalPort() { +// return this.localPort; + throw new UnsupportedOperationException(); + } + + @Override + public AsyncContext startAsync() { + return startAsync(this, null); + } + + @Override + public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) { +// Assert.state(this.asyncSupported, "Async not supported"); +// this.asyncStarted = true; +// this.asyncContext = new MockAsyncContext(request, response); +// return this.asyncContext; + throw new UnsupportedOperationException(); + } + + public void setAsyncStarted(boolean asyncStarted) { + this.asyncStarted = asyncStarted; + } + + @Override + public boolean isAsyncStarted() { + return this.asyncStarted; + } + + public void setAsyncSupported(boolean asyncSupported) { + this.asyncSupported = asyncSupported; + } + + @Override + public boolean isAsyncSupported() { + return this.asyncSupported; + } + + public void setAsyncContext(@Nullable AsyncContext asyncContext) { +// this.asyncContext = asyncContext; + throw new UnsupportedOperationException(); + } + + @Override + @Nullable + public AsyncContext getAsyncContext() { +// return this.asyncContext; +// throw new UnsupportedOperationException(); + return null; + } + + public void setDispatcherType(DispatcherType dispatcherType) { + this.dispatcherType = dispatcherType; + } + + @Override + public javax.servlet.DispatcherType getDispatcherType() { + return this.dispatcherType; + } + + // --------------------------------------------------------------------- + // HttpServletRequest interface + // --------------------------------------------------------------------- + + public void setAuthType(@Nullable String authType) { + this.authType = authType; + } + + @Override + @Nullable + public String getAuthType() { + return this.authType; + } + + public void setCookies(@Nullable Cookie... cookies) { + this.cookies = (ObjectUtils.isEmpty(cookies) ? null : cookies); + if (this.cookies == null) { + removeHeader(HttpHeaders.COOKIE); + } + else { + doAddHeaderValue(HttpHeaders.COOKIE, encodeCookies(this.cookies), true); + } + } + + private static String encodeCookies(@NonNull Cookie... cookies) { + return Arrays.stream(cookies).map(c -> c.getName() + '=' + (c.getValue() == null ? "" : c.getValue())) + .collect(Collectors.joining("; ")); + } + + @Override + @Nullable + public Cookie[] getCookies() { + return this.cookies; + } + + /** + * Add an HTTP header entry for the given name. + *

+ * While this method can take any {@code Object} as a parameter, it is + * recommended to use the following types: + *

    + *
  • String or any Object to be converted using {@code toString()}; see + * {@link #getHeader}.
  • + *
  • String, Number, or Date for date headers; see + * {@link #getDateHeader}.
  • + *
  • String or Number for integer headers; see {@link #getIntHeader}.
  • + *
  • {@code String[]} or {@code Collection} for multiple values; see + * {@link #getHeaders}.
  • + *
+ * + * @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 locales = headers.getAcceptLanguageAsLocales(); + this.locales.clear(); + this.locales.addAll(locales); + if (this.locales.isEmpty()) { + this.locales.add(Locale.ENGLISH); + } + } + catch (IllegalArgumentException ex) { + // Invalid Accept-Language format -> just store plain header + } + doAddHeaderValue(name, value, true); + } + else { + doAddHeaderValue(name, value, false); + } + } + + 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); + } + } + + /** + * Remove already registered entries for the specified HTTP header, if any. + * + * @since 4.3.20 + */ + public void removeHeader(String name) { + Assert.notNull(name, "Header name must not be null"); + this.headers.remove(name); + } + + /** + * Return the long timestamp for the date header with the given {@code name}. + *

+ * If the internal value representation is a String, this method will try to + * parse it as a date using the supported date formats: + *

    + *
  • "EEE, dd MMM yyyy HH:mm:ss zzz"
  • + *
  • "EEE, dd-MMM-yy HH:mm:ss zzz"
  • + *
  • "EEE MMM dd HH:mm:ss yyyy"
  • + *
+ * + * @param name the header name + * @see Section + * 7.1.1.1 of RFC 7231 + */ + @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); + } + else { + return -1L; + } + } + + private long parseDateHeader(String name, String value) { + for (String dateFormat : DATE_FORMATS) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); + simpleDateFormat.setTimeZone(GMT); + try { + return simpleDateFormat.parse(value).getTime(); + } + catch (ParseException ex) { + // ignore + } + } + 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 getHeaders(String name) { + HeaderValueHolder header = this.headers.get(name); + return Collections.enumeration(header != null ? header.getStringValues() : new LinkedList<>()); + } + + @Override + public Enumeration 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; + } + + @Override + @Nullable + public String getMethod() { + return this.method; + } + + public void setPathInfo(@Nullable String pathInfo) { + this.pathInfo = pathInfo; + } + + @Override + @Nullable + public String getPathInfo() { + return this.pathInfo; + } + + @Override + @Nullable + public String getPathTranslated() { + return (this.pathInfo != null ? getRealPath(this.pathInfo) : null); + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + @Override + public String getContextPath() { + return this.contextPath; + } + + public void setQueryString(@Nullable String queryString) { + this.queryString = queryString; + } + + @Override + @Nullable + public String getQueryString() { + return this.queryString; + } + + public void setRemoteUser(@Nullable String remoteUser) { + this.remoteUser = remoteUser; + } + + @Override + @Nullable + public String getRemoteUser() { + return this.remoteUser; + } + + public void addUserRole(String role) { + this.userRoles.add(role); + } + + @Override + public boolean isUserInRole(String role) { + throw new UnsupportedOperationException(); +// return (this.userRoles.contains(role) || (this.servletContext instanceof MockServletContext && +// ((MockServletContext) this.servletContext).getDeclaredRoles().contains(role))); + } + + public void setUserPrincipal(@Nullable Principal userPrincipal) { + this.userPrincipal = userPrincipal; + } + + @Override + @Nullable + public Principal getUserPrincipal() { + return this.userPrincipal; + } + + public void setRequestedSessionId(@Nullable String requestedSessionId) { + this.requestedSessionId = requestedSessionId; + } + + @Override + @Nullable + public String getRequestedSessionId() { + return this.requestedSessionId; + } + + public void setRequestURI(@Nullable String requestURI) { + this.requestURI = requestURI; + } + + @Override + @Nullable + public String getRequestURI() { + return this.requestURI; + } + + @Override + public StringBuffer getRequestURL() { + String scheme = getScheme(); + String server = getServerName(); + int port = getServerPort(); + String uri = getRequestURI(); + + StringBuffer url = new StringBuffer(scheme).append("://").append(server); + if (port > 0 + && ((HTTP.equalsIgnoreCase(scheme) && port != 80) || (HTTPS.equalsIgnoreCase(scheme) && port != 443))) { + url.append(':').append(port); + } + if (StringUtils.hasText(uri)) { + url.append(uri); + } + return url; + } + + public void setServletPath(String servletPath) { + this.servletPath = servletPath; + } + + @Override + public String getServletPath() { + return this.servletPath; + } + + public void setSession(HttpSession session) { +// this.session = session; +// if (session instanceof MockHttpSession) { +// MockHttpSession mockSession = ((MockHttpSession) session); +// mockSession.access(); +// } + throw new UnsupportedOperationException(); + } + + @Override + @Nullable + public HttpSession getSession(boolean create) { + checkActive(); + // Reset session if invalidated. +// if (this.session instanceof MockHttpSession && ((MockHttpSession) this.session).isInvalid()) { +// this.session = null; +// } +// // Create new session if necessary. +// if (this.session == null && create) { +// this.session = new MockHttpSession(this.servletContext); +// } + return this.session; +// throw new UnsupportedOperationException(); + } + + @Override + @Nullable + public HttpSession getSession() { + return getSession(true); + } + + /** + * The implementation of this (Servlet 3.1+) method calls + * {@link MockHttpSession#changeSessionId()} if the session is a mock session. + * Otherwise it simply returns the current session id. + * + * @since 4.0.3 + */ + @Override + public String changeSessionId() { +// Assert.isTrue(this.session != null, "The request does not have a session"); +// if (this.session instanceof MockHttpSession) { +// return ((MockHttpSession) this.session).changeSessionId(); +// } +// return this.session.getId(); + throw new UnsupportedOperationException(); + } + + public void setRequestedSessionIdValid(boolean requestedSessionIdValid) { + this.requestedSessionIdValid = requestedSessionIdValid; + } + + @Override + public boolean isRequestedSessionIdValid() { + return this.requestedSessionIdValid; + } + + public void setRequestedSessionIdFromCookie(boolean requestedSessionIdFromCookie) { + this.requestedSessionIdFromCookie = requestedSessionIdFromCookie; + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + return this.requestedSessionIdFromCookie; + } + + public void setRequestedSessionIdFromURL(boolean requestedSessionIdFromURL) { + this.requestedSessionIdFromURL = requestedSessionIdFromURL; + } + + @Override + public boolean isRequestedSessionIdFromURL() { + return this.requestedSessionIdFromURL; + } + + @Override + @Deprecated + public boolean isRequestedSessionIdFromUrl() { + return isRequestedSessionIdFromURL(); + } + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public void login(String username, String password) throws ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public void logout() throws ServletException { + this.userPrincipal = null; + this.remoteUser = null; + this.authType = null; + } + + public void addPart(Part part) { + this.parts.add(part.getName(), part); + } + + @Override + @Nullable + public Part getPart(String name) throws IOException, ServletException { + return this.parts.getFirst(name); + } + + @Override + public Collection getParts() throws IOException, ServletException { + List result = new LinkedList<>(); + for (List list : this.parts.values()) { + result.addAll(list); + } + return result; + } + + @Override + public T upgrade(Class handlerClass) throws IOException, ServletException { + throw new UnsupportedOperationException(); + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyHttpServletResponse.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyHttpServletResponse.java new file mode 100644 index 000000000..6feb0ebe1 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyHttpServletResponse.java @@ -0,0 +1,601 @@ +/* + * 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.web.client; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +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; +import javax.servlet.http.Cookie; +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; + +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 cookies = new ArrayList<>(); + + private final Map headers = new LinkedCaseInsensitiveMap<>(); + + 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 + public String getCharacterEncoding() { + return this.characterEncoding; + } + + @Override + public ServletOutputStream getOutputStream() { + Assert.state(this.outputStreamAccessAllowed, "OutputStream access not allowed"); + return this.outputStream; + } + + @Override + public PrintWriter getWriter() throws UnsupportedEncodingException { + throw new UnsupportedOperationException(); + } + + public byte[] getContentAsByteArray() { + return this.content.toByteArray(); + } + + /** + * Get the content of the response body as a {@code String}, 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}. If no charset has been explicitly defined, the + * {@linkplain #setDefaultCharacterEncoding(String) default character encoding} + * will be used. + * + * @return the content as a {@code String} + * @throws UnsupportedEncodingException if the character encoding is not + * supported + * @see #getContentAsString(Charset) + * @see #setCharacterEncoding(String) + * @see #setContentType(String) + */ + public String getContentAsString() throws UnsupportedEncodingException { + 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); + } + + @Override + public void setContentLength(int contentLength) { + throw new UnsupportedOperationException(); + } + + @Override + public void setContentLengthLong(long len) { + throw new UnsupportedOperationException(); + } + + @Override + public void setContentType(@Nullable String contentType) { + this.contentType = contentType; + } + + @Override + @Nullable + public String getContentType() { + return this.contentType; + } + + @Override + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + @Override + public int getBufferSize() { + return this.bufferSize; + } + + @Override + public void flushBuffer() { + + } + + @Override + public void resetBuffer() { + Assert.state(!isCommitted(), "Cannot reset buffer - response is already committed"); + this.content.reset(); + } + + public void setCommitted(boolean committed) { + this.committed = committed; + } + + @Override + public boolean isCommitted() { + return this.committed; + } + + @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(); + this.headers.clear(); + this.status = HttpServletResponse.SC_OK; + this.errorMessage = null; + } + + @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); + } + + @Override + public Locale getLocale() { + return this.locale; + } + + // --------------------------------------------------------------------- + // HttpServletResponse interface + // --------------------------------------------------------------------- + + @Override + public void addCookie(Cookie cookie) { + throw new UnsupportedOperationException(); + } + + @Nullable + public Cookie getCookie(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsHeader(String name) { + return this.headers.containsKey(name); + } + + /** + * Return the names of all specified headers as a Set of Strings. + *

+ * As of Servlet 3.0, this method is also defined in + * {@link HttpServletResponse}. + * + * @return the {@code Set} of header name {@code Strings}, or an empty + * {@code Set} if none + */ + @Override + public Collection getHeaderNames() { + return this.headers.keySet(); + } + + /** + * Return the primary value for the given header as a String, if any. Will + * return the first value in case of multiple values. + *

+ * As of Servlet 3.0, this method is also defined in + * {@link HttpServletResponse}. As of Spring 3.1, it returns a stringified value + * for Servlet 3.0 compatibility. Consider using {@link #getHeaderValue(String)} + * for raw Object access. + * + * @param name the name of the header + * @return the associated header value, or {@code null} if none + */ + @Override + @Nullable + public String getHeader(String name) { + HeaderValueHolder header = this.headers.get(name); + return (header != null ? header.getStringValue() : null); + } + + /** + * Return all values for the given header as a List of Strings. + *

+ * As of Servlet 3.0, this method is also defined in + * {@link HttpServletResponse}. As of Spring 3.1, it returns a List of + * stringified values for Servlet 3.0 compatibility. Consider using + * {@link #getHeaderValues(String)} for raw Object access. + * + * @param name the name of the header + * @return the associated header values, or an empty List if none + */ + @Override + public List getHeaders(String name) { + HeaderValueHolder header = this.headers.get(name); + if (header != null) { + return header.getStringValues(); + } + else { + return Collections.emptyList(); + } + } + + /** + * Return the primary value for the given header, if any. + *

+ * Will return the first value in case of multiple values. + * + * @param name the name of the header + * @return the associated header value, or {@code null} if none + */ + @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 getHeaderValues(String name) { + HeaderValueHolder header = this.headers.get(name); + if (header != null) { + return header.getValues(); + } + else { + return Collections.emptyList(); + } + } + + /** + * The default implementation returns the given URL String as-is. + *

+ * Can be overridden in subclasses, appending a session id or the like. + */ + @Override + public String encodeURL(String url) { + return url; + } + + /** + * The default implementation delegates to {@link #encodeURL}, returning the + * given URL String as-is. + *

+ * Can be overridden in subclasses, appending a session id or the like in a + * redirect-specific fashion. For general URL encoding rules, override the + * common {@link #encodeURL} method instead, applying to redirect URLs as well + * as to general URLs. + */ + @Override + public String encodeRedirectURL(String url) { + return encodeURL(url); + } + + @Override + @Deprecated + public String encodeUrl(String url) { + return encodeURL(url); + } + + @Override + @Deprecated + public String encodeRedirectUrl(String url) { + return encodeRedirectURL(url); + } + + @Override + public void sendError(int status, String errorMessage) throws IOException { + 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 + public void sendRedirect(String url) throws IOException { + Assert.state(!isCommitted(), "Cannot send redirect - response is already committed"); + Assert.notNull(url, "Redirect URL must not be null"); + setHeader(HttpHeaders.LOCATION, url); + setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); + setCommitted(true); + } + + @Nullable + public String getRedirectedUrl() { + return getHeader(HttpHeaders.LOCATION); + } + + @Override + public void setDateHeader(String name, long value) { + setHeaderValue(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); + } + } + + private String formatDate(long date) { + return newDateFormat().format(new Date(date)); + } + + 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); + } + + @Override + public void addHeader(String name, @Nullable String value) { + addHeaderValue(name, value); + } + + @Override + public void setIntHeader(String name, int value) { + setHeaderValue(name, 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); + } + } + + @Override + public void setStatus(int status) { + if (!this.isCommitted()) { + this.status = status; + } + } + + @Override + @Deprecated + public void setStatus(int status, String errorMessage) { + throw new UnsupportedOperationException(); + } + + @Override + public int getStatus() { + return this.status; + } + + @Nullable + public String getErrorMessage() { + 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. + */ + private class ResponseServletOutputStream extends ServletOutputStream { + + private WriteListener listener; + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + if (writeListener != null) { + try { + writeListener.onWritePossible(); + } + catch (IOException e) { + // log.error("Output stream is not writable", e); + } + + listener = writeListener; + } + } + + @Override + public void write(int b) throws IOException { + try { + content.write(b); + } + catch (Exception e) { + if (listener != null) { + listener.onError(e); + } + } + } + + @Override + public void close() throws IOException { + super.close(); + flushBuffer(); + } + } + +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyMvc.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyMvc.java new file mode 100644 index 000000000..e8423a323 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyMvc.java @@ -0,0 +1,214 @@ +/* + * 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.web.client; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.servlet.DispatcherServlet; + +public class ProxyMvc { + + static final String MVC_RESULT_ATTRIBUTE = ProxyMvc.class.getName().concat(".MVC_RESULT_ATTRIBUTE"); + + private final DispatcherServlet servlet; + + private final Filter[] filters; + + @Nullable + private Charset defaultResponseCharacterEncoding; + + /** + * Private constructor, not for direct instantiation. + * + * @see org.springframework.test.web.servlet.setup.MockMvcBuilders + */ + public ProxyMvc(DispatcherServlet servlet, Filter... filters) { + Assert.notNull(servlet, "DispatcherServlet is required"); + Assert.notNull(filters, "Filters cannot be null"); + Assert.noNullElements(filters, "Filters cannot contain null values"); + + this.servlet = servlet; + this.filters = filters; + } + + /** + * The default character encoding to be applied to every response. + * + * @see org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder#defaultResponseCharacterEncoding(Charset) + */ + void setDefaultResponseCharacterEncoding(@Nullable Charset defaultResponseCharacterEncoding) { + this.defaultResponseCharacterEncoding = defaultResponseCharacterEncoding; + } + + /** + * Return the underlying {@link DispatcherServlet} instance that this + * {@code MockMvc} was initialized with. + *

+ * This is intended for use in custom request processing scenario where a + * request handling component happens to delegate to the + * {@code DispatcherServlet} at runtime and therefore needs to be injected with + * it. + *

+ * For most processing scenarios, simply use {@link MockMvc#perform}, or if you + * need to configure the {@code DispatcherServlet}, provide a + * {@link DispatcherServletCustomizer} to the {@code MockMvcBuilder}. + * + * @since 5.1 + */ + public DispatcherServlet getDispatcherServlet() { + return this.servlet; + } + + /** + * Perform a request and return a type that allows chaining further actions, + * such as asserting expectations, on the result. + * + * @param requestBuilder used to prepare the request to execute; see static + * factory methods in + * {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders} + * @return an instance of {@link ResultActions} (never {@code null}) + * @see org.springframework.test.web.servlet.request.MockMvcRequestBuilders + * @see org.springframework.test.web.servlet.result.MockMvcResultMatchers + */ + public void perform(HttpServletRequest request, HttpServletResponse response) throws Exception { + ProxyFilterChain filterChain = new ProxyFilterChain(this.servlet, this.filters); + filterChain.doFilter(request, response); + } + + private static class ProxyFilterChain implements FilterChain { + + @Nullable + private ServletRequest request; + + @Nullable + private ServletResponse response; + + private final List filters; + + @Nullable + private Iterator iterator; + + + /** + * Create a {@code FilterChain} with Filter's and a Servlet. + * + * @param servlet the {@link Servlet} to invoke in this {@link FilterChain} + * @param filters the {@link Filter}'s to invoke in this {@link FilterChain} + * @since 3.2 + */ + ProxyFilterChain(Servlet servlet, Filter... filters) { + Assert.notNull(filters, "filters cannot be null"); + Assert.noNullElements(filters, "filters cannot contain null values"); + this.filters = initFilterList(servlet, filters); + } + + private static List initFilterList(Servlet servlet, Filter... filters) { + Filter[] allFilters = ObjectUtils.addObjectToArray(filters, new ServletFilterProxy(servlet)); + return Arrays.asList(allFilters); + } + + /** + * Return the request that {@link #doFilter} has been called with. + */ + @Nullable + public ServletRequest getRequest() { + return this.request; + } + + /** + * Return the response that {@link #doFilter} has been called with. + */ + @Nullable + public ServletResponse getResponse() { + return this.response; + } + + /** + * Invoke registered {@link Filter Filters} and/or {@link Servlet} also saving + * the request and response. + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + Assert.notNull(request, "Request must not be null"); + Assert.notNull(response, "Response must not be null"); + Assert.state(this.request == null, "This FilterChain has already been called!"); + + if (this.iterator == null) { + this.iterator = this.filters.iterator(); + } + + if (this.iterator.hasNext()) { + Filter nextFilter = this.iterator.next(); + nextFilter.doFilter(request, response, this); + } + + this.request = request; + this.response = response; + } + + /** + * A filter that simply delegates to a Servlet. + */ + private static final class ServletFilterProxy implements Filter { + + private final Servlet delegateServlet; + + private ServletFilterProxy(Servlet servlet) { + Assert.notNull(servlet, "servlet cannot be null"); + this.delegateServlet = servlet; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + this.delegateServlet.service(request, response); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void destroy() { + } + + @Override + public String toString() { + return this.delegateServlet.toString(); + } + } + + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyServletConfig.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyServletConfig.java new file mode 100644 index 000000000..c8462bde6 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyServletConfig.java @@ -0,0 +1,53 @@ +/* + * 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.web.client; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; + +public class ProxyServletConfig implements ServletConfig { + + private final ServletContext servletContext; + + public ProxyServletConfig(ServletContext servletContext) { + this.servletContext = servletContext; + } + + @Override + public String getServletName() { + return "hello-oleg"; + } + + @Override + public ServletContext getServletContext() { + return this.servletContext; + } + + @Override + public Enumeration getInitParameterNames() { + return Collections.enumeration(new ArrayList()); + } + + @Override + public String getInitParameter(String name) { + return null; + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyServletContext.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyServletContext.java new file mode 100644 index 000000000..504911b5e --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyServletContext.java @@ -0,0 +1,397 @@ +/* + * 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.web.client; + +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.Filter; +import javax.servlet.FilterRegistration; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import javax.servlet.ServletRegistration.Dynamic; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import javax.servlet.descriptor.JspConfigDescriptor; + +public class ProxyServletContext implements ServletContext { + + @Override + public Enumeration getInitParameterNames() { + List arrlist = new ArrayList(); + return Collections.enumeration(arrlist); + } + + @Override + public Enumeration getAttributeNames() { + List arrlist = new ArrayList(); + return Collections.enumeration(arrlist); + } + + @Override + public String getContextPath() { + // TODO Auto-generated method stub + return null; + } + + @Override + public ServletContext getContext(String uripath) { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getMajorVersion() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getMinorVersion() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getEffectiveMajorVersion() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getEffectiveMinorVersion() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public String getMimeType(String file) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Set getResourcePaths(String path) { + // TODO Auto-generated method stub + return null; + } + + @Override + public URL getResource(String path) throws MalformedURLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public InputStream getResourceAsStream(String path) { + // TODO Auto-generated method stub + return null; + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + // TODO Auto-generated method stub + return null; + } + + @Override + public RequestDispatcher getNamedDispatcher(String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Servlet getServlet(String name) throws ServletException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Enumeration getServlets() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Enumeration getServletNames() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void log(String msg) { + // TODO Auto-generated method stub + + } + + @Override + public void log(Exception exception, String msg) { + // TODO Auto-generated method stub + + } + + @Override + public void log(String message, Throwable throwable) { + // TODO Auto-generated method stub + + } + + @Override + public String getRealPath(String path) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getServerInfo() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getInitParameter(String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean setInitParameter(String name, String value) { + // TODO Auto-generated method stub + return false; + } + + @Override + public Object getAttribute(String name) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void setAttribute(String name, Object object) { + // TODO Auto-generated method stub + + } + + @Override + public void removeAttribute(String name) { + // TODO Auto-generated method stub + + } + + @Override + public String getServletContextName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Dynamic addServlet(String servletName, String className) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Dynamic addServlet(String servletName, Servlet servlet) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Dynamic addServlet(String servletName, Class servletClass) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Dynamic addJspFile(String jspName, String jspFile) { + // TODO Auto-generated method stub + return null; + } + + @Override + public T createServlet(Class c) throws ServletException { + // TODO Auto-generated method stub + return null; + } + + @Override + public ServletRegistration getServletRegistration(String servletName) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getServletRegistrations() { + // TODO Auto-generated method stub + return null; + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter(String filterName, String className) { + // TODO Auto-generated method stub + return null; + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter(String filterName, Filter filter) { + // TODO Auto-generated method stub + return null; + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter(String filterName, Class filterClass) { + // TODO Auto-generated method stub + return null; + } + + @Override + public T createFilter(Class c) throws ServletException { + // TODO Auto-generated method stub + return null; + } + + @Override + public FilterRegistration getFilterRegistration(String filterName) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map getFilterRegistrations() { + // TODO Auto-generated method stub + return null; + } + + @Override + public SessionCookieConfig getSessionCookieConfig() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void setSessionTrackingModes(Set sessionTrackingModes) { + // TODO Auto-generated method stub + + } + + @Override + public Set getDefaultSessionTrackingModes() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Set getEffectiveSessionTrackingModes() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void addListener(String className) { + // TODO Auto-generated method stub + + } + + @Override + public void addListener(T t) { + // TODO Auto-generated method stub + + } + + @Override + public void addListener(Class listenerClass) { + // TODO Auto-generated method stub + + } + + @Override + public T createListener(Class c) throws ServletException { + // TODO Auto-generated method stub + return null; + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + // TODO Auto-generated method stub + return null; + } + + @Override + public ClassLoader getClassLoader() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void declareRoles(String... roleNames) { + // TODO Auto-generated method stub + + } + + @Override + public String getVirtualServerName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getSessionTimeout() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public void setSessionTimeout(int sessionTimeout) { + // TODO Auto-generated method stub + + } + + @Override + public String getRequestCharacterEncoding() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void setRequestCharacterEncoding(String encoding) { + // TODO Auto-generated method stub + + } + + @Override + public String getResponseCharacterEncoding() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void setResponseCharacterEncoding(String encoding) { + // TODO Auto-generated method stub + + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/Pet.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/Pet.java new file mode 100644 index 000000000..846faca8e --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/Pet.java @@ -0,0 +1,58 @@ +/* + * 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.adapter.aws.web; + +import java.util.Date; + +public class Pet { + private String id; + private String breed; + private String name; + private Date dateOfBirth; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getDateOfBirth() { + return dateOfBirth; + } + + public void setDateOfBirth(Date dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetData.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetData.java new file mode 100644 index 000000000..c0fda4d4a --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetData.java @@ -0,0 +1,118 @@ +/* + * 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.adapter.aws.web; + + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public class PetData { + private static List breeds = new ArrayList<>(); + static { + breeds.add("Afghan Hound"); + breeds.add("Beagle"); + breeds.add("Bernese Mountain Dog"); + breeds.add("Bloodhound"); + breeds.add("Dalmatian"); + breeds.add("Jack Russell Terrier"); + breeds.add("Norwegian Elkhound"); + } + + private static List names = new ArrayList<>(); + static { + names.add("Bailey"); + names.add("Bella"); + names.add("Max"); + names.add("Lucy"); + names.add("Charlie"); + names.add("Molly"); + names.add("Buddy"); + names.add("Daisy"); + names.add("Rocky"); + names.add("Maggie"); + names.add("Jake"); + names.add("Sophie"); + names.add("Jack"); + names.add("Sadie"); + names.add("Toby"); + names.add("Chloe"); + names.add("Cody"); + names.add("Bailey"); + names.add("Buster"); + names.add("Lola"); + names.add("Duke"); + names.add("Zoe"); + names.add("Cooper"); + names.add("Abby"); + names.add("Riley"); + names.add("Ginger"); + names.add("Harley"); + names.add("Roxy"); + names.add("Bear"); + names.add("Gracie"); + names.add("Tucker"); + names.add("Coco"); + names.add("Murphy"); + names.add("Sasha"); + names.add("Lucky"); + names.add("Lily"); + names.add("Oliver"); + names.add("Angel"); + names.add("Sam"); + names.add("Princess"); + names.add("Oscar"); + names.add("Emma"); + names.add("Teddy"); + names.add("Annie"); + names.add("Winston"); + names.add("Rosie"); + } + + public static List getBreeds() { + return breeds; + } + + public static List getNames() { + return names; + } + + public static String getRandomBreed() { + return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1)); + } + + public static String getRandomName() { + return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1)); + } + + public static Date getRandomDoB() { + GregorianCalendar gc = new GregorianCalendar(); + + int year = ThreadLocalRandom.current().nextInt(Calendar.getInstance().get(Calendar.YEAR) - 15, + Calendar.getInstance().get(Calendar.YEAR)); + + gc.set(Calendar.YEAR, year); + + int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR)); + + gc.set(Calendar.DAY_OF_YEAR, dayOfYear); + return gc.getTime(); + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetStoreSpringAppConfig.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetStoreSpringAppConfig.java new file mode 100644 index 000000000..8498cb2c8 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetStoreSpringAppConfig.java @@ -0,0 +1,71 @@ +/* + * 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.adapter.aws.web; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +//@SpringBootApplication +@Configuration +@Import({ PetsController.class }) +public class PetStoreSpringAppConfig { + /* + * Create required HandlerMapping, to avoid several default HandlerMapping + * instances being created + */ + @Bean + public HandlerMapping handlerMapping() { + return new RequestMappingHandlerMapping(); + } + + /* + * Create required HandlerAdapter, to avoid several default HandlerAdapter + * instances being created + */ + @Bean + public HandlerAdapter handlerAdapter() { + return new RequestMappingHandlerAdapter(); + } + + @Bean + public Filter filter() { + return new Filter() { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + System.out.println("FILTER ===> Hello from: " + request.getLocalAddr()); + chain.doFilter(request, response); + } + }; + } + +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetsController.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetsController.java new file mode 100644 index 000000000..a22e57dc0 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetsController.java @@ -0,0 +1,76 @@ +/* + * 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.adapter.aws.web; + +import java.security.Principal; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@RestController +@EnableWebMvc +public class PetsController { + @RequestMapping(path = "/pets", method = RequestMethod.POST) + public Pet createPet(@RequestBody Pet newPet) { + if (newPet.getName() == null || newPet.getBreed() == null) { + return null; + } + + Pet dbPet = newPet; + dbPet.setId(UUID.randomUUID().toString()); + return dbPet; + } + + @RequestMapping(path = "/pets", method = RequestMethod.GET) + public Pet[] listPets(@RequestParam("limit") Optional limit, Principal principal) { + System.out.println("=====> EXECUTING"); + int queryLimit = 10; + if (limit.isPresent()) { + queryLimit = limit.get(); + } + + Pet[] outputPets = new Pet[queryLimit]; + + for (int i = 0; i < queryLimit; i++) { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setName(PetData.getRandomName()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + outputPets[i] = newPet; + } + + return outputPets; + } + + @RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET) + public Pet listPets() { + System.out.println("=====> Getting pet by id"); + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + newPet.setName(PetData.getRandomName()); + return newPet; + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvokerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvokerTests.java new file mode 100644 index 000000000..798bcd587 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvokerTests.java @@ -0,0 +1,126 @@ +/* + * 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.adapter.aws.web; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.Map; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.function.adapter.aws.TestContext; +import org.springframework.cloud.function.json.JacksonMapper; + + +public class WebProxyInvokerTests { + + static String apiGatewayEvent = "{\n" + + " \"resource\": \"/pets\",\n" + + " \"path\": \"/pets/64f56d94-a059-4111-9eeb-ee0c994b1ba8\",\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\": \"313369169943\",\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" + + "}"; + + @Test + public void testApiGatewayProxy() throws Exception { + System.setProperty("MAIN_CLASS", PetStoreSpringAppConfig.class.getName()); + WebProxyInvoker invoker = new WebProxyInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, new TestContext()); + + JacksonMapper mapper = new JacksonMapper(new ObjectMapper()); + System.out.println("RESULT: =======> " + new String(output.toByteArray())); + Map result = mapper.fromJson(output.toByteArray(), Map.class); + System.out.println(result); + } +}