tech.greenfield.vertx.irked.Request Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of irked-vertx Show documentation
Show all versions of irked-vertx Show documentation
Opinionated framework for vertx-web route configuration and dispatch
package tech.greenfield.vertx.irked;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.*;
import io.vertx.ext.web.RequestBody;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.impl.RoutingContextDecorator;
import io.vertx.ext.web.impl.RoutingContextInternal;
import tech.greenfield.vertx.irked.Controller.WebHandler;
import tech.greenfield.vertx.irked.auth.AuthorizationToken;
import tech.greenfield.vertx.irked.exceptions.MissingBodyException;
import tech.greenfield.vertx.irked.status.BadRequest;
import tech.greenfield.vertx.irked.status.OK;
/**
* Request handling wrapper which adds some useful routines for
* API writers.
*
* Can serve as a basis for local context parsers that API writers
* can use to expose path arguments from parent prefixes
*
* @author odeda
*/
public class Request extends RoutingContextDecorator {
private static RoutingContextInternal downCastOrFailWithExplanation(RoutingContext outerContext) {
if (outerContext instanceof RoutingContextInternal)
return (RoutingContextInternal) outerContext;
/*
* This is an issue because of https://github.com/vert-x3/vertx-web/commit/65972a2e43a853ae6a226a25cc24351d685e0a44
* Under the guise of "Feature." [sic], required functionality was added to the (supposedly internal) RoutingContextInternal
* extension interface and then requiring it for the RoutingContextDecorator c'tor, but without modifying all the other APIs
* that still use RoutingContext. If you think this code here is bad, than check out RouterImpl that does an *unchecked* cast down:
* https://github.com/vert-x3/vertx-web/blob/e9430acf6edd029ddc80bcb00f87a56e10171312/vertx-web/src/main/java/io/vertx/ext/web/impl/RouterImpl.java#L248
*
* At the time of this writing all vertx-web implementations of RoutingContext actually implement RoutingContextInternal, and all are
* created by RouterImpl - which we use explicitly through io.vertx.ext.web.Router.router() (in tech.greenfield.vertx.irked.Router), but
* this here now relies on vertx-web developers always paying attention instead on compiler type checking...
*/
throw new RuntimeException("Unexpected parent context that does not implement RoutingContextInternal! This is a bug in vertx-web 4.2.2");
}
private RoutingContext outerContext;
/**
* Create a new request wrapper as a {@link RoutingContextDecorator} around the specified parent routing context
* (what vertx-web calls "inner context").
*
* This is an internal constructor to be used by {@link Router} - use at your own risk.
* @param outerContext parent routing context to wrap
*/
public Request(RoutingContext outerContext) {
super(outerContext.currentRoute(), downCastOrFailWithExplanation(outerContext));
this.outerContext = outerContext;
}
@Override
public void next() {
this.outerContext.next();
}
@Override
public void fail(int statusCode) {
// we're overriding the fail handlers, which for some reason the decorator
// feels should be moved to another thread. Instead, use the outer implementation
// and let it do what's right
this.outerContext.fail(statusCode);
}
/**
* Fail helper that wraps Irked HTTP errors in Vert.x-web (final?!) HttpException class
* that is better handled by the RoutingContextImpl
* @param httpError error to fail with
*/
public void fail(HttpError httpError) {
fail(httpError.getStatusCode(), httpError);
}
@Override
public void fail(Throwable throwable) {
// we're overriding the fail handlers, which for some reason the decorator
// feels should be moved to another thread. Instead, use the outer implementation
// and let it do what's right
this.outerContext.fail(throwable);
}
/**
* Helper failure handler for CompletableFuture users.
* Use at the end of an async chain to succinctly propagate exceptions, as
* thus: .exceptionally(req::handleFailure)
.
* This method will call {@link #fail(Throwable)} after unwrapping
* {@link RuntimeException}s as needed.
* @param throwable A {@link Throwable} error to fail on
* @return null
*/
public Void handleFailure(Throwable throwable) {
var failure = HttpError.unwrap(throwable);
if (failure instanceof HttpError)
fail((HttpError)failure);
else
fail(failure);
return null;
}
/**
* Helper failure handler for CompletableFuture users.
* Use in the middle an async chain to succinctly propagate exceptions, or
* success values as thus: .whenComplete(req::handlePossibleFailure)
.
* This method will call {@link Request#fail(Throwable)} if a failure occurred,
* after unwrapping {@link RuntimeException}s as needed. It will also pass on
* the success value (or null if there was a failure) for the next async
* element. Subsequent code can check whether a failure was propagated
* by calling {@link #failed()}
* @param the type of previous completion value that will be returned as the completion value for completion stages running this method
* @param successValue successful completion value to return in case no failure occurred
* @param throwable A {@link Throwable} error to fail on
* @return null
*/
public V handlePossibleFailure(V successValue, Throwable throwable) {
if (Objects.nonNull(throwable))
fail(HttpError.unwrap(throwable));
return successValue;
}
/**
* Helper to easily configure standard failure handlers
* @return a WebHandler that sends Irked status exceptions as HTTP responses
*/
public static WebHandler failureHandler() {
return r -> {
r.sendError(HttpError.toHttpError(r));
};
}
/**
* Convert request body to an instance of the specified POJO
*
* Currently the followed request body content types are supported:
* * application/json
- the body is read using {@link RoutingContext#getBodyAsJson()} then
* mapped to the bean type using {@link JsonObject#mapTo(Class)}
* * application/x-www-form-urlencoded
- the body is read using {@link HttpServerRequest#formAttributes()}
* into a {@link JsonObject} as keys with string values, then mapped to the bean type using {@link JsonObject#mapTo(Class)}.
* If the same key is present multiple times, the values will be stored into the JsonObject as a {@link JsonArray} with string values.
*
* If no content-type header is specified in the request, application/json
is assumed.
*
* If no body is present, this method will throw an unchecked {@link MissingBodyException} - i.e. a "Bad Request"
* HTTP error with the text "Required request body is missing".
*
* @apiNote this API is very similar to the Vert.x 4.3 API {@link RequestBody#asPojo(Class)}. The notable difference
* is that this implementation checks the request Content-Type and if its a form POST, it will read the form post fields
* to mimic a JSON object. This may or may not be a desired behavior.
*
* @param
* @param type
* @return
*/
public T getBodyAs(Class type) {
String contentType = this.request().getHeader("Content-Type");
if (Objects.isNull(contentType)) contentType = "application/json"; // we love JSON
String[] ctParts = contentType.split(";\\s*");
switch (ctParts[0]) {
case "application/x-www-form-urlencoded":
JsonObject out = new JsonObject();
request().formAttributes().forEach(e -> {
Object old = out.getValue(e.getKey());
if (Objects.isNull(old)) {
out.put(e.getKey(), e.getValue());
return;
}
if (old instanceof JsonArray) {
((JsonArray)old).add(e.getValue());
return;
}
out.put(e.getKey(), new JsonArray().add(old).add(e.getValue()));
});
return out.mapTo(type);
case "application/json":
default:
try {
JsonObject body = body().asJsonObject();
if (body == null)
throw new MissingBodyException().unchecked();
return body.mapTo(type);
} catch (DecodeException e) {
throw new BadRequest("Unrecognized content-type " + ctParts[0] +
" and content does not decode as JSON: " + e.getMessage()).unchecked();
}
}
}
/**
* Helper method to terminate request processing with a success (200 OK) response
* containing a JSON body.
* @param json {@link JsonObject} containing the output to encode
* @return a promise that will complete when the body was sent successfully
*/
public Future sendJSON(JsonObject json) {
return sendJSON(json, new OK());
}
/**
* Helper method to terminate a request processing with a success (200 OK) response
* containing a JSON object mapped from the specified POJO
* @param data POJO containing the data to map to a JSON encoded object
* @return a promise that will complete when the body was sent successfully
*/
public Future sendObject(Object data) {
return sendJSON(JsonObject.mapFrom(data));
}
/**
* Helper method to terminate request processing with a success (200 OK) response
* containing a JSON body.
* @param json {@link JsonArray} containing the output to encode
* @return a promise that will complete when the body was sent successfully
*/
public Future sendJSON(JsonArray json) {
return sendJSON(json, new OK());
}
/**
* Helper method to terminate request processing with a custom response
* containing a JSON body and the specified status line.
* @param json {@link JsonObject} containing the output to encode
* @param status An HttpError object representing the HTTP status to be sent
* @return a promise that will complete when the body was sent successfully
*/
public Future sendJSON(JsonObject json, HttpError status) {
return sendContent(json.encode(), status, "application/json");
}
/**
* Helper method to terminate a request processing with a custom response
* containing a JSON object mapped from the specified POJO and the specified status line.
* @param data POJO containing the data to map to a JSON encoded object
* @param status An HttpError object representing the HTTP status to be sent
* @return a promise that will complete when the body was sent successfully
*/
public Future sendObject(Object data, HttpError status) {
return sendJSON(JsonObject.mapFrom(data), status);
}
/**
* Helper method to terminate request processing with a custom response
* containing a JSON body and the specified status line.
* @param json {@link JsonArray} containing the output to encode
* @param status HTTP status to send
* @return a promise that will complete when the body was sent successfully
*/
public Future sendJSON(JsonArray json, HttpError status) {
return sendContent(json.encode(), status, "application/json");
}
/**
* Helper method to terminate request processing with a custom response
* containing some text and the specified status line.
* @param content Text content to send in the response
* @param status An HttpError object representing the HTTP status to be sent
* @param contentType The MIME Content-Type to be set for the response
* @return a promise that will complete when the body was sent successfully
*/
public Future sendContent(String content, HttpError status, String contentType) {
return sendContent(Buffer.buffer(content), status, contentType);
}
/**
* Helper method to terminate request processing with a custom response
* containing some data and the specified status line.
* @param content Binary content to send in the response
* @param status An HttpError object representing the HTTP status to be sent
* @param contentType The MIME Content-Type to be set for the response
* @return a promise that will resolve when the body was sent successfully
*/
public Future sendContent(Buffer content, HttpError status, String contentType) {
var res = response(status)
.putHeader("Content-Type", contentType)
.putHeader("Content-Length", String.valueOf(content.length()));
if (isHead())
return res.end();
return res.end(content);
}
/**
* Helper method to terminate request processing with a custom response
* containing some text and the specifeid status line.
* @param content Text content to send in the response
* @param contentType The MIME Content-Type to be set for the response
* @return a promise that will complete when the body was sent successfully
*/
public Future sendContent(String content, String contentType) {
return sendContent(content, new OK(), contentType);
}
/**
* Helper method to terminate request processing with a custom response
* containing some text and the specifeid status line.
* @param content Text content to send in the response
* @param status An HttpError object representing the HTTP status to be sent
* @return a promise that will complete when the body was sent successfully
*/
public Future sendContent(String content, HttpError status) {
return sendContent(content, status, "text/plain");
}
/**
* Helper method to terminate request processing with a custom response
* containing some text and the specifeid status line.
* @param content Text content to send in the response
* @return a promise that will complete when the body was sent successfully
*/
public Future sendContent(String content) {
return sendContent(content, new OK(), "text/plain");
}
/**
* Helper method to terminate request processing with an HTTP error (non-200 OK) response.
* The resulting HTTP response will have the correct status line and an application/json content
* with a JSON encoded object containing the fields "status" set to "false" and "message" set
* to the {@link HttpError}'s message.
* @param status An HttpError object representing the HTTP status to be sent
* @return a promise that will complete when the body was sent successfully
*/
public Future sendError(HttpError status) {
return sendJSON(new JsonObject().put("status", status.getStatusCode() / 100 == 2).put("message", status.getMessage()), status);
}
/**
* Helper method to terminate request processing with an HTTP OK and a JSON response
* @param object {@link JsonObject} of data to send
* @return a promise that will complete when the body was sent successfully
*/
public Future send(JsonObject object) {
return sendJSON(object);
}
/**
* Helper method to terminate request processing with an HTTP OK and a JSON response
* @param list {@link JsonArray} of a list of data to send
* @return a promise that will complete when the body was sent successfully
*/
public Future send(JsonArray list) {
return sendJSON(list);
}
/**
* Helper method to terminate request processing with an HTTP OK and a text/plain response
* @param content text to send
* @return a promise that will complete when the body was sent successfully
*/
public Future send(String content) {
return sendContent(content);
}
/**
* Helper method to terminate request processing with an HTTP OK and a application/octet-stream response
* @param buffer binary data to send
* @return a promise that will complete when the body was sent successfully
*/
public Future send(Buffer buffer) {
return sendContent(buffer, new OK(), "application/octet-stream");
}
/**
* Helper method to terminate request processing with a non-OK HTTP response with default text
* @param status {@link HttpError} to send
* @return a promise that will complete when the body was sent successfully
*/
public Future send(HttpError status) {
return sendError(status);
}
/**
* Helper method to terminate request processing with an HTTP OK and an application/json
* response containing a list of {@link io.vertx.core.json.Json}-encoded objects
* @param type of objects in the list
* @param list List to convert to a JSON array for sending
* @return a promise that will complete when the body was sent successfully
*/
public Future sendList(List list) {
return sendStream(list.stream());
}
/**
* Helper method to terminate request processing with an HTTP OK and an application/json
* response containing a stream of {@link io.vertx.core.json.Json}-encoded objects.
* Please note that the response will be buffered in memory using a {@link io.vertx.core.json.JsonArray}
* based collector.
* @param type of objects in the stream
* @param stream Stream to convert to a JSON array for sending
* @return a promise that will complete when the body was sent successfully
*/
public Future sendStream(Stream stream) {
return sendJSON(stream.map(this::encodeToJsonType).collect(JsonArray::new, JsonArray::add, JsonArray::addAll));
}
/**
* Helper method to terminate request processing with an HTTP OK and a JSON response
* @param object custom object to process through Jackson's {@link ObjectMapper} to generate JSON content
* @return a promise that will complete when the body was sent successfully
*/
@SuppressWarnings("unchecked")
public Future send(Object object) {
if (object instanceof List)
return sendList((List