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

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

There is a newer version: 4.6.5
Show newest version
/*
 * Copyright 2017-2020 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.AnnotationMetadata;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.bind.ArgumentBinder;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.convert.ConversionError;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.exceptions.ConversionErrorException;
import io.micronaut.core.type.Argument;
import io.micronaut.core.type.ReturnType;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.bind.RequestBinderRegistry;
import io.micronaut.http.bind.binders.PendingRequestBindingResult;
import io.micronaut.http.bind.binders.PostponedRequestArgumentBinder;
import io.micronaut.http.bind.binders.RequestArgumentBinder;
import io.micronaut.http.bind.binders.UnmatchedRequestArgumentBinder;
import io.micronaut.inject.ExecutableMethod;
import io.micronaut.inject.MethodExecutionHandle;
import io.micronaut.inject.UnsafeExecutionHandle;
import io.micronaut.web.router.exceptions.UnsatisfiedRouteException;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * Abstract implementation of the {@link RouteMatch} interface.
 *
 * @param  The target type
 * @param  Route Match
 * @author Graeme Rocher
 * @author Denis Stepanov
 * @since 1.0
 */
abstract class AbstractRouteMatch implements MethodBasedRouteMatch {

    protected final ConversionService conversionService;
    protected final MethodBasedRouteInfo routeInfo;
    protected final MethodExecutionHandle methodExecutionHandle;
    protected final UnsafeExecutionHandle unsafeMethodExecutionHandle;
    protected final ExecutableMethod executableMethod;

    private final Argument[] arguments;
    private final String[] argumentNames;
    private final Object[] argumentValues;
    private final PostponedRequestArgumentBinder[] postponedArgumentBinders;
    private final PendingRequestBindingResult[] pendingRequestBindingResults;
    private final boolean[] fulfilledArguments;
    private boolean fulfilled;
    private boolean beforeBindersApplied;
    private boolean afterBindersApplied;

    /**
     * Constructor.
     *
     * @param routeInfo         The route info
     * @param conversionService The conversion service
     */
    protected AbstractRouteMatch(MethodBasedRouteInfo routeInfo, ConversionService conversionService) {
        this.routeInfo = routeInfo;
        this.conversionService = conversionService;
        this.methodExecutionHandle = routeInfo.getTargetMethod();
        this.executableMethod = methodExecutionHandle.getExecutableMethod();
        this.arguments = executableMethod.getArguments();
        this.argumentNames = routeInfo.getArgumentNames();
        int length = arguments.length;
        if (length == 0) {
            fulfilled = true;
            this.argumentValues = null;
            this.fulfilledArguments = null;
            this.postponedArgumentBinders = null;
            this.pendingRequestBindingResults = null;
        } else {
            this.argumentValues = new Object[length];
            this.fulfilledArguments = new boolean[length];
            this.postponedArgumentBinders = new PostponedRequestArgumentBinder[length];
            this.pendingRequestBindingResults = new PendingRequestBindingResult[length];
        }
        if (methodExecutionHandle instanceof UnsafeExecutionHandle) {
            unsafeMethodExecutionHandle = (UnsafeExecutionHandle) methodExecutionHandle;
        } else {
            unsafeMethodExecutionHandle = null;
        }
    }

    @Override
    public RouteInfo getRouteInfo() {
        return routeInfo;
    }

    @Override
    public T getTarget() {
        return routeInfo.getTargetMethod().getTarget();
    }

    @NonNull
    @Override
    public ExecutableMethod getExecutableMethod() {
        return executableMethod;
    }

    @Override
    @NonNull
    public AnnotationMetadata getAnnotationMetadata() {
        return executableMethod.getAnnotationMetadata();
    }

    @Override
    public Optional> getRequiredInput(String name) {
        for (int i = 0; i < argumentNames.length; i++) {
            String argumentName = argumentNames[i];
            if (name.equals(argumentName)) {
                return Optional.of(arguments[i]);
            }
        }
        return Optional.empty();
    }

