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

com.yahoo.restapi.RestApiImpl Maven / Gradle / Ivy

// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.restapi;

import ai.vespa.http.HttpURL;
import ai.vespa.http.HttpURL.Query;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yahoo.container.jdisc.AclMapping;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.RequestHandlerSpec;
import com.yahoo.container.jdisc.RequestView;
import com.yahoo.jdisc.http.HttpRequest.Method;
import com.yahoo.jdisc.http.server.jetty.RequestUtils;
import com.yahoo.restapi.RestApiMappers.ExceptionMapperHolder;
import com.yahoo.restapi.RestApiMappers.RequestMapperHolder;
import com.yahoo.restapi.RestApiMappers.ResponseMapperHolder;
import com.yahoo.security.tls.Capability;
import com.yahoo.security.tls.CapabilitySet;
import com.yahoo.security.tls.ConnectionAuthContext;
import com.yahoo.security.tls.TransportSecurityUtils;

import javax.net.ssl.SSLSession;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @author bjorncs
 */
class RestApiImpl implements RestApi {

    private static final Logger log = Logger.getLogger(RestApiImpl.class.getName());

    private final Route defaultRoute;
    private final List routes;
    private final List> exceptionMappers;
    private final List> responseMappers;
    private final List> requestMappers;
    private final List filters;
    private final ObjectMapper jacksonJsonMapper;
    private final boolean disableDefaultAclMapping;
    private final CapabilitySet requiredCapabilities;

    private RestApiImpl(RestApi.Builder builder) {
        BuilderImpl builderImpl = (BuilderImpl) builder;
        ObjectMapper jacksonJsonMapper = builderImpl.jacksonJsonMapper != null ? builderImpl.jacksonJsonMapper : JacksonJsonMapper.instance.copy();
        this.defaultRoute = builderImpl.defaultRoute != null ? builderImpl.defaultRoute : createDefaultRoute();
        this.routes = List.copyOf(builderImpl.routes);
        this.exceptionMappers = combineWithDefaultExceptionMappers(
                builderImpl.exceptionMappers, Boolean.TRUE.equals(builderImpl.disableDefaultExceptionMappers));
        this.responseMappers = combineWithDefaultResponseMappers(
                builderImpl.responseMappers, Boolean.TRUE.equals(builderImpl.disableDefaultResponseMappers));
        this.requestMappers = combineWithDefaultRequestMappers(builderImpl.requestMappers);
        this.filters = List.copyOf(builderImpl.filters);
        this.jacksonJsonMapper = jacksonJsonMapper;
        this.disableDefaultAclMapping = Boolean.TRUE.equals(builderImpl.disableDefaultAclMapping);
        this.requiredCapabilities = builderImpl.requiredCapabilities;
    }

    @Override
    public HttpResponse handleRequest(HttpRequest request) {
        Path pathMatcher = new Path(request.getUri());
        Route resolvedRoute = resolveRoute(pathMatcher);
        AclMapping.Action aclAction = getAclMapping(request.getMethod(), request.getUri());
        RequestContextImpl requestContext = new RequestContextImpl(request, pathMatcher, aclAction, jacksonJsonMapper);
        FilterContextImpl filterContext =
                createFilterContextRecursive(
                        resolvedRoute, requestContext, filters,
                        createFilterContextRecursive(resolvedRoute, requestContext, resolvedRoute.filters, null));
        if (filterContext != null) {
            try {
                return filterContext.executeFirst();
            } catch (RuntimeException e) {
                return mapException(requestContext, e);
            }
        } else {
            return dispatchToRoute(resolvedRoute, requestContext);
        }
    }

    @Override public ObjectMapper jacksonJsonMapper() { return jacksonJsonMapper; }

    @Override
    public RequestHandlerSpec requestHandlerSpec() {
        return RequestHandlerSpec.builder()
                .withAclMapping(requestView -> getAclMapping(requestView.method(), requestView.uri()))
                .build();
    }

