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

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

There is a newer version: 4.7.9
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.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.BeanLocator;
import io.micronaut.context.ExecutionHandleLocator;
import io.micronaut.context.env.Environment;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationMetadataResolver;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.type.Argument;
import io.micronaut.core.type.ReturnType;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Status;
import io.micronaut.http.filter.HttpFilter;
import io.micronaut.http.uri.UriMatchInfo;
import io.micronaut.http.uri.UriMatchTemplate;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.ExecutableMethod;
import io.micronaut.inject.MethodExecutionHandle;
import io.micronaut.web.router.exceptions.RoutingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * A DefaultRouteBuilder implementation for building roots.
 *
 * @author Graeme Rocher
 * @since 1.0
 */
public abstract class DefaultRouteBuilder implements RouteBuilder {

    /**
     * A {@link io.micronaut.web.router.RouteBuilder.UriNamingStrategy} where by camel case conventions are used.
     */
    public static final UriNamingStrategy CAMEL_CASE_NAMING_STRATEGY = new UriNamingStrategy() {
    };

    protected static final Logger LOG = LoggerFactory.getLogger(DefaultRouteBuilder.class);

    static final Object NO_VALUE = new Object();
    protected final ExecutionHandleLocator executionHandleLocator;
    protected final UriNamingStrategy uriNamingStrategy;
    protected final ConversionService conversionService;
    protected final Charset defaultCharset;

    private DefaultUriRoute currentParentRoute = null;
    private List uriRoutes = new ArrayList<>();
    private List statusRoutes = new ArrayList<>();
    private List errorRoutes = new ArrayList<>();
    private List filterRoutes = new ArrayList<>();
    private Set exposedPorts = new HashSet<>(5);

    /**
     * @param executionHandleLocator The execution handler locator
     */
    public DefaultRouteBuilder(ExecutionHandleLocator executionHandleLocator) {
        this(executionHandleLocator, CAMEL_CASE_NAMING_STRATEGY);
    }

    /**
     * @param executionHandleLocator The execution handler locator
     * @param uriNamingStrategy The URI naming strategy
     */
    public DefaultRouteBuilder(ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy) {
        this(executionHandleLocator, uriNamingStrategy, ConversionService.SHARED);
    }

    /**
     * @param executionHandleLocator The execution handler locator
     * @param uriNamingStrategy The URI naming strategy
     * @param conversionService The conversion service
     */
    public DefaultRouteBuilder(ExecutionHandleLocator executionHandleLocator, UriNamingStrategy uriNamingStrategy, ConversionService conversionService) {
        this.executionHandleLocator = executionHandleLocator;
        this.uriNamingStrategy = uriNamingStrategy;
        this.conversionService = conversionService;
        if (executionHandleLocator instanceof ApplicationContext) {
            ApplicationContext applicationContext = (ApplicationContext) executionHandleLocator;
            Environment environment = applicationContext.getEnvironment();
            defaultCharset = environment.get("micronaut.application.default-charset", Charset.class, StandardCharsets.UTF_8);
        } else {
            defaultCharset = StandardCharsets.UTF_8;
        }
    }

    @Override
    public Set getExposedPorts() {
        return exposedPorts;
    }

    @Override
    public List getFilterRoutes() {
        return filterRoutes;
    }

    @Override
    public FilterRoute addFilter(String pathPattern, Supplier filter) {
        DefaultFilterRoute route = new DefaultFilterRoute(
                pathPattern,
                filter,
                (AnnotationMetadataResolver) executionHandleLocator
        );
        filterRoutes.add(route);
        return route;
    }

    @Override
    public FilterRoute addFilter(String pathPattern, BeanLocator beanLocator, BeanDefinition beanDefinition) {
        DefaultFilterRoute route = new BeanDefinitionFilterRoute(
                pathPattern,
                beanLocator,
                beanDefinition
        );
        filterRoutes.add(route);
        return route;
    }

    @Override
    public List getStatusRoutes() {
        return Collections.unmodifiableList(statusRoutes);
    }

    @Override
    public List getErrorRoutes() {
        return Collections.unmodifiableList(errorRoutes);
    }

    @Override
    public List getUriRoutes() {
        return Collections.unmodifiableList(uriRoutes);
    }

    @Override
    public UriNamingStrategy getUriNamingStrategy() {
        return uriNamingStrategy;
    }

    @Override
    public ResourceRoute resources(Class cls) {
        return new DefaultResourceRoute(cls);
    }

    @Override
    public ResourceRoute single(Class cls) {
        return new DefaultSingleRoute(cls);
    }

