From de54e0a49ca4047ce28aba687b5572cc0914db0a Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Mon, 20 Feb 2023 16:29:23 +0100 Subject: [PATCH 1/7] Initial commit of web-app-as-lambda support --- .../spring-cloud-function-adapter-aws/pom.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml index 102147ed7..c70ebff6a 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml @@ -98,6 +98,17 @@ true + + org.springframework + spring-webmvc + true + + + javax.servlet + javax.servlet-api + + provided + org.springframework.cloud spring-cloud-function-web From 24c0fce166cb294534e7fd9061339b5c42d11517 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Tue, 21 Feb 2023 11:52:06 +0100 Subject: [PATCH 2/7] working on AWS --- .../spring-cloud-function-adapter-aws/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml index c70ebff6a..b6d4ac090 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml @@ -95,19 +95,19 @@ ObjectMapper --> org.springframework spring-web - true + org.springframework spring-webmvc - true + javax.servlet javax.servlet-api - provided + org.springframework.cloud From b2b97d8077c6a5a8b3dc1690a841303c33be4339 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Tue, 21 Feb 2023 13:05:43 +0100 Subject: [PATCH 3/7] 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); + } +} From cb2a83e47266c6d8e4936974a8e17df91e0c688f Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Wed, 22 Feb 2023 12:56:13 +0100 Subject: [PATCH 4/7] Stopping point with initial POC --- .../.jdk8 | 0 .../README.md | 3 + .../pom.xml | 44 ++++ .../sample/pet-store/.aws-sam/build.toml | 13 ++ .../sample/pet-store/README.md | 38 ++++ .../sample/pet-store/pom.xml | 192 ++++++++++++++++++ .../sample/pet-store/src/assembly/bin.xml | 24 +++ .../petstore/PetStoreSpringAppConfig.java | 71 +++++++ .../oz/spring/petstore/PetsController.java | 80 ++++++++ .../java/oz/spring/petstore/model/Error.java | 33 +++ .../java/oz/spring/petstore/model/Pet.java | 58 ++++++ .../oz/spring/petstore/model/PetData.java | 115 +++++++++++ .../sample/pet-store/template.yml | 37 ++++ .../adapter/aws/web/FunctionClassUtils.java | 153 ++++++++++++++ .../cloud/function/adapter/aws/web/README.md | 5 + .../adapter/aws/web/WebProxyInvoker.java | 178 ++++++++++++++++ .../web/client/HeaderValueHolder.java | 0 .../web/client/ProxyHttpServletRequest.java | 127 ------------ .../web/client/ProxyHttpServletResponse.java | 0 .../springframework/web/client/ProxyMvc.java | 0 .../web/client/ProxyServletContext.java | 0 .../org/springframework/web/client/README.md | 3 + .../cloud/function/adapter/aws/web/Pet.java | 0 .../function/adapter/aws/web/PetData.java | 0 .../aws/web/PetStoreSpringAppConfig.java | 2 - .../adapter/aws/web/PetsController.java | 0 .../adapter/aws/web/WebProxyInvokerTests.java | 12 +- .../adapter/aws/web/WebProxyInvoker.java | 104 ---------- .../web/client/ProxyServletConfig.java | 53 ----- 29 files changed, 1052 insertions(+), 293 deletions(-) create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/.jdk8 create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/pom.xml create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/.aws-sam/build.toml create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/README.md create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/pom.xml create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/assembly/bin.xml create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/PetStoreSpringAppConfig.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/PetsController.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/model/Error.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/model/Pet.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/model/PetData.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/template.yml create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/cloud/function/adapter/aws/web/FunctionClassUtils.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/cloud/function/adapter/aws/web/README.md create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvoker.java rename spring-cloud-function-adapters/{spring-cloud-function-adapter-aws => spring-cloud-function-adapter-aws-web}/src/main/java/org/springframework/web/client/HeaderValueHolder.java (100%) rename spring-cloud-function-adapters/{spring-cloud-function-adapter-aws => spring-cloud-function-adapter-aws-web}/src/main/java/org/springframework/web/client/ProxyHttpServletRequest.java (89%) rename spring-cloud-function-adapters/{spring-cloud-function-adapter-aws => spring-cloud-function-adapter-aws-web}/src/main/java/org/springframework/web/client/ProxyHttpServletResponse.java (100%) rename spring-cloud-function-adapters/{spring-cloud-function-adapter-aws => spring-cloud-function-adapter-aws-web}/src/main/java/org/springframework/web/client/ProxyMvc.java (100%) rename spring-cloud-function-adapters/{spring-cloud-function-adapter-aws => spring-cloud-function-adapter-aws-web}/src/main/java/org/springframework/web/client/ProxyServletContext.java (100%) create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/web/client/README.md rename spring-cloud-function-adapters/{spring-cloud-function-adapter-aws => spring-cloud-function-adapter-aws-web}/src/test/java/org/springframework/cloud/function/adapter/aws/web/Pet.java (100%) rename spring-cloud-function-adapters/{spring-cloud-function-adapter-aws => spring-cloud-function-adapter-aws-web}/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetData.java (100%) rename spring-cloud-function-adapters/{spring-cloud-function-adapter-aws => spring-cloud-function-adapter-aws-web}/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetStoreSpringAppConfig.java (94%) rename spring-cloud-function-adapters/{spring-cloud-function-adapter-aws => spring-cloud-function-adapter-aws-web}/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetsController.java (100%) rename spring-cloud-function-adapters/{spring-cloud-function-adapter-aws => spring-cloud-function-adapter-aws-web}/src/test/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvokerTests.java (92%) delete mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvoker.java delete mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyServletConfig.java diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/.jdk8 b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/.jdk8 new file mode 100644 index 000000000..e69de29bb diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md new file mode 100644 index 000000000..46ee43c43 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md @@ -0,0 +1,3 @@ +Classes in this package should ideally reside in spring-web somewhere as a light weight HTTP proxy, since they are independent of the +context of the execution (i.e., AWS or Azure or whatever). +In fact classes in these package is a slimed-down copy of similar classes in MockMVC. diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/pom.xml new file mode 100644 index 000000000..db6aa34ce --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + spring-cloud-function-adapter-aws-web + jar + spring-cloud-function-adapter-aws-web + AWS Lambda Adapter for Spring Cloud Function + + org.springframework.cloud + spring-cloud-function-adapter-parent + 3.2.9-SNAPSHOT + + + UTF-8 + UTF-8 + 1.8 + + + + com.fasterxml.jackson.core + jackson-databind + + + org.springframework + spring-webmvc + + + javax.servlet + javax.servlet-api + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web + test + + + diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/.aws-sam/build.toml b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/.aws-sam/build.toml new file mode 100644 index 000000000..05c08b5fa --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/.aws-sam/build.toml @@ -0,0 +1,13 @@ +# This file is auto generated by SAM CLI build command + +[function_build_definitions] +[function_build_definitions.9341c1d5-9265-48ef-836e-25df000b0c59] +codeuri = "/Users/ozhurakousky/Documents/dev/repo/spring-cloud-function/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store" +runtime = "java11" +architecture = "x86_64" +handler = "org.springframework.cloud.function.adapter.aws.web.WebProxyInvoker::handleRequest" +manifest_hash = "" +packagetype = "Zip" +functions = ["PetStoreFunction"] + +[layer_build_definitions] diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/README.md b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/README.md new file mode 100644 index 000000000..bbe5db289 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/README.md @@ -0,0 +1,38 @@ +Copied from https://github.com/awslabs/aws-serverless-java-container/tree/main/samples/spring/pet-store + +# Serverless Spring example +A basic pet store written with the [Spring framework](https://projects.spring.io/spring-framework/). The `StreamLambdaHandler` object is the main entry point for Lambda. + +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. + +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package +``` +$ sam build +``` + +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. + +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen + +``` +$ sam deploy --guided +``` + +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL + +``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- + +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` \ No newline at end of file diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/pom.xml new file mode 100644 index 000000000..59aa050ae --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/pom.xml @@ -0,0 +1,192 @@ + + + 4.0.0 + + oz.spring.petstore + pet-store + 1.0-SNAPSHOT + pet-store + Simple pet store written with the Spring framework + https://aws.amazon.com/lambda/ + + + https://github.com/awslabs/aws-serverless-java-container.git + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 1.8 + 1.8 + 5.3.25 + 4.13.2 + 2.19.0 + + + + + org.springframework.cloud + spring-cloud-function-adapter-aws-web + 3.2.9-SNAPSHOT + + + + org.springframework + spring-context-indexer + ${spring.version} + true + + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + + com.amazonaws + aws-lambda-java-log4j2 + 1.5.1 + + + + junit + junit + ${junit.version} + test + + + + + + shaded-jar + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + false + + + + + + + + + + com.github.edwgiz + maven-shade-plugin.log4j2-cachefile-transformer + 2.8.1 + + + + + + + + assembly-zip + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + default-jar + none + + + + + org.apache.maven.plugins + maven-install-plugin + 3.0.0-M1 + + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.2.0 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/lib + runtime + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + zip-assembly + package + + single + + + ${project.artifactId}-${project.version} + + src${file.separator}assembly${file.separator}bin.xml + + false + + + + + + + + + diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/assembly/bin.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/assembly/bin.xml new file mode 100644 index 000000000..1ffd82d1c --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/assembly/bin.xml @@ -0,0 +1,24 @@ + + lambda-package + + zip + + false + + + + ${project.build.directory}${file.separator}lib + lib + + + + ${project.build.directory}${file.separator}classes + + ** + + ${file.separator} + + + \ No newline at end of file diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/PetStoreSpringAppConfig.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/PetStoreSpringAppConfig.java new file mode 100644 index 000000000..3969ea641 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/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 oz.spring.petstore; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +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.HandlerExceptionResolver; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + + +@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(); + } + + /* + * optimization - avoids creating default exception resolvers; not required as the serverless container handles + * all exceptions + * + * By default, an ExceptionHandlerExceptionResolver is created which creates many dependent object, including + * an expensive ObjectMapper instance. + * + * To enable custom @ControllerAdvice classes remove this bean. + */ + @Bean + public HandlerExceptionResolver handlerExceptionResolver() { + return new HandlerExceptionResolver() { + + @Override + public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + return null; + } + }; + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/PetsController.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/PetsController.java new file mode 100644 index 000000000..b04a26d92 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/PetsController.java @@ -0,0 +1,80 @@ +/* + * 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 oz.spring.petstore; + +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import oz.spring.petstore.model.Pet; +import oz.spring.petstore.model.PetData; + +import java.security.Principal; +import java.util.Optional; +import java.util.UUID; + +@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) { + 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; + } + + @GetMapping("favicon.ico") + @ResponseBody + void returnNoFavicon() { + } + + @RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET) + public Pet listPets() { + 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-web/sample/pet-store/src/main/java/oz/spring/petstore/model/Error.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/model/Error.java new file mode 100644 index 000000000..bb19a9027 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/model/Error.java @@ -0,0 +1,33 @@ +/* + * 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 oz.spring.petstore.model; + +public class Error { + private String message; + + public Error(String errorMessage) { + message = errorMessage; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/model/Pet.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/model/Pet.java new file mode 100644 index 000000000..20f170a99 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/model/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 oz.spring.petstore.model; + +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-web/sample/pet-store/src/main/java/oz/spring/petstore/model/PetData.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/model/PetData.java new file mode 100644 index 000000000..1df3632cc --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/src/main/java/oz/spring/petstore/model/PetData.java @@ -0,0 +1,115 @@ +/* + * 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 oz.spring.petstore.model; + +import java.util.*; +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-web/sample/pet-store/template.yml b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/template.yml new file mode 100644 index 000000000..7c5cea2e3 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/template.yml @@ -0,0 +1,37 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example Pet Store API written with spring-cloud-function web-proxy support + +Globals: + Api: + # API Gateway regional endpoints + EndpointConfiguration: REGIONAL + +Resources: + PetStoreFunction: + Type: AWS::Serverless::Function + Properties: + Handler: org.springframework.cloud.function.adapter.aws.web.WebProxyInvoker::handleRequest + Runtime: java11 + CodeUri: . + MemorySize: 512 + Policies: AWSLambdaBasicExecutionRole + Timeout: 30 + Environment: + Variables: + MAIN_CLASS: oz.spring.petstore.PetStoreSpringAppConfig + Events: + HttpApiEvent: + Type: HttpApi + Properties: + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' + +Outputs: + SpringPetStoreApi: + Description: URL for application + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' + Export: + Name: PetStoreLambda + + diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/cloud/function/adapter/aws/web/FunctionClassUtils.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/cloud/function/adapter/aws/web/FunctionClassUtils.java new file mode 100644 index 000000000..030a15902 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/cloud/function/adapter/aws/web/FunctionClassUtils.java @@ -0,0 +1,153 @@ +/* + * Copyright 2019-2021 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.InputStream; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +//import org.springframework.boot.SpringBootConfiguration; +//import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.core.KotlinDetector; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * General utility class which aggregates various class-level utility functions + * used by the framework. + * + * @author Oleg Zhurakousky + * @since 3.0.1 + */ +public final class FunctionClassUtils { + + private static Log logger = LogFactory.getLog(FunctionClassUtils.class); + + private FunctionClassUtils() { + + } + + /** + * Discovers the start class in the currently running application. + * The discover search order is 'MAIN_CLASS' environment property, + * 'MAIN_CLASS' system property, META-INF/MANIFEST.MF:'Start-Class' attribute, + * meta-inf/manifest.mf:'Start-Class' attribute. + * + * @return instance of Class which represent the start class of the application. + */ + public static Class getStartClass() { + ClassLoader classLoader = FunctionClassUtils.class.getClassLoader(); + return getStartClass(classLoader); + } + + static Class getStartClass(ClassLoader classLoader) { + Class mainClass = null; + if (System.getenv("MAIN_CLASS") != null) { + mainClass = ClassUtils.resolveClassName(System.getenv("MAIN_CLASS"), classLoader); + } + else if (System.getProperty("MAIN_CLASS") != null) { + mainClass = ClassUtils.resolveClassName(System.getProperty("MAIN_CLASS"), classLoader); + } + else { + try { + Class result = getStartClass( + Collections.list(classLoader.getResources(JarFile.MANIFEST_NAME)), classLoader); + if (result == null) { + result = getStartClass(Collections + .list(classLoader.getResources("meta-inf/manifest.mf")), classLoader); + } + Assert.notNull(result, "Failed to locate main class"); + mainClass = result; + } + catch (Exception ex) { + throw new IllegalStateException("Failed to discover main class. An attempt was made to discover " + + "main class as 'MAIN_CLASS' environment variable, system property as well as " + + "entry in META-INF/MANIFEST.MF (in that order).", ex); + } + } + logger.info("Main class: " + mainClass); + return mainClass; + } + + private static Class getStartClass(List list, ClassLoader classLoader) { + if (logger.isTraceEnabled()) { + logger.trace("Searching manifests: " + list); + } + for (URL url : list) { + try { + InputStream inputStream = null; + Manifest manifest = new Manifest(url.openStream()); + logger.info("Searching for start class in manifest: " + url); + if (logger.isDebugEnabled()) { + manifest.write(System.out); + } + try { + String startClassName = manifest.getMainAttributes().getValue("Start-Class"); + if (!StringUtils.hasText(startClassName)) { + startClassName = manifest.getMainAttributes().getValue("Main-Class"); + } + + if (StringUtils.hasText(startClassName)) { + Class startClass = ClassUtils.forName(startClassName, classLoader); + + if (KotlinDetector.isKotlinType(startClass)) { + PathMatchingResourcePatternResolver r = new PathMatchingResourcePatternResolver(classLoader); + String packageName = startClass.getPackage().getName(); + Resource[] resources = r.getResources("classpath:" + packageName.replace(".", "/") + "/*.class"); + for (int i = 0; i < resources.length; i++) { + Resource resource = resources[i]; + String className = packageName + "." + (resource.getFilename().replace("/", ".")).replace(".class", ""); + startClass = ClassUtils.forName(className, classLoader); +// if (isSpringBootApplication(startClass)) { +// logger.info("Loaded Main Kotlin Class: " + startClass); +// return startClass; +// } + } + } +// else if (isSpringBootApplication(startClass)) { +// logger.info("Loaded Start Class: " + startClass); +// return startClass; +// } + } + } + finally { + if (inputStream != null) { + inputStream.close(); + } + } + } + catch (Exception ex) { + logger.debug("Failed to determine Start-Class in manifest file of " + url, ex); + } + } + return null; + } + +// private static boolean isSpringBootApplication(Class startClass) { +// return startClass.getDeclaredAnnotation(SpringBootApplication.class) != null +// || startClass.getDeclaredAnnotation(SpringBootConfiguration.class) != null; +// } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/cloud/function/adapter/aws/web/README.md b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/cloud/function/adapter/aws/web/README.md new file mode 100644 index 000000000..de06b2658 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/cloud/function/adapter/aws/web/README.md @@ -0,0 +1,5 @@ +Classes in this package would remain specific to AWS (in this case). There would be something similar in Azure and others. + +And these classes would depend on what is currently in `org.springframework.web.client` package of this module. +However, ideally the contents of the `org.springframework.web.client` package should reside in spring-web somewhere as a light weight +HTTP proxy as we technically already have it in a form of MockMVC. \ No newline at end of file diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvoker.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvoker.java new file mode 100644 index 000000000..11075e5b8 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvoker.java @@ -0,0 +1,178 @@ +/* + * 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.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.Filter; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.client.ProxyHttpServletRequest; +import org.springframework.web.client.ProxyHttpServletResponse; +import org.springframework.web.client.ProxyMvc; +import org.springframework.web.client.ProxyServletContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * + * AWS Lambda specific handler that will proxy API Gateway request to Spring Web-app + * This class represents AWS Lambda fronted by API Gateway and is identified as 'handler' during the deployment. + * + * @author Oleg Zhurakousky + * + */ +public class WebProxyInvoker { + + private static Log logger = LogFactory.getLog(WebProxyInvoker.class); + + private final ProxyMvc mvc; + + private final ServletContext servletContext; + + ObjectMapper mapper = new ObjectMapper(); + + public WebProxyInvoker() throws ServletException { + Class startClass = FunctionClassUtils.getStartClass(); + AnnotationConfigWebApplicationContext applpicationContext = new AnnotationConfigWebApplicationContext(); + applpicationContext.register(startClass); + + this.servletContext = new ProxyServletContext(); + ServletConfig servletConfig = new ProxyServletConfig(this.servletContext); + + DispatcherServlet servlet = new DispatcherServlet(applpicationContext); + servlet.init(servletConfig); + this.mvc = new ProxyMvc(servlet, applpicationContext.getBeansOfType(Filter.class).values().toArray(new Filter[0])); + } + + /* + * TODO + * - Security context propagation from AWS API Gateway (easy) + * - Error handling + */ + @SuppressWarnings("unchecked") + private HttpServletRequest prepareRequest(InputStream input) throws IOException { + + Map request = mapper.readValue(input, Map.class); + if (logger.isDebugEnabled()) { + logger.debug("Request: " + request); + } + String httpMethod = (String) request.get("httpMethod"); + String path = (String) request.get("path"); + if (logger.isDebugEnabled()) { + logger.debug("httpMethod: " + httpMethod); + logger.debug("path: " + path); + } + ProxyHttpServletRequest httpRequest = new ProxyHttpServletRequest(servletContext, httpMethod, path); + if (StringUtils.hasText((String) request.get("body"))) { + httpRequest.setContent(((String) request.get("body")).getBytes()); + } + if (request.get("queryStringParameters") != null) { + httpRequest.setParameters((Map) request.get("queryStringParameters")); + } + + Map headers = (Map) request.get("headers"); + headers.putAll((Map) request.get("multiValueHeaders")); + for (Entry entry : headers.entrySet()) { + httpRequest.addHeader(entry.getKey(), entry.getValue()); + } + return httpRequest; + } + + + public void handleRequest(InputStream input, OutputStream output) throws IOException { + HttpServletRequest httpRequest = this.prepareRequest(input); + + ProxyHttpServletResponse httpResponse = new ProxyHttpServletResponse(); + try { + this.mvc.perform(httpRequest, httpResponse); + } + catch (Exception e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } + + String responseString = httpResponse.getContentAsString(); + if (StringUtils.hasText(responseString)) { + if (logger.isDebugEnabled()) { + logger.debug("Response: " + responseString); + } + Map apiGatewayResponseStructure = new HashMap(); + apiGatewayResponseStructure.put("isBase64Encoded", false); + apiGatewayResponseStructure.put("statusCode", 200); + apiGatewayResponseStructure.put("body", responseString); + + Map> multiValueHeaders = new HashMap<>(); + for (String headerName : httpResponse.getHeaderNames()) { + multiValueHeaders.put(headerName, httpResponse.getHeaders(headerName)); + } + // TODO investigate why AWS doesn't like List as value +// apiGatewayResponseStructure.put("headers", multiValueHeaders); + + byte[] apiGatewayResponseBytes = mapper.writeValueAsBytes(apiGatewayResponseStructure); + StreamUtils.copy(apiGatewayResponseBytes, output); + } + } + + private static class ProxyServletConfig implements ServletConfig { + + private final ServletContext servletContext; + + ProxyServletConfig(ServletContext servletContext) { + this.servletContext = servletContext; + } + + @Override + public String getServletName() { + return "serverless-proxy"; + } + + @Override + public ServletContext getServletContext() { + return this.servletContext; + } + + @Override + public Enumeration 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/HeaderValueHolder.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/web/client/HeaderValueHolder.java similarity index 100% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/HeaderValueHolder.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/web/client/HeaderValueHolder.java 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-web/src/main/java/org/springframework/web/client/ProxyHttpServletRequest.java similarity index 89% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyHttpServletRequest.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/web/client/ProxyHttpServletRequest.java index 203f1e031..4c64ca23a 100644 --- 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-web/src/main/java/org/springframework/web/client/ProxyHttpServletRequest.java @@ -164,30 +164,9 @@ public class ProxyHttpServletRequest implements HttpServletRequest { 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; @@ -249,29 +228,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest { // 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}. @@ -470,18 +426,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest { @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(); } @@ -635,64 +579,24 @@ public class ProxyHttpServletRequest implements HttpServletRequest { @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(); } @@ -720,24 +624,20 @@ public class ProxyHttpServletRequest implements HttpServletRequest { } 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(); } @@ -840,18 +740,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest { 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}. @@ -860,13 +748,11 @@ public class ProxyHttpServletRequest implements HttpServletRequest { */ @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(); } @@ -877,29 +763,24 @@ public class ProxyHttpServletRequest implements HttpServletRequest { } 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(); } @@ -909,7 +790,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest { } public void setLocalPort(int localPort) { -// this.localPort = localPort; throw new UnsupportedOperationException(); } @@ -926,10 +806,6 @@ public class ProxyHttpServletRequest implements HttpServletRequest { @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(); } @@ -952,15 +828,12 @@ public class ProxyHttpServletRequest implements HttpServletRequest { } 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; } 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-web/src/main/java/org/springframework/web/client/ProxyHttpServletResponse.java similarity index 100% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyHttpServletResponse.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/web/client/ProxyHttpServletResponse.java 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-web/src/main/java/org/springframework/web/client/ProxyMvc.java similarity index 100% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyMvc.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/web/client/ProxyMvc.java 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-web/src/main/java/org/springframework/web/client/ProxyServletContext.java similarity index 100% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyServletContext.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/web/client/ProxyServletContext.java diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/web/client/README.md b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/web/client/README.md new file mode 100644 index 000000000..46ee43c43 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/main/java/org/springframework/web/client/README.md @@ -0,0 +1,3 @@ +Classes in this package should ideally reside in spring-web somewhere as a light weight HTTP proxy, since they are independent of the +context of the execution (i.e., AWS or Azure or whatever). +In fact classes in these package is a slimed-down copy of similar classes in MockMVC. 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-web/src/test/java/org/springframework/cloud/function/adapter/aws/web/Pet.java similarity index 100% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/Pet.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/test/java/org/springframework/cloud/function/adapter/aws/web/Pet.java 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-web/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetData.java similarity index 100% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetData.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetData.java 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-web/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetStoreSpringAppConfig.java similarity index 94% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetStoreSpringAppConfig.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetStoreSpringAppConfig.java index 8498cb2c8..93888ccdd 100644 --- 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-web/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetStoreSpringAppConfig.java @@ -24,8 +24,6 @@ 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; 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-web/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetsController.java similarity index 100% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetsController.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/test/java/org/springframework/cloud/function/adapter/aws/web/PetsController.java 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-web/src/test/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvokerTests.java similarity index 92% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvokerTests.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/src/test/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvokerTests.java index 798bcd587..ebfd545c7 100644 --- 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-web/src/test/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvokerTests.java @@ -21,19 +21,17 @@ 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" + + " \"path\": \"/pets/64f56d94-a059-4111-9eeb-ee0c994b1ba8?foo=bar\",\n" + " \"httpMethod\": \"GET\",\n" + " \"headers\": {\n" + " \"accept\": \"*/*\",\n" + @@ -116,11 +114,11 @@ public class WebProxyInvokerTests { InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes()); ByteArrayOutputStream output = new ByteArrayOutputStream(); - invoker.handleRequest(targetStream, output, new TestContext()); + invoker.handleRequest(targetStream, output); - JacksonMapper mapper = new JacksonMapper(new ObjectMapper()); + ObjectMapper mapper = new ObjectMapper(); System.out.println("RESULT: =======> " + new String(output.toByteArray())); - Map result = mapper.fromJson(output.toByteArray(), Map.class); + Map result = mapper.readValue(output.toByteArray(), Map.class); System.out.println(result); } } 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 deleted file mode 100644 index 9b5bc44de..000000000 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/web/WebProxyInvoker.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.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/ProxyServletConfig.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyServletConfig.java deleted file mode 100644 index c8462bde6..000000000 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/web/client/ProxyServletConfig.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.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; - } -} From a761ce7a88637f115a483aabd68bd88c3eb20b66 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Wed, 22 Feb 2023 13:02:07 +0100 Subject: [PATCH 5/7] Docs --- .../spring-cloud-function-adapter-aws-web/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md index 46ee43c43..1ae0d83c2 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md @@ -1,3 +1,6 @@ -Classes in this package should ideally reside in spring-web somewhere as a light weight HTTP proxy, since they are independent of the -context of the execution (i.e., AWS or Azure or whatever). -In fact classes in these package is a slimed-down copy of similar classes in MockMVC. +#### Introduction + +This module represents a concept of a light weight AWS forwarding proxy which deploys and interacts with existing +Spring Boot web application deployed as AWS Lambda. + +A sample is provided in `/sample` directory \ No newline at end of file From 86fb73c40b8622e1265a054bfc2666544c2863dc Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Wed, 22 Feb 2023 13:08:45 +0100 Subject: [PATCH 6/7] Update README.md --- .../spring-cloud-function-adapter-aws-web/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md index 1ae0d83c2..fbc13b631 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md @@ -3,4 +3,10 @@ This module represents a concept of a light weight AWS forwarding proxy which deploys and interacts with existing Spring Boot web application deployed as AWS Lambda. -A sample is provided in `/sample` directory \ No newline at end of file +A sample is provided in [sample](https://github.com/spring-cloud/spring-cloud-function/tree/serverless-web/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store) directory. It contain README and SAM template file to simplify the deployment. This module is identified as the only additional dependnecy to the existing web-app. + +_NOTE: Although this module is AWS specific, this dependency is protocol only (not binary), tehrefore there is no AWS dependnecies._ + +The aformentioned proxy is identified as AWS Lambda [handler](https://github.com/spring-cloud/spring-cloud-function/blob/serverless-web/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/template.yml#L14) + +The main Spring Boot configuration file is identified as [MAIN_CLASS](https://github.com/spring-cloud/spring-cloud-function/blob/serverless-web/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/template.yml#L22) From 97a8c829271d59b717a851b99507425636c0c8dd Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Wed, 22 Feb 2023 13:12:00 +0100 Subject: [PATCH 7/7] Update README.md --- .../spring-cloud-function-adapter-aws-web/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md index fbc13b631..19552843c 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/README.md @@ -5,7 +5,7 @@ Spring Boot web application deployed as AWS Lambda. A sample is provided in [sample](https://github.com/spring-cloud/spring-cloud-function/tree/serverless-web/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store) directory. It contain README and SAM template file to simplify the deployment. This module is identified as the only additional dependnecy to the existing web-app. -_NOTE: Although this module is AWS specific, this dependency is protocol only (not binary), tehrefore there is no AWS dependnecies._ +_NOTE: Although this module is AWS specific, this dependency is protocol only (not binary), therefore there is no AWS dependnecies._ The aformentioned proxy is identified as AWS Lambda [handler](https://github.com/spring-cloud/spring-cloud-function/blob/serverless-web/spring-cloud-function-adapters/spring-cloud-function-adapter-aws-web/sample/pet-store/template.yml#L14)