    @Override
    public boolean isFulfilled() {
        if (fulfilled) {
            return true;
        }
        for (int i = 0; i < arguments.length; i++) {
            boolean isFulfilled = fulfilledArguments[i];
            if (isFulfilled) {
                continue;
            }
            PendingRequestBindingResult pendingRequestBindingResult = pendingRequestBindingResults[i];
            if (pendingRequestBindingResult != null && !pendingRequestBindingResult.isPending()) {
                Argument argument = arguments[i];
                setBindingResult(i, argument, pendingRequestBindingResult);
                failOnConversionErrors(argument, pendingRequestBindingResult);
            }
        }
        checkIfFulfilled();
        return fulfilled;
    }

    @Override
    public boolean isSatisfied(String name) {
        for (int i = 0; i < argumentNames.length; i++) {
            String argumentName = argumentNames[i];
            if (name.equals(argumentName)) {
                return fulfilledArguments[i];
            }
        }
        return false;
    }

    @Override
    public Method getTargetMethod() {
        return routeInfo.getTargetMethod().getTargetMethod();
    }

    @Override
    public String getMethodName() {
        return executableMethod.getMethodName();
    }

    @Override
    public Class getDeclaringType() {
        return executableMethod.getDeclaringType();
    }

    @Override
    public Argument[] getArguments() {
        return executableMethod.getArguments();
    }

    @Override
    public ReturnType getReturnType() {
        return executableMethod.getReturnType();
    }

    @Override
    public R invoke(Object... arguments) {
        Argument[] targetArguments = getArguments();
        if (targetArguments.length == 0) {
            return methodExecutionHandle.invoke();
        } else {
            List argumentList = new ArrayList<>(arguments.length);
            Map variables = getVariableValues();
            Iterator valueIterator = variables.values().iterator();
            int i = 0;
            for (Argument targetArgument : targetArguments) {
                String name = targetArgument.getName();
                Object value = variables.get(name);
                if (value != null) {
                    Optional result = conversionService.convert(value, targetArgument.getType());
                    argumentList.add(result.orElseThrow(() -> new IllegalArgumentException("Wrong argument types to method: " + executableMethod)));
                } else if (valueIterator.hasNext()) {
                    Optional result = conversionService.convert(valueIterator.next(), targetArgument.getType());
                    argumentList.add(result.orElseThrow(() -> new IllegalArgumentException("Wrong argument types to method: " + executableMethod)));
                } else if (i < arguments.length) {
                    Optional result = conversionService.convert(arguments[i++], targetArgument.getType());
                    argumentList.add(result.orElseThrow(() -> new IllegalArgumentException("Wrong argument types to method: " + executableMethod)));
                } else {
                    throw new IllegalArgumentException("Wrong number of arguments to method: " + executableMethod);
                }
            }
            return methodExecutionHandle.invoke(argumentList.toArray());
        }
    }

    @Override
    public R execute() {
        Argument[] targetArguments = getArguments();
        if (targetArguments.length == 0) {
            if (unsafeMethodExecutionHandle != null) {
                return unsafeMethodExecutionHandle.invokeUnsafe();
            }
            return methodExecutionHandle.invoke();
        }
        if (fulfilled) {
            if (unsafeMethodExecutionHandle != null) {
                return unsafeMethodExecutionHandle.invokeUnsafe(argumentValues);
            }
            return methodExecutionHandle.invoke(argumentValues);
        }
        if (!beforeBindersApplied) {
            throw new IllegalStateException("Argument binders before filters not processed!");
        }
        if (!afterBindersApplied) {
            throw new IllegalStateException("Argument binders after filters not processed!");
        }
        for (int i = 0; i < arguments.length; i++) {
            if (fulfilledArguments[i]) {
                continue;
            }
            PendingRequestBindingResult pendingRequestBindingResult = pendingRequestBindingResults[i];
            Argument argument = arguments[i];
            if (pendingRequestBindingResult != null) {
                setBindingResultOfFail(i, argument, pendingRequestBindingResult);
                continue;
            }
            Object value = getVariableValues().get(argumentNames[i]);
            if (value != null) {
                setValue(i, argument, value);
                continue;
            }
            if (argument.isOptional()) {
                setValue(i, argument, Optional.empty());
                continue;
            }
            if (!argument.isNullable()) {
                throw UnsatisfiedRouteException.create(argument);
            }
        }
        if (methodExecutionHandle instanceof UnsafeExecutionHandle) {
            UnsafeExecutionHandle unsafeExecutionHandle = (UnsafeExecutionHandle) methodExecutionHandle;
            return unsafeExecutionHandle.invokeUnsafe(argumentValues);
        }
        return methodExecutionHandle.invoke(argumentValues);
    }