    @Override
    public StatusRoute status(Class originatingClass, HttpStatus status, Class type, String method, Class[] parameterTypes) {
        Optional> executionHandle = executionHandleLocator.findExecutionHandle(type, method, parameterTypes);

        MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() ->
                new RoutingException("No such route: " + type.getName() + "." + method)
        );

        DefaultStatusRoute statusRoute = new DefaultStatusRoute(originatingClass, status, executableHandle, conversionService);
        this.statusRoutes.add(statusRoute);
        return statusRoute;
    }

    @Override
    public StatusRoute status(HttpStatus status, Class type, String method, Class[] parameterTypes) {
        Optional> executionHandle = executionHandleLocator.findExecutionHandle(type, method, parameterTypes);

        MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() ->
            new RoutingException("No such route: " + type.getName() + "." + method)
        );

        DefaultStatusRoute statusRoute = new DefaultStatusRoute(status, executableHandle, conversionService);
        this.statusRoutes.add(statusRoute);
        return statusRoute;
    }

    @Override
    public ErrorRoute error(Class originatingClass, Class error, Class type, String method, Class[] parameterTypes) {
        Optional> executionHandle = executionHandleLocator.findExecutionHandle(type, method, parameterTypes);

        MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() ->
            new RoutingException("No such route: " + type.getName() + "." + method)
        );

        DefaultErrorRoute errorRoute = new DefaultErrorRoute(originatingClass, error, executableHandle, conversionService);
        this.errorRoutes.add(errorRoute);
        return errorRoute;
    }

    @Override
    public ErrorRoute error(Class error, Class type, String method, Class[] parameterTypes) {
        Optional> executionHandle = executionHandleLocator.findExecutionHandle(type, method, parameterTypes);

        MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() ->
            new RoutingException("No such route: " + type.getName() + "." + method)
        );

        DefaultErrorRoute errorRoute = new DefaultErrorRoute(error, executableHandle, conversionService);
        this.errorRoutes.add(errorRoute);
        return errorRoute;
    }

    @Override
    public UriRoute GET(String uri, Object target, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.GET, uri, target.getClass(), method, parameterTypes);
    }

    @Override
    public UriRoute GET(String uri, Class type, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.GET, uri, type, method, parameterTypes);
    }

    @Override
    public UriRoute POST(String uri, Object target, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.POST, uri, target.getClass(), method, parameterTypes);
    }

    @Override
    public UriRoute POST(String uri, Class type, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.POST, uri, type, method, parameterTypes);
    }

    @Override
    public UriRoute PUT(String uri, Object target, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.PUT, uri, target.getClass(), method, parameterTypes);
    }

    @Override
    public UriRoute PUT(String uri, Class type, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.PUT, uri, type, method, parameterTypes);
    }

    @Override
    public UriRoute PATCH(String uri, Object target, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.PATCH, uri, target.getClass(), method, parameterTypes);
    }

    @Override
    public UriRoute PATCH(String uri, Class type, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.PATCH, uri, type, method, parameterTypes);
    }

    @Override
    public UriRoute DELETE(String uri, Object target, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.DELETE, uri, target.getClass(), method, parameterTypes);
    }

    @Override
    public UriRoute DELETE(String uri, Class type, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.DELETE, uri, type, method, parameterTypes);
    }

    @Override
    public UriRoute OPTIONS(String uri, Object target, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.OPTIONS, uri, target.getClass(), method, parameterTypes);
    }

    @Override
    public UriRoute OPTIONS(String uri, Class type, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.OPTIONS, uri, type, method, parameterTypes);
    }

    @Override
    public UriRoute HEAD(String uri, Object target, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.HEAD, uri, target.getClass(), method, parameterTypes);
    }

    @Override
    public UriRoute HEAD(String uri, Class type, String method, Class... parameterTypes) {
        return buildRoute(HttpMethod.HEAD, uri, type, method, parameterTypes);
    }

    @Override
    public UriRoute TRACE(String uri, Object target, String method, Class[] parameterTypes) {
        return buildRoute(HttpMethod.TRACE, uri, target.getClass(), method, parameterTypes);
    }

    @Override
    public UriRoute TRACE(String uri, Class type, String method, Class[] parameterTypes) {
        return buildRoute(HttpMethod.TRACE, uri, type, method, parameterTypes);
    }

    @Override
    public UriRoute GET(String uri, BeanDefinition beanDefinition, ExecutableMethod method) {
        return buildBeanRoute(HttpMethod.GET, uri, beanDefinition, method);
    }

    @Override
    public UriRoute POST(String uri, BeanDefinition beanDefinition, ExecutableMethod method) {
        return buildBeanRoute(HttpMethod.POST, uri, beanDefinition, method);
    }

    @Override
    public UriRoute PUT(String uri, BeanDefinition beanDefinition, ExecutableMethod method) {
        return buildBeanRoute(HttpMethod.PUT, uri, beanDefinition, method);
    }

    @Override
    public UriRoute PATCH(String uri, BeanDefinition beanDefinition, ExecutableMethod method) {
        return buildBeanRoute(HttpMethod.PATCH, uri, beanDefinition, method);
    }

    @Override
    public UriRoute DELETE(String uri, BeanDefinition beanDefinition, ExecutableMethod method) {
        return buildBeanRoute(HttpMethod.DELETE, uri, beanDefinition, method);
    }

    @Override
    public UriRoute OPTIONS(String uri, BeanDefinition beanDefinition, ExecutableMethod method) {
        return buildBeanRoute(HttpMethod.OPTIONS, uri, beanDefinition, method);
    }

    @Override
    public UriRoute HEAD(String uri, BeanDefinition beanDefinition, ExecutableMethod method) {
        return buildBeanRoute(HttpMethod.HEAD, uri, beanDefinition, method);
    }

    @Override
    public UriRoute TRACE(String uri, BeanDefinition beanDefinition, ExecutableMethod method) {
        return buildBeanRoute(HttpMethod.TRACE, uri, beanDefinition, method);
    }

    /**
     * Build a route.
     *
     * @param httpMethod The HTTP method
     * @param uri The URI
     * @param type The type
     * @param method The method
     * @param parameterTypes Parameters
     *
     * @return an {@link UriRoute}
     */
    protected UriRoute buildRoute(HttpMethod httpMethod, String uri, Class type, String method, Class... parameterTypes) {
        Optional> executionHandle = executionHandleLocator.findExecutionHandle(type, method, parameterTypes);

        MethodExecutionHandle executableHandle = executionHandle.orElseThrow(() ->
            new RoutingException("No such route: " + type.getName() + "." + method)
        );

        return buildRoute(httpMethod, uri, executableHandle);
    }

    /**
     * Build a route.
     *
     * @param httpMethod The HTTP method
     * @param uri The URI
     * @param executableHandle The executable handle
     *
     * @return an {@link UriRoute}
     */
    protected UriRoute buildRoute(HttpMethod httpMethod, String uri, MethodExecutionHandle executableHandle) {
        return buildRoute(httpMethod.name(), httpMethod, uri, executableHandle);
    }

    private UriRoute buildRoute(String httpMethodName, HttpMethod httpMethod, String uri, MethodExecutionHandle executableHandle) {
        UriRoute route;
        if (currentParentRoute != null) {
            route = new DefaultUriRoute(httpMethod, currentParentRoute.uriMatchTemplate.nest(uri), executableHandle, httpMethodName);
            currentParentRoute.nestedRoutes.add((DefaultUriRoute) route);
        } else {
            route = new DefaultUriRoute(httpMethod, uri, executableHandle, httpMethodName);
        }

        this.uriRoutes.add(route);
        return route;
    }

    private UriRoute buildBeanRoute(HttpMethod httpMethod, String uri, BeanDefinition beanDefinition, ExecutableMethod method) {
        return buildBeanRoute(httpMethod.name(), httpMethod, uri, beanDefinition, method);
    }

    /**
     * A special case that is required for non standard http methods.
     * @param httpMethodName The name of method. For standard http methods matches {@link HttpMethod#name()}
     * @param httpMethod The http method. Is {@link HttpMethod#CUSTOM} for non standard http methods.
     * @param uri The uri.
     * @param beanDefinition The definition of the bean.
     * @param method The method description
     * @return The uri route corresponding to the method.
     */
    protected UriRoute buildBeanRoute(String httpMethodName, HttpMethod httpMethod, String uri, BeanDefinition beanDefinition, ExecutableMethod method) {
        MethodExecutionHandle executionHandle = executionHandleLocator
                                                                .createExecutionHandle(beanDefinition, (ExecutableMethod) method);
        return buildRoute(httpMethodName, httpMethod, uri, executionHandle);
    }

    /**
     * Abstract class for base {@link MethodBasedRoute}.
     */
    abstract class AbstractRoute implements MethodBasedRoute, RouteInfo {
        protected final List>> conditions = new ArrayList<>();
        protected final MethodExecutionHandle targetMethod;
        protected final ConversionService conversionService;
        protected List consumesMediaTypes;
        protected List producesMediaTypes;
        protected String bodyArgumentName;
        protected Argument bodyArgument;
        protected final Map requiredInputs;
        protected final Class declaringType;
        protected boolean consumesMediaTypesContainsAll;
        protected boolean producesMediaTypesContainsAll;
        protected final HttpStatus definedStatus;
        protected final boolean isWebSocketRoute;
        private final boolean isVoid;
        private final boolean suspended;
        private final boolean reactive;
        private final boolean single;
        private final boolean async;
        private final boolean specifiedSingle;
        private final boolean isAsyncOrReactive;

        /**
         * @param targetMethod The target method execution handle
         * @param conversionService The conversion service
         * @param mediaTypes The media types
         */
        AbstractRoute(MethodExecutionHandle targetMethod, ConversionService conversionService, List mediaTypes) {
            this.targetMethod = targetMethod;
            this.conversionService = conversionService;
            this.consumesMediaTypes = mediaTypes;
            this.declaringType = targetMethod.getDeclaringType();
            this.producesMediaTypes = RouteInfo.super.getProduces();
            this.consumesMediaTypes = RouteInfo.super.getConsumes();
            suspended = targetMethod.getExecutableMethod().isSuspend();
            reactive = RouteInfo.super.isReactive();
            async = RouteInfo.super.isAsync();
            single = RouteInfo.super.isSingleResult();
            isVoid = RouteInfo.super.isVoid();
            specifiedSingle = RouteInfo.super.isSpecifiedSingle();
            isAsyncOrReactive = RouteInfo.super.isAsyncOrReactive();
            for (Argument argument : targetMethod.getArguments()) {
                if (argument.getAnnotationMetadata().hasAnnotation(Body.class)) {
                    this.bodyArgument = argument;
                }
            }
            Argument[] requiredArguments = targetMethod.getArguments();
            if (requiredArguments.length > 0) {
                Map requiredInputs = new LinkedHashMap<>(requiredArguments.length);
                for (Argument requiredArgument : requiredArguments) {
                    String inputName = resolveInputName(requiredArgument);
                    requiredInputs.put(inputName, requiredArgument);
                }
                this.requiredInputs = Collections.unmodifiableMap(requiredInputs);
            } else {
                this.requiredInputs = Collections.emptyMap();
            }
            setConsumesMediaTypesContainsAll();
            setProducesMediaTypesContainsAll();
            this.definedStatus = targetMethod.enumValue(Status.class, HttpStatus.class).orElse(null);
            this.isWebSocketRoute = targetMethod.hasAnnotation("io.micronaut.websocket.annotation.OnMessage");
        }

        @Override
        public Class getDeclaringType() {
            return declaringType;
        }

        private void setConsumesMediaTypesContainsAll() {
            this.consumesMediaTypesContainsAll = consumesMediaTypes == null || consumesMediaTypes.isEmpty() || consumesMediaTypes.contains(MediaType.ALL_TYPE);
        }

        private void setProducesMediaTypesContainsAll() {
            this.producesMediaTypesContainsAll = producesMediaTypes == null || producesMediaTypes.isEmpty() || producesMediaTypes.contains(MediaType.ALL_TYPE);
        }

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

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

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

        @Override
        public boolean isSuspended() {
            return suspended;
        }

        @Override
        public boolean isReactive() {
            return reactive;
        }

        @Override
        public boolean isSingleResult() {
            return single;
        }

        @Override
        public boolean isSpecifiedSingle() {
            return specifiedSingle;
        }

        @Override
        public boolean isAsync() {
            return async;
        }

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

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

        @Override
        public Route consumes(MediaType... mediaTypes) {
            if (mediaTypes != null) {
                this.consumesMediaTypes = Collections.unmodifiableList(Arrays.asList(mediaTypes));
                setConsumesMediaTypesContainsAll();
            }
            return this;
        }

        @Override
        public List getConsumes() {
            return consumesMediaTypes;
        }

        @Override
        public Route consumesAll() {
            this.consumesMediaTypes = Collections.emptyList();
            setConsumesMediaTypesContainsAll();
            return this;
        }

        @Override
        public Route where(Predicate> condition) {
            if (condition != null) {
                conditions.add(condition);
            }
            return this;
        }

        @Override
        public Route body(String argument) {
            this.bodyArgumentName = argument;
            return this;
        }

        @Override
        public Route body(Argument argument) {
            this.bodyArgument = argument;
            return this;
        }

        @Override
        public Route produces(MediaType... mediaType) {
            if (mediaType != null) {
                this.producesMediaTypes = Collections.unmodifiableList(Arrays.asList(mediaType));
                setProducesMediaTypesContainsAll();
            }
            return this;
        }

        @Override
        public List getProduces() {
            return producesMediaTypes;
        }

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

        /**
         * Whether the route permits a request body.
         * @return True if the route permits a request body
         */
        protected boolean permitsRequestBody() {
            return true;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof AbstractRoute)) {
                return false;
            }
            AbstractRoute that = (AbstractRoute) o;
            return Objects.equals(consumesMediaTypes, that.consumesMediaTypes) &&
                    Objects.equals(producesMediaTypes, that.producesMediaTypes);
        }

        @Override
        public int hashCode() {
            return Objects.hash(consumesMediaTypes, producesMediaTypes);
        }
    }

    /**
     * Default Error Route.
     */
    class DefaultErrorRoute extends AbstractRoute implements ErrorRoute {

        private final Class error;
        private final Class originatingClass;

        /**
         * @param error The throwable
         * @param targetMethod The target method execution handle
         * @param conversionService The conversion service
         */
        public DefaultErrorRoute(Class error, MethodExecutionHandle targetMethod, ConversionService conversionService) {
            this(null, error, targetMethod, conversionService);
        }

        /**
         * @param originatingClass The originating class
         * @param error The throwable
         * @param targetMethod The target method execution handle
         * @param conversionService The conversion service
         */
        public DefaultErrorRoute(
                Class originatingClass, Class error,
                MethodExecutionHandle targetMethod,
                ConversionService conversionService) {
            super(targetMethod, conversionService, Collections.emptyList());
            this.originatingClass = originatingClass;
            this.error = error;
        }

        @Override
        @Nullable
        public Class originatingType() {
            return originatingClass;
        }

        @Override
        public Class exceptionType() {
            return error;
        }

        @SuppressWarnings("unchecked")
        @Override
        public  Optional> match(Class originatingClass, Throwable exception) {
            if (originatingClass == this.originatingClass && error.isInstance(exception)) {
                return Optional.of(new ErrorRouteMatch(exception, this, conversionService));
            }
            return Optional.empty();
        }

        @SuppressWarnings("unchecked")
        @Override
        public  Optional> match(Throwable exception) {
            if (originatingClass == null && error.isInstance(exception)) {
                return Optional.of(new ErrorRouteMatch(exception, this, conversionService));
            }
            return Optional.empty();
        }

        @Override
        public ErrorRoute consumes(MediaType... mediaType) {
            return (ErrorRoute) super.consumes(mediaType);
        }

        @Override
        public ErrorRoute produces(MediaType... mediaType) {
            return (ErrorRoute) super.produces(mediaType);
        }

        @Override
        public Route consumesAll() {
            super.consumesAll();
            return this;
        }

        @Override
        public ErrorRoute nest(Runnable nested) {
            return this;
        }

        @Override
        public ErrorRoute where(Predicate> condition) {
            return (ErrorRoute) super.where(condition);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            if (!super.equals(o)) {
                return false;
            }
            DefaultErrorRoute that = (DefaultErrorRoute) o;
            return error.equals(that.error) &&
                    Objects.equals(originatingClass, that.originatingClass);
        }

        @Override
        public int hashCode() {
            return Objects.hash(super.hashCode(), error, originatingClass);
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            return builder.append(' ')
                .append(error.getSimpleName())
                .append(" -> ")
                .append(targetMethod.getDeclaringType().getSimpleName())
                .append('#')
                .append(targetMethod)
                .toString();
        }
    }

    /**
     * Represents a route for an {@link io.micronaut.http.HttpStatus} code.
     */
    class DefaultStatusRoute extends AbstractRoute implements StatusRoute {

        private final HttpStatus status;
        private final Class originatingClass;

        /**
         * @param status The HTTP Status
         * @param targetMethod The target method execution handle
         * @param conversionService The conversion service
         */
        public DefaultStatusRoute(HttpStatus status, MethodExecutionHandle targetMethod, ConversionService conversionService) {
            this(null, status, targetMethod, conversionService);
        }

        /**
         * @param originatingClass The originating class
         * @param status The HTTP Status
         * @param targetMethod The target method execution handle
         * @param conversionService The conversion service
         */
        public DefaultStatusRoute(Class originatingClass, HttpStatus status, MethodExecutionHandle targetMethod, ConversionService conversionService) {
            super(targetMethod, conversionService, Collections.emptyList());
            this.originatingClass = originatingClass;
            this.status = status;
        }

        @Override
        @Nullable
        public Class originatingType() {
            return originatingClass;
        }

        @Override
        public HttpStatus status() {
            return status;
        }

        @SuppressWarnings("unchecked")
        @Override
        public  Optional> match(Class originatingClass, HttpStatus status) {
            if (originatingClass == this.originatingClass && this.status == status) {
                return Optional.of(new StatusRouteMatch(status, this, conversionService));
            }
            return Optional.empty();
        }

        @SuppressWarnings("unchecked")
        @Override
        public  Optional> match(HttpStatus status) {
            if (this.originatingClass == null && this.status == status) {
                return Optional.of(new StatusRouteMatch(status, this, conversionService));
            }
            return Optional.empty();
        }

        @Override
        public StatusRoute consumes(MediaType... mediaType) {
            return this;
        }

        @Override
        public Route consumesAll() {
            return this;
        }

        @Override
        public StatusRoute nest(Runnable nested) {
            return this;
        }

        @Override
        public StatusRoute where(Predicate> condition) {
            return (StatusRoute) super.where(condition);
        }

        /**
         * @return The {@link io.micronaut.http.HttpStatus}
         */
        public HttpStatus getStatus() {
            return status;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof DefaultStatusRoute)) {
                return false;
            }
            if (!super.equals(o)) {
                return false;
            }
            DefaultStatusRoute that = (DefaultStatusRoute) o;
            return status == that.status &&
                    Objects.equals(originatingClass, that.originatingClass);
        }

        @Override
        public int hashCode() {
            return Objects.hash(super.hashCode(), status, originatingClass);
        }
    }

    /**
     * The default route impl.
     */
    class DefaultUriRoute extends AbstractRoute implements UriRoute {
        final String httpMethodName;
        final HttpMethod httpMethod;
        final UriMatchTemplate uriMatchTemplate;
        final List nestedRoutes = new ArrayList<>(2);
        private Integer port;

        /**
         * @param httpMethod The HTTP method
         * @param uriTemplate The URI Template as a {@link CharSequence}
         * @param targetMethod The target method execution handle
         */
        DefaultUriRoute(HttpMethod httpMethod, CharSequence uriTemplate, MethodExecutionHandle targetMethod) {
            this(httpMethod, uriTemplate, targetMethod, httpMethod.name());
        }

        /**
         * @param httpMethod The HTTP method
         * @param uriTemplate The URI Template as a {@link CharSequence}
         * @param targetMethod The target method execution handle
         * @param httpMethodName The actual name of the method - may differ from {@link HttpMethod#name()} for non-standard http methods
         */
        DefaultUriRoute(HttpMethod httpMethod, CharSequence uriTemplate, MethodExecutionHandle targetMethod, String httpMethodName) {
            this(httpMethod, uriTemplate, MediaType.APPLICATION_JSON_TYPE, targetMethod, httpMethodName);
        }

        /**
         * @param httpMethod The HTTP method
         * @param uriTemplate The URI Template as a {@link CharSequence}
         * @param mediaType The Media type
         * @param targetMethod The target method execution handle
         */
        DefaultUriRoute(HttpMethod httpMethod, CharSequence uriTemplate, MediaType mediaType, MethodExecutionHandle targetMethod) {
            this(httpMethod, uriTemplate, mediaType, targetMethod, httpMethod.name());
        }

        /**
         * @param httpMethod The HTTP method
         * @param uriTemplate The URI Template as a {@link CharSequence}
         * @param mediaType The Media type
         * @param targetMethod The target method execution handle
         * @param httpMethodName The actual name of the method - may differ from {@link HttpMethod#name()} for non-standard http methods
         */
        DefaultUriRoute(HttpMethod httpMethod, CharSequence uriTemplate, MediaType mediaType, MethodExecutionHandle targetMethod, String httpMethodName) {
            this(httpMethod, new UriMatchTemplate(uriTemplate), Collections.singletonList(mediaType), targetMethod, httpMethodName);
        }

        /**
         * @param httpMethod The HTTP method
         * @param uriTemplate The URI Template as a {@link UriMatchTemplate}
         * @param targetMethod The target method execution handle
         */
        DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, MethodExecutionHandle targetMethod) {
            this(httpMethod, uriTemplate, targetMethod, httpMethod.name());
        }

        /**
         * @param httpMethod The HTTP method
         * @param uriTemplate The URI Template as a {@link UriMatchTemplate}
         * @param targetMethod The target method execution handle
         * @param httpMethodName The actual name of the method - may differ from {@link HttpMethod#name()} for non-standard http methods
         */
        DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, MethodExecutionHandle targetMethod, String httpMethodName) {
            this(httpMethod, uriTemplate, Collections.singletonList(MediaType.APPLICATION_JSON_TYPE), targetMethod, httpMethodName);
        }

        /**
         * @param httpMethod The HTTP method
         * @param uriTemplate The URI Template as a {@link UriMatchTemplate}
         * @param mediaTypes The media types
         * @param targetMethod The target method execution handle
         */
        DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, List mediaTypes, MethodExecutionHandle targetMethod) {
            this(httpMethod, uriTemplate, mediaTypes, targetMethod, httpMethod.name());
        }

        /**
         * @param httpMethod The HTTP method
         * @param uriTemplate The URI Template as a {@link UriMatchTemplate}
         * @param mediaTypes The media types
         * @param targetMethod The target method execution handle
         * @param httpMethodName The actual name of the method - may differ from {@link HttpMethod#name()} for non-standard http methods
         */
        DefaultUriRoute(HttpMethod httpMethod, UriMatchTemplate uriTemplate, List mediaTypes, MethodExecutionHandle targetMethod, String httpMethodName) {
            super(targetMethod, ConversionService.SHARED, mediaTypes);
            this.httpMethod = httpMethod;
            this.uriMatchTemplate = uriTemplate;
            this.httpMethodName = httpMethodName;
        }

        @Override
        public String getHttpMethodName() {
            return httpMethodName;
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder(getHttpMethodName());
            return builder.append(' ')
                .append(uriMatchTemplate)
                .append(" -> ")
                .append(targetMethod.getDeclaringType().getSimpleName())
                .append('#')
                .append(targetMethod.getName())
                .append(" (")
                .append(String.join(",", consumesMediaTypes))
                .append(")")
                .toString();
        }

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

        @Override
        public UriRoute body(String argument) {
            return (UriRoute) super.body(argument);
        }

        @Override
        public UriRoute exposedPort(int port) {
            this.port = port;
            where(httpRequest -> httpRequest.getServerAddress().getPort() == port);
            DefaultRouteBuilder.this.exposedPorts.add(port);
            return this;
        }

        @Override
        public Integer getPort() {
            return port;
        }

        @Override
        public UriRoute consumes(MediaType... mediaTypes) {
            return (UriRoute) super.consumes(mediaTypes);
        }

        @Override
        public UriRoute produces(MediaType... mediaType) {
            return (UriRoute) super.produces(mediaType);
        }

        @Override
        public UriRoute consumesAll() {
            return (UriRoute) super.consumesAll();
        }

        @Override
        public UriRoute nest(Runnable nested) {
            DefaultUriRoute previous = DefaultRouteBuilder.this.currentParentRoute;
            DefaultRouteBuilder.this.currentParentRoute = this;
            try {
                nested.run();
            } finally {
                DefaultRouteBuilder.this.currentParentRoute = previous;
            }
            return this;
        }

        @Override
        public UriRoute where(Predicate> condition) {
            return (UriRoute) super.where(condition);
        }

        @SuppressWarnings("unchecked")
        @Override
        public Optional match(String uri) {
            Optional matchInfo = uriMatchTemplate.match(uri);
            return matchInfo.map(info -> new DefaultUriRouteMatch(info, this, defaultCharset, conversionService));
        }

        @Override
        public UriMatchTemplate getUriMatchTemplate() {
            return this.uriMatchTemplate;
        }

        @Override
        public int compareTo(UriRoute o) {
            return uriMatchTemplate.compareTo(o.getUriMatchTemplate());
        }

        @Override
        protected boolean permitsRequestBody() {
            return HttpMethod.permitsRequestBody(httpMethod);
        }
    }

    /**
     * Define a single route.
     */
    class DefaultSingleRoute extends DefaultResourceRoute {

        /**
         * @param resourceRoutes The resource routes
         * @param getRoute The default Uri route
         */
        DefaultSingleRoute(Map resourceRoutes, DefaultUriRoute getRoute) {
            super(resourceRoutes, getRoute);
        }

        /**
         * @param type The class
         */
        DefaultSingleRoute(Class type) {
            super(type);
        }

        @Override
        protected ResourceRoute newResourceRoute(Map newMap, DefaultUriRoute getRoute) {
            return new DefaultSingleRoute(newMap, getRoute);
        }

        @Override
        protected DefaultUriRoute buildGetRoute(Class type, Map routeMap) {
            DefaultUriRoute getRoute = (DefaultUriRoute) DefaultRouteBuilder.this.GET(type);
            routeMap.put(
                HttpMethod.GET, getRoute
            );
            return getRoute;
        }

        @Override
        protected void buildRemainingRoutes(Class type, Map routeMap) {
            // POST /foo
            routeMap.put(
                HttpMethod.POST, DefaultRouteBuilder.this.POST(type)
            );
            // DELETE /foo
            routeMap.put(
                HttpMethod.DELETE, DefaultRouteBuilder.this.DELETE(type)
            );
            // PATCH /foo
            routeMap.put(
                HttpMethod.PATCH, DefaultRouteBuilder.this.PATCH(type)
            );
            // PUT /foo
            routeMap.put(
                HttpMethod.PUT, DefaultRouteBuilder.this.PUT(type)
            );
        }
    }

    /**
     * Default resource route.
     */
    class DefaultResourceRoute implements ResourceRoute {

        private final Map resourceRoutes;
        private final DefaultUriRoute getRoute;

        /**
         * @param resourceRoutes The resource routes
         * @param getRoute The default Uri route
         */
        DefaultResourceRoute(Map resourceRoutes, DefaultUriRoute getRoute) {
            this.resourceRoutes = resourceRoutes;
            this.getRoute = getRoute;
        }

        /**
         * @param type The class
         */
        DefaultResourceRoute(Class type) {
            this.resourceRoutes = new LinkedHashMap<>();
            // GET /foo/1
            Map routeMap = this.resourceRoutes;
            this.getRoute = buildGetRoute(type, routeMap);
            buildRemainingRoutes(type, routeMap);
        }

        @Override
        public ResourceRoute consumes(MediaType... mediaTypes) {
            if (mediaTypes != null) {
                for (Route route : resourceRoutes.values()) {
                    route.produces(mediaTypes);
                }
            }
            return this;
        }

        @Override
        public Route consumesAll() {
            return consumes(MediaType.EMPTY_ARRAY);
        }

        @Override
        public ResourceRoute nest(Runnable nested) {
            DefaultUriRoute previous = DefaultRouteBuilder.this.currentParentRoute;
            DefaultRouteBuilder.this.currentParentRoute = getRoute;
            try {
                nested.run();
            } finally {
                DefaultRouteBuilder.this.currentParentRoute = previous;
            }
            return this;
        }

        @Override
        public ResourceRoute where(Predicate> condition) {
            for (Route route : resourceRoutes.values()) {
                route.where(condition);
            }
            return this;
        }

        @Override
        public ResourceRoute produces(MediaType... mediaType) {
            if (mediaType != null) {
                for (Route route : resourceRoutes.values()) {
                    route.produces(mediaType);
                }
            }
            return this;
        }

        @Override
        public ResourceRoute body(String argument) {
            return this;
        }

        @Override
        public Route body(Argument argument) {
            return this;
        }

        @Override
        public ResourceRoute readOnly(boolean readOnly) {
            List excluded = Arrays.asList(HttpMethod.DELETE, HttpMethod.PATCH, HttpMethod.POST, HttpMethod.PUT);
            return handleExclude(excluded);
        }

        @Override
        public ResourceRoute exclude(HttpMethod... methods) {
            return handleExclude(Arrays.asList(methods));
        }

        /**
         * @param newMap New map info
         * @param getRoute The default route
         *
         * @return The {@link ResourceRoute}
         */
        protected ResourceRoute newResourceRoute(Map newMap, DefaultUriRoute getRoute) {
            return new DefaultResourceRoute(newMap, getRoute);
        }

        /**
         * @param type The class
         * @param routeMap The route info
         *
         * @return The {@link DefaultUriRoute}
         */
        protected DefaultUriRoute buildGetRoute(Class type, Map routeMap) {
            DefaultUriRoute getRoute = (DefaultUriRoute) DefaultRouteBuilder.this.GET(type, ID);
            routeMap.put(
                HttpMethod.GET, getRoute
            );
            return getRoute;
        }

        /**
         * Build the remaining routes.
         *
         * @param type The class
         * @param routeMap The route info
         */
        protected void buildRemainingRoutes(Class type, Map routeMap) {
            // GET /foo
            routeMap.put(
                HttpMethod.GET, DefaultRouteBuilder.this.GET(type)
            );
            // POST /foo
            routeMap.put(
                HttpMethod.POST, DefaultRouteBuilder.this.POST(type)
            );
            // DELETE /foo/1
            routeMap.put(
                HttpMethod.DELETE, DefaultRouteBuilder.this.DELETE(type, ID)
            );
            // PATCH /foo/1
            routeMap.put(
                HttpMethod.PATCH, DefaultRouteBuilder.this.PATCH(type, ID)
            );
            // PUT /foo/1
            routeMap.put(
                HttpMethod.PUT, DefaultRouteBuilder.this.PUT(type, ID)
            );
        }

        private ResourceRoute handleExclude(List excluded) {
            Map newMap = new LinkedHashMap<>();
            this.resourceRoutes.forEach((key, value) -> {
                if (excluded.contains(key)) {
                    DefaultRouteBuilder.this.uriRoutes.remove(value);
                } else {
                    newMap.put(key, value);
                }
            });
            return newResourceRoute(newMap, getRoute);
        }
    }
}