    private static final CapabilitySet DEFAULT_REQUIRED_CAPABILITIES = Capability.RESTAPI_UNCLASSIFIED.toCapabilitySet();

    @Override
    public CapabilitySet requiredCapabilities(RequestView req) {
        Path pathMatcher = new Path(req.uri());
        Route route = resolveRoute(pathMatcher);
        HandlerHolder handler = resolveHandler(req.method(), route);
        return Optional.ofNullable(handler.config.requiredCapabilities)
                .or(() -> Optional.ofNullable(route.requiredCapabilities))
                .or(() -> Optional.ofNullable(requiredCapabilities))
                .orElse(DEFAULT_REQUIRED_CAPABILITIES);
    }

    private AclMapping.Action getAclMapping(Method method, URI uri) {
        Path pathMatcher = new Path(uri);
        Route route = resolveRoute(pathMatcher);
        HandlerHolder handler = resolveHandler(method, route);
        AclMapping.Action aclAction = handler.config.aclAction;
        if (aclAction != null) return aclAction;
        if (!disableDefaultAclMapping) {
            // Fallback to default request handler spec which is used by the default implementation of
            // HttpRequestHandler.requestHandlerSpec().
            return RequestHandlerSpec.DEFAULT_INSTANCE.aclMapping().get(
                    new RequestView() {
                        @Override public Method method() { return method; }
                        @Override public URI uri() { return uri; }
                    });
        }
        throw new IllegalStateException(String.format("No ACL mapping configured for '%s' to '%s'", method, route.name));
    }

    private HttpResponse dispatchToRoute(Route route, RequestContextImpl context) {
        HandlerHolder resolvedHandler = resolveHandler(context.request.getMethod(), route);
        RequestMapperHolder resolvedRequestMapper = resolveRequestMapper(resolvedHandler);
        Object requestEntity;
        try {
            requestEntity = resolvedRequestMapper.mapper.toRequestEntity(context).orElse(null);
        } catch (RuntimeException e) {
            return mapException(context, e);
        }
        Object responseEntity;
        try {
            responseEntity = resolvedHandler.executeHandler(context, requestEntity);
        } catch (RuntimeException e) {
            return mapException(context, e);
        }
        if (responseEntity == null) throw new NullPointerException("Handler must return non-null value");
        ResponseMapperHolder resolvedResponseMapper = resolveResponseMapper(responseEntity);
        try {
            return resolvedResponseMapper.toHttpResponse(context, responseEntity);
        } catch (RuntimeException e) {
            return mapException(context, e);
        }
    }

    private HandlerHolder resolveHandler(Method method, Route route) {
        HandlerHolder resolvedHandler = route.handlerPerMethod.get(method);
        return resolvedHandler == null ? route.defaultHandler : resolvedHandler;
    }

    private RequestMapperHolder resolveRequestMapper(HandlerHolder resolvedHandler) {
        return requestMappers.stream()
                .filter(holder -> resolvedHandler.type.isAssignableFrom(holder.type))
                .findFirst().orElseThrow(() -> new IllegalStateException("No mapper configured for " + resolvedHandler.type));
    }

    private ResponseMapperHolder resolveResponseMapper(Object responseEntity) {
        return responseMappers.stream()
                .filter(holder -> holder.type.isAssignableFrom(responseEntity.getClass()))
                .findFirst().orElseThrow(() -> new IllegalStateException("No mapper configured for " + responseEntity.getClass()));
    }

    private HttpResponse mapException(RequestContextImpl context, RuntimeException e) {
        log.log(Level.FINE, e, e::getMessage);
        ExceptionMapperHolder mapper = exceptionMappers.stream()
                .filter(holder -> holder.type.isAssignableFrom(e.getClass()))
                // Topologically sort children before superclasses, so most the specific match is found by iterating through mappers in order.
                .min((a, b) -> (a.type.isAssignableFrom(b.type) ? 1 : 0) + (b.type.isAssignableFrom(a.type) ? -1 : 0))
                .orElseThrow(() -> e);
        return mapper.toResponse(context, e);
    }

