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

io.micronaut.azure.function.http.AzureFunctionHttpRequest Maven / Gradle / Ivy

There is a newer version: 5.7.1
Show newest version
/*
 * 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.azure.function.http;

import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
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.core.execution.ExecutionFlow;
import io.micronaut.core.io.buffer.ByteBuffer;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.util.SupplierUtil;
import io.micronaut.function.BinaryTypeConfiguration;
import io.micronaut.http.CaseInsensitiveMutableHttpHeaders;
import io.micronaut.http.FullHttpRequest;
import io.micronaut.http.HttpHeaders;
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.ServerHttpRequest;
import io.micronaut.http.body.AvailableByteBody;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.http.cookie.Cookies;
import io.micronaut.http.simple.SimpleHttpParameters;
import io.micronaut.servlet.http.BodyBuilder;
import io.micronaut.servlet.http.MutableServletHttpRequest;
import io.micronaut.servlet.http.ParsedBodyHolder;
import io.micronaut.servlet.http.ServletExchange;
import io.micronaut.servlet.http.ServletHttpRequest;
import io.micronaut.servlet.http.ServletHttpResponse;
import io.micronaut.servlet.http.body.AvailableByteArrayBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.ArrayList;
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.function.BooleanSupplier;
import java.util.function.Supplier;

/**
 * Servlet request implementation for Azure Functions.
 *
 * @param  The body type
 */
