/*
* COMSAT
* Copyright (c) 2013-2014, Parallel Universe Software Co. All rights reserved.
*
* This program and the accompanying materials are dual-licensed under
* either the terms of the Eclipse Public License v1.0 as published by
* the Eclipse Foundation
*
* or (per the licensee's choosing)
*
* under the terms of the GNU Lesser General Public License version 3.0
* as published by the Free Software Foundation.
*/
package co.paralleluniverse.comsat.webactors;
import co.paralleluniverse.actors.ActorRef;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* An HTTP response message sent as a {@link HttpRequest#sender() response} to an {@link HttpRequest}.
* When this response is sent to an {@link HttpRequest}'s {@link HttpRequest#sender() sender}, the connection stream will be closed
* if {@link HttpRequest#openChannel() openChannel} has not been called on the request,
* and will be flushed but not closed if {@link HttpRequest#openChannel() openChannel} <i>has</i> been called on the request.
*/
public abstract class HttpResponse extends HttpMessage {
@Override
public abstract ActorRef<WebMessage> getFrom();
/**
* The {@link HttpRequest} this is a response to.
*/
public abstract HttpRequest getRequest();
/**
* The response's HTTP status code.
*/
public abstract int getStatus();
/**
* An exception optionally associated with an error status code.
*/
public abstract Throwable getError();
/**
* The redirect URL target if this is a {@link #redirect(HttpRequest, String) redirect} response.
*/
public abstract String getRedirectPath();
public abstract boolean shouldStartActor();
@Override
protected String contentString() {
StringBuilder sb = new StringBuilder();
sb.append(" ").append(getStatus());
sb.append(" headers: ").append(getHeaders());
sb.append(" cookies: ").append(getCookies());
sb.append(" contentLength: ").append(getContentLength());
sb.append(" charEncoding: ").append(getCharacterEncoding());
if (getStringBody() != null)
sb.append(" body: ").append(getStringBody());
if (getRedirectPath() != null)
sb.append(" redirectPath: ").append(getRedirectPath());
if (getError() != null)
sb.append(" error: ").append(getError());
sb.append(" shouldStartActor: ").append(shouldStartActor());
return super.contentString() + sb;
}
/**
* Creates an {@link HttpResponse} with a text body and response code {@code 200}.
*
* @param request the {@link HttpRequest} this is a response to.
* @param body the response body
* @return A response {@link Builder} that can be used to add headers and other metadata to the response.
*/
public static Builder ok(ActorRef<? super WebMessage> from, HttpRequest request, String body) {
return new Builder(from, request, body);
}
/**
* Creates an {@link HttpResponse} with a binary body and response code {@code 200}.
*
* @param request the {@link HttpRequest} this is a response to.
* @param body the response body
* @return A response {@link Builder} that can be used to add headers and other metadata to the response.
*/
public static Builder ok(ActorRef<? super WebMessage> from, HttpRequest request, ByteBuffer body) {
return new Builder(from, request, body);
}
/**
* Creates an {@link HttpResponse} indicating an error, with a given status code and an attached exception that may be reported
* back to the client.
*
* @param request the {@link HttpRequest} this is a response to.
* @param status the response status code
* @param cause the exception that caused the error
* @return A response {@link Builder} that can be used to add headers and other metadata to the response.
*/
public static Builder error(ActorRef<? super WebMessage> from, HttpRequest request, int status, Throwable cause) {
return new Builder(from, request).status(status).error(cause);
}
/**
* Creates an {@link HttpResponse} indicating an error, with a given status code and a text body.
*
* @param request the {@link HttpRequest} this is a response to.
* @param status the response status code
* @param body the response body
* @return A response {@link Builder} that can be used to add headers and other metadata to the response.
*/
public static Builder error(ActorRef<? super WebMessage> from, HttpRequest request, int status, String body) {
return new Builder(from, request, body).status(status);
}
/**
* Sends a temporary redirect response to the client using the
* specified redirect location URL and clears the buffer.
* The status code is set to {@code SC_FOUND} 302 (Found).
* This method can accept relative URLs;
* the container must convert the relative URL to an absolute URL before sending the response to the client.
* If the location is relative without a leading '/' the container interprets it as relative to
* the current request URI.
* If the location is relative with a leading '/' the container interprets it as relative to the container root.
* If the location is relative with two leading '/' the container interprets
* it as a network-path reference
* (see <a href="http://www.ietf.org/rfc/rfc3986.txt"> RFC 3986: Uniform Resource Identifier (URI): Generic Syntax</a>, section 4.2 "Relative Reference").
*
* @param redirectPath the redirect location URL
*/
public static Builder redirect(HttpRequest request, String redirectPath) {
return new Builder(request).redirect(redirectPath);
}
public static class Builder {
private final ActorRef<WebMessage> sender;
private final HttpRequest request;
private final String strBody;
private final ByteBuffer binBody;
private String contentType;
private Charset charset;
private List<Cookie> cookies;
private ListMultimap<String, String> headers;
private int status;
private Throwable error;
private String redirectPath;
private boolean startActor;
public Builder(ActorRef<? super WebMessage> from, HttpRequest request, String body) {
this.sender = (ActorRef<WebMessage>) from;
this.request = request;
this.strBody = body;
this.binBody = null;
this.status = 200;
}
public Builder(ActorRef<? super WebMessage> from, HttpRequest request, ByteBuffer body) {
this.sender = (ActorRef<WebMessage>) from;
this.request = request;
this.binBody = body;
this.strBody = null;
this.status = 200;
}
public Builder(ActorRef<? super WebMessage> from, HttpRequest request) {
this(from, request, (String) null);
}
Builder(HttpRequest request, String body) {
this(null, request, body);
}
Builder(HttpRequest request, ByteBuffer body) {
this(null, request, body);
}
Builder(HttpRequest request) {
this(request, (String) null);
}
/**
* Sets the content type of the response being sent to the client.
* <p/>
* The given content type may include a character encoding
* specification, for example, {@code text/html;charset=UTF-8}.
* <p/>
* The {@code Content-Type} header is used to communicate the content type and the character
* encoding used in the response writer to the client
*
* @param contentType the MIME type of the content
*
*/
public Builder setContentType(String contentType) {
this.contentType = contentType;
return this;
}
/**
* Sets the character encoding (MIME charset) of the response being sent to the client,
* for example, {@code UTF-8}.
* If the character encoding has already been set by {@link #setContentType}, this method overrides it.
* Calling {@link #setContentType} with {@code "text/html"} and calling this method with {@code Charset.forName("UTF-8")}
* is equivalent with calling {@code setContentType} with {@code "text/html; charset=UTF-8"}.
* <p/>
* Note that the character encoding cannot be communicated via HTTP headers if
* content type is not specified; however, it is still used to encode text
* written in this response's body.
*
* @param charset only the character sets defined by IANA Character Sets
* (http://www.iana.org/assignments/character-sets)
*
* @see #setContentType
*/
public Builder setCharacterEncoding(Charset charset) {
this.charset = charset;
return this;
}
/**
* Adds a response header with the given name and value.
* This method allows response headers to have multiple values.
*
* @param name the name of the header
* @param value the additional header value.
* If it contains octet string, it should be encoded according to RFC 2047 (http://www.ietf.org/rfc/rfc2047.txt)
*/
public Builder addHeader(final String name, final String value) {
if (headers == null)
headers = LinkedListMultimap.create();
headers.put(name, value);
return this;
}
/**
* Adds the specified cookie to the response.
* This method can be called multiple times to set multiple cookies.
*
* @param cookie the {@link Cookie} to return to the client
* @return {@code this}
*/
public Builder addCookie(Cookie cookie) {
if (cookies == null)
cookies = new ArrayList<>();
cookies.add(cookie);
return this;
}
/**
* Sets the status code for this response.
*
* <p>
* This method is used to set the return status code
* Valid status codes are those in the 2XX, 3XX, 4XX, and 5XX ranges.
* Other status codes are treated as container specific.
* <p>
* codes in the 4XX and 5XX range will be treated as error codes, and
* will trigger the container's error reporting.
*
* @param sc the status code
* @return {@code this}
* @see #error
*/
public Builder status(int sc) {
this.status = sc;
return this;
}
/**
* Associates an exception with an error status. The exception may be used in the error report
* which might be sent to the client.
*
* @param error the exception responsible for the error
* @return {@code this}
*/
public Builder error(Throwable error) {
this.error = error;
return this;
}
/**
* Indicates that the connection to the client must not be closed after sending this response;
* rather an {@link HttpStreamOpened} message will be sent to the actor sending this response.
*
* @return {@code this}
*/
public Builder startActor() {
this.startActor = true;
return this;
}
Builder redirect(String redirectPath) {
this.redirectPath = redirectPath;
this.status = 302;
return this;
}
/**
* Instantiates a new immutable {@link HttpResponse} based on the values set in this builder.
*
* @return a new {@link HttpResponse}
*/
public HttpResponse build() {
return new SimpleHttpResponse(sender, this);
}
}
private static class SimpleHttpResponse extends HttpResponse {
//
private final ActorRef<WebMessage> sender;
private final HttpRequest request;
private final String contentType;
private final Charset charset;
private final String strBody;
private final ByteBuffer binBody;
private final Collection<Cookie> cookies;
private final ListMultimap<String, String> headers;
private final int status;
private final Throwable error;
private final String redirectPath;
private final boolean startActor;
/**
* Use when forwarding
*
* @param from
* @param httpResponse
*/
public SimpleHttpResponse(ActorRef<? super WebMessage> from, HttpResponse httpResponse) {
this.sender = (ActorRef<WebMessage>) from;
this.request = httpResponse.getRequest();
this.contentType = httpResponse.getContentType();
this.charset = httpResponse.getCharacterEncoding();
this.strBody = httpResponse.getStringBody();
this.binBody = httpResponse.getByteBufferBody() != null ? httpResponse.getByteBufferBody().asReadOnlyBuffer() : null;
this.cookies = httpResponse.getCookies();
this.headers = httpResponse.getHeaders();
this.error = httpResponse.getError();
this.status = httpResponse.getStatus();
this.redirectPath = httpResponse.getRedirectPath();
this.startActor = httpResponse.shouldStartActor();
}
public SimpleHttpResponse(ActorRef<? super WebMessage> from, Builder builder) {
this.sender = (ActorRef<WebMessage>) from;
this.request = builder.request;
this.contentType = builder.contentType;
this.charset = builder.charset;
this.strBody = builder.strBody;
this.binBody = builder.binBody != null ? builder.binBody.asReadOnlyBuffer() : null;
this.cookies = builder.cookies != null ? ImmutableList.copyOf(builder.cookies) : null;
this.headers = builder.headers != null ? ImmutableListMultimap.copyOf(builder.headers) : null;
this.error = builder.error;
this.status = builder.status;
this.redirectPath = builder.redirectPath;
this.startActor = builder.startActor;
}
@Override
public ActorRef<WebMessage> getFrom() {
return sender;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public Charset getCharacterEncoding() {
return charset;
}
@Override
public int getContentLength() {
if (binBody != null)
return binBody.remaining();
else
return -1;
}
@Override
public String getStringBody() {
return strBody;
}
@Override
public ByteBuffer getByteBufferBody() {
return binBody != null ? binBody.duplicate() : null;
}
@Override
public Collection<Cookie> getCookies() {
return cookies;
}
@Override
public ListMultimap<String, String> getHeaders() {
return headers;
}
/**
* The {@link HttpRequest} this is a response to.
*/
@Override
public HttpRequest getRequest() {
return request;
}
/**
* The response's HTTP status code.
*/
@Override
public int getStatus() {
return status;
}
/**
* An exception optionally associated with an error status code.
*/
@Override
public Throwable getError() {
return error;
}
/**
* The redirect URL target if this is a {@link #redirect(HttpRequest, String) redirect} response.
*/
@Override
public String getRedirectPath() {
return redirectPath;
}
@Override
public boolean shouldStartActor() {
return startActor;
}
}
}