    @Override
    public void fulfill(Map newValues) {
        if (fulfilled) {
            return;
        }
        for (int i = 0; i < argumentNames.length; i++) {
            if (fulfilledArguments[i]) {
                continue;
            }
            String argumentName = argumentNames[i];
            Object value = newValues.get(argumentName);
            if (value != null) {
                setValue(i, arguments[i], value);
            }
        }
        checkIfFulfilled();
    }

    @Override
    public void fulfillBeforeFilters(RequestBinderRegistry requestBinderRegistry, HttpRequest request) {
        if (fulfilled) {
            return;
        }
        if (beforeBindersApplied) {
            throw new IllegalStateException("Argument before filters already processed!");
        }
        RequestArgumentBinder[] argumentBinders = routeInfo.resolveArgumentBinders(requestBinderRegistry);
        for (int i = 0; i < arguments.length; i++) {
            if (fulfilledArguments[i]) {
                continue;
            }
            Argument argument = (Argument) arguments[i];
            Object value = getVariableValues().get(argumentNames[i]);
            if (value != null) {
                setValue(i, argument, value);
                continue;
            }
            RequestArgumentBinder argumentBinder = argumentBinders[i];
            if (argumentBinder instanceof PostponedRequestArgumentBinder postponedRequestArgumentBinder) {
                postponedArgumentBinders[i] = postponedRequestArgumentBinder;
                if (!(argumentBinder instanceof UnmatchedRequestArgumentBinder)) {
                    // Allow for the unmatched request argument binder to run even so it's postponed
                    continue;
                }
            }
            if (argumentBinder != null) {
                fulfillValue(
                    i,
                    argumentBinder,
                    argument,
                    request
                );
            }
        }
        checkIfFulfilled();
        beforeBindersApplied = true;
    }

    @Override
    public void fulfillAfterFilters(RequestBinderRegistry requestBinderRegistry, HttpRequest request) {
        if (fulfilled) {
            return;
        }
        if (afterBindersApplied) {
            throw new IllegalStateException("Argument binders after filters already processed!");
        }
        for (int i = 0; i < arguments.length; i++) {
            if (fulfilledArguments[i]) {
                continue;
            }
            Argument argument = (Argument) arguments[i];
            PostponedRequestArgumentBinder argumentBinder = postponedArgumentBinders[i];
            if (argumentBinder != null) {
                fulfillValuePostponed(
                    i,
                    argumentBinder,
                    argument,
                    request
                );
            }
        }
        checkIfFulfilled();
        afterBindersApplied = true;
    }

    private  void fulfillValue(int index,
                                  RequestArgumentBinder argumentBinder,
                                  Argument argument,
                                  HttpRequest request) {
        ArgumentConversionContext conversionContext = newContext(argument, request);
        ArgumentBinder.BindingResult bindingResult = argumentBinder.bind(conversionContext, request);
        fulfillValue(index, argument, bindingResult);
    }

    private  void fulfillValuePostponed(int index,
                                           PostponedRequestArgumentBinder argumentBinder,
                                           Argument argument,
                                           HttpRequest request) {
        ArgumentConversionContext conversionContext = newContext(argument, request);
        ArgumentBinder.BindingResult bindingResult = argumentBinder.bindPostponed(conversionContext, request);
        fulfillValue(index, argument, bindingResult);
    }

