package com.vtence.molecule; import com.vtence.molecule.helpers.Headers; import com.vtence.molecule.http.ContentType; import com.vtence.molecule.http.HttpStatus; import com.vtence.molecule.lib.BinaryBody; import java.nio.charset.Charset; import java.time.Instant; import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.BiConsumer; import java.util.function.Consumer; import static com.vtence.molecule.http.HeaderNames.CONTENT_LENGTH; import static com.vtence.molecule.http.HeaderNames.CONTENT_TYPE; import static com.vtence.molecule.http.HeaderNames.LOCATION; import static com.vtence.molecule.http.HttpDate.httpDate; import static com.vtence.molecule.http.HttpStatus.SEE_OTHER; import static com.vtence.molecule.lib.BinaryBody.bytes; import static com.vtence.molecule.lib.TextBody.text; import static java.lang.Long.parseLong; import static java.lang.String.valueOf; import static java.nio.charset.StandardCharsets.ISO_8859_1; /** * The HTTP response to write back to the client. */ public class Response { private final CompletableFuture<Response> done = new CompletableFuture<>(); private final Headers headers = new Headers(); private CompletableFuture<Response> postProcessing = done; private int statusCode = HttpStatus.OK.code; private String statusText = HttpStatus.OK.text; private Body body = BinaryBody.empty(); public Response() {} /** * Sets the HTTP status for this response. This will set both the status code and the status text. * * <p> * The status is set to 200 OK by default. * </p> * * @param status the HTTP status to set */ public Response status(HttpStatus status) { statusCode(status.code); statusText(status.text); return this; } /** * Sets the status code for this response. It is usually preferable to set the status and text * together with Response#status(com.vtence.molecule.http.HttpStatus). * * @see Response#status(com.vtence.molecule.http.HttpStatus) * @param code the status code to set */ public Response statusCode(int code) { statusCode = code; return this; } /** * Gets the status code set on this response. * * @return the response status code */ public int statusCode() { return statusCode; } /** * Sets the status text for this response. It is usually preferable to set the status and text * together with Response#status(com.vtence.molecule.http.HttpStatus). * * @see Response#status(com.vtence.molecule.http.HttpStatus) * @param text the status text to set */ public Response statusText(String text) { statusText = text; return this; } /** * Gets the status text of this response. * * @return the response status text */ public String statusText() { return statusText; } /** * Sends a SEE OTHER (303) redirect response to the client using the specified redirect location. * * @param location the url of the other location */ public Response redirectTo(String location) { status(SEE_OTHER); header(LOCATION, location); return this; } /** * Checks if this response has a header of the specified name. * * @param name the header name * @return true is the header was found, false otherwise */ public boolean hasHeader(String name) { return headers.has(name); } /** * Gets the names of all the headers to be sent with this response. * If the response has no header, the set will be empty. * <p> * Modifications to the returned set will not modify the response. * </p> * @return a (possibly empty) <code>Set</code> of header names to send */ public Set<String> headerNames() { return headers.names(); } /** * Gets the list of values of the specified header of this response. The name is case insensitive. * * @param name the name of the header to retrieve * @return the (possibly empty) list of header values */ public List<String> headers(String name) { return headers.list(name); } /** * Gets the value of the specified header of this response. The name is case insensitive. * * <p> * In case there are multiple headers with that name, a comma separated list of values is returned. * </p> * * This method returns a null value if the response does not include any header of the specified name. * * @param name the name of the header to retrieve * @return the header value or null */ public String header(String name) { return headers.get(name); } /** * Gets the value of the specified header as a <code>long</code>. The name is case insensitive. * * <p> * In case there are multiple headers with that name, a comma separated list of values is returned. * </p> * * This method returns -1 if the response does not include any header of the specified name. * * @param name the name of the header to retrieve * @return the long header value or -1 * @throws java.lang.NumberFormatException is the header value cannot be converted to a <code>long</code> */ public long headerAsLong(String name) { String value = header(name); return value != null ? parseLong(value) : -1; } /** * Adds a response header with the given name and value to this response. * <p> * This method allows response headers to have multiple values. The new value will be added to the list * of existing values for that header name. * </p> * * @param name the name of the header to send * @param value the additional value for that header */ public Response addHeader(String name, String value) { headers.add(name, value); return this; } /** * Sets a header with the given name and value to be sent with this response. * If the header has already been set, the new value overwrites the previous one. * <p> * The {@link Response#hasHeader(String)} method can be used to test for the presence of the header before setting its value. * </p> * * @param name the name of the header to send * @param value the new value for that header */ public Response header(String name, String value) { headers.put(name, value); return this; } /** * Sets a header with the given name and date value to be sent with this response. * If the header has already been set, the new value overwrites the previous one. * <p> * The {@link Response#hasHeader(String)} method can be used to test for the presence of the header before setting its value. * </p> * * @param name the name of the header to send * @param value the new date value for that header */ public Response header(String name, Instant value) { return header(name, httpDate(value)); } /** * Sets a header with the given name and value to be sent with this response. * If the header has already been set, the new value overwrites the previous one. * <p> * The {@link Response#hasHeader(String)} method can be used to test for the presence of the header before setting its value. * </p> * * @param name the name of the header to send * @param value the new value for that header */ public Response header(String name, Object value) { return header(name, valueOf(value)); } /** * Removes the value of the specified header on this response. The name is case insensitive. * * <p> * In case there are multiple headers with that name, all values are removed. * </p> * * @param name the name of the header to remove */ public Response removeHeader(String name) { headers.remove(name); return this; } /** * Gets the content type of this response. * * <p>Note that getting the content type can also be done explicitly using * {@link Response#header}. This is a convenient method for doing so.</p> * * @return the content type header value or null */ public String contentType() { return header(CONTENT_TYPE); } /** * Sets the content type for this response. If the content type specifies a charset, this charset will * be used to encode text based responses. * * <p>Note that setting the content type can also be done explicitly using {@link Response#header}. * This is a convenient method for doing so.</p> * * @see Response#charset(String) * @param contentType the new content type value to set */ public Response contentType(String contentType) { header(CONTENT_TYPE, contentType); return this; } /** * Gets the content length of this response. * * <p>Note that getting the content length can also be done explicitly using * {@link Response#header}. This is a convenient method for doing so.</p> * * @return the content length header value or null */ public long contentLength() { return headerAsLong(CONTENT_LENGTH); } /** * Sets the content length for this response. * * <p>Note that setting the content length can also be done explicitly using {@link Response#header}. * This is a convenient method for doing so</p> * * @param length the new content length value to set */ public Response contentLength(long length) { header(CONTENT_LENGTH, length); return this; } /** * Replaces the MIME charset of the content type of this response. If no charset is set, <code>ISO-8859-1</code> * is assumed. This method has no effect if no content type has been set on the response. * * Calling {@link Response#contentType(String)} with the value of <code>text/html</code> then * calling this method with the value <code>UTF-8</code> is equivalent to calling <code>contentType</code> * with the value <code>text/html; charset=UTF-8</code> * <p> * Note that the charset will be used for encoding character based bodies. * </p> * * @param charsetName the name of the character encoding to use */ public Response charset(String charsetName) { ContentType contentType = ContentType.of(this); if (contentType == null) return this; contentType(new ContentType(contentType.type(), contentType.subType(), charsetName).toString()); return this; } /** * Reads the MIME charset of this response. The charset is read from the content-type header. A default charset * of <code> ISO-8859-1</code> is assumed. * * @return the charset set in the content type header or <code> ISO-8859-1</code> */ public Charset charset() { ContentType contentType = ContentType.of(this); if (contentType == null || contentType.charset() == null) { return ISO_8859_1; } return contentType.charset(); } /** * Sets the text content to write back to the client as the body of this response. The text content will be encoded * using the charset of the response. * * @see Response#charset(String) * @see Response#contentType(String) * @param text the text of the body to write back to the client */ public Response body(String text) { return body(text(text)); } /** * Sets the binary content to write back to the client as the body of this response. * * @param content the binary of the body to write back to the client */ public Response body(byte[] content) { return body(bytes(content)); } /** * Sets the body to write back to the client with this response. Character bodies will be encoded * using the charset of the response. * * @see Response#charset(String) * @see Response#contentType(String) * @param body the body to write back to the client */ public Response body(Body body) { this.body = body; return this; } /** * Gets the body to write back to the client with this response. * * @return the body to send to the client */ public Body body() { return body; } /** * Gets the size of the body of this response. * * @return the size of the body as a long */ public long size() { return body.size(charset()); } /** * Checks whether the body of this response is empty, i.e. has a size of <code>0</code>. * * @return true is the body is empty, false otherwise */ public boolean empty() { return size() == 0; } /** * Sets the text content to write back to the client as the body of this response * then triggers a normal (i.e successful) completion of this response if not already completed. * The text content will be encoded using the charset of the response. * <p> * A call to <code>done</code> has no effect if this response has already completed, whether normally or * abnormally. * </p> * @param text the body text to write back to the client **/ public void done(String text) { done(text(text)); } /** * Sets the body to write back to the client then triggers a normal (i.e successful) completion of this response * if not already completed. * <p> * A call to <code>done</code> has no effect if this response has already completed, whether normally or * abnormally. * </p> * @param body the body to write back to the client **/ public void done(Body body) { body(body).done(); } /** * If not already completed, triggers a normal (i.e successful) completion of this response. * <p> * A call to <code>done</code> has no effect if this response has already completed, whether normally or * abnormally. * </p> **/ public void done() { done.complete(this); } /** * If not already completed, triggers an abnormal (i.e failed) completion of this response with the * given exception. * * <p> * A call to <code>done</code> has no effect if this response has already completed, whether normally or * abnormally. * </p> **/ public void done(Throwable error) { done.completeExceptionally(error); } /** * When this response completes normally (i.e. without an exception), * executes the given action, with this response. * <p> * Actions supplied will be executed in the order they are registered on this response. * <br> * To trigger normal completion of this response, call {@link #done()} . * </p> * * @param action the action to perform when this response completes successfully */ public Response whenSuccessful(Consumer<Response> action) { postProcessing = postProcessing.whenComplete((response, error) -> { if (response != null) action.accept(response); }); return this; } /** * When this response completes abnormally (i.e. with an exception), * executes the given action, with this response. * <p> * Actions supplied will be executed in the order they are registered on this response. * <br> * To trigger abnormal completion of this response, call {@link #done(Throwable)} . * </p> * * @param action the action to perform when this response completes abnormally */ public Response whenFailed(BiConsumer<Response, Throwable> action) { postProcessing = postProcessing.whenComplete((response, error) -> { if (error != null) action.accept(Response.this, unwrap(error)); }); return this; } /** * When this response completes either normally or abnormally (i.e. with or without an exception), * executes the given action with this response and the * exception (or {@code null} if the response completed normally). * <p> * Note that this method allows injection of an action regardless of outcome, * otherwise preserving the outcome in its completion, unlike {@link #rescue}. * <br> * Actions supplied will be executed in the order they are registered on this response. * </p> * <p> * To trigger completion of this response, call either forms of {@link #done}. * </p> * @param action the action to perform when this response completes */ public Response whenComplete(BiConsumer<Response, Throwable> action) { postProcessing = postProcessing.whenComplete((response, error) -> action.accept(Response.this, unwrap(error))); return this; } /** * When this response completes abnormally (i.e. with an exception), * executes the given action with this response and the exception, then continue processing this response * normally. * <p> * Note that this method replaces the failed result with this response before triggering the next action, * as if the response had completed normally. * <br> * Actions supplied will be executed in the order they are registered on this response. * </p> * @param action the action to perform when this response completes abnormally */ public Response rescue(BiConsumer<Response, Throwable> action) { postProcessing = postProcessing.handle((response, error) -> { if (error != null) action.accept(Response.this, unwrap(error)); return this; }); return this; } /** * Returns {@code true} if this response completed. * * Completion may be due to normal termination or an exception -- in all of these cases, this method will return * {@code true}. * * @return {@code true} if this response completed, false otherwise */ public boolean isDone() { return done.isDone(); } /** * Waits if necessary for this response to complete, and then * retrieves its result. * * @return this response if it completed normally * @throws ExecutionException if this response completed with an exception * @throws InterruptedException if the current thread was interrupted while waiting */ public Response await() throws ExecutionException, InterruptedException { return postProcessing.get(); } /** * Waits if necessary for at most the given time for this response * to complete, and then retrieves its result, if available. * * @param timeout the maximum time to wait * @param unit the time unit of the timeout argument * @return this response if it completed normally * @throws ExecutionException if this response completed with an exception * @throws InterruptedException if the current thread was interrupted while waiting * @throws TimeoutException if the wait timed out */ public Response await(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException { return postProcessing.get(timeout, unit); } private Throwable unwrap(Throwable error) { return error instanceof CompletionException ? error.getCause() : error; } }