All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.vertx.ext.web.RoutingContext Maven / Gradle / Ivy

There is a newer version: 5.0.0.CR1
Show newest version
/*
 * Copyright 2014 Red Hat, Inc.
 *
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *  The Eclipse License is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *
 *  The Apache License v2.0 is available at
 *  http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */

package io.vertx.ext.web;

import io.vertx.codegen.annotations.*;
import io.vertx.core.*;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.*;
import io.vertx.core.http.impl.MimeMapping;
import io.vertx.core.impl.ContextInternal;
import io.vertx.core.json.EncodeException;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.User;
import io.vertx.ext.web.impl.ParsableMIMEValue;
import io.vertx.ext.web.impl.Utils;

import java.nio.charset.Charset;
import java.time.Instant;
import java.util.List;
import java.util.Map;

import static io.vertx.codegen.annotations.GenIgnore.PERMITTED_TYPE;

/**
 * Represents the context for the handling of a request in Vert.x-Web.
 * 

* A new instance is created for each HTTP request that is received in the * {@link Router#handle(Object)} of the router. *

* The same instance is passed to any matching request or failure handlers during the routing of the request or * failure. *

* The context provides access to the {@link HttpServerRequest} and {@link HttpServerResponse} * and allows you to maintain arbitrary data that lives for the lifetime of the context. Contexts are discarded once they * have been routed to the handler for the request. *

* The context also provides access to the {@link Session}, cookies and body for the request, given the correct handlers * in the application. *

* If you use the internal error handler * * @author Tim Fox */ @VertxGen public interface RoutingContext { /** * @return the HTTP request object */ @CacheReturn HttpServerRequest request(); /** * @return the HTTP response object */ @CacheReturn HttpServerResponse response(); /** * Tell the router to route this context to the next matching route (if any). * This method, if called, does not need to be called during the execution of the handler, it can be called * some arbitrary time later, if required. *

* If next is not called for a handler then the handler should make sure it ends the response or no response * will be sent. */ void next(); /** * Fail the context with the specified status code. *

* This will cause the router to route the context to any matching failure handlers for the request. If no failure handlers * match It will trigger the error handler matching the status code. You can define such error handler with * {@link Router#errorHandler(int, Handler)}. If no error handler is not defined, It will send a default failure response with provided status code. * * @param statusCode the HTTP status code */ void fail(int statusCode); /** * Fail the context with the specified throwable and 500 status code. *

* This will cause the router to route the context to any matching failure handlers for the request. If no failure handlers * match It will trigger the error handler matching the status code. You can define such error handler with * {@link Router#errorHandler(int, Handler)}. If no error handler is not defined, It will send a default failure response with 500 status code. * * @param throwable a throwable representing the failure */ void fail(Throwable throwable); /** * Fail the context with the specified throwable and the specified the status code. *

* This will cause the router to route the context to any matching failure handlers for the request. If no failure handlers * match It will trigger the error handler matching the status code. You can define such error handler with * {@link Router#errorHandler(int, Handler)}. If no error handler is not defined, It will send a default failure response with provided status code. * * @param statusCode the HTTP status code * @param throwable a throwable representing the failure */ void fail(int statusCode, Throwable throwable); /** * Put some arbitrary data in the context. This will be available in any handlers that receive the context. * * @param key the key for the data * @param obj the data * @return a reference to this, so the API can be used fluently */ @Fluent RoutingContext put(String key, Object obj); /** * Get some data from the context. The data is available in any handlers that receive the context. * * @param key the key for the data * @param the type of the data * @return the data * @throws ClassCastException if the data is not of the expected type */ @Nullable T get(String key); /** * Get some data from the context. The data is available in any handlers that receive the context. * * @param key the key for the data * @param the type of the data * @param defaultValue when the underlying data doesn't contain the key this will be the return value. * @return the data * @throws ClassCastException if the data is not of the expected type */ T get(String key, T defaultValue); /** * Remove some data from the context. The data is available in any handlers that receive the context. * * @param key the key for the data * @param the type of the data * @return the previous data associated with the key * @throws ClassCastException if the data is not of the expected type */ @Nullable T remove(String key); /** * @return all the context data as a map */ @GenIgnore(PERMITTED_TYPE) Map data(); /** * @return the Vert.x instance associated to the initiating {@link Router} for this context */ @CacheReturn Vertx vertx(); /** * @return the mount point for this router. It will be null for a top level router. For a sub-router it will be the path * at which the subrouter was mounted. */ @Nullable String mountPoint(); /** * @return the current route this context is being routed through. */ @Nullable Route currentRoute(); /** * Use {@link #normalizedPath} instead */ @Deprecated() default String normalisedPath() { return this.normalizedPath(); } /** * Return the normalized path for the request. *

* The normalized path is where the URI path has been decoded, i.e. any unicode or other illegal URL characters that * were encoded in the original URL with `%` will be returned to their original form. E.g. `%20` will revert to a space. * Also `+` reverts to a space in a query. *

* The normalized path will also not contain any `..` character sequences to prevent resources being accessed outside * of the permitted area. *

* It's recommended to always use the normalized path as opposed to {@link HttpServerRequest#path()} * if accessing server resources requested by a client. * * @return the normalized path */ String normalizedPath(); /** * @deprecated Use {@link HttpServerRequest#getCookie(String)} * Get the cookie with the specified name. * * @param name the cookie name * @return the cookie */ @Deprecated @Nullable Cookie getCookie(String name); /** * @deprecated Use {@link HttpServerResponse#addCookie(Cookie)} * Add a cookie. This will be sent back to the client in the response. * * @param cookie the cookie * @return a reference to this, so the API can be used fluently */ @Deprecated @Fluent RoutingContext addCookie(io.vertx.core.http.Cookie cookie); /** * @deprecated Use {@link HttpServerResponse#removeCookie(String)} * Expire a cookie, notifying a User Agent to remove it from its cookie jar. * * @param name the name of the cookie * @return the cookie, if it existed, or null */ @Deprecated default @Nullable Cookie removeCookie(String name) { return removeCookie(name, true); } /** * @deprecated Use {@link HttpServerResponse#removeCookie(String, boolean)} * Remove a cookie from the cookie set. If invalidate is true then it will expire a cookie, notifying a User Agent to * remove it from its cookie jar. * * @param name the name of the cookie * @return the cookie, if it existed, or null */ @Deprecated @Nullable Cookie removeCookie(String name, boolean invalidate); /** * @deprecated Use {@link HttpServerRequest#cookieCount()} * @return the number of cookies. */ @Deprecated int cookieCount(); /** * @deprecated Use {@link HttpServerRequest#cookieMap()} * @return a map of all the cookies. */ @Deprecated Map cookieMap(); /** * @deprecated Use {@link #body()} instead. * * @return the entire HTTP request body as a string, assuming UTF-8 encoding if the request does not provide the * content type charset attribute. If a charset is provided in the request that it shall be respected. The context * must have first been routed to a {@link io.vertx.ext.web.handler.BodyHandler} for this to be populated. */ @Deprecated default @Nullable String getBodyAsString() { return body().asString(); } /** * @deprecated Use {@link #body()} instead. * * Get the entire HTTP request body as a string, assuming the specified encoding. The context must have first been routed to a * {@link io.vertx.ext.web.handler.BodyHandler} for this to be populated. * * @param encoding the encoding, e.g. "UTF-16" * @return the body */ @Deprecated default @Nullable String getBodyAsString(String encoding) { return body().asString(encoding); } /** * @deprecated Use {@link #body()} instead. * * Gets the current body buffer as a {@link JsonObject}. If a positive limit is provided the parsing will only happen * if the buffer length is smaller or equal to the limit. Otherwise an {@link IllegalStateException} is thrown. * * When the application is only handling uploads in JSON format, it is recommended to set a limit on * {@link io.vertx.ext.web.handler.BodyHandler#setBodyLimit(long)} as this will avoid the upload to be parsed and * loaded into the application memory. * * @param maxAllowedLength if the current buffer length is greater than the limit an {@link IllegalStateException} is * thrown. This can be used to avoid DDoS attacks on very long JSON payloads that could take * over the CPU while attempting to parse the data. * * @return Get the entire HTTP request body as a {@link JsonObject}. The context must have first been routed to a * {@link io.vertx.ext.web.handler.BodyHandler} for this to be populated. *
* When the body is {@code null} or the {@code "null"} JSON literal then {@code null} is returned. */ @Deprecated default @Nullable JsonObject getBodyAsJson(int maxAllowedLength) { return body().asJsonObject(maxAllowedLength); } /** * @deprecated Use {@link #body()} instead. * * Gets the current body buffer as a {@link JsonArray}. If a positive limit is provided the parsing will only happen * if the buffer length is smaller or equal to the limit. Otherwise an {@link IllegalStateException} is thrown. * * When the application is only handling uploads in JSON format, it is recommended to set a limit on * {@link io.vertx.ext.web.handler.BodyHandler#setBodyLimit(long)} as this will avoid the upload to be parsed and * loaded into the application memory. * * @param maxAllowedLength if the current buffer length is greater than the limit an {@link IllegalStateException} is * thrown. This can be used to avoid DDoS attacks on very long JSON payloads that could take * over the CPU while attempting to parse the data. * * @return Get the entire HTTP request body as a {@link JsonArray}. The context must have first been routed to a * {@link io.vertx.ext.web.handler.BodyHandler} for this to be populated. *
* When the body is {@code null} or the {@code "null"} JSON literal then {@code null} is returned. */ @Deprecated default @Nullable JsonArray getBodyAsJsonArray(int maxAllowedLength) { return body().asJsonArray(maxAllowedLength); } /** * @deprecated Use {@link #body()} instead. * * @return Get the entire HTTP request body as a {@link JsonObject}. The context must have first been routed to a * {@link io.vertx.ext.web.handler.BodyHandler} for this to be populated. *
* When the body is {@code null} or the {@code "null"} JSON literal then {@code null} is returned. */ @Deprecated default @Nullable JsonObject getBodyAsJson() { return body().asJsonObject(); } /** * @deprecated Use {@link #body()} instead. * * @return Get the entire HTTP request body as a {@link JsonArray}. The context must have first been routed to a * {@link io.vertx.ext.web.handler.BodyHandler} for this to be populated. *
* When the body is {@code null} or the {@code "null"} JSON literal then {@code null} is returned. */ @Deprecated default @Nullable JsonArray getBodyAsJsonArray() { return body().asJsonArray(); } /** * @deprecated Use {@link #body()} instead. * * @return Get the entire HTTP request body as a {@link Buffer}. The context must have first been routed to a * {@link io.vertx.ext.web.handler.BodyHandler} for this to be populated. */ @Deprecated default @Nullable Buffer getBody() { return body().buffer(); } RequestBody body(); /** * @return a list of {@link FileUpload} (if any) for the request. The context must have first been routed to a * {@link io.vertx.ext.web.handler.BodyHandler} for this to work. */ List fileUploads(); /** * Cancel all unfinished file upload in progress and delete all uploaded files. */ void cancelAndCleanupFileUploads(); /** * Get the session. The context must have first been routed to a {@link io.vertx.ext.web.handler.SessionHandler} * for this to be populated. * Sessions live for a browser session, and are maintained by session cookies. * @return the session. */ @Nullable Session session(); /** * Whether the {@link RoutingContext#session()} has been already called or not. This is usually used by the * {@link io.vertx.ext.web.handler.SessionHandler}. * * @return true if the session has been accessed. */ boolean isSessionAccessed(); /** * Get the authenticated user (if any). This will usually be injected by an auth handler if authentication if successful. * @return the user, or null if the current user is not authenticated. */ @Nullable User user(); /** * If the context is being routed to failure handlers after a failure has been triggered by calling * {@link #fail(Throwable)} then this will return that throwable. It can be used by failure handlers to render a response, * e.g. create a failure response page. * * @return the throwable used when signalling failure */ @CacheReturn @Nullable Throwable failure(); /** * If the context is being routed to failure handlers after a failure has been triggered by calling * {@link #fail(int)} then this will return that status code. It can be used by failure handlers to render a response, * e.g. create a failure response page. * * When the status code has not been set yet (it is undefined) its value will be -1. * * @return the status code used when signalling failure */ @CacheReturn int statusCode(); /** * If the route specifies produces matches, e.g. produces `text/html` and `text/plain`, and the `accept` header * matches one or more of these then this returns the most acceptable match. * * @return the most acceptable content type. */ @Nullable String getAcceptableContentType(); /** * The headers: *

    *
  1. Accept
  2. *
  3. Accept-Charset
  4. *
  5. Accept-Encoding
  6. *
  7. Accept-Language
  8. *
  9. Content-Type
  10. *
* Parsed into {@link ParsedHeaderValue} * @return A container with the parsed headers. */ @CacheReturn ParsedHeaderValues parsedHeaders(); /** * Add a handler that will be called just before headers are written to the response. This gives you a hook where * you can write any extra headers before the response has been written when it will be too late. * * @param handler the handler * @return the id of the handler. This can be used if you later want to remove the handler. */ int addHeadersEndHandler(Handler handler); /** * Remove a headers end handler * * @param handlerID the id as returned from {@link io.vertx.ext.web.RoutingContext#addHeadersEndHandler(Handler)}. * @return true if the handler existed and was removed, false otherwise */ boolean removeHeadersEndHandler(int handlerID); /** * Provides a handler that will be called after the last part of the body is written to the wire. * The handler is called asynchronously of when the response has been received by the client. * This provides a hook allowing you to do more operations once the request has been sent over the wire. * Do not use this for resource cleanup as this handler might never get called (e.g. if the connection is reset). * * @param handler the handler * @return the id of the handler. This can be used if you later want to remove the handler. */ int addBodyEndHandler(Handler handler); /** * Remove a body end handler * * @param handlerID the id as returned from {@link io.vertx.ext.web.RoutingContext#addBodyEndHandler(Handler)}. * @return true if the handler existed and was removed, false otherwise */ boolean removeBodyEndHandler(int handlerID); /** * Add an end handler for the request/response context. This will be called when the response is disposed or an * exception has been encountered to allow consistent cleanup. The handler is called asynchronously of when the * response has been received by the client. * * @param handler the handler that will be called with either a success or failure result. * @return the id of the handler. This can be used if you later want to remove the handler. */ int addEndHandler(Handler> handler); /** * Add an end handler for the request/response context. This will be called when the response is disposed or an * exception has been encountered to allow consistent cleanup. The handler is called asynchronously of when the * response has been received by the client. * * @see #addEndHandler(Handler) * * @return future that will be called with either a success or failure result. */ default Future addEndHandler() { Promise promise = Promise.promise(); addEndHandler(promise); return promise.future(); } /** * Remove an end handler * * @param handlerID the id as returned from {@link io.vertx.ext.web.RoutingContext#addEndHandler(Handler)}. * @return true if the handler existed and was removed, false otherwise */ boolean removeEndHandler(int handlerID); /** * @return true if the context is being routed to failure handlers. */ boolean failed(); /** * @deprecated This method is internal. Users that really need to use it should refer to {@link io.vertx.ext.web.impl.RoutingContextInternal#setBody(Buffer)} * Set the body. Used by the {@link io.vertx.ext.web.handler.BodyHandler}. You will not normally call this method. * * @param body the body */ @Deprecated void setBody(Buffer body); /** * @deprecated This method is internal. Users that really need to use it should refer to {@link io.vertx.ext.web.impl.RoutingContextInternal#setSession(Session)} * Set the session. Used by the {@link io.vertx.ext.web.handler.SessionHandler}. You will not normally call this method. * * @param session the session */ @Deprecated void setSession(Session session); /** * Set the user. Usually used by auth handlers to inject a User. You will not normally call this method. * * @param user the user */ void setUser(User user); /** * Clear the current user object in the context. This usually is used for implementing a log out feature, since the * current user is unbounded from the routing context. */ void clearUser(); /** * Set the acceptable content type. Used by * @param contentType the content type */ void setAcceptableContentType(@Nullable String contentType); /** * Restarts the current router with a new path and reusing the original method. All path parameters are then parsed * and available on the params list. Query params will also be allowed and available. * * @param path the new http path. */ default void reroute(String path) { reroute(request().method(), path); } /** * Restarts the current router with a new method and path. All path parameters are then parsed and available on the * params list. Query params will also be allowed and available. * * @param method the new http request * @param path the new http path. */ void reroute(HttpMethod method, String path); /** * Returns the languages for the current request. The languages are determined from the Accept-Language * header and sorted on quality. * * When 2 or more entries have the same quality then the order used to return the best match is based on the lowest * index on the original list. For example if a user has en-US and en-GB with same quality and this order the best * match will be en-US because it was declared as first entry by the client. * * @return The best matched language for the request */ @CacheReturn default List acceptableLanguages(){ return parsedHeaders().acceptLanguage(); } /** * Helper to return the user preferred language. * It is the same action as returning the first element of the acceptable languages. * * @return the users preferred locale. */ @CacheReturn default LanguageHeader preferredLanguage() { List acceptableLanguages = acceptableLanguages(); return acceptableLanguages.size() > 0 ? acceptableLanguages.get(0) : null; } /** * Returns a map of named parameters as defined in path declaration with their actual values * * @return the map of named parameters */ Map pathParams(); /** * Gets the value of a single path parameter * * @param name the name of parameter as defined in path declaration * @return the actual value of the parameter or null if it doesn't exist */ @Nullable String pathParam(String name); /** * Returns a map of all query parameters inside the query string
* The query parameters are lazily decoded: the decoding happens on the first time this method is called. If the query string is invalid * it fails the context * * @return the multimap of query parameters */ MultiMap queryParams(); /** * Always decode the current query string with the given {@code encoding}. The decode result is never cached. Callers * to this method are expected to cache the result if needed. Usually users should use {@link #queryParams()}. * * This method is only useful when the requests without content type ({@code GET} requests as an example) expect that * query params are in the ASCII format {@code ISO-5559-1}. * * @param encoding a non null character set. * @return the multimap of query parameters */ @GenIgnore(PERMITTED_TYPE) MultiMap queryParams(Charset encoding); /** * Gets the value of a single query parameter. For more info {@link RoutingContext#queryParams()} * * @param name The name of query parameter * @return The list of all parameters matching the parameter name. It returns an empty list if no query parameter with {@code name} was found */ List queryParam(String name); /** * Set Content-Disposition get to "attachment" with optional {@code filename} mime type. * * @param filename the filename for the attachment */ @Fluent default RoutingContext attachment(String filename) { if (filename != null) { String contentType = MimeMapping.getMimeTypeForFilename(filename); if (contentType != null) { response() .putHeader(HttpHeaders.CONTENT_TYPE, contentType); } } response() .putHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename); return this; } /** * Perform a 302 redirect to {@code url}. If a custom 3xx code is already defined, then that * one will be preferred. *

* The string "back" is special-cased * to provide Referrer support, when Referrer * is not present "/" is used. *

* Examples: *

* redirect('back'); * redirect('/login'); * redirect('http://google.com'); * * @param url the target url */ default Future redirect(String url) { // location if ("back".equals(url)) { url = request().getHeader(HttpHeaders.REFERER); if (url == null) { url = "/"; } } response() .putHeader(HttpHeaders.LOCATION, url); // status int status = response().getStatusCode(); if (status < 300 || status >= 400) { // if a custom code is in use that will be // respected response().setStatusCode(302); } return response() .putHeader(HttpHeaders.CONTENT_TYPE, "text/plain; charset=utf-8") .end("Redirecting to " + url + "."); } /** * See {@link #redirect(String)}. */ @Fluent default RoutingContext redirect(String url, Handler> handler) { redirect(url).onComplete(handler); return this; } /** * Encode an Object to JSON and end the request. * When {@code Content-Type} is not set then correct {@code Content-Type} will be applied to the response * @param json the json * @return a future to handle the end of the request */ default Future json(Object json) { final HttpServerResponse res = response(); final boolean hasContentType = res.headers().contains(HttpHeaders.CONTENT_TYPE); if (json == null) { // http://www.iana.org/assignments/media-types/application/json // No "charset" parameter is defined for this registration. // Adding one really has no effect on compliant recipients. // apply the content type header only if content type header is not set if(!hasContentType) { res.putHeader(HttpHeaders.CONTENT_TYPE, "application/json"); } return res.end("null"); } else { try { Buffer buffer = Json.encodeToBuffer(json); // http://www.iana.org/assignments/media-types/application/json // No "charset" parameter is defined for this registration. // Adding one really has no effect on compliant recipients. // apply the content type header only if the encoding succeeds and content type header is not set if(!hasContentType) { res.putHeader(HttpHeaders.CONTENT_TYPE, "application/json"); } return res.end(buffer); } catch (EncodeException | UnsupportedOperationException e) { // handle the failure fail(e); // as the operation failed return a failed future // this is purely a notification return ((ContextInternal) vertx().getOrCreateContext()).failedFuture(e); } } } /** * See {@link #json(Object)}. */ @Fluent default RoutingContext json(Object json, Handler> handler) { json(json).onComplete(handler); return this; } /** * Check if the incoming request contains the "Content-Type" * get field, and it contains the give mime `type`. * If there is no request body, `false` is returned. * If there is no content type, `false` is returned. * Otherwise, it returns true if the `type` that matches. *

* Examples: *

* // With Content-Type: text/html; charset=utf-8 * is("html"); // => true * is("text/html"); // => true *

* // When Content-Type is application/json * is("application/json"); // => true * is("html"); // => false * * @param type content type * @return The most close value */ @CacheReturn default boolean is(String type) { MIMEHeader contentType = parsedHeaders().contentType(); if (contentType == null) { return false; } ParsedHeaderValue value; // if we received an incomplete CT if (type.indexOf('/') == -1) { // when the content is incomplete we assume */type, e.g.: // json -> */json value = new ParsableMIMEValue("*/" + type).forceParse(); } else { value = new ParsableMIMEValue(type).forceParse(); } return contentType.isMatchedBy(value); } /** * Check if the request is fresh, aka * Last-Modified and/or the ETag * still match. * * @return true if content is fresh according to the cache. */ default boolean isFresh() { final HttpMethod method = request().method(); // GET or HEAD for weak freshness validation only if (method != HttpMethod.GET && method != HttpMethod.HEAD) { return false; } final int s = response().getStatusCode(); // 2xx or 304 as per rfc2616 14.26 if ((s >= 200 && s < 300) || 304 == s) { return Utils.fresh(this); } return false; } /** * Set the ETag of a response. * This will normalize the quotes if necessary. *

* etag('md5hashsum'); * etag('"md5hashsum"'); * ('W/"123456789"'); * * @param etag the etag value */ @Fluent default RoutingContext etag(String etag) { boolean quoted = // at least 2 characters etag.length() > 2 && // either starts with " or W/" (etag.charAt(0) == '\"' || etag.startsWith("W/\"")) && // ends with " etag.charAt(etag.length() -1) == '\"'; if (!quoted) { response().putHeader(HttpHeaders.ETAG, "\"" + etag + "\""); } else { response().putHeader(HttpHeaders.ETAG, etag); } return this; } /** * Set the Last-Modified date using a Instant. * * @param instant the last modified instant */ @Fluent @GenIgnore(PERMITTED_TYPE) default RoutingContext lastModified(Instant instant) { response().putHeader(HttpHeaders.LAST_MODIFIED, Utils.formatRFC1123DateTime(instant.toEpochMilli())); return this; } /** * Set the Last-Modified date using a String. * * @param instant the last modified instant */ @Fluent default RoutingContext lastModified(String instant) { response().putHeader(HttpHeaders.LAST_MODIFIED, instant); return this; } /** * Shortcut to the response end. * @param chunk a chunk * @return future */ default Future end(String chunk) { return response().end(chunk); } /** * See {@link #end(String)} */ @Fluent default RoutingContext end(String chunk, Handler> handler) { end(chunk).onComplete(handler); return this; } /** * Shortcut to the response end. * @param buffer a chunk * @return future */ default Future end(Buffer buffer) { return response().end(buffer); } /** * See {@link #end(Buffer)} */ @Fluent default RoutingContext end(Buffer buffer, Handler> handler) { end(buffer).onComplete(handler); return this; } /** * Shortcut to the response end. * @return future */ default Future end() { return response().end(); } /** * See {@link #end()} */ @Fluent default RoutingContext end(Handler> handler) { end().onComplete(handler); return this; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy