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

io.micronaut.http.bind.DefaultRequestBinderRegistry Maven / Gradle / Ivy

/*
 * 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.http.bind;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.bind.ArgumentBinder;
import io.micronaut.core.bind.annotation.Bindable;
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.type.Argument;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpParameters;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpRequestWrapper;
import io.micronaut.http.PushCapableHttpRequest;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder;
import io.micronaut.http.bind.binders.ContinuationArgumentBinder;
import io.micronaut.http.bind.binders.CookieObjectArgumentBinder;
import io.micronaut.http.bind.binders.CookieAnnotationBinder;
import io.micronaut.http.bind.binders.DefaultBodyAnnotationBinder;
import io.micronaut.http.bind.binders.DefaultUnmatchedRequestArgumentBinder;
import io.micronaut.http.bind.binders.HeaderAnnotationBinder;
import io.micronaut.http.bind.binders.PartAnnotationBinder;
import io.micronaut.http.bind.binders.PathVariableAnnotationBinder;
import io.micronaut.http.bind.binders.PendingRequestBindingResult;
import io.micronaut.http.bind.binders.QueryValueArgumentBinder;
import io.micronaut.http.bind.binders.RequestArgumentBinder;
import io.micronaut.http.bind.binders.RequestAttributeAnnotationBinder;
import io.micronaut.http.bind.binders.RequestBeanAnnotationBinder;
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.http.cookie.Cookies;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static io.micronaut.core.util.KotlinUtils.KOTLIN_COROUTINES_SUPPORTED;

/**
 * Default implementation of the {@link RequestBinderRegistry} interface.
 *
 * @author Graeme Rocher
 * @since 1.0
 */
@Singleton
public class DefaultRequestBinderRegistry implements RequestBinderRegistry {

    private static final long CACHE_MAX_SIZE = 30;

    private final Map, RequestArgumentBinder> byAnnotation = new LinkedHashMap<>();
    private final Map byTypeAndAnnotation = new LinkedHashMap<>();
    private final Map byType = new LinkedHashMap<>();
    private final ConversionService conversionService;
    private final Map> argumentBinderCache =
        new ConcurrentLinkedHashMap.Builder>().maximumWeightedCapacity(CACHE_MAX_SIZE).build();
    private final List> unmatchedBinders = new ArrayList<>();
    private final DefaultUnmatchedRequestArgumentBinder defaultUnmatchedRequestArgumentBinder;

    /**
     * @param conversionService The conversion service
     * @param binders           The request argument binders
     */
    public DefaultRequestBinderRegistry(ConversionService conversionService, RequestArgumentBinder... binders) {
        this(conversionService, Arrays.asList(binders));
    }

    public DefaultRequestBinderRegistry(ConversionService conversionService, List binders) {
        this(conversionService, binders, new DefaultBodyAnnotationBinder(conversionService));
    }

    /**
     * @param conversionService    The conversion service
     * @param binders              The request argument binders
     * @param bodyAnnotationBinder The body annotation binder
     */
    @Inject
    public DefaultRequestBinderRegistry(
        ConversionService conversionService,
        List binders,
        DefaultBodyAnnotationBinder bodyAnnotationBinder
    ) {
        this.conversionService = conversionService;
        if (CollectionUtils.isNotEmpty(binders)) {
            for (RequestArgumentBinder binder : binders) {
                addArgumentBinder(binder);
            }
        }

        byAnnotation.put(Body.class, bodyAnnotationBinder);
        registerDefaultAnnotationBinders(byAnnotation);

        byType.put(Argument.of(HttpHeaders.class).typeHashCode(), (RequestArgumentBinder) (argument, source) -> () -> Optional.of(source.getHeaders()));
        byType.put(Argument.of(HttpRequest.class).typeHashCode(), (RequestArgumentBinder>) (argument, source) -> convertBodyIfNecessary(bodyAnnotationBinder, argument, source, false));
        byType.put(Argument.of(PushCapableHttpRequest.class).typeHashCode(), (RequestArgumentBinder>) (argument, source) -> {
            if (source instanceof PushCapableHttpRequest) {
                return convertBodyIfNecessary(bodyAnnotationBinder, argument, source, true);
            } else {
                return ArgumentBinder.BindingResult.unsatisfied();
            }
        });
        byType.put(Argument.of(HttpParameters.class).typeHashCode(), (RequestArgumentBinder) (argument, source) -> () -> Optional.of(source.getParameters()));
        byType.put(Argument.of(Cookies.class).typeHashCode(), (RequestArgumentBinder) (argument, source) -> () -> Optional.of(source.getCookies()));
        byType.put(Argument.of(Cookie.class).typeHashCode(), new CookieObjectArgumentBinder());

        defaultUnmatchedRequestArgumentBinder = new DefaultUnmatchedRequestArgumentBinder<>(
            List.of(
                new QueryValueArgumentBinder<>(conversionService),
                new RequestAttributeAnnotationBinder<>(conversionService)
            ),
            unmatchedBinders,
            List.of(bodyAnnotationBinder)
        );
    }