    private Route resolveRoute(Path pathMatcher) {
        Route matchingRoute = routes.stream()
                .filter(route -> pathMatcher.matches(route.pathPattern))
                .findFirst()
                .orElse(null);
        if (matchingRoute != null) return matchingRoute;
        pathMatcher.matches(defaultRoute.pathPattern); // to populate any path parameters
        return defaultRoute;
    }

    private FilterContextImpl createFilterContextRecursive(
            Route route, RequestContextImpl requestContext, List filters, FilterContextImpl previousContext) {
        FilterContextImpl filterContext = previousContext;
        ListIterator iterator = filters.listIterator(filters.size());
        while (iterator.hasPrevious()) {
            filterContext = new FilterContextImpl(route, iterator.previous(), requestContext, filterContext);
        }
        return filterContext;
    }

    private static Route createDefaultRoute() {
        RouteBuilder routeBuilder = new RouteBuilderImpl("{*}")
                .defaultHandler(context -> {
                    throw new RestApiException.NotFound(context.request());
                });
        return ((RouteBuilderImpl)routeBuilder).build();
    }

    private static List> combineWithDefaultExceptionMappers(
            List> configuredExceptionMappers, boolean disableDefaultMappers) {
        List> exceptionMappers = new ArrayList<>(configuredExceptionMappers);
        if (!disableDefaultMappers){
            exceptionMappers.addAll(RestApiMappers.DEFAULT_EXCEPTION_MAPPERS);
        }
        return exceptionMappers;
    }

    private static List> combineWithDefaultResponseMappers(
            List> configuredResponseMappers, boolean disableDefaultMappers) {
        List> responseMappers = new ArrayList<>(configuredResponseMappers);
        if (!disableDefaultMappers) {
            responseMappers.addAll(RestApiMappers.DEFAULT_RESPONSE_MAPPERS);
        }
        return responseMappers;
    }

    private static List> combineWithDefaultRequestMappers(
            List> configuredRequestMappers) {
        List> requestMappers = new ArrayList<>(configuredRequestMappers);
        requestMappers.addAll(RestApiMappers.DEFAULT_REQUEST_MAPPERS);
        return requestMappers;
    }

    static class BuilderImpl implements RestApi.Builder {
        private final List routes = new ArrayList<>();
        private final List> exceptionMappers = new ArrayList<>();
        private final List> responseMappers = new ArrayList<>();
        private final List> requestMappers = new ArrayList<>();
        private final List filters = new ArrayList<>();
        private Route defaultRoute;
        private ObjectMapper jacksonJsonMapper;
        private Boolean disableDefaultExceptionMappers;
        private Boolean disableDefaultResponseMappers;
        private Boolean disableDefaultAclMapping;
        private CapabilitySet requiredCapabilities;

        @Override public RestApi.Builder setObjectMapper(ObjectMapper mapper) { this.jacksonJsonMapper = mapper; return this; }
        @Override public RestApi.Builder setDefaultRoute(RestApi.RouteBuilder route) { this.defaultRoute = ((RouteBuilderImpl)route).build(); return this; }
        @Override public RestApi.Builder addRoute(RestApi.RouteBuilder route) { routes.add(((RouteBuilderImpl)route).build()); return this; }
        @Override public RestApi.Builder addFilter(RestApi.Filter filter) { filters.add(filter); return this; }

        @Override public  RestApi.Builder addExceptionMapper(Class type, RestApi.ExceptionMapper mapper) {
            exceptionMappers.add(new ExceptionMapperHolder<>(type, mapper)); return this;
        }

        @Override public  RestApi.Builder addResponseMapper(Class type, RestApi.ResponseMapper mapper) {
            responseMappers.add(new ResponseMapperHolder<>(type, mapper)); return this;
        }

