All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.limemojito.aws.lambda.ApiGatewayResponseDecorator Maven / Gradle / Ivy

/*
 * Copyright 2011-2024 Lime Mojito Pty Ltd
 *
 *    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
 *
 *        http://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 com.limemojito.aws.lambda;

import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.ConstraintViolationException;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;

import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static org.apache.logging.log4j.util.Strings.isBlank;
import static org.springframework.http.HttpStatus.*;

/**
 * A decorator for APIGatewayV2HTTPResponse that converts the output of a function into an APIGatewayV2HTTPResponse.
 * 

* This decorator manages the conversion of spring-web style exceptions being thrown as RuntimeExceptions * by Cloud Function implementations. ConstraintViolations are also handled automatically and converted to 400 Bad * Request responses. { @code @ResponseStatus } style exceptions will be mapped correctly to Api Gateway responses * using the basic Lambda V2 Integration. *

* * @param the type of the input to the decorator. * @see ResponseStatus * @see ConstraintViolationException */ @RequiredArgsConstructor @Slf4j public class ApiGatewayResponseDecorator implements Function { /** * The default content type used for requests and responses. * The value of this constant is "application/json". */ public static final String DEFAULT_CONTENT_TYPE = "application/json"; private final ObjectMapper mapper; private final String contentType; private final Function next; /** * Applies the next function in the pipeline to the given input and returns the result. * * @param input the input to be processed by the next function * @return the result of applying the next function to the input; * if the output is an instance of APIGatewayV2HTTPResponse, it is returned as is; * otherwise, the output is transformed into an APIGatewayV2HTTPResponse using the rebuildOutputJson method */ @Override public APIGatewayV2HTTPResponse apply(Input input) { try { Object output = next.apply(input); if (output instanceof APIGatewayV2HTTPResponse) { log.debug("Decorated function returned APIGatewayV2HTTPResponse"); return (APIGatewayV2HTTPResponse) output; } else { return rebuildOutputJson(output); } } catch (Throwable e) { log.error("Building failure response for {} {}", e.getClass().getSimpleName(), e.getMessage(), e); return create(DEFAULT_CONTENT_TYPE, false, newError(mapper, e), statusFor(e)); } } /** * INTERNAL_SERVER_ERROR by default. * Or @ResponseStatus if he annotation is present on the exception * 400 for ConstraintViolationException * * @param e Throwable to convert * @return an http error code. * @see ConstraintViolationException * @see ResponseStatus */ public static HttpStatus statusFor(Throwable e) { final ResponseStatus responseStatusType = e.getClass().getAnnotation(ResponseStatus.class); if (responseStatusType != null) { // these can be set to different values. We pay attention if not the default (500). if (responseStatusType.code() != INTERNAL_SERVER_ERROR) { return responseStatusType.code(); } if (responseStatusType.value() != INTERNAL_SERVER_ERROR) { return responseStatusType.value(); } else { return INTERNAL_SERVER_ERROR; } } else { if (e instanceof ConstraintViolationException) { return BAD_REQUEST; } return INTERNAL_SERVER_ERROR; } } /** * Annotation aware converter than will obey annotation reason over message. Reverts to * simple class name if message is null on throwable. * * @param e Throwable to determine message for. * @return reason message for exception. * @see ResponseStatus * @see Throwable#getMessage() * @see Class#getSimpleName() */ public static String messageFor(Throwable e) { final ResponseStatus responseStatusType = e.getClass().getAnnotation(ResponseStatus.class); if (responseStatusType != null && !isBlank(responseStatusType.reason())) { return responseStatusType.reason(); } return e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage(); } /** * Writes the given function output as bytes using Object Output Stream. * * @param functionOutput the function output to be written as bytes * @return the byte array representation of the function output * @throws IOException if an I/O error occurs while writing the bytes * @see ObjectOutputStream */ static byte[] writeDataAsBytes(Object functionOutput) throws IOException { final int bufferSize = 512; try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(bufferSize); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) { objectOutputStream.writeObject(functionOutput); objectOutputStream.flush(); return byteArrayOutputStream.toByteArray(); } } @SneakyThrows private static String newError(ObjectMapper mapper, Throwable e) { return mapper.writeValueAsString(new TreeMap<>(Map.of("errorMessage", messageFor(e), "errorType", e.getClass().getName()))); } private APIGatewayV2HTTPResponse rebuildOutputJson(Object functionOutput) throws IOException { log.debug("Map output to APIGatewayV2HTTPResponse"); String body; boolean isBase64Encoded; if (contentType.toLowerCase().contains(DEFAULT_CONTENT_TYPE)) { body = mapper.writeValueAsString(functionOutput); isBase64Encoded = false; } else { byte[] data = writeDataAsBytes(functionOutput); body = Base64.getEncoder().encodeToString(data); isBase64Encoded = true; } APIGatewayV2HTTPResponse response = create(contentType, isBase64Encoded, body, OK); log.debug("lime:aws-lambda api response: {}", response); return response; } private static APIGatewayV2HTTPResponse create(String contentType, boolean isBase64Encoded, String body, HttpStatus status) { return new APIGatewayV2HTTPResponse(status.value(), Map.of("content-type", contentType), emptyMap(), emptyList(), body, isBase64Encoded); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy