pre-major refactoring + ext api.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# flash-ext-jackson
|
||||
|
||||
Jackson JSON integration for the Flash HTTP server.
|
||||
|
||||
## What it provides
|
||||
|
||||
| Component | Description |
|
||||
|---|---|
|
||||
| `JacksonExtension` | Installs Jackson into the extension layer |
|
||||
| `JacksonHandler` | Base class for handlers that need JSON I/O |
|
||||
| Global exception handler | Maps `HttpException` → JSON error; all other exceptions → 500 |
|
||||
|
||||
## Installation
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>dev.relism</groupId>
|
||||
<artifactId>flash-ext-jackson</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Install before any extension that needs JSON (e.g. `flash-ext-openapi`, `flash-ext-oidc`):
|
||||
|
||||
```java
|
||||
FlashApp.of(new HttpServer(config))
|
||||
.install(new JacksonExtension())
|
||||
// ... other extensions
|
||||
```
|
||||
|
||||
### Custom mapper
|
||||
|
||||
```java
|
||||
ObjectMapper mapper = JsonMapper.builder()
|
||||
.addModule(new JavaTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
.build();
|
||||
|
||||
FlashApp.of(new HttpServer(config))
|
||||
.install(new JacksonExtension(mapper));
|
||||
```
|
||||
|
||||
## JacksonHandler
|
||||
|
||||
Extend `JacksonHandler` to get `bodyAs` and `json` helpers without constructor boilerplate.
|
||||
The `ObjectMapper` is injected once at startup and shared across all subclasses.
|
||||
|
||||
```java
|
||||
@Route(method = HttpMethod.POST, path = "/api/blogs")
|
||||
@ApiOperation(summary = "Create blog")
|
||||
@ApiResponse(status = 201, description = "Created", schema = Blog.class)
|
||||
@ApiResponse(status = 400, description = "Invalid body")
|
||||
public class CreateBlog extends JacksonHandler {
|
||||
|
||||
@Override
|
||||
public Object handle(Request req, Response res) throws Exception {
|
||||
CreateBlogRequest body = bodyAs(req, CreateBlogRequest.class);
|
||||
Blog created = service.create(body);
|
||||
res.setStatusCode(201);
|
||||
return json(res, created); // sets Content-Type: application/json
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `bodyAs(req, Type.class)` | Deserializes the request body; throws `HttpException` 400 on parse errors |
|
||||
| `json(res, obj)` | Serializes `obj`, sets `Content-Type: application/json`, returns the JSON string |
|
||||
| `jsonView(res, obj, View.class)` | Like `json` but applies a Jackson `@JsonView` filter |
|
||||
|
||||
## Exception handling
|
||||
|
||||
`JacksonExtension` installs a global exception handler that applies to every route:
|
||||
|
||||
```
|
||||
HttpException(status, message) → HTTP <status> {"error": "<message>"}
|
||||
Any other Throwable → HTTP 500 {"error": "Internal Server Error"}
|
||||
```
|
||||
|
||||
To throw a handled HTTP error from any handler:
|
||||
|
||||
```java
|
||||
throw HttpException.notFound("Blog not found");
|
||||
throw HttpException.badRequest("Missing field: title");
|
||||
throw HttpException.unauthorized();
|
||||
throw HttpException.forbidden();
|
||||
```
|
||||
|
||||
## Lambda routes
|
||||
|
||||
For lambda-style routes, use the `ObjectMapper` directly from the context:
|
||||
|
||||
```java
|
||||
ObjectMapper mapper = app.ctx().require(ObjectMapper.class);
|
||||
|
||||
app.get("/api/status", (req, res) -> {
|
||||
res.setContentType(ContentType.JSON);
|
||||
return mapper.writeValueAsString(Map.of("status", "ok"));
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>dev.relism</groupId>
|
||||
<artifactId>flash-extensions</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>flash-ext-jackson</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>dev.relism</groupId>
|
||||
<artifactId>flash</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package dev.relism.ext.jackson;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import dev.relism.exceptions.HttpException;
|
||||
import dev.relism.extension.ExtensionContext;
|
||||
import dev.relism.extension.FlashApp;
|
||||
import dev.relism.extension.FlashExtension;
|
||||
import dev.relism.http.ContentType;
|
||||
|
||||
/**
|
||||
* Registers Jackson into the extension layer.
|
||||
*
|
||||
* <p>What this installs:
|
||||
* <ul>
|
||||
* <li>Exposes the {@link ObjectMapper} in {@link ExtensionContext} — consumed by
|
||||
* {@code flash-ext-openapi} and any extension that needs JSON serialization.</li>
|
||||
* <li>Sets a global exception handler that maps {@link HttpException} to a JSON
|
||||
* error body and catches all other exceptions as 500.</li>
|
||||
* <li>Injects the mapper into {@link JacksonHandler} so all subclasses gain
|
||||
* {@code bodyAs} and {@code json} without constructor boilerplate.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <pre>{@code
|
||||
* FlashApp.of(new HttpServer(config))
|
||||
* .install(new JacksonExtension());
|
||||
*
|
||||
* // Custom mapper:
|
||||
* ObjectMapper mapper = JsonMapper.builder()
|
||||
* .addModule(new JavaTimeModule())
|
||||
* .build();
|
||||
* .install(new JacksonExtension(mapper));
|
||||
* }</pre>
|
||||
*/
|
||||
public class JacksonExtension implements FlashExtension {
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
public JacksonExtension() {
|
||||
this(JsonMapper.builder().build());
|
||||
}
|
||||
|
||||
public JacksonExtension(ObjectMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void install(FlashApp app, ExtensionContext ctx) {
|
||||
ctx.provide(ObjectMapper.class, mapper);
|
||||
JacksonHandler.mapper = mapper;
|
||||
|
||||
app.onException((ex, req, res) -> {
|
||||
if (ex instanceof HttpException e) {
|
||||
res.setStatusCode(e.status());
|
||||
res.setContentType(ContentType.JSON);
|
||||
return "{\"error\":\"" + escapeJson(e.getMessage()) + "\"}";
|
||||
}
|
||||
res.setStatusCode(500);
|
||||
res.setContentType(ContentType.JSON);
|
||||
return "{\"error\":\"Internal Server Error\"}";
|
||||
});
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package dev.relism.ext.jackson;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.relism.exceptions.HttpException;
|
||||
import dev.relism.http.ContentType;
|
||||
import dev.relism.models.Request;
|
||||
import dev.relism.models.RequestHandler;
|
||||
import dev.relism.models.Response;
|
||||
|
||||
/**
|
||||
* Base class for handlers that need JSON I/O via Jackson.
|
||||
*
|
||||
* <p>The {@link ObjectMapper} is injected by {@link JacksonExtension#install} once at
|
||||
* startup — all subclasses share the same instance. If no {@code JacksonExtension} is
|
||||
* installed the field remains {@code null} and the first call to {@link #bodyAs} or
|
||||
* {@link #json} will throw an {@link IllegalStateException}.
|
||||
*
|
||||
* <pre>{@code
|
||||
* @Route(method = HttpMethod.POST, path = "/api/blogs")
|
||||
* public class CreateBlog extends JacksonHandler {
|
||||
* public Object handle(Request req, Response res) throws Exception {
|
||||
* CreateBlogRequest body = bodyAs(req, CreateBlogRequest.class);
|
||||
* Blog created = service.create(body);
|
||||
* res.setStatusCode(201);
|
||||
* return json(res, created);
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
public abstract class JacksonHandler extends RequestHandler {
|
||||
|
||||
/**
|
||||
* Shared mapper set by {@link JacksonExtension}. Package-visible so the extension
|
||||
* can assign it; {@code volatile} ensures visibility across virtual threads.
|
||||
*/
|
||||
static volatile ObjectMapper mapper;
|
||||
|
||||
/**
|
||||
* Deserializes the request body bytes into {@code type}.
|
||||
* Wraps Jackson parse errors as {@link HttpException} 400.
|
||||
*/
|
||||
protected <T> T bodyAs(Request req, Class<T> type) throws Exception {
|
||||
requireMapper();
|
||||
try {
|
||||
return mapper.readValue(req.body().bytes(), type);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw HttpException.badRequest("Invalid request body: " + e.getOriginalMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes {@code obj} to JSON, sets {@code Content-Type: application/json},
|
||||
* and returns the JSON string as the response body.
|
||||
*/
|
||||
protected String json(Response res, Object obj) throws Exception {
|
||||
requireMapper();
|
||||
res.setContentType(ContentType.JSON);
|
||||
return mapper.writeValueAsString(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #json} but serializes only fields visible under the given
|
||||
* {@code view} class (see Jackson {@code @JsonView}).
|
||||
*/
|
||||
protected String jsonView(Response res, Object obj, Class<?> view) throws Exception {
|
||||
requireMapper();
|
||||
res.setContentType(ContentType.JSON);
|
||||
return mapper.writerWithView(view).writeValueAsString(obj);
|
||||
}
|
||||
|
||||
private static void requireMapper() {
|
||||
if (mapper == null)
|
||||
throw new IllegalStateException(
|
||||
"JacksonExtension not installed: call FlashApp.install(new JacksonExtension()) first");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user