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

io.micronaut.web.router.DefaultMethodBasedRouteInfo Maven / Gradle / Ivy

There is a newer version: 4.6.5
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.web.router;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.bind.ArgumentBinder;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.convert.value.ConvertibleValues;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.bind.RequestBinderRegistry;
import io.micronaut.http.bind.binders.RequestArgumentBinder;
import io.micronaut.http.body.MessageBodyHandlerRegistry;
import io.micronaut.http.body.MessageBodyReader;
import io.micronaut.inject.MethodExecutionHandle;
import io.micronaut.inject.beans.KotlinExecutableMethodUtils;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * The default {@link MethodBasedRouteInfo} implementation.
 *
 * @param  The target
 * @param  The result
 * @author Denis Stepanov
 * @since 4.0.0
 */
@Internal
sealed class DefaultMethodBasedRouteInfo extends DefaultRouteInfo implements MethodBasedRouteInfo
    permits DefaultRequestMatcher {

    private static final RequestArgumentBinder[] ZERO_BINDERS = new RequestArgumentBinder[0];
    private final MethodExecutionHandle targetMethod;
    private final String[] argumentNames;
    private final boolean isVoid;
    private final Optional> optionalBodyArgument;
    private final Optional> optionalFullBodyArgument;
    private final MessageBodyReader messageBodyReader;

    private RequestArgumentBinder[] argumentBinders;
    private final boolean needsBody;

    public DefaultMethodBasedRouteInfo(MethodExecutionHandle targetMethod,
                                       @Nullable
                                       Argument bodyArgument,
                                       @Nullable
                                       String bodyArgumentName,
                                       List consumesMediaTypes,
                                       List producesMediaTypes,
                                       boolean isPermitsBody,
                                       boolean isErrorRoute,
                                       MessageBodyHandlerRegistry messageBodyHandlerRegistry) {
        super(targetMethod, targetMethod.getReturnType(), consumesMediaTypes, producesMediaTypes, targetMethod.getDeclaringType(), isErrorRoute, isPermitsBody, messageBodyHandlerRegistry);
        this.targetMethod = targetMethod;

        Argument[] arguments = targetMethod.getArguments();
         argumentNames = new String[arguments.length];
        Map> requiredInputs;
        if (arguments.length > 0) {
            Map> ri = CollectionUtils.newLinkedHashMap(arguments.length);
            for (int i = 0; i < arguments.length; i++) {
                Argument requiredArgument = arguments[i];
                String inputName = resolveInputName(requiredArgument);
                ri.put(inputName, requiredArgument);
                argumentNames[i] = inputName;
            }
            requiredInputs = ri;
        } else {
            requiredInputs = Collections.emptyMap();
        }
        if (returnType.isVoid()) {
            isVoid = true;
        } else if (isSuspended()) {
            isVoid = KotlinExecutableMethodUtils.isKotlinFunctionReturnTypeUnit(targetMethod.getExecutableMethod());
        } else {
            isVoid = false;
        }
        if (bodyArgument != null) {
            optionalBodyArgument = Optional.of(bodyArgument);
        } else if (bodyArgumentName != null) {
            optionalBodyArgument = Optional.ofNullable(requiredInputs.get(bodyArgumentName));
        } else {
            optionalBodyArgument = Optional.empty();
        }
        optionalFullBodyArgument = super.getFullRequestBodyType();
        this.messageBodyReader = optionalBodyArgument.flatMap(b -> {
            if (b.getAnnotationMetadata().stringValue(Body.class).isPresent() || !b.getAnnotationMetadata().hasAnnotation(Body.class)) {
                // Special case for `@Body("myProperty")`
                b = Argument.of(ConvertibleValues.class);
            } else if (b.isAsyncOrReactive() || b.isOptional()) {
                b = b.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT);
            }
            return messageBodyHandlerRegistry.findReader(b, consumesMediaTypes);
        }).orElse(null);
        needsBody = optionalBodyArgument.isPresent() || hasArg(arguments, HttpRequest.class);
    }

    @Override
    public final MessageBodyReader getMessageBodyReader() {
        return messageBodyReader;
    }

    private static boolean hasArg(Argument[] arguments, Class type) {
        for (Argument argument : arguments) {
            if (argument.getType() == type) {
                return true;
            }
        }
        return false;
    }

    @Override
    public RequestArgumentBinder[] resolveArgumentBinders(RequestBinderRegistry requestBinderRegistry) {
        // Allow concurrent access
        if (argumentBinders == null) {
            argumentBinders = resolveArgumentBindersInternal(requestBinderRegistry);
        }
        return argumentBinders;
    }

    private RequestArgumentBinder[] resolveArgumentBindersInternal(RequestBinderRegistry requestBinderRegistry) {
        Argument[] arguments = targetMethod.getArguments();
        if (arguments.length == 0) {
            return ZERO_BINDERS;
        }

        RequestArgumentBinder[] binders = new RequestArgumentBinder[arguments.length];
        for (int i = 0; i < arguments.length; i++) {
            Argument argument = arguments[i];
            Optional>> argumentBinder = requestBinderRegistry.findArgumentBinder(argument);
            binders[i] = (RequestArgumentBinder) argumentBinder.orElse(null);
        }
        return binders;
    }

    @Override
    public boolean isVoid() {
        return isVoid;
    }

    /**
     * Resolves the name for an argument.
     *
     * @param argument the argument
     * @return the name
     */
    private static @NonNull String resolveInputName(@NonNull Argument argument) {
        String inputName = argument.getAnnotationMetadata().stringValue(Bindable.NAME).orElse(null);
        if (StringUtils.isEmpty(inputName)) {
            inputName = argument.getName();
        }
        return inputName;
    }

    @Override
    public MethodExecutionHandle getTargetMethod() {
        return targetMethod;
    }

    @Override
    public Optional> getRequestBodyType() {
        return optionalBodyArgument;
    }

    @Override
    public Optional> getFullRequestBodyType() {
        return optionalFullBodyArgument;
    }

    @Override
    public String[] getArgumentNames() {
        return argumentNames;
    }

    @Override
    public boolean needsRequestBody() {
        return needsBody || super.needsRequestBody();
    }
}