        @Override public  Builder addRequestMapper(Class type, RequestMapper mapper) {
            requestMappers.add(new RequestMapperHolder<>(type, mapper)); return this;
        }

        @Override public  Builder registerJacksonResponseEntity(Class type) {
            addResponseMapper(type, new RestApiMappers.JacksonResponseMapper<>()); return this;
        }

        @Override public  Builder registerJacksonRequestEntity(Class type) {
            addRequestMapper(type, new RestApiMappers.JacksonRequestMapper<>(type)); return this;
        }

        @Override public Builder disableDefaultExceptionMappers() { this.disableDefaultExceptionMappers = true; return this; }
        @Override public Builder disableDefaultResponseMappers() { this.disableDefaultResponseMappers = true; return this; }
        @Override public Builder disableDefaultAclMapping() { this.disableDefaultAclMapping = true; return this; }

        @Override public Builder requiredCapabilities(Capability... capabilities) {
            return requiredCapabilities(CapabilitySet.of(capabilities));
        }
        @Override public Builder requiredCapabilities(CapabilitySet capabilities) {
            if (requiredCapabilities != null) throw new IllegalStateException("Capabilities already set");
            requiredCapabilities = capabilities;
            return this;
        }

        @Override public RestApi build() { return new RestApiImpl(this); }
    }

    static class RouteBuilderImpl implements RestApi.RouteBuilder {
        private final String pathPattern;
        private String name;
        private final Map> handlerPerMethod = new HashMap<>();
        private final List filters = new ArrayList<>();
        private HandlerHolder defaultHandler;
        private CapabilitySet requiredCapabilities;

        RouteBuilderImpl(String pathPattern) { this.pathPattern = pathPattern; }

        @Override public RestApi.RouteBuilder name(String name) { this.name = name; return this; }

        @Override public RestApi.RouteBuilder requiredCapabilities(Capability... capabilities) {
            return requiredCapabilities(CapabilitySet.of(capabilities));
        }
        @Override public RestApi.RouteBuilder requiredCapabilities(CapabilitySet capabilities) {
            if (requiredCapabilities != null) throw new IllegalStateException("Capabilities already set");
            requiredCapabilities = capabilities;
            return this;
        }

        @Override public RestApi.RouteBuilder addFilter(RestApi.Filter filter) { filters.add(filter); return this; }

        // GET
        @Override public RouteBuilder get(Handler handler) { return get(handler, null); }
        @Override public RouteBuilder get(Handler handler, HandlerConfigBuilder config) {
            return addHandler(Method.GET, handler, config);
        }

        // POST
        @Override public RouteBuilder post(Handler handler) { return post(handler, null); }
        @Override public  RouteBuilder post(
                Class type, HandlerWithRequestEntity handler) {
            return post(type, handler, null);
        }
        @Override public RouteBuilder post(Handler handler, HandlerConfigBuilder config) {
            return addHandler(Method.POST, handler, config);
        }
        @Override public  RouteBuilder post(
                Class type, HandlerWithRequestEntity handler, HandlerConfigBuilder config) {
            return addHandler(Method.POST, type, handler, config);
        }

        // PUT
        @Override public RouteBuilder put(Handler handler) { return put(handler, null); }
        @Override public  RouteBuilder put(
                Class type, HandlerWithRequestEntity handler) {
            return put(type, handler, null);
        }
        @Override public RouteBuilder put(Handler handler, HandlerConfigBuilder config) {
            return addHandler(Method.PUT, handler, null);
        }
        @Override public  RouteBuilder put(
                Class type, HandlerWithRequestEntity handler, HandlerConfigBuilder config) {
            return addHandler(Method.PUT, type, handler, config);
        }

        // DELETE
        @Override public RouteBuilder delete(Handler handler) { return delete(handler, null); }
        @Override public RouteBuilder delete(Handler handler, HandlerConfigBuilder config) {
            return addHandler(Method.DELETE, handler, config);
        }

