aboutsummaryrefslogtreecommitdiffstats
path: root/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java
blob: 9fbbb03fa09d7baf9bae1e9014f59982b215a604 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// 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<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()));

    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)));

    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);
        }
    }

    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 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));
        }
    }

    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);
        }
    }

}