Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Application Load Balancer Handler #1765

Merged
merged 14 commits into from
Jun 12, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2017-2023 original 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 io.micronaut.function.aws.proxy.test;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.MediaType;
import io.micronaut.json.JsonMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Optional;
import java.util.function.Supplier;

/**
* Utility class to provide conversion for HTTP request body.
* @author Sergio del Amo
* @since 4.0.0
*/
@Internal
public final class BodyUtils {

private static final Logger LOG = LoggerFactory.getLogger(BodyUtils.class);

private BodyUtils() {
}

@NonNull
public static Optional<String> bodyAsString(@NonNull JsonMapper jsonMapper,
@NonNull Supplier<MediaType> contentTypeSupplier,
@NonNull Supplier<Charset> characterEncodingSupplier ,
@NonNull Supplier<Object> bodySupplier) {
Object body = bodySupplier.get();
MediaType mediaType = contentTypeSupplier.get();
boolean mapFromJson = mediaType == null || mediaType.equals(MediaType.APPLICATION_JSON_TYPE);
if (body instanceof CharSequence) {
return Optional.of(body.toString());
} else if (body instanceof byte[] bytes) {
return Optional.of(new String(bytes, characterEncodingSupplier.get()));
} else if (mapFromJson) {
try {
return Optional.of(jsonMapper.writeValueAsString(body));
} catch (IOException e) {
LOG.error("IOException writing body to JSON String", e);
return Optional.empty();
}
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.amazonaws.services.lambda.runtime.Context;
import io.micronaut.core.annotation.NonNull;

import io.micronaut.function.aws.proxy.MockLambdaContext;
import jakarta.inject.Singleton;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright 2017-2023 original 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 io.micronaut.function.aws.proxy;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.value.MutableConvertibleValues;
import io.micronaut.core.convert.value.MutableConvertibleValuesMap;
import io.micronaut.http.*;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.http.netty.cookies.NettyCookie;
import io.micronaut.servlet.http.ServletHttpResponse;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Optional;

/**
* Abstract class for implementations of {@link ServletHttpResponse}.
* @author Sergio del Amo
* @since 4.0.0
* @param <R> Response Type
* @param <B> Body Type
*/
@Internal
public abstract class AbstractServletHttpResponse<R, B> implements ServletHttpResponse<R, B> {
protected final ByteArrayOutputStream body = new ByteArrayOutputStream();
protected int status = HttpStatus.OK.getCode();
protected final MutableHttpHeaders headers;
protected final BinaryContentConfiguration binaryContentConfiguration;
private MutableConvertibleValues<Object> attributes;
private B bodyObject;
private String reason = HttpStatus.OK.getReason();

protected AbstractServletHttpResponse(ConversionService conversionService, BinaryContentConfiguration binaryContentConfiguration) {
this.headers = new CaseInsensitiveMutableHttpHeaders(conversionService);
this.binaryContentConfiguration = binaryContentConfiguration;
}

@Override
public OutputStream getOutputStream() {
return body;
}

@Override
public BufferedWriter getWriter() {
return new BufferedWriter(new OutputStreamWriter(body, getCharacterEncoding()));
}

@Override
public MutableHttpResponse<B> cookie(Cookie cookie) {
if (cookie instanceof NettyCookie nettyCookie) {
final String encoded = ServerCookieEncoder.STRICT.encode(nettyCookie.getNettyCookie());
header(HttpHeaders.SET_COOKIE, encoded);
}
return this;
}

@Override
public MutableHttpHeaders getHeaders() {
return headers;
}

@Override
public MutableConvertibleValues<Object> getAttributes() {
MutableConvertibleValues<Object> localAttributes = this.attributes;
if (localAttributes == null) {
synchronized (this) { // double check
localAttributes = this.attributes;
if (localAttributes == null) {
localAttributes = new MutableConvertibleValuesMap<>();
this.attributes = localAttributes;
}
}
}
return localAttributes;
}

@Override
public Optional<B> getBody() {
return Optional.ofNullable(bodyObject);
}

@Override
@SuppressWarnings("unchecked")
public <T> MutableHttpResponse<T> body(@Nullable T body) {
this.bodyObject = (B) body;
return (MutableHttpResponse<T>) this;
}

@Override
public MutableHttpResponse<B> status(int status, CharSequence message) {
this.status = status;
if (message == null) {
this.reason = HttpStatus.getDefaultReason(status);
} else {
this.reason = message.toString();
}
return this;
}

@Override
public int code() {
return status;
}

@Override
public String reason() {
return reason;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@
import io.micronaut.core.io.buffer.ByteBuffer;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.util.SupplierUtil;
import io.micronaut.http.CaseInsensitiveMutableHttpHeaders;
import io.micronaut.http.FullHttpRequest;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.MediaType;
import io.micronaut.http.MutableHttpHeaders;
import io.micronaut.http.MutableHttpParameters;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.codec.MediaTypeCodecRegistry;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.http.cookie.Cookies;
import io.micronaut.servlet.http.MutableServletHttpRequest;
Expand All @@ -47,10 +50,15 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;

/**
Expand All @@ -72,7 +80,6 @@ public abstract class ApiGatewayServletRequest<T, REQ, RES> implements MutableSe
private final HttpMethod httpMethod;
private final Logger log;
private Cookies cookies;
private final MediaTypeCodecRegistry codecRegistry;
private MutableConvertibleValues<Object> attributes;
private Supplier<Optional<T>> body;
private T parsedBody;
Expand All @@ -82,15 +89,13 @@ public abstract class ApiGatewayServletRequest<T, REQ, RES> implements MutableSe

protected ApiGatewayServletRequest(
ConversionService conversionService,
MediaTypeCodecRegistry codecRegistry,
REQ request,
URI uri,
HttpMethod httpMethod,
Logger log,
BodyBuilder bodyBuilder
) {
this.conversionService = conversionService;
this.codecRegistry = codecRegistry;
this.requestEvent = request;
this.uri = uri;
this.httpMethod = httpMethod;
Expand All @@ -103,6 +108,14 @@ protected ApiGatewayServletRequest(

public abstract byte[] getBodyBytes() throws IOException;

protected static HttpMethod parseMethod(Supplier<String> httpMethodConsumer) {
try {
return HttpMethod.valueOf(httpMethodConsumer.get());
} catch (IllegalArgumentException e) {
return HttpMethod.CUSTOM;
}
}

@Override
public InputStream getInputStream() throws IOException {
return servletByteBuffer != null ? servletByteBuffer.toInputStream() : new ByteArrayInputStream(getBodyBytes());
Expand Down Expand Up @@ -254,4 +267,70 @@ public void setParsedBody(T body) {
}
return ExecutionFlow.just(contents);
}

/**
*
* @param bodySupplier HTTP Request's Body Supplier
* @param base64EncodedSupplier Whether the body is Base 64 encoded
* @return body bytes
* @throws IOException if the body is empty
*/
protected byte[] getBodyBytes(@NonNull Supplier<String> bodySupplier, @NonNull BooleanSupplier base64EncodedSupplier) throws IOException {
String requestBody = bodySupplier.get();
if (StringUtils.isEmpty(requestBody)) {
throw new IOException("Empty Body");
}
return base64EncodedSupplier.getAsBoolean() ?
Base64.getDecoder().decode(requestBody) : requestBody.getBytes(getCharacterEncoding());
}

@NonNull
protected static List<String> splitCommaSeparatedValue(@Nullable String value) {
if (value == null) {
return Collections.emptyList();
}
return Arrays.asList(value.split(","));
}

@NonNull
protected static Map<String, List<String>> transformCommaSeparatedValue(@Nullable Map<String, String> input) {
if (input == null) {
return Collections.emptyMap();
}
Map<String, List<String>> output = new HashMap<>();
for (var entry: input.entrySet()) {
output.put(entry.getKey(), splitCommaSeparatedValue(entry.getValue()));
}
return output;
}

/**
*
* @param queryStringParametersSupplier Query String parameters as a map with key string and value string
* @param multiQueryStringParametersSupplier Query String parameters as a map with key string and value list of strings
* @return Mutable HTTP parameters
*/
@NonNull
protected MutableHttpParameters getParameters(@NonNull Supplier<Map<String, String>> queryStringParametersSupplier,
@NonNull Supplier<Map<String, List<String>>> multiQueryStringParametersSupplier) {
Map<String, List<String>> multi = multiQueryStringParametersSupplier.get();
Map<String, String> single = queryStringParametersSupplier.get();
MediaType mediaType = getContentType().orElse(MediaType.APPLICATION_JSON_TYPE);
if (isFormSubmission(mediaType)) {
return getParametersFromBody(MapCollapseUtils.collapse(MapCollapseUtils.collapse(multi, single)));
} else {
return new MapListOfStringAndMapStringMutableHttpParameters(conversionService, multi, single);
}
}

/**
*
* @param singleHeaders Single value headers
* @param multiValueHeaders Multi-value headers
* @return The combined mutable headers
*/
@NonNull
protected MutableHttpHeaders getHeaders(@NonNull Supplier<Map<String, String>> singleHeaders, @NonNull Supplier<Map<String, List<String>>> multiValueHeaders) {
return new CaseInsensitiveMutableHttpHeaders(MapCollapseUtils.collapse(multiValueHeaders.get(), singleHeaders.get()), conversionService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,18 @@ public static Map<String, List<String>> collapse(Map<String, List<String>> multi
}
return values;
}

/**
* Collapse a map whose value is a list of strings into a map whose value is a comma separated string.
*
* @param input Map with key String and value List of Strings
* @return Map with key String and value String with comma separated values
*/
public static Map<String, String> collapse(Map<String, List<String>> input) {
Map<String, String> result = new HashMap<>();
for (Map.Entry<String, List<String>> entry : input.entrySet()) {
result.put(entry.getKey(), String.join(",", entry.getValue()));
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.function.aws.proxy.test;
package io.micronaut.function.aws.proxy;

import com.amazonaws.services.lambda.runtime.ClientContext;
import com.amazonaws.services.lambda.runtime.CognitoIdentity;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import io.micronaut.core.annotation.Internal;

/**
* Mock Lambda context.
* Implementation of {@link com.amazonaws.services.lambda.runtime.Context} which returns null for every overriden method. For example ot use it in tests.
* @author Sergio del Amo
* @since 4.0.0
*/
@Internal
public class MockLambdaContext implements Context {

//-------------------------------------------------------------
// Variables - Private - Static
//-------------------------------------------------------------

private static LambdaLogger logger = new MockLambdaConsoleLogger();
private static LambdaLogger logger = new SystemOutLambdaLogger();

//-------------------------------------------------------------
// Implementation - Context
Expand Down
Loading