        // PATCH
        @Override public RouteBuilder patch(Handler handler) { return patch(handler, null); }
        @Override public  RouteBuilder patch(
                Class type, HandlerWithRequestEntity handler) {
            return patch(type, handler, null);
        }
        @Override public RouteBuilder patch(Handler handler, HandlerConfigBuilder config) {
            return addHandler(Method.PATCH, handler, config);
        }
        @Override public  RouteBuilder patch(
                Class type, HandlerWithRequestEntity handler, HandlerConfigBuilder config) {
            return addHandler(Method.PATCH, type, handler, config);
        }

        // Default
        @Override public RouteBuilder defaultHandler(Handler handler) {
            return defaultHandler(handler, null);
        }
        @Override public RouteBuilder defaultHandler(Handler handler, HandlerConfigBuilder config) {
            defaultHandler = HandlerHolder.of(handler, build(config)); return this;
        }
        @Override public  RouteBuilder defaultHandler(
                Class type, HandlerWithRequestEntity handler) {
            return defaultHandler(type, handler, null);
        }
        @Override
        public  RouteBuilder defaultHandler(
                Class type, HandlerWithRequestEntity handler, HandlerConfigBuilder config) {
            defaultHandler = HandlerHolder.of(type, handler, build(config)); return this;
        }

        private RestApi.RouteBuilder addHandler(Method method, Handler handler, HandlerConfigBuilder config) {
            handlerPerMethod.put(method, HandlerHolder.of(handler, build(config))); return this;
        }

        private  RestApi.RouteBuilder addHandler(
                Method method, Class type, HandlerWithRequestEntity handler, HandlerConfigBuilder config) {
            handlerPerMethod.put(method, HandlerHolder.of(type, handler, build(config))); return this;
        }

        private static HandlerConfig build(HandlerConfigBuilder builder) {
            if (builder == null) return HandlerConfig.empty();
            return ((HandlerConfigBuilderImpl)builder).build();
        }

        private Route build() { return new Route(this); }
    }

    static class HandlerConfigBuilderImpl implements HandlerConfigBuilder {
        private AclMapping.Action aclAction;
        private CapabilitySet requiredCapabilities;

        @Override public HandlerConfigBuilder withRequiredCapabilities(Capability... capabilities) {
            return withRequiredCapabilities(CapabilitySet.of(capabilities));
        }
        @Override public HandlerConfigBuilder withRequiredCapabilities(CapabilitySet capabilities) {
            if (requiredCapabilities != null) throw new IllegalStateException("Capabilities already set");
            requiredCapabilities = capabilities;
            return this;
        }
        @Override public HandlerConfigBuilder withReadAclAction() { return withCustomAclAction(AclMapping.Action.READ); }
        @Override public HandlerConfigBuilder withWriteAclAction() { return withCustomAclAction(AclMapping.Action.WRITE); }
        @Override public HandlerConfigBuilder withCustomAclAction(AclMapping.Action action) {
            this.aclAction = action; return this;
        }

        HandlerConfig build() { return new HandlerConfig(this); }
    }

    private static class HandlerConfig {
        final AclMapping.Action aclAction;
        final CapabilitySet requiredCapabilities;

        HandlerConfig(HandlerConfigBuilderImpl builder) {
            this.aclAction = builder.aclAction;
            this.requiredCapabilities = builder.requiredCapabilities;
        }

        static HandlerConfig empty() { return new HandlerConfigBuilderImpl().build(); }
    }

    private static class RequestContextImpl implements RestApi.RequestContext {
        final HttpRequest request;
        final Path pathMatcher;
        final ObjectMapper jacksonJsonMapper;
        final PathParameters pathParameters = new PathParametersImpl();
        final QueryParameters queryParameters = new QueryParametersImpl();
        final Headers headers = new HeadersImpl();
        final Attributes attributes = new AttributesImpl();
        final RequestContent requestContent;
        final AclMapping.Action aclAction;