@Internal
@SuppressWarnings("java:S119") // More descriptive generics are better here
public final class AzureFunctionHttpRequest implements
    MutableServletHttpRequest>, T>,
    ServletExchange>, HttpResponseMessage>,
    ServerHttpRequest,
    FullHttpRequest,
    ParsedBodyHolder {

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

    private final ExecutionContext executionContext;
    private final BinaryTypeConfiguration binaryTypeConfiguration;
    private ConversionService conversionService;
    private final HttpRequestMessage> requestEvent;
    private final AzureFunctionHttpResponse response;
    private URI uri;
    private final HttpMethod httpMethod;
    private Cookies cookies;
    private MutableConvertibleValues attributes;
    private Supplier> body;
    private T parsedBody;
    private T overriddenBody;

    public AzureFunctionHttpRequest(
        HttpRequestMessage> request,
        AzureFunctionHttpResponse response,
        ExecutionContext executionContext,
        ConversionService conversionService,
        BinaryTypeConfiguration binaryTypeConfiguration,
        BodyBuilder bodyBuilder
    ) {
        this.executionContext = executionContext;
        this.conversionService = conversionService;
        this.binaryTypeConfiguration = binaryTypeConfiguration;
        this.requestEvent = request;
        this.response = response;
        this.uri = requestEvent.getUri();
        this.httpMethod = parseMethod(requestEvent.getHttpMethod()::name);
        this.body = SupplierUtil.memoizedNonEmpty(() -> {
            T built = parsedBody != null ? parsedBody :  (T) bodyBuilder.buildBody(this::getInputStream, this);
            return Optional.ofNullable(built);
        });
    }

    public ExecutionContext getExecutionContext() {
        return executionContext;
    }

    public byte[] getBodyBytes() throws IOException {
        if (requestEvent.getBody().isPresent()) {
            return getBodyBytes(requestEvent.getBody()::get, () -> binaryTypeConfiguration.isMediaTypeBinary(requestEvent.getHeaders().get(HttpHeaders.CONTENT_TYPE)));
        } else {
            return ArrayUtils.EMPTY_BYTE_ARRAY;
        }
    }

    @Override
    public @NonNull AvailableByteBody byteBody() {
        try {
            return new AvailableByteArrayBody(getBodyBytes());
        } catch (IOException e) {
            // empty body
            return new AvailableByteArrayBody(ArrayUtils.EMPTY_BYTE_ARRAY);
        }
    }

    private static HttpMethod parseMethod(Supplier httpMethodConsumer) {
        try {
            return HttpMethod.valueOf(httpMethodConsumer.get());
        } catch (IllegalArgumentException e) {
            return HttpMethod.CUSTOM;
        }
    }

    @Override
    public MutableHttpHeaders getHeaders() {
        Map> headersMap = transformCommaSeparatedValue(requestEvent.getHeaders());
        return new CaseInsensitiveMutableHttpHeaders(headersMap, conversionService);
    }

    @Override
    public MutableHttpParameters getParameters() {
        MediaType mediaType = getContentType().orElse(MediaType.APPLICATION_JSON_TYPE);
        Map> values = new HashMap<>(transformCommaSeparatedValue(requestEvent.getQueryParameters()));
        if (isFormSubmission(mediaType)) {
            Map> parameters = null;
            try {
                parameters = new QueryStringDecoder(new String(getBodyBytes(), getCharacterEncoding()), false).parameters();
            } catch (IOException ex) {
                if (LOG.isErrorEnabled()) {
                    LOG.error("Error decoding form data: " + ex.getMessage(), ex);
                }
                parameters = new HashMap<>();
            }
            values.putAll(parameters);
        }
        return new SimpleHttpParameters(values, conversionService);
    }

    @Override
    public ServletHttpResponse getResponse() {
        return response;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return byteBody().split().toInputStream();
    }

    @Override
    @SuppressWarnings({"unchecked", "rawtypes"})
    public ServletHttpRequest>, ? super Object> getRequest() {
        return (ServletHttpRequest) this;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
    }

    @Override
    public HttpRequestMessage> getNativeRequest() {
        return requestEvent;
    }

    @Override
    public HttpMethod getMethod() {
        return httpMethod;
    }

    @Override
    public URI getUri() {
        return uri;
    }

    @NonNull
    @Override
    public Cookies getCookies() {
        Cookies localCookies = this.cookies;
        if (localCookies == null) {
            synchronized (this) { // double check
                localCookies = this.cookies;
                if (localCookies == null) {
                    localCookies = new AzureCookies(getPath(), getHeaders(), conversionService);
                    this.cookies = localCookies;
                }
            }
        }
        return localCookies;
    }

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

    @NonNull
    @Override
    public Optional getBody() {
        if (overriddenBody != null) {
            return Optional.of(overriddenBody);
        }
        return this.body.get();
    }

    @NonNull
    @Override
    public  Optional getBody(Argument arg) {
        return getBody().map(t -> conversionService.convertRequired(t, arg));
    }

    /**
     *
     * @param contentType Content Type
     * @return returns true if the content type is either application/x-www-form-urlencoded or multipart/form-data
     */
    private boolean isFormSubmission(MediaType contentType) {
        return MediaType.APPLICATION_FORM_URLENCODED_TYPE.equals(contentType) || MediaType.MULTIPART_FORM_DATA_TYPE.equals(contentType);
    }

    @Override
    public MutableHttpRequest cookie(Cookie cookie) {
        return this;
    }

    @Override
    public MutableHttpRequest uri(URI uri) {
        this.uri = uri;
        return this;
    }

    @Override
    @SuppressWarnings("unchecked")
    public  MutableHttpRequest body(B body) {
        this.overriddenBody = (T) body;
        return (MutableHttpRequest) this;
    }

    @Override
    public void setConversionService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    @Override
    public void setParsedBody(T body) {
        this.parsedBody = body;
    }

    @Override
    public @Nullable ByteBuffer contents() {
        return byteBody().split().toByteBuffer();
    }

    @Override
    public @Nullable ExecutionFlow> bufferContents() {
        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
     */
    private byte[] getBodyBytes(@NonNull Supplier 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
    private static List splitCommaSeparatedValue(@Nullable String value) {
        if (value == null) {
            return Collections.emptyList();
        }
        String[] arr = value.split(",");
        List result = new ArrayList<>();
        for (String str : arr) {
            result.add(str.trim());
        }
        return result;
    }

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