    @SuppressWarnings("rawtypes")
    @Override
    public  void addArgumentBinder(ArgumentBinder> binder) {
        if (binder instanceof AnnotatedRequestArgumentBinder annotatedRequestArgumentBinder) {
            Class annotationType = annotatedRequestArgumentBinder.getAnnotationType();
            if (binder instanceof TypedRequestArgumentBinder typedRequestArgumentBinder) {
                Argument argumentType = typedRequestArgumentBinder.argumentType();
                byTypeAndAnnotation.put(new TypeAndAnnotation(argumentType, annotationType), (RequestArgumentBinder) binder);
                List> superTypes = typedRequestArgumentBinder.superTypes();
                if (CollectionUtils.isNotEmpty(superTypes)) {
                    for (Class superType : superTypes) {
                        byTypeAndAnnotation.put(new TypeAndAnnotation(Argument.of(superType), annotationType), (RequestArgumentBinder) binder);
                    }
                }
            } else {
                byAnnotation.put(annotationType, annotatedRequestArgumentBinder);
            }

        } else if (binder instanceof TypedRequestArgumentBinder typedRequestArgumentBinder) {
            byType.put(typedRequestArgumentBinder.argumentType().typeHashCode(), typedRequestArgumentBinder);
        }
    }

    @Override
    public void addUnmatchedRequestArgumentBinder(RequestArgumentBinder binder) {
        unmatchedBinders.add(binder);
    }

    @Override
    public  Optional>> findArgumentBinder(Argument argument) {
        Optional> opt = argument.getAnnotationMetadata().getAnnotationTypeByStereotype(Bindable.class);
        if (opt.isPresent()) {
            Class annotationType = opt.get();
            RequestArgumentBinder binder = findBinder(argument, annotationType);
            if (binder == null) {
                binder = byAnnotation.get(annotationType);
            }
            if (binder != null) {
                return Optional.of(binder.createSpecific(argument));
            }
        } else {
            RequestArgumentBinder binder = byType.get(argument.typeHashCode());
            if (binder == null) {
                binder = byType.get(Argument.of(argument.getType()).typeHashCode());
            }
            if (binder != null) {
                return Optional.of(binder.createSpecific(argument));
            }
        }
        return Optional.of(defaultUnmatchedRequestArgumentBinder.createSpecific(argument));
    }

    /**
     * @param argument       The argument
     * @param annotationType The class for annotation
     * @param             The type
     * @return The request argument binder
     */
    protected  RequestArgumentBinder findBinder(Argument argument, Class annotationType) {
        TypeAndAnnotation key = new TypeAndAnnotation(argument, annotationType);
        return argumentBinderCache.computeIfAbsent(key, key1 -> {
            RequestArgumentBinder requestArgumentBinder = byTypeAndAnnotation.get(key1);
            if (requestArgumentBinder == null) {
                Class javaType = key1.type.getType();
                for (Map.Entry entry : byTypeAndAnnotation.entrySet()) {
                    TypeAndAnnotation typeAndAnnotation = entry.getKey();
                    if (typeAndAnnotation.annotation == annotationType) {

                        Argument t = typeAndAnnotation.type;
                        if (t.getType().isAssignableFrom(javaType)) {
                            requestArgumentBinder = entry.getValue();
                            if (requestArgumentBinder != null) {
                                break;
                            }
                        }
                    }
                }

                if (requestArgumentBinder == null) {
                    // try the raw type
                    requestArgumentBinder = byTypeAndAnnotation.get(new TypeAndAnnotation(Argument.of(argument.getType()), annotationType));
                }
            }
            return Optional.ofNullable(requestArgumentBinder);
        }).orElse(null);

    }