        RequestContextImpl(HttpRequest request, Path pathMatcher, AclMapping.Action aclAction, ObjectMapper jacksonJsonMapper) {
            this.request = request;
            this.pathMatcher = pathMatcher;
            this.jacksonJsonMapper = jacksonJsonMapper;
            this.requestContent = request.getData() != null ? new RequestContentImpl() : null;
            this.aclAction = aclAction;
        }

        @Override public HttpRequest request() { return request; }
        @Override public Method method() { return request.getMethod(); }
        @Override public PathParameters pathParameters() { return pathParameters; }
        @Override public QueryParameters queryParameters() { return queryParameters; }
        @Override public Headers headers() { return headers; }
        @Override public Attributes attributes() { return attributes; }
        @Override public Optional requestContent() { return Optional.ofNullable(requestContent); }
        @Override public RequestContent requestContentOrThrow() {
            return requestContent().orElseThrow(() -> new RestApiException.BadRequest("Request content missing"));
        }
        @Override public ObjectMapper jacksonJsonMapper() { return jacksonJsonMapper; }
        @Override public HttpURL baseRequestURL() {
            URI uri = request.getUri();
            // Reconstruct the URI used by the client to access the API.
            // This is needed for producing URIs in the response that links to other parts of the Rest API.
            // request.getUri() cannot be used as its port is the local listen port (as it's intended for request routing).
            StringBuilder sb = new StringBuilder(uri.getScheme()).append("://");
            String hostHeader = request.getHeader("X-Forwarded-Host");
            if (hostHeader == null || hostHeader.isBlank()) {
                hostHeader = request.getHeader("Host");
            }
            if (hostHeader != null && ! hostHeader.isBlank()) {
                sb.append(hostHeader);
            } else {
                sb.append(uri.getHost());
                if (uri.getPort() > 0) {
                    sb.append(":").append(uri.getPort());
                }
            }
            return HttpURL.from(URI.create(sb.toString()));
        }
        @Override public HttpURL url() { return HttpURL.from(request.getUri()); }
        @Override public AclMapping.Action aclAction() { return aclAction; }
        @Override public Optional userPrincipal() {
            return Optional.ofNullable(request.getJDiscRequest().getUserPrincipal());
        }
        @Override public Principal userPrincipalOrThrow() {
            return userPrincipal().orElseThrow(RestApiException.Unauthorized::new);
        }
        @Override public Optional sslSession() {
            return Optional.ofNullable((SSLSession) request.context().get(RequestUtils.JDISC_REQUEST_SSLSESSION));
        }
        @Override public Optional connectionAuthContext() {
            return sslSession().flatMap(TransportSecurityUtils::getConnectionAuthContext);
        }
        @Override public InetSocketAddress remoteAddress() { return (InetSocketAddress) request.getJDiscRequest().getRemoteAddress(); }

        private class PathParametersImpl implements RestApi.RequestContext.PathParameters {
            @Override
            public Optional getString(String name) {
                return Optional.ofNullable(pathMatcher.get(name));
            }
            @Override public String getStringOrThrow(String name) {
                return getString(name)
                        .orElseThrow(() -> new RestApiException.NotFound("Path parameter '" + name + "' is missing"));
            }
            @Override public HttpURL.Path getFullPath() {
                return pathMatcher.getPath();
            }
            @Override public Optional getRest() {
                return Optional.ofNullable(pathMatcher.getRest());
            }
        }

        private class QueryParametersImpl implements RestApi.RequestContext.QueryParameters {
            @Override public Optional getString(String name) { return Optional.ofNullable(request.getProperty(name)); }
            @Override public String getStringOrThrow(String name) {
                return getString(name)
                        .orElseThrow(() -> new RestApiException.BadRequest("Query parameter '" + name + "' is missing"));
            }
            @Override public List getStringList(String name) {
                List result = request.getJDiscRequest().parameters().get(name);
                if (result == null) return List.of();
                return List.copyOf(result);
            }
            @Override public HttpURL.Query getFullQuery() { return Query.empty().add(request.getJDiscRequest().parameters()); }
        }

