com.yahoo.restapi.RestApiImpl Maven / Gradle / Ivy
// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.restapi;
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.restapi.RestApiMappers.ExceptionMapperHolder;
import com.yahoo.restapi.RestApiMappers.RequestMapperHolder;
import com.yahoo.restapi.RestApiMappers.ResponseMapperHolder;
import java.io.InputStream;
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 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);
}
@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) {
return filterContext.executeFirst();
} 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 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()))
.findFirst().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);
}
// Topologically sort children before superclasses, so most the specific match is found by iterating through mappers in order.
exceptionMappers.sort((a, b) -> (a.type.isAssignableFrom(b.type) ? 1 : 0) + (b.type.isAssignableFrom(a.type) ? -1 : 0));
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;
@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 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;
RouteBuilderImpl(String pathPattern) { this.pathPattern = pathPattern; }
@Override public RestApi.RouteBuilder name(String name) { this.name = name; 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;
@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;
HandlerConfig(HandlerConfigBuilderImpl builder) {
this.aclAction = builder.aclAction;
}
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 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 UriBuilder uriBuilder() {
URI uri = request.getUri();
int uriPort = uri.getPort();
return uriPort != -1
? new UriBuilder(uri.getScheme() + "://" + uri.getHost() + ':' + uriPort)
: new UriBuilder(uri.getScheme() + "://" + uri.getHost());
}
@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);
}
private class PathParametersImpl implements RestApi.RequestContext.PathParameters {
@Override
public Optional getString(String name) {
if (name.equals("*")) {
String rest = pathMatcher.getRest();
return rest.isEmpty() ? Optional.empty() : Optional.of(rest);
}
return Optional.ofNullable(pathMatcher.get(name));
}
@Override public String getStringOrThrow(String name) {
return getString(name)
.orElseThrow(() -> new RestApiException.BadRequest("Path parameter '" + name + "' is missing"));
}
}
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);
}
}
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
© 2015 - 2025 Weber Informatics LLC | Privacy Policy