    /**
     * @param byAnnotation The request argument binder
     */
    protected void registerDefaultAnnotationBinders(Map, RequestArgumentBinder> byAnnotation) {
        CookieAnnotationBinder cookieAnnotationBinder = new CookieAnnotationBinder<>(conversionService);
        byAnnotation.put(cookieAnnotationBinder.getAnnotationType(), cookieAnnotationBinder);

        HeaderAnnotationBinder headerAnnotationBinder = new HeaderAnnotationBinder<>(conversionService);
        byAnnotation.put(headerAnnotationBinder.getAnnotationType(), headerAnnotationBinder);

        QueryValueArgumentBinder queryValueAnnotationBinder = new QueryValueArgumentBinder<>(conversionService);
        byAnnotation.put(queryValueAnnotationBinder.getAnnotationType(), queryValueAnnotationBinder);

        RequestAttributeAnnotationBinder requestAttributeAnnotationBinder = new RequestAttributeAnnotationBinder<>(conversionService);
        byAnnotation.put(requestAttributeAnnotationBinder.getAnnotationType(), requestAttributeAnnotationBinder);

        PathVariableAnnotationBinder pathVariableAnnotationBinder = new PathVariableAnnotationBinder<>(conversionService);
        byAnnotation.put(pathVariableAnnotationBinder.getAnnotationType(), pathVariableAnnotationBinder);

        RequestBeanAnnotationBinder requestBeanAnnotationBinder = new RequestBeanAnnotationBinder<>(this);
        byAnnotation.put(requestBeanAnnotationBinder.getAnnotationType(), requestBeanAnnotationBinder);

        PartAnnotationBinder partAnnotationBinder = new PartAnnotationBinder<>();
        byAnnotation.put(partAnnotationBinder.getAnnotationType(), partAnnotationBinder);

        if (KOTLIN_COROUTINES_SUPPORTED) {
            ContinuationArgumentBinder continuationArgumentBinder = new ContinuationArgumentBinder();
            byType.put(continuationArgumentBinder.argumentType().typeHashCode(), continuationArgumentBinder);
        }
    }

    private static ArgumentBinder.BindingResult> convertBodyIfNecessary(
        DefaultBodyAnnotationBinder bodyAnnotationBinder,
        ArgumentConversionContext> context,
        HttpRequest source,
        boolean pushCapable
    ) {
        if (source.getMethod().permitsRequestBody()) {
            Optional> typeVariable = context.getFirstTypeVariable()
                .filter(arg -> arg.getType() != Object.class)
                .filter(arg -> arg.getType() != Void.class);
            if (typeVariable.isPresent()) {
                @SuppressWarnings("unchecked")
                ArgumentConversionContext unwrappedConversionContext = ConversionContext.of((Argument) typeVariable.get());
                ArgumentBinder.BindingResult bodyBound = bodyAnnotationBinder.bindFullBody(unwrappedConversionContext, source);
                // can't use flatMap here because we return a present optional even when the body conversion failed
                return new PendingRequestBindingResult<>() {
                    @Override
                    public boolean isPending() {
                        return bodyBound instanceof PendingRequestBindingResult p && p.isPending();
                    }

                    @Override
                    public List getConversionErrors() {
                        return bodyBound.getConversionErrors();
                    }

                    @Override
                    public Optional> getValue() {
                        Optional body = bodyBound.getValue();
                        if (pushCapable) {
                            return Optional.of(new PushCapableRequestWrapper((HttpRequest) source, (PushCapableHttpRequest) source) {
                                @Override
                                public Optional getBody() {
                                    return body;
                                }
                            });
                        } else {
                            return Optional.of(new HttpRequestWrapper((HttpRequest) source) {
                                @Override
                                public Optional getBody() {
                                    return body;
                                }
                            });
                        }
                    }
                };
            }
        }
        return () -> Optional.of(source);
    }

    /**
     * Type and annotation.
     */
    private static final class TypeAndAnnotation {
        private final Argument type;
        private final Class annotation;

        /**
         * @param type       The type
         * @param annotation The annotation
         */
        public TypeAndAnnotation(Argument type, Class annotation) {
            this.type = type;
            this.annotation = annotation;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            TypeAndAnnotation that = (TypeAndAnnotation) o;

            if (!type.equalsType(that.type)) {
                return false;
            }
            return annotation.equals(that.annotation);
        }

        @Override
        public int hashCode() {
            int result = type.typeHashCode();
            result = 31 * result + annotation.hashCode();
            return result;
        }
    }

    private static class PushCapableRequestWrapper extends HttpRequestWrapper implements PushCapableHttpRequest {
        private final PushCapableHttpRequest push;

        public PushCapableRequestWrapper(HttpRequest primary, PushCapableHttpRequest push) {
            super(primary);
            this.push = push;
        }

        @Override
        public boolean isServerPushSupported() {
            return push.isServerPushSupported();
        }

        @Override
        public PushCapableHttpRequest serverPush(@NonNull HttpRequest request) {
            push.serverPush(request);
            return this;
        }
    }
}