        private class HeadersImpl implements RestApi.RequestContext.Headers {
            @Override public Optional getString(String name) { return Optional.ofNullable(request.getHeader(name)); }
            @Override public String getStringOrThrow(String name) {
                return getString(name)
                        .orElseThrow(() -> new RestApiException.BadRequest("Header '" + name + "' missing"));
            }
        }

        private class RequestContentImpl implements RestApi.RequestContext.RequestContent {
            @Override public String contentType() { return request.getHeader("Content-Type"); }
            @Override public InputStream content() { return request.getData(); }
        }

        private class AttributesImpl implements RestApi.RequestContext.Attributes {
            @Override public Optional get(String name) { return Optional.ofNullable(request.getJDiscRequest().context().get(name)); }
            @Override public void set(String name, Object value) { request.getJDiscRequest().context().put(name, value); }
        }
    }

    private class FilterContextImpl implements RestApi.FilterContext {
        final Route route;
        final RestApi.Filter filter;
        final RequestContextImpl requestContext;
        final FilterContextImpl next;

        FilterContextImpl(Route route, RestApi.Filter filter, RequestContextImpl requestContext, FilterContextImpl next) {
            this.route = route;
            this.filter = filter;
            this.requestContext = requestContext;
            this.next = next;
        }

        @Override public RestApi.RequestContext requestContext() { return requestContext; }
        @Override public String route() { return route.name != null ? route.name : route.pathPattern; }
        @Override public void setPrincipal(Principal p) { requestContext.request.getJDiscRequest().setUserPrincipal(p); }

        HttpResponse executeFirst() { return filter.filterRequest(this); }

        @Override
        public HttpResponse executeNext() {
            if (next != null) {
                return next.filter.filterRequest(next);
            } else {
                return dispatchToRoute(route, requestContext);
            }
        }
    }

    private static class HandlerHolder {
        final Class type;
        final HandlerWithRequestEntity handler;
        final HandlerConfig config;

        private HandlerHolder(
                Class type,
                HandlerWithRequestEntity handler,
                HandlerConfig config) {
            this.type = type;
            this.handler = handler;
            this.config = config;
        }

        static  HandlerHolder of(
                Class type,
                HandlerWithRequestEntity handler,
                HandlerConfig config) {
            return new HandlerHolder<>(type, handler, config);
        }

        static  HandlerHolder of(Handler handler, HandlerConfig config) {
            return new HandlerHolder<>(
                    Void.class,
                    (HandlerWithRequestEntity) (context, nullEntity) -> handler.handleRequest(context),
                    config);
        }

        Object executeHandler(RestApi.RequestContext context, Object entity) { return handler.handleRequest(context, type.cast(entity)); }
    }

    static class Route {
        private final String pathPattern;
        private final String name;
        private final Map> handlerPerMethod;
        private final HandlerHolder defaultHandler;
        private final List filters;
        private final CapabilitySet requiredCapabilities;

        private Route(RestApi.RouteBuilder builder) {
            RouteBuilderImpl builderImpl = (RouteBuilderImpl)builder;
            this.pathPattern = builderImpl.pathPattern;
            this.name = builderImpl.name;
            this.handlerPerMethod = Map.copyOf(builderImpl.handlerPerMethod);
            this.defaultHandler = builderImpl.defaultHandler != null ? builderImpl.defaultHandler : createDefaultMethodHandler();
            this.filters = List.copyOf(builderImpl.filters);
            this.requiredCapabilities = builderImpl.requiredCapabilities;
        }

        private HandlerHolder createDefaultMethodHandler() {
            return HandlerHolder.of(
                    context -> { throw new RestApiException.MethodNotAllowed(context.request()); },
                    HandlerConfig.empty());
        }
    }

}