diff options
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; |