Skip to content

Commit 8e8a70b

Browse files
committed
feat!: Replace JsonMapper with bodyMapper(mediaType, TypeMapper) + Gson fallback
1 parent 86149d2 commit 8e8a70b

11 files changed

Lines changed: 232 additions & 161 deletions

src/main/java/com/retailsvc/http/JsonMapper.java

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/main/java/com/retailsvc/http/OpenApiServer.java

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
import com.retailsvc.http.internal.DispatchHandler;
88
import com.retailsvc.http.internal.ExceptionFilter;
9+
import com.retailsvc.http.internal.FormTypeMapper;
910
import com.retailsvc.http.internal.RequestPreparationFilter;
1011
import com.retailsvc.http.internal.Router;
12+
import com.retailsvc.http.internal.TextTypeMapper;
1113
import com.retailsvc.http.spec.Spec;
1214
import com.retailsvc.http.validate.DefaultValidator;
1315
import com.sun.net.httpserver.HttpContext;
@@ -30,47 +32,16 @@ public class OpenApiServer implements AutoCloseable {
3032

3133
private static final Logger LOG = LoggerFactory.getLogger(OpenApiServer.class);
3234
private static final int DEFAULT_PORT = 8080;
35+
private static final String JSON = "application/json";
36+
private static final String GSON_CLASS = "com.google.gson.Gson";
37+
private static final String GSON_MAPPER_CLASS = "com.retailsvc.http.internal.gson.GsonJsonMapper";
3338

3439
private final HttpServer httpServer;
3540
private final int shutdownTimeoutSeconds;
3641

37-
/**
38-
* @param spec The parsed {@link Spec}
39-
* @param jsonMapper Body deserializer
40-
* @param handlers Mappings between operationId and {@link HttpHandler}
41-
* @param exceptionHandler Error handler receiving exceptions thrown from a handler
42-
* @throws IOException If an error occurs during server start
43-
*/
44-
public OpenApiServer(
45-
Spec spec,
46-
JsonMapper jsonMapper,
47-
Map<String, HttpHandler> handlers,
48-
ExceptionHandler exceptionHandler)
49-
throws IOException {
50-
this(spec, jsonMapper, handlers, exceptionHandler, DEFAULT_PORT, Map.of(), 0);
51-
}
52-
53-
/**
54-
* @param spec The parsed {@link Spec}
55-
* @param jsonMapper Body deserializer
56-
* @param handlers Mappings between operationId and {@link HttpHandler}
57-
* @param exceptionHandler Error handler receiving exceptions thrown from a handler
58-
* @param port The server port to use
59-
* @throws IOException If an error occurs during server start
60-
*/
61-
public OpenApiServer(
62-
Spec spec,
63-
JsonMapper jsonMapper,
64-
Map<String, HttpHandler> handlers,
65-
ExceptionHandler exceptionHandler,
66-
int port)
67-
throws IOException {
68-
this(spec, jsonMapper, handlers, exceptionHandler, port, Map.of(), 0);
69-
}
70-
7142
OpenApiServer(
7243
Spec spec,
73-
JsonMapper jsonMapper,
44+
Map<String, TypeMapper> bodyMappers,
7445
Map<String, HttpHandler> handlers,
7546
ExceptionHandler exceptionHandler,
7647
int port,
@@ -79,7 +50,7 @@ public OpenApiServer(
7950
throws IOException {
8051

8152
requireNonNull(spec, "Spec must not be null");
82-
requireNonNull(jsonMapper, "JsonMapper must not be null");
53+
requireNonNull(bodyMappers, "bodyMappers must not be null");
8354
requireNonNull(handlers, "handlers must not be null");
8455
if (exceptionHandler == null) {
8556
LOG.warn("No ExceptionHandler set, using default");
@@ -95,7 +66,7 @@ public OpenApiServer(
9566

9667
HttpContext ctx = httpServer.createContext(Optional.ofNullable(spec.basePath()).orElse("/"));
9768
ctx.getFilters().add(new ExceptionFilter(exceptionHandler));
98-
ctx.getFilters().add(new RequestPreparationFilter(spec, router, validator, jsonMapper));
69+
ctx.getFilters().add(new RequestPreparationFilter(spec, router, validator, bodyMappers));
9970
ctx.setHandler(new DispatchHandler(handlers));
10071

10172
for (Map.Entry<String, HttpHandler> e : extras.entrySet()) {
@@ -144,7 +115,7 @@ public static Builder builder() {
144115
public static final class Builder {
145116

146117
private Spec spec;
147-
private JsonMapper jsonMapper;
118+
private final LinkedHashMap<String, TypeMapper> bodyMappers = new LinkedHashMap<>();
148119
private Map<String, HttpHandler> handlers;
149120
private ExceptionHandler exceptionHandler;
150121
private int port = DEFAULT_PORT;
@@ -158,8 +129,10 @@ public Builder spec(Spec spec) {
158129
return this;
159130
}
160131

161-
public Builder jsonMapper(JsonMapper jsonMapper) {
162-
this.jsonMapper = jsonMapper;
132+
public Builder bodyMapper(String mediaType, TypeMapper mapper) {
133+
requireNonNull(mediaType, "mediaType must not be null");
134+
requireNonNull(mapper, "mapper must not be null");
135+
bodyMappers.put(mediaType.toLowerCase(java.util.Locale.ROOT), mapper);
163136
return this;
164137
}
165138

@@ -204,7 +177,6 @@ public Builder addHandler(String path, HttpHandler handler) {
204177

205178
public OpenApiServer build() throws IOException {
206179
requireNonNull(spec, "Spec must not be null");
207-
requireNonNull(jsonMapper, "JsonMapper must not be null");
208180
requireNonNull(handlers, "handlers must not be null");
209181
String basePath = Optional.ofNullable(spec.basePath()).orElse("/");
210182
for (String path : extras.keySet()) {
@@ -213,8 +185,43 @@ public OpenApiServer build() throws IOException {
213185
"extra handler path " + path + " conflicts with spec basePath " + basePath);
214186
}
215187
}
188+
Map<String, TypeMapper> resolved = resolveBodyMappers(bodyMappers);
216189
return new OpenApiServer(
217-
spec, jsonMapper, handlers, exceptionHandler, port, extras, shutdownTimeoutSeconds);
190+
spec, resolved, handlers, exceptionHandler, port, extras, shutdownTimeoutSeconds);
191+
}
192+
193+
private static Map<String, TypeMapper> resolveBodyMappers(
194+
Map<String, TypeMapper> userSupplied) {
195+
LinkedHashMap<String, TypeMapper> out = new LinkedHashMap<>();
196+
out.put("application/x-www-form-urlencoded", new FormTypeMapper());
197+
out.put("text/plain", new TextTypeMapper());
198+
out.putAll(userSupplied);
199+
if (!out.containsKey(JSON)) {
200+
TypeMapper fallback = tryLoadGsonMapper();
201+
if (fallback != null) {
202+
out.put(JSON, fallback);
203+
}
204+
}
205+
if (!out.containsKey(JSON)) {
206+
throw new IllegalStateException(
207+
"No TypeMapper registered for application/json and Gson not found on classpath; "
208+
+ "register one via Builder.bodyMapper(\"application/json\", ...)");
209+
}
210+
return out;
211+
}
212+
213+
private static TypeMapper tryLoadGsonMapper() {
214+
try {
215+
Class.forName(GSON_CLASS, false, OpenApiServer.class.getClassLoader());
216+
} catch (ClassNotFoundException _) {
217+
return null;
218+
}
219+
try {
220+
Class<?> cls = Class.forName(GSON_MAPPER_CLASS, true, OpenApiServer.class.getClassLoader());
221+
return (TypeMapper) cls.getDeclaredConstructor().newInstance();
222+
} catch (ReflectiveOperationException e) {
223+
throw new IllegalStateException("Failed to load " + GSON_MAPPER_CLASS, e);
224+
}
218225
}
219226
}
220227
}

src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.retailsvc.http.internal;
22

3-
import com.retailsvc.http.JsonMapper;
43
import com.retailsvc.http.MethodNotAllowedException;
54
import com.retailsvc.http.NotFoundException;
65
import com.retailsvc.http.Request;
6+
import com.retailsvc.http.TypeMapper;
77
import com.retailsvc.http.ValidationException;
88
import com.retailsvc.http.spec.HttpMethod;
99
import com.retailsvc.http.spec.MediaType;
@@ -26,16 +26,14 @@ public final class RequestPreparationFilter extends Filter {
2626
private final Spec spec;
2727
private final Router router;
2828
private final Validator validator;
29-
private final JsonMapper jsonMapper;
30-
private final FormUrlEncodedParser formParser = new FormUrlEncodedParser();
31-
private final TextPlainParser textParser = new TextPlainParser();
29+
private final Map<String, TypeMapper> bodyMappers;
3230

3331
public RequestPreparationFilter(
34-
Spec spec, Router router, Validator validator, JsonMapper jsonMapper) {
32+
Spec spec, Router router, Validator validator, Map<String, TypeMapper> bodyMappers) {
3533
this.spec = spec;
3634
this.router = router;
3735
this.validator = validator;
38-
this.jsonMapper = jsonMapper;
36+
this.bodyMappers = Map.copyOf(bodyMappers);
3937
}
4038

4139
@Override
@@ -150,13 +148,18 @@ private Object validateAndParseBody(HttpExchange exchange, Operation op, byte[]
150148
new ValidationError(
151149
"/body", "content-type", "unsupported content type: " + mediaType, null));
152150
}
153-
Object parsed =
154-
switch (mediaType) {
155-
case "application/x-www-form-urlencoded" ->
156-
FormBodyCoercion.coerce(formParser.parse(body, header), mt.schema());
157-
case "text/plain" -> textParser.parse(body, header);
158-
default -> jsonMapper.mapFrom(body);
159-
};
151+
TypeMapper mapper = bodyMappers.get(mediaType);
152+
if (mapper == null) {
153+
throw new ValidationException(
154+
new ValidationError(
155+
"/body", "content-type", "unsupported content type: " + mediaType, null));
156+
}
157+
Object parsed = mapper.readFrom(body, header);
158+
if (mediaType.equals("application/x-www-form-urlencoded") && parsed instanceof Map<?, ?> map) {
159+
@SuppressWarnings("unchecked")
160+
Map<String, Object> typed = (Map<String, Object>) map;
161+
parsed = FormBodyCoercion.coerce(typed, mt.schema());
162+
}
160163
validator.validate(parsed, mt.schema(), "");
161164
return parsed;
162165
}

src/test/java/com/retailsvc/http/ExtraHandlersIT.java

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22

33
import static com.retailsvc.http.Handlers.defaultExceptionHandler;
44
import static org.assertj.core.api.Assertions.assertThat;
5-
import static org.junit.jupiter.api.Assertions.fail;
65

7-
import com.sun.net.httpserver.HttpHandler;
8-
import java.io.IOException;
96
import java.net.URI;
107
import java.net.http.HttpRequest;
118
import java.net.http.HttpResponse.BodyHandlers;
@@ -19,7 +16,6 @@ void aliveExtraReturns204AndBypassesValidation() throws Exception {
1916
try (var s =
2017
OpenApiServer.builder()
2118
.spec(spec)
22-
.jsonMapper(jsonMapper())
2319
.handlers(Map.of())
2420
.exceptionHandler(defaultExceptionHandler())
2521
.port(0)
@@ -44,7 +40,6 @@ void specHandlerServesClasspathResource() throws Exception {
4440
try (var s =
4541
OpenApiServer.builder()
4642
.spec(spec)
47-
.jsonMapper(jsonMapper())
4843
.handlers(Map.of())
4944
.exceptionHandler(defaultExceptionHandler())
5045
.port(0)
@@ -67,15 +62,14 @@ void specHandlerServesClasspathResource() throws Exception {
6762

6863
@Test
6964
void extraHandlerExceptionFlowsThroughExceptionHandler() throws Exception {
70-
HttpHandler boom =
65+
com.sun.net.httpserver.HttpHandler boom =
7166
ex -> {
7267
throw new RuntimeException("boom");
7368
};
7469

7570
try (var s =
7671
OpenApiServer.builder()
7772
.spec(spec)
78-
.jsonMapper(jsonMapper())
7973
.handlers(Map.of())
8074
.exceptionHandler(defaultExceptionHandler())
8175
.port(0)
@@ -93,15 +87,4 @@ void extraHandlerExceptionFlowsThroughExceptionHandler() throws Exception {
9387
assertThat(resp.statusCode()).isEqualTo(500);
9488
}
9589
}
96-
97-
@Test
98-
void existingPublicConstructorStillWorks() {
99-
try {
100-
try (var s = new OpenApiServer(spec, jsonMapper(), Map.of(), defaultExceptionHandler(), 0)) {
101-
assertThat(s.listenPort()).isGreaterThan(0);
102-
}
103-
} catch (IOException io) {
104-
fail(io);
105-
}
106-
}
10790
}

src/test/java/com/retailsvc/http/JsonMapperTest.java

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)