
com.yahoo.restapi.RestApiMappers Maven / Gradle / Ivy
// Copyright Yahoo. 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());
static List> 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()));
static List> 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)));
static List> DEFAULT_EXCEPTION_MAPPERS = List.of(
new ExceptionMapperHolder<>(RestApiException.class, (context, exception) -> exception.response()));
private RestApiMappers() {}
public static class JacksonRequestMapper implements RequestMapper {
private final Class type;
JacksonRequestMapper(Class type) { this.type = type; }
@Override
public Optional 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 implements ResponseMapper {
@Override
public HttpResponse toHttpResponse(RestApi.RequestContext context, ENTITY responseEntity) throws RestApiException {
return new JacksonJsonResponse<>(200, responseEntity, context.jacksonJsonMapper(), true);
}
}
static class RequestMapperHolder {
final Class type;
final RestApi.RequestMapper mapper;
RequestMapperHolder(Class type, RequestMapper mapper) {
this.type = type;
this.mapper = mapper;
}
}
static class ResponseMapperHolder {
final Class type;
final RestApi.ResponseMapper mapper;
ResponseMapperHolder(Class type, RestApi.ResponseMapper mapper) {
this.type = type;
this.mapper = mapper;
}
HttpResponse toHttpResponse(RestApi.RequestContext ctx, Object entity) {
return mapper.toHttpResponse(ctx, type.cast(entity));
}
}
static class ExceptionMapperHolder {
final Class type;
final RestApi.ExceptionMapper mapper;
ExceptionMapperHolder(Class type, RestApi.ExceptionMapper mapper) {
this.type = type;
this.mapper = mapper;
}
HttpResponse toResponse(RestApi.RequestContext ctx, RuntimeException e) {
return mapper.toResponse(ctx, type.cast(e));
}
}
private static Optional toInputStream(RestApi.RequestContext context) {
return context.requestContent().map(RestApi.RequestContext.RequestContent::content);
}
private static Optional 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 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 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 get() throws IOException; }
private static T convertIoException(String messagePrefix, SupplierThrowingIoException 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 convertIoException(SupplierThrowingIoException supplier) {
return convertIoException("Failed to read request content", supplier);
}
private static Optional 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);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy