summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityProviderRequestHandler.java13
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceConfirmation.java3
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/StateRequestHandler.java5
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApi.java49
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java344
-rw-r--r--container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java14
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java3
7 files changed, 284 insertions, 147 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityProviderRequestHandler.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityProviderRequestHandler.java
index 8593401b887..fe6fa0baf2d 100644
--- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityProviderRequestHandler.java
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityProviderRequestHandler.java
@@ -42,9 +42,12 @@ public class IdentityProviderRequestHandler extends RestApiRequestHandler<Identi
.addRoute(RestApi.route("/athenz/v1/provider/identity-document/tenant/{host}")
.get(self::getTenantIdentityDocument))
.addRoute(RestApi.route("/athenz/v1/provider/instance")
- .post(self::confirmInstance))
+ .post(InstanceConfirmation.class, self::confirmInstance))
.addRoute(RestApi.route("/athenz/v1/provider/refresh")
- .post(self::confirmInstanceRefresh))
+ .post(InstanceConfirmation.class, self::confirmInstanceRefresh))
+ .registerJacksonRequestEntity(InstanceConfirmation.class)
+ .registerJacksonResponseEntity(InstanceConfirmation.class)
+ .registerJacksonResponseEntity(SignedIdentityDocumentEntity.class)
// Overriding object mapper to change serialization of timestamps
.setObjectMapper(new ObjectMapper()
.registerModule(new JavaTimeModule())
@@ -63,8 +66,7 @@ public class IdentityProviderRequestHandler extends RestApiRequestHandler<Identi
return getIdentityDocument(host, IdentityType.TENANT);
}
- private InstanceConfirmation confirmInstance(RestApi.RequestContext context) {
- InstanceConfirmation instanceConfirmation = context.requestContentOrThrow().consumeJacksonEntity(InstanceConfirmation.class);
+ private InstanceConfirmation confirmInstance(RestApi.RequestContext context, InstanceConfirmation instanceConfirmation) {
log.log(Level.FINE, instanceConfirmation.toString());
if (!instanceValidator.isValidInstance(instanceConfirmation)) {
log.log(Level.SEVERE, "Invalid instance: " + instanceConfirmation);
@@ -73,8 +75,7 @@ public class IdentityProviderRequestHandler extends RestApiRequestHandler<Identi
return instanceConfirmation;
}
- private InstanceConfirmation confirmInstanceRefresh(RestApi.RequestContext context) {
- InstanceConfirmation instanceConfirmation = context.requestContentOrThrow().consumeJacksonEntity(InstanceConfirmation.class);
+ private InstanceConfirmation confirmInstanceRefresh(RestApi.RequestContext context, InstanceConfirmation instanceConfirmation) {
log.log(Level.FINE, instanceConfirmation.toString());
if (!instanceValidator.isValidRefresh(instanceConfirmation)) {
log.log(Level.SEVERE, "Invalid instance refresh: " + instanceConfirmation);
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceConfirmation.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceConfirmation.java
index 27507c425cd..5641eb51668 100644
--- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceConfirmation.java
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceConfirmation.java
@@ -13,7 +13,6 @@ import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-import com.yahoo.restapi.RestApi;
import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocumentEntity;
import java.io.IOException;
@@ -26,7 +25,7 @@ import java.util.Objects;
*
* @author bjorncs
*/
-public class InstanceConfirmation implements RestApi.JacksonRequestEntity, RestApi.JacksonResponseEntity {
+public class InstanceConfirmation {
@JsonProperty("provider") public final String provider;
@JsonProperty("domain") public final String domain;
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/StateRequestHandler.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/StateRequestHandler.java
index 4fbb001b880..06f3ba56c0c 100644
--- a/configserver/src/main/java/com/yahoo/vespa/serviceview/StateRequestHandler.java
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/StateRequestHandler.java
@@ -4,7 +4,6 @@ package com.yahoo.vespa.serviceview;
import com.google.inject.Inject;
import com.yahoo.cloud.config.ConfigserverConfig;
import com.yahoo.container.jdisc.LoggingRequestHandler;
-import com.yahoo.restapi.JacksonJsonResponse;
import com.yahoo.restapi.RestApi;
import com.yahoo.restapi.RestApiRequestHandler;
import com.yahoo.vespa.serviceview.bindings.ApplicationView;
@@ -79,8 +78,8 @@ public class StateRequestHandler extends RestApiRequestHandler<StateRequestHandl
.get(self::getUserInfo))
.addRoute(RestApi.route("/serviceview/v1/tenant/{tenantName}/application/{applicationName}/environment/{environmentName}/region/{regionName}/instance/{instanceName}/service/{serviceIdentifier}/{*}")
.get(self::singleService))
- .addResponseMapper(HashMap.class, (hashMap, context) -> new JacksonJsonResponse<>(200, hashMap, true))
- .addResponseMapper(ApplicationView.class, (applicationView, context) -> new JacksonJsonResponse<>(200, applicationView, true))
+ .registerJacksonResponseEntity(HashMap.class)
+ .registerJacksonResponseEntity(ApplicationView.class)
.build();
}
diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApi.java b/container-core/src/main/java/com/yahoo/restapi/RestApi.java
index 08bac710001..7a2e71e1388 100644
--- a/container-core/src/main/java/com/yahoo/restapi/RestApi.java
+++ b/container-core/src/main/java/com/yahoo/restapi/RestApi.java
@@ -1,11 +1,9 @@
// 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.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.slime.Slime;
import java.io.InputStream;
import java.util.Optional;
@@ -23,7 +21,6 @@ public interface RestApi {
static RouteBuilder route(String pathPattern) { return new RestApiImpl.RouteBuilderImpl(pathPattern); }
HttpResponse handleRequest(HttpRequest request);
- ObjectMapper jacksonJsonMapper();
interface Builder {
Builder setObjectMapper(ObjectMapper mapper);
@@ -32,6 +29,9 @@ public interface RestApi {
Builder addFilter(Filter filter);
<EXCEPTION extends RuntimeException> Builder addExceptionMapper(Class<EXCEPTION> type, ExceptionMapper<EXCEPTION> mapper);
<ENTITY> Builder addResponseMapper(Class<ENTITY> type, ResponseMapper<ENTITY> mapper);
+ <ENTITY> Builder addRequestMapper(Class<ENTITY> type, RequestMapper<ENTITY> mapper);
+ <ENTITY> Builder registerJacksonResponseEntity(Class<ENTITY> type);
+ <ENTITY> Builder registerJacksonRequestEntity(Class<ENTITY> type);
Builder disableDefaultExceptionMappers();
Builder disableDefaultResponseMappers();
RestApi build();
@@ -39,28 +39,34 @@ public interface RestApi {
interface RouteBuilder {
RouteBuilder name(String name);
- RouteBuilder get(MethodHandler<?> handler);
- RouteBuilder post(MethodHandler<?> handler);
- RouteBuilder put(MethodHandler<?> handler);
- RouteBuilder delete(MethodHandler<?> handler);
- RouteBuilder patch(MethodHandler<?> handler);
- RouteBuilder defaultHandler(MethodHandler<?> handler);
+ RouteBuilder get(Handler<?> handler);
+ RouteBuilder post(Handler<?> handler);
+ <ENTITY> RouteBuilder post(Class<ENTITY> type, HandlerWithRequestEntity<?, ENTITY> handler);
+ RouteBuilder put(Handler<?> handler);
+ <ENTITY> RouteBuilder put(Class<ENTITY> type, HandlerWithRequestEntity<?, ENTITY> handler);
+ RouteBuilder delete(Handler<?> handler);
+ RouteBuilder patch(Handler<?> handler);
+ <ENTITY> RouteBuilder patch(Class<ENTITY> type, HandlerWithRequestEntity<?, ENTITY> handler);
+ RouteBuilder defaultHandler(Handler<?> handler);
+ <ENTITY> RouteBuilder defaultHandler(Class<ENTITY> type, HandlerWithRequestEntity<?, ENTITY> handler);
RouteBuilder addFilter(Filter filter);
}
- @FunctionalInterface interface ExceptionMapper<EXCEPTION extends RuntimeException> { HttpResponse toResponse(EXCEPTION exception, RequestContext context); }
+ @FunctionalInterface interface Handler<ENTITY> {
+ ENTITY handleRequest(RequestContext context) throws RestApiException;
+ }
- @FunctionalInterface interface MethodHandler<ENTITY> { ENTITY handleRequest(RequestContext context) throws RestApiException; }
+ @FunctionalInterface interface HandlerWithRequestEntity<RESPONSE_ENTITY, REQUEST_ENTITY> {
+ RESPONSE_ENTITY handleRequest(RequestContext context, REQUEST_ENTITY requestEntity) throws RestApiException;
+ }
- @FunctionalInterface interface ResponseMapper<ENTITY> { HttpResponse toHttpResponse(ENTITY responseEntity, RequestContext context); }
+ @FunctionalInterface interface ExceptionMapper<EXCEPTION extends RuntimeException> { HttpResponse toResponse(RequestContext context, EXCEPTION exception); }
- @FunctionalInterface interface Filter { HttpResponse filterRequest(FilterContext context); }
+ @FunctionalInterface interface ResponseMapper<ENTITY> { HttpResponse toHttpResponse(RequestContext context, ENTITY responseEntity) throws RestApiException; }
- /** Marker interface required for automatic serialization of Jackson response entities */
- interface JacksonResponseEntity {}
+ @FunctionalInterface interface RequestMapper<ENTITY> { Optional<ENTITY> toRequestEntity(RequestContext context) throws RestApiException; }
- /** Marker interface required for automatic serialization of Jackson request entities */
- interface JacksonRequestEntity {}
+ @FunctionalInterface interface Filter { HttpResponse filterRequest(FilterContext context); }
interface RequestContext {
HttpRequest request();
@@ -70,6 +76,7 @@ public interface RestApi {
Attributes attributes();
Optional<RequestContent> requestContent();
RequestContent requestContentOrThrow();
+ ObjectMapper jacksonJsonMapper();
interface Parameters {
Optional<String> getString(String name);
@@ -97,13 +104,7 @@ public interface RestApi {
interface RequestContent {
String contentType();
- InputStream inputStream();
- ObjectMapper jacksonJsonMapper();
- byte[] consumeByteArray();
- String consumeString();
- JsonNode consumeJsonNode();
- Slime consumeSlime();
- <T extends JacksonRequestEntity> T consumeJacksonEntity(Class<T> type);
+ InputStream content();
}
}
diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java
index af816f41411..316ec06ef52 100644
--- a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java
+++ b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java
@@ -1,18 +1,17 @@
// 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.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.jdisc.http.HttpRequest.Method;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.yolean.Exceptions;
import java.io.IOException;
import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -22,6 +21,8 @@ import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
/**
* @author bjorncs
*/
@@ -33,6 +34,7 @@ class RestApiImpl implements RestApi {
private final List<Route> routes;
private final List<ExceptionMapperHolder<?>> exceptionMappers;
private final List<ResponseMapperHolder<?>> responseMappers;
+ private final List<RequestMapperHolder<?>> requestMappers;
private final List<Filter> filters;
private final ObjectMapper jacksonJsonMapper;
@@ -45,6 +47,8 @@ class RestApiImpl implements RestApi {
builderImpl.exceptionMappers, Boolean.TRUE.equals(builderImpl.disableDefaultExceptionMappers));
this.responseMappers = combineWithDefaultResponseMappers(
builderImpl.responseMappers, jacksonJsonMapper, Boolean.TRUE.equals(builderImpl.disableDefaultResponseMappers));
+ this.requestMappers = combineWithDefaultRequestMappers(
+ builderImpl.requestMappers, jacksonJsonMapper);
this.filters = List.copyOf(builderImpl.filters);
this.jacksonJsonMapper = jacksonJsonMapper;
}
@@ -65,27 +69,53 @@ class RestApiImpl implements RestApi {
}
}
- @Override public ObjectMapper jacksonJsonMapper() { return jacksonJsonMapper; }
-
private HttpResponse dispatchToRoute(Route route, RequestContextImpl context) {
- RestApi.MethodHandler<?> resolvedHandler = route.handlerPerMethod.get(context.request().getMethod());
- if (resolvedHandler == null) {
- resolvedHandler = route.defaultHandler;
+ HandlerHolder<?> resolvedHandler = resolveHandler(context, 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.toHttpResponse(context, requestEntity);
+ } catch (RuntimeException e) {
+ return mapException(context, e);
}
- Object entity;
+ if (responseEntity == null) throw new NullPointerException("Handler must return non-null value");
+ ResponseMapperHolder<?> resolvedResponseMapper = resolveResponseMapper(responseEntity);
try {
- entity = resolvedHandler.handleRequest(context);
+ return resolvedResponseMapper.toHttpResponse(context, responseEntity);
} catch (RuntimeException e) {
- ExceptionMapperHolder<?> mapper = exceptionMappers.stream()
- .filter(holder -> holder.matches(e))
- .findFirst().orElseThrow(() -> e);
- return mapper.toResponse(e, context);
- }
- if (entity == null) throw new NullPointerException("Handler must return non-null value");
- ResponseMapperHolder<?> mapper = responseMappers.stream()
- .filter(holder -> holder.matches(entity))
- .findFirst().orElseThrow(() -> new IllegalStateException("No mapper configured for " + entity.getClass()));
- return mapper.toHttpResponse(entity, context);
+ return mapException(context, e);
+ }
+ }
+
+ private HandlerHolder<?> resolveHandler(RequestContextImpl context, Route route) {
+ HandlerHolder<?> resolvedHandler = route.handlerPerMethod.get(context.request().getMethod());
+ 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) {
@@ -120,7 +150,7 @@ class RestApiImpl implements RestApi {
List<ExceptionMapperHolder<?>> configuredExceptionMappers, boolean disableDefaultMappers) {
List<ExceptionMapperHolder<?>> exceptionMappers = new ArrayList<>(configuredExceptionMappers);
if (!disableDefaultMappers){
- exceptionMappers.add(new ExceptionMapperHolder<>(RestApiException.class, (exception, context) -> exception.response()));
+ exceptionMappers.add(new ExceptionMapperHolder<>(RestApiException.class, (context, exception) -> exception.response()));
}
return exceptionMappers;
}
@@ -129,19 +159,73 @@ class RestApiImpl implements RestApi {
List<ResponseMapperHolder<?>> configuredResponseMappers, ObjectMapper jacksonJsonMapper, boolean disableDefaultMappers) {
List<ResponseMapperHolder<?>> responseMappers = new ArrayList<>(configuredResponseMappers);
if (!disableDefaultMappers) {
- responseMappers.add(new ResponseMapperHolder<>(HttpResponse.class, (entity, context) -> entity));
- responseMappers.add(new ResponseMapperHolder<>(String.class, (entity, context) -> new MessageResponse(entity)));
- responseMappers.add(new ResponseMapperHolder<>(Slime.class, (entity, context) -> new SlimeJsonResponse(entity)));
- responseMappers.add(new ResponseMapperHolder<>(JsonNode.class, (entity, context) -> new JacksonJsonResponse<>(200, entity, jacksonJsonMapper, true)));
- responseMappers.add(new ResponseMapperHolder<>(RestApi.JacksonResponseEntity.class, (entity, context) -> new JacksonJsonResponse<>(200, entity, jacksonJsonMapper, true)));
+ responseMappers.add(new ResponseMapperHolder<>(HttpResponse.class, (context, entity) -> entity));
+ responseMappers.add(new ResponseMapperHolder<>(String.class, (context, entity) -> new MessageResponse(entity)));
+ responseMappers.add(new ResponseMapperHolder<>(Slime.class, (context, entity) -> new SlimeJsonResponse(entity)));
+ responseMappers.add(new ResponseMapperHolder<>(JsonNode.class, (context, entity) -> new JacksonJsonResponse<>(200, entity, jacksonJsonMapper, true)));
}
return responseMappers;
}
+ private static List<RequestMapperHolder<?>> combineWithDefaultRequestMappers(
+ List<RequestMapperHolder<?>> configuredRequestMappers, ObjectMapper jacksonJsonMapper) {
+ List<RequestMapperHolder<?>> requestMappers = new ArrayList<>(configuredRequestMappers);
+ requestMappers.add(new RequestMapperHolder<>(Slime.class, RestApiImpl::toSlime));
+ requestMappers.add(new RequestMapperHolder<>(JsonNode.class, ctx -> toJsonNode(ctx, jacksonJsonMapper)));
+ requestMappers.add(new RequestMapperHolder<>(String.class, RestApiImpl::toString));
+ requestMappers.add(new RequestMapperHolder<>(byte[].class, RestApiImpl::toByteArray));
+ requestMappers.add(new RequestMapperHolder<>(InputStream.class, RestApiImpl::toInputStream));
+ requestMappers.add(new RequestMapperHolder<>(Void.class, ctx -> Optional.empty()));
+ return requestMappers;
+ }
+
+ private static Optional<InputStream> toInputStream(RequestContext context) {
+ return context.requestContent().map(RequestContext.RequestContent::content);
+ }
+
+ private static Optional<byte[]> toByteArray(RequestContext context) {
+ InputStream in = toInputStream(context).orElse(null);
+ if (in == null) return Optional.empty();
+ return convertIoException(() -> Optional.of(in.readAllBytes()));
+ }
+
+ private static Optional<String> toString(RequestContext context) {
+ try {
+ return toByteArray(context).map(bytes -> new String(bytes, UTF_8));
+ } catch (RuntimeException e) {
+ throw new RestApiException.BadRequest("Failed parse request content as UTF-8: " + Exceptions.toMessageString(e), e);
+ }
+ }
+
+ private static Optional<JsonNode> toJsonNode(RequestContext context, ObjectMapper jacksonJsonMapper) {
+ if (log.isLoggable(Level.FINE)) {
+ return toString(context).map(string -> {
+ log.fine(() -> "Request content: " + string);
+ return convertIoException("Failed to parse JSON", () -> jacksonJsonMapper.readTree(string));
+ });
+ } else {
+ return toInputStream(context)
+ .map(in -> convertIoException("Invalid JSON", () -> jacksonJsonMapper.readTree(in)));
+ }
+ }
+
+ private static Optional<Slime> toSlime(RequestContext context) {
+ try {
+ return toString(context).map(string -> {
+ log.fine(() -> "Request content: " + string);
+ return SlimeUtils.jsonToSlimeOrThrow(string);
+ });
+ } catch (com.yahoo.slime.JsonParseException e) {
+ log.log(Level.FINE, e.getMessage(), e);
+ throw new RestApiException.BadRequest("Invalid JSON: " + Exceptions.toMessageString(e), e);
+ }
+ }
+
static class BuilderImpl implements RestApi.Builder {
private final List<Route> routes = new ArrayList<>();
private final List<ExceptionMapperHolder<?>> exceptionMappers = new ArrayList<>();
private final List<ResponseMapperHolder<?>> responseMappers = new ArrayList<>();
+ private final List<RequestMapperHolder<?>> requestMappers = new ArrayList<>();
private final List<RestApi.Filter> filters = new ArrayList<>();
private Route defaultRoute;
private ObjectMapper jacksonJsonMapper;
@@ -161,31 +245,73 @@ class RestApiImpl implements RestApi {
responseMappers.add(new ResponseMapperHolder<>(type, mapper)); return this;
}
+ @Override public <ENTITY> Builder addRequestMapper(Class<ENTITY> type, RequestMapper<ENTITY> mapper) {
+ requestMappers.add(new RequestMapperHolder<>(type, mapper)); return this;
+ }
+
+ @Override public <ENTITY> Builder registerJacksonResponseEntity(Class<ENTITY> type) {
+ addResponseMapper(type, new JacksonResponseMapper<>()); return this;
+ }
+
+ @Override public <ENTITY> Builder registerJacksonRequestEntity(Class<ENTITY> type) {
+ addRequestMapper(type, new JacksonRequestMapper<>(type)); return this;
+ }
+
@Override public Builder disableDefaultExceptionMappers() { this.disableDefaultExceptionMappers = true; return this; }
@Override public Builder disableDefaultResponseMappers() { this.disableDefaultResponseMappers = true; return this; }
+
@Override public RestApi build() { return new RestApiImpl(this); }
}
public static class RouteBuilderImpl implements RestApi.RouteBuilder {
private final String pathPattern;
private String name;
- private final Map<com.yahoo.jdisc.http.HttpRequest.Method, RestApi.MethodHandler<?>> handlerPerMethod = new HashMap<>();
+ private final Map<Method, HandlerHolder<?>> handlerPerMethod = new HashMap<>();
private final List<RestApi.Filter> filters = new ArrayList<>();
- private RestApi.MethodHandler<?> defaultHandler;
+ 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 get(RestApi.MethodHandler<?> handler) { return addHandler(com.yahoo.jdisc.http.HttpRequest.Method.GET, handler); }
- @Override public RestApi.RouteBuilder post(RestApi.MethodHandler<?> handler) { return addHandler(com.yahoo.jdisc.http.HttpRequest.Method.POST, handler); }
- @Override public RestApi.RouteBuilder put(RestApi.MethodHandler<?> handler) { return addHandler(com.yahoo.jdisc.http.HttpRequest.Method.PUT, handler); }
- @Override public RestApi.RouteBuilder delete(RestApi.MethodHandler<?> handler) { return addHandler(com.yahoo.jdisc.http.HttpRequest.Method.DELETE, handler); }
- @Override public RestApi.RouteBuilder patch(RestApi.MethodHandler<?> handler) { return addHandler(com.yahoo.jdisc.http.HttpRequest.Method.PATCH, handler); }
- @Override public RestApi.RouteBuilder defaultHandler(RestApi.MethodHandler<?> handler) { defaultHandler = handler; return this; }
+ @Override public RestApi.RouteBuilder get(Handler<?> handler) {
+ return addHandler(Method.GET, handler);
+ }
+ @Override public RestApi.RouteBuilder post(Handler<?> handler) {
+ return addHandler(Method.POST, handler);
+ }
+ @Override public <ENTITY> RouteBuilder post(Class<ENTITY> type, HandlerWithRequestEntity<?, ENTITY> handler) {
+ return addHandler(Method.POST, type, handler);
+ }
+ @Override public RestApi.RouteBuilder put(Handler<?> handler) {
+ return addHandler(Method.PUT, handler);
+ }
+ @Override public <ENTITY> RouteBuilder put(Class<ENTITY> type, HandlerWithRequestEntity<?, ENTITY> handler) {
+ return addHandler(Method.PUT, type, handler);
+ }
+ @Override public RestApi.RouteBuilder delete(Handler<?> handler) {
+ return addHandler(Method.DELETE, handler);
+ }
+ @Override public RestApi.RouteBuilder patch(Handler<?> handler) {
+ return addHandler(Method.PATCH, handler);
+ }
+ @Override public <ENTITY> RouteBuilder patch(Class<ENTITY> type, HandlerWithRequestEntity<?, ENTITY> handler) {
+ return addHandler(Method.PATCH, type, handler);
+ }
+ @Override public RestApi.RouteBuilder defaultHandler(Handler<?> handler) {
+ defaultHandler = HandlerHolder.of(handler); return this;
+ }
+ @Override public <ENTITY> RouteBuilder defaultHandler(Class<ENTITY> type, HandlerWithRequestEntity<?, ENTITY> handler) {
+ defaultHandler = HandlerHolder.of(type, handler); return this;
+ }
@Override public RestApi.RouteBuilder addFilter(RestApi.Filter filter) { filters.add(filter); return this; }
- private RestApi.RouteBuilder addHandler(com.yahoo.jdisc.http.HttpRequest.Method method, RestApi.MethodHandler<?> handler) {
- handlerPerMethod.put(method, handler); return this;
+ private RestApi.RouteBuilder addHandler(Method method, Handler<?> handler) {
+ handlerPerMethod.put(method, HandlerHolder.of(handler)); return this;
+ }
+
+ private <ENTITY> RestApi.RouteBuilder addHandler(
+ Method method, Class<ENTITY> type, HandlerWithRequestEntity<?, ENTITY> handler) {
+ handlerPerMethod.put(method, HandlerHolder.of(type, handler)); return this;
}
private Route build() { return new Route(this); }
@@ -217,6 +343,7 @@ class RestApiImpl implements RestApi {
@Override public RequestContent requestContentOrThrow() {
return requestContent().orElseThrow(() -> new RestApiException.BadRequest("Request content missing"));
}
+ @Override public ObjectMapper jacksonJsonMapper() { return jacksonJsonMapper; }
private class PathParametersImpl implements RestApi.RequestContext.PathParameters {
@Override
@@ -251,73 +378,13 @@ class RestApiImpl implements RestApi {
private class RequestContentImpl implements RestApi.RequestContext.RequestContent {
@Override public String contentType() { return request.getHeader("Content-Type"); }
- @Override public InputStream inputStream() { return request.getData(); }
- @Override public ObjectMapper jacksonJsonMapper() { return jacksonJsonMapper; }
- @Override public byte[] consumeByteArray() { return convertIoException(() -> inputStream().readAllBytes()); }
- @Override public String consumeString() { return new String(consumeByteArray(), StandardCharsets.UTF_8); }
-
- @Override
- public JsonNode consumeJsonNode() {
- return convertIoException(() -> {
- try {
- if (log.isLoggable(Level.FINE)) {
- String content = consumeString();
- log.fine(() -> "Request content: " + content);
- return jacksonJsonMapper.readTree(content);
- } else {
- return jacksonJsonMapper.readTree(request.getData());
- }
- } catch (com.fasterxml.jackson.core.JsonParseException e) {
- log.log(Level.FINE, e.getMessage(), e);
- throw new RestApiException.BadRequest("Invalid json request content: " + Exceptions.toMessageString(e), e);
- }
- });
- }
-
- @Override
- public Slime consumeSlime() {
- try {
- String content = consumeString();
- log.fine(() -> "Request content: " + content);
- return SlimeUtils.jsonToSlimeOrThrow(content);
- } catch (com.yahoo.slime.JsonParseException e) {
- log.log(Level.FINE, e.getMessage(), e);
- throw new RestApiException.BadRequest("Invalid json request content: " + Exceptions.toMessageString(e), e);
- }
- }
-
- @Override
- public <T extends JacksonRequestEntity> T consumeJacksonEntity(Class<T> type) {
- return convertIoException(() -> {
- try {
- if (log.isLoggable(Level.FINE)) {
- String content = consumeString();
- log.fine(() -> "Request content: " + content);
- return jacksonJsonMapper.readValue(content, type);
- } else {
- return jacksonJsonMapper.readValue(request.getData(), type);
- }
- } catch (com.fasterxml.jackson.core.JsonParseException | JsonMappingException e) {
- log.log(Level.FINE, e.getMessage(), e);
- throw new RestApiException.BadRequest("Invalid json request content: " + Exceptions.toMessageString(e), e);
- }
- });
- }
+ @Override public InputStream content() { return request.getData(); }
}
private class AttributesImpl implements RestApi.RequestContext.Attributes {
@Override public Optional<Object> 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); }
}
-
- @FunctionalInterface private interface SupplierThrowingIoException<T> { T get() throws IOException; }
- private static <T> T convertIoException(SupplierThrowingIoException<T> supplier) {
- try {
- return supplier.get();
- } catch (IOException e) {
- throw new RestApiException.InternalServerError("Failed to read request content: " + Exceptions.toMessageString(e), e);
- }
- }
}
private class FilterContextImpl implements RestApi.FilterContext {
@@ -357,8 +424,7 @@ class RestApiImpl implements RestApi {
this.mapper = mapper;
}
- boolean matches(RuntimeException e) { return type.isAssignableFrom(e.getClass()); }
- HttpResponse toResponse(RuntimeException e, RestApi.RequestContext context) { return mapper.toResponse(type.cast(e), context); }
+ HttpResponse toResponse(RestApi.RequestContext context, RuntimeException e) { return mapper.toResponse(context, type.cast(e)); }
}
private static class ResponseMapperHolder<ENTITY> {
@@ -370,16 +436,47 @@ class RestApiImpl implements RestApi {
this.mapper = mapper;
}
- boolean matches(Object entity) { return type.isAssignableFrom(entity.getClass()); }
- HttpResponse toHttpResponse(Object entity, RestApi.RequestContext context) { return mapper.toHttpResponse(type.cast(entity), context); }
+ HttpResponse toHttpResponse(RestApi.RequestContext context, Object entity) { return mapper.toHttpResponse(context, type.cast(entity)); }
}
+ private static class HandlerHolder<REQUEST_ENTITY> {
+ final Class<REQUEST_ENTITY> type;
+ final HandlerWithRequestEntity<?, REQUEST_ENTITY> handler;
+
+ HandlerHolder(Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<?, REQUEST_ENTITY> handler) {
+ this.type = type;
+ this.handler = handler;
+ }
+
+ static <RESPONSE_ENTITY, REQUEST_ENTITY> HandlerHolder<REQUEST_ENTITY> of(
+ Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<RESPONSE_ENTITY, REQUEST_ENTITY> handler) {
+ return new HandlerHolder<>(type, handler);
+ }
+
+ static <RESPONSE_ENTITY> HandlerHolder<Void> of(Handler<RESPONSE_ENTITY> handler) {
+ return new HandlerHolder<>(
+ Void.class,
+ (HandlerWithRequestEntity<RESPONSE_ENTITY, Void>) (context, nullEntity) -> handler.handleRequest(context));
+ }
+
+ Object toHttpResponse(RestApi.RequestContext context, Object entity) { return handler.handleRequest(context, type.cast(entity)); }
+ }
+
+ private static class RequestMapperHolder<ENTITY> {
+ final Class<ENTITY> type;
+ final RestApi.RequestMapper<ENTITY> mapper;
+
+ RequestMapperHolder(Class<ENTITY> type, RequestMapper<ENTITY> mapper) {
+ this.type = type;
+ this.mapper = mapper;
+ }
+ }
static class Route {
private final String pathPattern;
private final String name;
- private final Map<com.yahoo.jdisc.http.HttpRequest.Method, RestApi.MethodHandler<?>> handlerPerMethod;
- private final RestApi.MethodHandler<?> defaultHandler;
+ private final Map<Method, HandlerHolder<?>> handlerPerMethod;
+ private final HandlerHolder<?> defaultHandler;
private final List<Filter> filters;
private Route(RestApi.RouteBuilder builder) {
@@ -391,9 +488,48 @@ class RestApiImpl implements RestApi {
this.filters = List.copyOf(builderImpl.filters);
}
- private RestApi.MethodHandler<?> createDefaultMethodHandler() {
- return context -> { throw new RestApiException.MethodNotAllowed(context.request()); };
+ private HandlerHolder<?> createDefaultMethodHandler() {
+ return HandlerHolder.of(context -> { throw new RestApiException.MethodNotAllowed(context.request()); });
}
}
+ private static class JacksonRequestMapper<ENTITY> implements RequestMapper<ENTITY> {
+ private final Class<ENTITY> type;
+
+ JacksonRequestMapper(Class<ENTITY> type) { this.type = type; }
+
+ @Override
+ public Optional<ENTITY> toRequestEntity(RequestContext context) throws RestApiException {
+ if (log.isLoggable(Level.FINE)) {
+ return RestApiImpl.toString(context).map(string -> {
+ log.fine(() -> "Request content: " + string);
+ return convertIoException("Failed to parse JSON", () -> context.jacksonJsonMapper().readValue(string, type));
+ });
+ } else {
+ return RestApiImpl.toInputStream(context)
+ .map(in -> convertIoException("Invalid JSON", () -> context.jacksonJsonMapper().readValue(in, type)));
+ }
+ }
+ }
+
+ private static class JacksonResponseMapper<ENTITY> implements ResponseMapper<ENTITY> {
+ @Override
+ public HttpResponse toHttpResponse(RequestContext context, ENTITY responseEntity) throws RestApiException {
+ return new JacksonJsonResponse<>(200, responseEntity, context.jacksonJsonMapper(), true);
+ }
+ }
+
+ @FunctionalInterface private interface SupplierThrowingIoException<T> { T get() throws IOException; }
+ private static <T> T convertIoException(String messagePrefix, SupplierThrowingIoException<T> supplier) {
+ try {
+ return supplier.get();
+ } catch (IOException e) {
+ log.log(Level.FINE, e.getMessage(), e);
+ throw new RestApiException.InternalServerError(messagePrefix + ": " + Exceptions.toMessageString(e), e);
+ }
+ }
+
+ private static <T> T convertIoException(SupplierThrowingIoException<T> supplier) {
+ return convertIoException("Failed to read request content", supplier);
+ }
}
diff --git a/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java b/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java
index 16cc2353986..1de8184ce22 100644
--- a/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java
+++ b/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java
@@ -43,7 +43,7 @@ class RestApiImplTest {
@Test
void executes_filters_and_handler_in_correct_order() {
List<String> actualEvaluationOrdering = new ArrayList<>();
- RestApi.MethodHandler<HttpResponse> handler = context -> {
+ RestApi.Handler<HttpResponse> handler = context -> {
actualEvaluationOrdering.add("handler");
return new MessageResponse("get-method-response");
};
@@ -83,8 +83,8 @@ class RestApiImplTest {
.disableDefaultResponseMappers()
.addRoute(route("/long").get(ctx -> 123456L))
.addRoute(route("/exception").get(ctx -> 123L / 0L))
- .addResponseMapper(Long.class, (entity, ctx) -> new MessageResponse("long value is " + entity))
- .addExceptionMapper(ArithmeticException.class, (exception, ctx) -> ErrorResponse.internalServerError("oops division by zero"))
+ .addResponseMapper(Long.class, (ctx, entity) -> new MessageResponse("long value is " + entity))
+ .addExceptionMapper(ArithmeticException.class, (ctx, exception) -> ErrorResponse.internalServerError("oops division by zero"))
.build();
verifyJsonResponse(restApi, Method.GET, "/long", null, 200, "{\"message\":\"long value is 123456\"}");
verifyJsonResponse(restApi, Method.GET, "/exception", null, 500, "{\"message\":\"oops division by zero\", \"error-code\":\"INTERNAL_SERVER_ERROR\"}");
@@ -92,9 +92,11 @@ class RestApiImplTest {
@Test
void method_handler_can_consume_and_produce_json() {
+ RestApi.HandlerWithRequestEntity<TestEntity, TestEntity> handler = (context, requestEntity) -> requestEntity;
RestApi restApi = RestApi.builder()
- .addRoute(route("/api").post(
- ctx -> ctx.requestContent().get().consumeJacksonEntity(TestEntity.class)))
+ .registerJacksonRequestEntity(TestEntity.class)
+ .registerJacksonResponseEntity(TestEntity.class)
+ .addRoute(route("/api").post(TestEntity.class, handler))
.build();
String rawJson = "{\"mystring\":\"my-string-value\", \"myinstant\":\"2000-01-01T00:00:00Z\"}";
verifyJsonResponse(restApi, Method.POST, "/api", rawJson, 200, rawJson);
@@ -118,7 +120,7 @@ class RestApiImplTest {
}
}
- public static class TestEntity implements RestApi.JacksonRequestEntity, RestApi.JacksonResponseEntity {
+ public static class TestEntity {
@JsonProperty("mystring") public String stringValue;
@JsonProperty("myinstant") public Instant instantValue;
}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java
index 9cc21925f52..e6b74d9df4c 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/bindings/SignedIdentityDocumentEntity.java
@@ -4,7 +4,6 @@ package com.yahoo.vespa.athenz.identityprovider.api.bindings;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.restapi.RestApi;
import java.time.Instant;
import java.util.Objects;
@@ -14,7 +13,7 @@ import java.util.Set;
* @author bjorncs
*/
@JsonIgnoreProperties(ignoreUnknown = true)
-public class SignedIdentityDocumentEntity implements RestApi.JacksonResponseEntity {
+public class SignedIdentityDocumentEntity {
@JsonProperty("signature") public final String signature;
@JsonProperty("signing-key-version") public final int signingKeyVersion;