    private  ArgumentConversionContext newContext(Argument argument, HttpRequest request) {
        ArgumentConversionContext conversionContext = ConversionContext.of(
            argument,
            request.getLocale().orElse(null),
            request.getCharacterEncoding()
        );
        return conversionContext;
    }

    private  void fulfillValue(int index, Argument argument, ArgumentBinder.BindingResult bindingResult) {
        if (bindingResult instanceof PendingRequestBindingResult pendingRequestBindingResult) {
            pendingRequestBindingResults[index] = pendingRequestBindingResult;
            return;
        }
        failOnConversionErrors(argument, bindingResult);
        setBindingResult(index, argument, bindingResult);
    }

    private void setBindingResultOfFail(int index, Argument argument, ArgumentBinder.BindingResult bindingResult) {
        boolean isSet = setBindingResult(index, argument, bindingResult);
        failOnConversionErrors(argument, bindingResult);
        if (isSet) {
            return;
        }
        if (argument.isNullable()) {
            setValue(index, argument, null);
            return;
        }
        if (argument.isOptional()) {
            setValue(index, argument, Optional.empty());
            return;
        }
        throw UnsatisfiedRouteException.create(argument);
    }

    private void failOnConversionErrors(Argument argument, ArgumentBinder.BindingResult bindingResult) {
        List conversionErrors = bindingResult.getConversionErrors();
        if (!conversionErrors.isEmpty()) {
            // should support multiple errors
            ConversionError conversionError = conversionErrors.iterator().next();
            throw new ConversionErrorException(argument, conversionError);
        }
    }

    private boolean setBindingResult(int index, Argument argument, ArgumentBinder.BindingResult bindingResult) {
        if (!bindingResult.isSatisfied()) {
            return false;
        }
        Object value;
        if (argument.getType() == Optional.class) {
            Optional optionalValue = bindingResult.getValue();
            if (optionalValue.isPresent()) {
                value = optionalValue.get();
            } else {
                return false;
            }
        } else if (bindingResult.isPresentAndSatisfied()) {
            value = bindingResult.get();
        } else {
            return false;
        }
        setValue(index, argument, value);
        return true;
    }

    private void setValue(int index, Argument argument, Object value) {
        if (value != null) {
            argumentValues[index] = convertValue(conversionService, argument, value);
        }
        fulfilledArguments[index] = true;
    }

    private void checkIfFulfilled() {
        if (fulfilled) {
            return;
        }
        for (boolean isFulfilled : fulfilledArguments) {
            if (!isFulfilled) {
                return;
            }
        }
        fulfilled = true;
    }

    private Object convertValue(ConversionService conversionService, Argument argument, Object value) {
        if (value instanceof ConversionError conversionError) {
            throw new ConversionErrorException(argument, conversionError);
        }
        Class argumentType = argument.getType();
        if (argumentType.isInstance(value)) {
            if (argument.isContainerType()) {
                if (argument.hasTypeVariables()) {
                    ConversionContext conversionContext = ConversionContext.of(argument);
                    Optional result = conversionService.convert(value, argumentType, conversionContext);
                    return resolveValueOrError(argument, conversionContext, result);
                }
            }
            return value;
        } else {
            ConversionContext conversionContext = ConversionContext.of(argument);
            Optional result = conversionService.convert(value, argumentType, conversionContext);
            return resolveValueOrError(argument, conversionContext, result);
        }
    }

    private Object resolveValueOrError(Argument argument, ConversionContext conversionContext, Optional result) {
        if (result.isEmpty()) {
            Optional lastError = conversionContext.getLastError();
            if (lastError.isEmpty() && argument.isDeclaredNullable()) {
                return null;
            }
            throw lastError.map(conversionError ->
                (RuntimeException) new ConversionErrorException(argument, conversionError)).orElseGet(() -> UnsatisfiedRouteException.create(argument)
            );
        }
        return result.get();
    }

}