aboutsummaryrefslogtreecommitdiffstats
path: root/container-core/src/main/java/com/yahoo/restapi
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@verizonmedia.com>2021-07-14 15:57:08 +0200
committerBjørn Christian Seime <bjorncs@verizonmedia.com>2021-07-14 15:57:08 +0200
commit45b8927aded1633b648bc3acb1f9fdf8ad53e3bc (patch)
treec5e55dd4a52e8b6cfc34f7b0772a458c2cdf6c48 /container-core/src/main/java/com/yahoo/restapi
parent8d2e351b21bc418b90d8ea56be3265e54da85137 (diff)
Move mapper implementations to RestApiMappers
Diffstat (limited to 'container-core/src/main/java/com/yahoo/restapi')
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApi.java5
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java153
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java172
3 files changed, 189 insertions, 141 deletions
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 f438dd66cf1..2ef14679553 100644
--- a/container-core/src/main/java/com/yahoo/restapi/RestApi.java
+++ b/container-core/src/main/java/com/yahoo/restapi/RestApi.java
@@ -35,12 +35,17 @@ public interface RestApi {
Builder setDefaultRoute(RouteBuilder route);
Builder addRoute(RouteBuilder route);
Builder addFilter(Filter filter);
+ /** see {@link RestApiMappers#DEFAULT_EXCEPTION_MAPPERS} for default mappers */
<EXCEPTION extends RuntimeException> Builder addExceptionMapper(Class<EXCEPTION> type, ExceptionMapper<EXCEPTION> mapper);
+ /** see {@link RestApiMappers#DEFAULT_RESPONSE_MAPPERS} for default mappers */
<RESPONSE_ENTITY> Builder addResponseMapper(Class<RESPONSE_ENTITY> type, ResponseMapper<RESPONSE_ENTITY> mapper);
+ /** see {@link RestApiMappers#DEFAULT_REQUEST_MAPPERS} for default mappers */
<REQUEST_ENTITY> Builder addRequestMapper(Class<REQUEST_ENTITY> type, RequestMapper<REQUEST_ENTITY> mapper);
<RESPONSE_ENTITY> Builder registerJacksonResponseEntity(Class<RESPONSE_ENTITY> type);
<REQUEST_ENTITY> Builder registerJacksonRequestEntity(Class<REQUEST_ENTITY> type);
+ /** Disables mappers listed in {@link RestApiMappers#DEFAULT_EXCEPTION_MAPPERS} */
Builder disableDefaultExceptionMappers();
+ /** Disables mappers listed in {@link RestApiMappers#DEFAULT_RESPONSE_MAPPERS} */
Builder disableDefaultResponseMappers();
Builder disableDefaultAclMapping();
RestApi build();
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 115780c9833..646177e60db 100644
--- a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java
+++ b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java
@@ -1,7 +1,6 @@
// 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.AclMapping;
import com.yahoo.container.jdisc.HttpRequest;
@@ -9,11 +8,10 @@ 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.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.yolean.Exceptions;
+import com.yahoo.restapi.RestApiMappers.ExceptionMapperHolder;
+import com.yahoo.restapi.RestApiMappers.RequestMapperHolder;
+import com.yahoo.restapi.RestApiMappers.ResponseMapperHolder;
-import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
@@ -25,8 +23,6 @@ import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
/**
* @author bjorncs
*/
@@ -51,9 +47,8 @@ class RestApiImpl implements RestApi {
this.exceptionMappers = combineWithDefaultExceptionMappers(
builderImpl.exceptionMappers, Boolean.TRUE.equals(builderImpl.disableDefaultExceptionMappers));
this.responseMappers = combineWithDefaultResponseMappers(
- builderImpl.responseMappers, jacksonJsonMapper, Boolean.TRUE.equals(builderImpl.disableDefaultResponseMappers));
- this.requestMappers = combineWithDefaultRequestMappers(
- builderImpl.requestMappers, jacksonJsonMapper);
+ 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);
@@ -184,7 +179,7 @@ class RestApiImpl implements RestApi {
List<ExceptionMapperHolder<?>> configuredExceptionMappers, boolean disableDefaultMappers) {
List<ExceptionMapperHolder<?>> exceptionMappers = new ArrayList<>(configuredExceptionMappers);
if (!disableDefaultMappers){
- exceptionMappers.add(new ExceptionMapperHolder<>(RestApiException.class, (context, exception) -> exception.response()));
+ 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));
@@ -192,71 +187,21 @@ class RestApiImpl implements RestApi {
}
private static List<ResponseMapperHolder<?>> combineWithDefaultResponseMappers(
- List<ResponseMapperHolder<?>> configuredResponseMappers, ObjectMapper jacksonJsonMapper, boolean disableDefaultMappers) {
+ List<ResponseMapperHolder<?>> configuredResponseMappers, boolean disableDefaultMappers) {
List<ResponseMapperHolder<?>> responseMappers = new ArrayList<>(configuredResponseMappers);
if (!disableDefaultMappers) {
- 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)));
+ responseMappers.addAll(RestApiMappers.DEFAULT_RESPONSE_MAPPERS);
}
return responseMappers;
}
private static List<RequestMapperHolder<?>> combineWithDefaultRequestMappers(
- List<RequestMapperHolder<?>> configuredRequestMappers, ObjectMapper jacksonJsonMapper) {
+ List<RequestMapperHolder<?>> configuredRequestMappers) {
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()));
+ requestMappers.addAll(RestApiMappers.DEFAULT_REQUEST_MAPPERS);
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<>();
@@ -287,11 +232,11 @@ class RestApiImpl implements RestApi {
}
@Override public <ENTITY> Builder registerJacksonResponseEntity(Class<ENTITY> type) {
- addResponseMapper(type, new JacksonResponseMapper<>()); return this;
+ addResponseMapper(type, new RestApiMappers.JacksonResponseMapper<>()); return this;
}
@Override public <ENTITY> Builder registerJacksonRequestEntity(Class<ENTITY> type) {
- addRequestMapper(type, new JacksonRequestMapper<>(type)); return this;
+ addRequestMapper(type, new RestApiMappers.JacksonRequestMapper<>(type)); return this;
}
@Override public Builder disableDefaultExceptionMappers() { this.disableDefaultExceptionMappers = true; return this; }
@@ -534,31 +479,6 @@ class RestApiImpl implements RestApi {
return dispatchToRoute(route, requestContext);
}
}
-
- }
-
- private static class ExceptionMapperHolder<EXCEPTION extends RuntimeException> {
- final Class<EXCEPTION> type;
- final RestApi.ExceptionMapper<EXCEPTION> mapper;
-
- ExceptionMapperHolder(Class<EXCEPTION> type, RestApi.ExceptionMapper<EXCEPTION> mapper) {
- this.type = type;
- this.mapper = mapper;
- }
-
- HttpResponse toResponse(RestApi.RequestContext context, RuntimeException e) { return mapper.toResponse(context, type.cast(e)); }
- }
-
- private static class ResponseMapperHolder<ENTITY> {
- final Class<ENTITY> type;
- final RestApi.ResponseMapper<ENTITY> mapper;
-
- ResponseMapperHolder(Class<ENTITY> type, RestApi.ResponseMapper<ENTITY> mapper) {
- this.type = type;
- this.mapper = mapper;
- }
-
- HttpResponse toHttpResponse(RestApi.RequestContext context, Object entity) { return mapper.toHttpResponse(context, type.cast(entity)); }
}
private static class HandlerHolder<REQUEST_ENTITY> {
@@ -592,16 +512,6 @@ class RestApiImpl implements RestApi {
Object executeHandler(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;
@@ -625,43 +535,4 @@ class RestApiImpl implements RestApi {
}
}
- 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/main/java/com/yahoo/restapi/RestApiMappers.java b/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java
new file mode 100644
index 00000000000..36d98421e6a
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java
@@ -0,0 +1,172 @@
+// 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.HttpResponse;
+import com.yahoo.restapi.RestApi.ExceptionMapper;
+import com.yahoo.restapi.RestApi.RequestMapper;
+import com.yahoo.restapi.RestApi.ResponseMapper;
+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.util.List;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Implementations of {@link ExceptionMapper}, {@link RequestMapper} and {@link ResponseMapper}.
+ *
+ * @author bjorncs
+ */
+public class RestApiMappers {
+
+ private static final Logger log = Logger.getLogger(RestApiMappers.class.getName());
+
+ public static List<RequestMapperHolder<?>> DEFAULT_REQUEST_MAPPERS = List.of(
+ new RequestMapperHolder<>(Slime.class, RestApiMappers::toSlime),
+ new RequestMapperHolder<>(JsonNode.class, ctx -> toJsonNode(ctx, ctx.jacksonJsonMapper())),
+ new RequestMapperHolder<>(String.class, RestApiMappers::toString),
+ new RequestMapperHolder<>(byte[].class, RestApiMappers::toByteArray),
+ new RequestMapperHolder<>(InputStream .class, RestApiMappers::toInputStream),
+ new RequestMapperHolder<>(Void.class, ctx -> Optional.empty()));
+
+ public static List<ResponseMapperHolder<?>> DEFAULT_RESPONSE_MAPPERS = List.of(
+ new ResponseMapperHolder<>(HttpResponse.class, (context, entity) -> entity),
+ new ResponseMapperHolder<>(String.class, (context, entity) -> new MessageResponse(entity)),
+ new ResponseMapperHolder<>(Slime.class, (context, entity) -> new SlimeJsonResponse(entity)),
+ new ResponseMapperHolder<>(JsonNode.class,
+ (context, entity) -> new JacksonJsonResponse<>(200, entity, context.jacksonJsonMapper(), true)));
+
+ public static List<ExceptionMapperHolder<?>> DEFAULT_EXCEPTION_MAPPERS = List.of(
+ new ExceptionMapperHolder<>(RestApiException.class, (context, exception) -> exception.response()));
+
+ private RestApiMappers() {}
+
+ public static class JacksonRequestMapper<ENTITY> implements RequestMapper<ENTITY> {
+ private final Class<ENTITY> type;
+
+ JacksonRequestMapper(Class<ENTITY> type) { this.type = type; }
+
+ @Override
+ public Optional<ENTITY> toRequestEntity(RestApi.RequestContext context) throws RestApiException {
+ if (log.isLoggable(Level.FINE)) {
+ return RestApiMappers.toString(context).map(string -> {
+ log.fine(() -> "Request content: " + string);
+ return convertIoException("Failed to parse JSON", () -> context.jacksonJsonMapper().readValue(string, type));
+ });
+ } else {
+ return toInputStream(context)
+ .map(in -> convertIoException("Invalid JSON", () -> context.jacksonJsonMapper().readValue(in, type)));
+ }
+ }
+ }
+
+ public static class JacksonResponseMapper<ENTITY> implements ResponseMapper<ENTITY> {
+ @Override
+ public HttpResponse toHttpResponse(RestApi.RequestContext context, ENTITY responseEntity) throws RestApiException {
+ return new JacksonJsonResponse<>(200, responseEntity, context.jacksonJsonMapper(), true);
+ }
+ }
+
+ public 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;
+ }
+ }
+
+ public static class ResponseMapperHolder<ENTITY> {
+ final Class<ENTITY> type;
+ final RestApi.ResponseMapper<ENTITY> mapper;
+
+ ResponseMapperHolder(Class<ENTITY> type, RestApi.ResponseMapper<ENTITY> mapper) {
+ this.type = type;
+ this.mapper = mapper;
+ }
+
+ HttpResponse toHttpResponse(RestApi.RequestContext ctx, Object entity) {
+ return mapper.toHttpResponse(ctx, type.cast(entity));
+ }
+ }
+
+ public static class ExceptionMapperHolder<EXCEPTION extends RuntimeException> {
+ final Class<EXCEPTION> type;
+ final RestApi.ExceptionMapper<EXCEPTION> mapper;
+
+ ExceptionMapperHolder(Class<EXCEPTION> type, RestApi.ExceptionMapper<EXCEPTION> mapper) {
+ this.type = type;
+ this.mapper = mapper;
+ }
+
+ HttpResponse toResponse(RestApi.RequestContext ctx, RuntimeException e) {
+ return mapper.toResponse(ctx, type.cast(e));
+ }
+ }
+
+ private static Optional<InputStream> toInputStream(RestApi.RequestContext context) {
+ return context.requestContent().map(RestApi.RequestContext.RequestContent::content);
+ }
+
+ private static Optional<byte[]> toByteArray(RestApi.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(RestApi.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(RestApi.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)));
+ }
+ }
+
+ @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);
+ }
+
+ private static Optional<Slime> toSlime(RestApi.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);
+ }
+ }
+
+}