Files
Nextus-Web/src/main/java/dev/coph/nextusweb/server/router/Request.java
T

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