168 lines
5.7 KiB
Java
168 lines
5.7 KiB
Java
package dev.coph.nextusweb.server.router;
|
|
|
|
import dev.coph.nextusweb.server.json.JsonMapper;
|
|
import dev.coph.nextusweb.server.router.exception.BadRequestException;
|
|
import io.netty.handler.codec.http.*;
|
|
import io.netty.util.CharsetUtil;
|
|
import tools.jackson.core.JacksonException;
|
|
import tools.jackson.databind.JsonNode;
|
|
|
|
import java.util.*;
|
|
|
|
/**
|
|
* A convenience wrapper around a Netty {@link FullHttpRequest} that exposes the parts of an
|
|
* HTTP request handlers typically need: path parameters, query parameters, headers and the
|
|
* request body (raw, as a parsed JSON tree, or deserialized into a type).
|
|
*
|
|
* <p>Query parameters and the parsed JSON body are computed lazily and cached, so repeated
|
|
* accessors do not re-parse the request. A single {@code Request} instance is not intended to
|
|
* be shared across threads.</p>
|
|
*/
|
|
public final class Request {
|
|
|
|
/** The underlying Netty request this wrapper delegates to. */
|
|
private final FullHttpRequest raw;
|
|
|
|
/** Path parameters captured by the router while matching, keyed by name. */
|
|
private final Map<String, String> pathParams;
|
|
|
|
/** Lazily decoded query-string parameters; {@code null} until first accessed. */
|
|
private Map<String, List<String>> queryParams;
|
|
|
|
/** Lazily parsed JSON body; {@code null} until {@link #json()} is first called. */
|
|
private JsonNode jsonCache;
|
|
|
|
/**
|
|
* Creates a request wrapper.
|
|
*
|
|
* @param raw the underlying Netty request
|
|
* @param pathParams the path parameters captured during routing, keyed by name
|
|
*/
|
|
public Request(FullHttpRequest raw, Map<String, String> pathParams) {
|
|
this.raw = raw;
|
|
this.pathParams = pathParams;
|
|
}
|
|
|
|
/**
|
|
* Returns the value of a path parameter captured during routing.
|
|
*
|
|
* @param name the parameter name as declared in the route (without braces)
|
|
* @return the captured value, or {@code null} if no such parameter was matched
|
|
*/
|
|
public String pathParam(String name) {
|
|
return pathParams.get(name);
|
|
}
|
|
|
|
/**
|
|
* Returns the first value of a query-string parameter, decoding the query string on first
|
|
* access.
|
|
*
|
|
* @param name the query parameter name
|
|
* @return the first value, or {@code null} if the parameter is absent or has no value
|
|
*/
|
|
public String queryParam(String name) {
|
|
if (queryParams == null) {
|
|
queryParams = new QueryStringDecoder(raw.uri()).parameters();
|
|
}
|
|
var values = queryParams.get(name);
|
|
return values == null || values.isEmpty() ? null : values.getFirst();
|
|
}
|
|
|
|
/**
|
|
* Returns all values of a query-string parameter, decoding the query string on first
|
|
* access.
|
|
*
|
|
* @param name the query parameter name
|
|
* @return the (possibly empty) list of values for the parameter; never {@code null}
|
|
*/
|
|
public List<String> queryParams(String name) {
|
|
if (queryParams == null) {
|
|
queryParams = new QueryStringDecoder(raw.uri()).parameters();
|
|
}
|
|
return queryParams.getOrDefault(name, List.of());
|
|
}
|
|
|
|
/**
|
|
* Returns the value of a request header.
|
|
*
|
|
* @param name the (case-insensitive) header name
|
|
* @return the header value, or {@code null} if not present
|
|
*/
|
|
public String header(String name) {
|
|
return raw.headers().get(name);
|
|
}
|
|
|
|
/**
|
|
* Returns the request body decoded as a UTF-8 string.
|
|
*
|
|
* @return the body as text (empty if there is no body)
|
|
*/
|
|
public String body() {
|
|
return raw.content().toString(CharsetUtil.UTF_8);
|
|
}
|
|
|
|
/**
|
|
* Parses the request body as a JSON tree, caching the result for subsequent calls. An
|
|
* empty body resolves to a JSON {@code null} node rather than an error.
|
|
*
|
|
* @return the parsed JSON tree
|
|
* @throws BadRequestException if the body is not valid JSON
|
|
*/
|
|
public JsonNode json() {
|
|
if (jsonCache == null) {
|
|
try {
|
|
byte[] bytes = new byte[raw.content().readableBytes()];
|
|
raw.content().getBytes(raw.content().readerIndex(), bytes);
|
|
if (bytes.length == 0) {
|
|
jsonCache = JsonMapper.MAPPER.nullNode();
|
|
} else {
|
|
jsonCache = JsonMapper.MAPPER.readTree(bytes);
|
|
}
|
|
} catch (JacksonException e) {
|
|
throw new BadRequestException("Invalid JSON: " + e.getOriginalMessage());
|
|
}
|
|
}
|
|
return jsonCache;
|
|
}
|
|
|
|
/**
|
|
* Deserializes the request body directly into an instance of the given type.
|
|
*
|
|
* <p>Unlike {@link #json()}, the result is not cached and the body is read fresh on each
|
|
* call.</p>
|
|
*
|
|
* @param type the target type to deserialize into
|
|
* @param <T> the target type
|
|
* @return the deserialized value
|
|
* @throws BadRequestException if the body cannot be deserialized into {@code type}
|
|
*/
|
|
public <T> T jsonAs(Class<T> type) {
|
|
try {
|
|
byte[] bytes = new byte[raw.content().readableBytes()];
|
|
raw.content().getBytes(raw.content().readerIndex(), bytes);
|
|
return JsonMapper.MAPPER.readValue(bytes, type);
|
|
} catch (JacksonException e) {
|
|
throw new BadRequestException(
|
|
"Could not deserialize body as " + type.getSimpleName() + ": " + e.getOriginalMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the request's HTTP method.
|
|
*
|
|
* @return the HTTP method
|
|
*/
|
|
public HttpMethod method() {
|
|
return raw.method();
|
|
}
|
|
|
|
/**
|
|
* Returns the request's path, with any query string stripped off.
|
|
*
|
|
* @return the decoded request path
|
|
*/
|
|
public String path() {
|
|
return new QueryStringDecoder(raw.uri()).path();
|
|
}
|
|
}
|