The name must conform to RFC 2109. However, vendors may
* provide a configuration option that allows cookie names conforming
* to the original Netscape Cookie Specification to be accepted.
*
*
The name of a cookie cannot be changed once the cookie has been created.
*
*
The value can be anything the server chooses to send.
* Its value is probably of interest only to the server.
*
*
By default, cookies are created according to the Netscape cookie specification.
* The version can be changed with the {@link Builder#setVersion(int) setVersion} method.
*
* @param name the name of the cookie
* @param value the value of the cookie
* @return a new {@link Cookie.Builder}
*/
public static Builder cookie(String name, String value) {
return new Builder(name, value);
}
public static class Builder {
private final String name;
private final String value;
private String path;
private String domain;
private int maxAge = -1;
private String comment;
private int version;
private boolean httpOnly;
private boolean secure;
private Builder(String name, String value) {
this.name = name;
this.value = value;
}
/**
* Specifies a path for the cookie to which the client should return the cookie.
*
*
The cookie is visible to all the pages in the directory
* you specify, and all the pages in that directory's subdirectories.
* A cookie's path must include the servlet that set the cookie,
* for example, /catalog, which makes the cookie
* visible to all directories on the server under /catalog.
*
*
Consult RFC 2109 (available on the Internet) for more
* information on setting path names for cookies.
*
* @param uri a {@code String} specifying a path
*
* @see Cookie#getPath
*/
public Builder setPath(String uri) {
this.path = uri;
return this;
}
/**
*
* Specifies the domain within which this cookie should be presented.
*
*
The form of the domain name is specified by RFC 2109.
* A domain name begins with a dot ({@code .foo.com}) and means that
* the cookie is visible to servers in a specified Domain Name System
* (DNS) zone (for example,
* {@code www.foo.com}, but not {@code a.b.foo.com}).
* By default, cookies are only returned to the server that sent them.
*
* @param domain the domain name within which this cookie is visible;
* form is according to RFC 2109
*
* @see Cookie#getDomain
*/
public Builder setDomain(String domain) {
this.domain = domain;
return this;
}
/**
* Sets the maximum age in seconds for this Cookie.
*
*
A positive value indicates that the cookie will expire
* after that many seconds have passed. Note that the value is
* the maximum age when the cookie will expire, not the cookie's
* current age.
*
*
A negative value means that the cookie is not stored persistently and will
* be deleted when the Web browser exits.
* A zero value causes the cookie to be deleted.
*
* @param maxAge an integer specifying the maximum age of the
* cookie in seconds; if negative, means
* the cookie is not stored; if zero, deletes the cookie
*
* @see Cookie#getMaxAge
*/
public Builder setMaxAge(int maxAge) {
this.maxAge = maxAge;
return this;
}
/**
* Specifies a comment that describes a cookie's purpose.
* The comment is useful if the browser presents the cookie
* to the user. Comments
* are not supported by Netscape Version 0 cookies.
*
* @param comment a {@code String} specifying the comment to display to the user
*
* @see Cookie#getComment
*/
public Builder setComment(String comment) {
this.comment = comment;
return this;
}
/**
* Sets the version of the cookie protocol that this Cookie complies with.
*
*
Version 0 complies with the original Netscape cookie specification.
* Version 1 complies with RFC 2109.
*
*
Since RFC 2109 is still somewhat new, consider
* version 1 as experimental; do not use it yet on production sites.
*
* @param version 0 if the cookie should comply with the original Netscape
* specification; 1 if the cookie should comply with RFC 2109
*
* @see Cookie#getVersion
*/
public Builder setVersion(int version) {
this.version = version;
return this;
}
/**
* Marks or unmarks this Cookie as HttpOnly.
*
*
If {@code httpOnly} is set to {@code true}, this cookie is
* marked as HttpOnly, by adding the {@code HttpOnly} attribute to it.
*
*
HttpOnly cookies are not supposed to be exposed to
* client-side scripting code, and may therefore help mitigate certain
* kinds of cross-site scripting attacks.
*
* @param httpOnly {@code true} if this cookie is to be marked as HttpOnly, {@code false} otherwise
*
* @see Cookie#isHttpOnly()
*/
public Builder setHttpOnly(boolean httpOnly) {
this.httpOnly = httpOnly;
return this;
}
/**
* Indicates to the browser whether the cookie should only be sent
* using a secure protocol, such as HTTPS or SSL.
*
*
The default value is {@code false}.
*
* @param flag if {@code true}, sends the cookie from the browser
* to the server only when using a secure protocol;
* if {@code false}, sent on any protocol
*
* @see Cookie#isSecure
*/
public Builder setSecure(boolean flag) {
this.secure = flag;
return this;
}
/**
* Instantiates a new immutable {@link Cookie} based on the values set in this builder.
*
* @return a new {@link Cookie}
*/
public Cookie build() {
return new Cookie(this);
}
}
private final String name;
private final String value;
private final String path;
private final String domain;
private final int maxAge;
private final String comment;
private final int version;
private final boolean httpOnly;
private final boolean secure;
private Cookie(Builder builder) {
this.name = builder.name;
this.value = builder.value;
this.path = builder.path;
this.domain = builder.domain;
this.maxAge = builder.maxAge;
this.comment = builder.comment;
this.version = builder.version;
this.httpOnly = builder.httpOnly;
this.secure = builder.secure;
}
/**
* The name of the cookie.
*
* @see #cookie(String, String)
*/
public String getName() {
return name;
}
/**
* The value of this Cookie.
*
* @see #cookie(String, String)
*/
public String getValue() {
return value;
}
/**
* Returns the path on the server to which the browser returns this cookie.
* The cookie is visible to all subpaths on the server.
*
* @return a {@code String} specifying a path that contains a servlet name, for example, /catalog
*
* @see Builder#setPath
*/
public String getPath() {
return path;
}
/**
* The domain name of this Cookie.
*
*
Domain names are formatted according to RFC 2109.
*
* @see Builder#setDomain
*/
public String getDomain() {
return domain;
}
/**
* The maximum age in seconds of this Cookie.
*
*
By default, {@code -1} is returned, which indicates that the cookie will persist until browser shutdown.
*
* @return an integer specifying the maximum age of the cookie in seconds;
* if negative, means the cookie persists until browser shutdown
*
* @see Builder#setMaxAge
*/
public int getMaxAge() {
return maxAge;
}
/**
* The comment describing the purpose of this cookie, or {@code null} if the cookie has no comment.
*
* @see Builder#setComment
*/
public String getComment() {
return comment;
}
/**
* The version of the protocol this cookie complies with.
* Version 1 complies with RFC 2109,
* and version 0 complies with the original cookie specification drafted by Netscape.
* Cookies provided by a browser use and identify the browser's cookie version.
*
* @return {@code 0} if the cookie complies with the original Netscape specification;
* {@code 1} if the cookie complies with RFC 2109
*
* @see Builder#setVersion
*/
public int getVersion() {
return version;
}
/**
* Whether this Cookie has been marked as HttpOnly.
*
* @return {@code true} if this Cookie has been marked as HttpOnly, {@code false} otherwise
*/
public boolean isHttpOnly() {
return httpOnly;
}
/**
* Returns {@code true} if the browser is sending cookies only over a secure protocol,
* or {@code false} if the browser can send cookies using any protocol.
*
* @return {@code true} if the browser uses a secure protocol, {@code false} otherwise
*
* @see Builder#setSecure
*/
public boolean isSecure() {
return secure;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Cookie[");
sb.append("name: ").append(name);
sb.append(" value: ").append(value);
if (path != null)
sb.append(" path: ").append(path);
if (domain != null)
sb.append(" domain: ").append(domain);
sb.append(" maxAge: ").append(maxAge);
if (comment != null)
sb.append(" comment: ").append(comment);
sb.append(" version: ").append(version);
if (httpOnly)
sb.append(" httpOnly: ").append(httpOnly);
sb.append(" secure: ").append(secure);
sb.append(']');
return sb.toString();
}
}
================================================
FILE: comsat-actors-api/src/main/java/co/paralleluniverse/comsat/webactors/HttpMessage.java
================================================
/*
* 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 com.google.common.collect.ListMultimap;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Locale;
/**
* A message sent over an HTTP connection. Can be either a request or a response.
*/
public abstract class HttpMessage extends WebMessage {
/**
* A multimap of the headers contained in this message and (all) their values.
* If the request has no headers, returns an empty multimap.
*
* Note that header names' case is not preserved necessarily due to normalization for
* concordance with the HTTP specification.
*/
public abstract ListMultimap getHeaders();
/**
* A collection all of the {@link Cookie} objects the client sent with this message.
* This method returns an empty collection if no cookies were sent.
*/
public abstract Collection getCookies();
/**
* The length, in bytes, of the message body and made available by the input stream,
* or {@code -1} if the length is not known or is greater than {@code Integer.MAX_VALUE}.
*/
public abstract int getContentLength();
/**
* The {@link Charset} representing character encoding used in the body of this message.
* This method returns {@code null} if the message does not specify a character encoding.
*/
public abstract Charset getCharacterEncoding();
/**
* The MIME type of the body of the request, or {@code null} if the type is not known
* For example, {@code text/html; charset=UTF-8}.
*/
public abstract String getContentType();
/**
* Returns all values associated with the given header name
*
* @param name the header name
* @return all values associated with the given header name
*/
public Collection getHeaderValues(String name) {
return getHeaders().get(name);
}
/**
* Returns the value of the given header name.
* If the header is not found in the message, this method returns {@code null}.
* If the header has more than one value, this method returns the first value.
*
* In concordance with the HTTP specification, this method treats HTTP header names as case-insensitive.
*
* @param name the header name
* @return the (first) value of the given header name; {@code null} if the header is not found
*/
public String getHeader(String name) {
return first(getHeaders().get(name.toLowerCase(Locale.ENGLISH)));
}
static V first(Collection c) {
if (c == null || c.isEmpty())
return null;
return c.iterator().next();
}
}
================================================
FILE: comsat-actors-api/src/main/java/co/paralleluniverse/comsat/webactors/HttpRequest.java
================================================
/*
* COMSAT
* Copyright (c) 2013-2016, 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 com.google.common.collect.Multimap;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
/**
* An HTTP request message.
*/
public abstract class HttpRequest extends HttpMessage {
/**
* The Internet Protocol (IP) host that sent the request or {@code null} if not available.
*/
public abstract String getSourceHost();
/**
* The Internet Protocol (IP) port from which the request was sent or {@code -1} if not available.
*/
public abstract int getSourcePort();
/**
* A multimap of the parameters contained in this message and (all) their values.
* If the request has no parameters, returns an empty multimap.
*/
public abstract Multimap getParameters();
/**
* Returns the names values of all attributed associated with this request.
*
*
* The container may set
* attributes to make available custom information about a request.
* For example, for requests made using HTTPS, the attribute
* javax.servlet.request.X509Certificate can be used to
* retrieve information on the certificate of the client.
*
* @return an {@code Object} containing the value of the attribute,
* or {@code null} if the attribute does not exist
*/
public abstract Map getAttributes();
/**
* Returns all values associated with the given parameter
*
* @param name the parameter name
* @return all values associated with the given parameter
*/
public Collection getParametersValues(String name) {
return getParameters().get(name);
}
/**
* Returns the value of the given parameter.
* If the parameter is not found in the message, this method returns {@code null}.
* If the parameter has more than one value, this method returns the first value.
*
* @param name the parameter name
* @return the (first) value of the given parameter name; {@code null} if the parameter is not found
*/
public String getParameter(String name) {
return first(getParameters().get(name));
}
/**
* Returns the value of the given attribute.
* If the attribute is not found in the message, this method returns {@code null}.
*
* @param name the attribute name
* @return the value of the given attribute; {@code null} if the attribute is not found
*/
public Object getAttribute(String name) {
return getAttributes().get(name);
}
/**
* Returns the name of the scheme used to make this request, for example, {@code http}, {@code https}, or {@code ftp}.
* Different schemes have different rules for constructing URLs, as noted in RFC 1738.
*/
public abstract String getScheme();
/**
* The name of the HTTP method with which this request was made;
* for example, GET, POST, or PUT.
*
* @return the name of the method with which this request was made
*/
public abstract String getMethod();
/**
* Returns the value of the specified request header as a {@code long} value that represents a {@code Date} object.
* Use this method with headers that contain dates, such as {@code If-Modified-Since}.
*
*
* The date is returned as the number of milliseconds since January 1, 1970 GMT.
*
*
* If the request does not have a header of the specified name, this method returns -1.
* If the header can't be converted to a date, the method throws an {@code IllegalArgumentException}.
*
* @param name the name of the header
*
* @return a {@code long} value representing the date specified in the header expressed as
* the number of milliseconds since January 1, 1970 GMT,
* or {@code -1} if the named header was not included with the request
*
* @exception IllegalArgumentException If the header value can't be converted to a date
*/
public long getDateHeader(String name) {
String value = getHeader(name);
if (value == null)
return (-1L);
long result = parseDate(value);
if (result != (-1L))
return result;
throw new IllegalArgumentException(value);
}
/**
* Returns any extra path information associated with the URL the client sent when it made this request.
* The extra path information follows the servlet path but precedes the query string and will start with
* a "/" character.
*
*
* This method returns {@code null} if there was no extra path information.
*
* @return a {@code String} decoded by the web container, specifying
* extra path information that comes after the servlet path but before the query string in the request URL;
* or {@code null} if the URL does not have any extra path information
*/
public abstract String getPathInfo();
/**
* The portion of the request URI that indicates the context of the request.
* The context path always comes first in a request URI.
* The path starts with a "/" character but does not end with a "/" character.
* For servlets in the default (root) context, this method returns "".
* The container does not decode this string.
*
*
* It is possible that a container may match a context by
* more than one context path. In such cases this method will return the
* actual context path used by the request.
*
* @return the portion of the request URI that indicates the context of the request
*/
public abstract String getContextPath();
/**
* The query string that is contained in the request URL after the path,
* or {@code null} if the URL does not have a query string.
*/
public abstract String getQueryString();
/**
* Returns the part of this request's URL from the protocol name up to the query string in the first line of the HTTP request.
* The web container does not decode this string.
* For example:
*
*
*
*
First line of HTTP request
Returned Value
*
POST /some/path.html HTTP/1.1
/some/path.html
*
GET http://foo.bar/a.html HTTP/1.0
/a.html
*
HEAD /xyz?a=b HTTP/1.1
/xyz
*
*
* @return the part of the URL from the protocol name up to the query string
*/
public abstract String getRequestURI();
/**
* Reconstructs the URL the client used to make the request.
* The returned URL contains a protocol, server name, port
* number, and server path, but it does not include query
* string parameters.
*
*
* This method is useful for creating redirect messages
* and for reporting errors.
*
* @return the reconstructed URL
*/
public String getRequestURL() {
StringBuilder url = new StringBuilder();
String scheme = getScheme();
int port = getServerPort();
if (port < 0)
port = 80; // Work around java.net.URL bug
url.append(scheme);
url.append("://");
url.append(getServerName());
if ((scheme.equals("http") && (port != 80))
|| (scheme.equals("https") && (port != 443))) {
url.append(':');
url.append(port);
}
url.append(getRequestURI());
return url.toString();
}
/**
* The host name of the server to which the request was sent.
* It is the value of the part before ":" in the {@code Host} header value, if any,
* or the resolved server name, or the server IP address.
*/
public abstract String getServerName();
/**
* The port number to which the request was sent.
* It is the value of the part after ":" in the {@code Host} header value, if any,
* or the server port where the client connection was accepted on.
*
* @return the port number
*/
public abstract int getServerPort();
/**
* Returns an actor representing the client to which an {@link HttpResponse} should be sent as a response to this request.
* All {@code HttpRequest}s from the same session will have the same sender. It will appear to have died (i.e. send an
* {@link co.paralleluniverse.actors.ExitMessage ExitMessage} if {@link co.paralleluniverse.actors.Actor#watch(co.paralleluniverse.actors.ActorRef) watched})
* when the session is terminated.
*
* @return an actor representing the client
*/
@Override
public abstract ActorRef getFrom();
@Override
protected String contentString() {
StringBuilder sb = new StringBuilder();
sb.append(" ").append(getMethod());
sb.append(" sourceHost: ").append(getSourceHost());
sb.append(" uri: ").append(getRequestURI());
sb.append(" query: ").append(getQueryString());
sb.append(" params: ").append(getParameters());
sb.append(" headers: ").append(getHeaders());
sb.append(" cookies: ").append(getCookies());
sb.append(" contentLength: ").append(getContentLength());
sb.append(" charEncoding: ").append(getCharacterEncoding());
sb.append(" body: ").append(getStringBody());
return super.contentString() + sb;
}
public static class Builder {
private final ActorRef sender;
private final String strBody;
private final ByteBuffer binBody;
private String sourceHost;
private int sourcePort;
private String contentType;
private Charset charset;
private List cookies;
private ListMultimap headers;
private String method;
private String scheme;
private String server;
private int port;
private String path;
private Multimap params;
public Builder(ActorRef super WebMessage> from, String body) {
this.sender = (ActorRef) from;
this.strBody = body;
this.binBody = null;
}
public Builder(ActorRef super WebMessage> from, ByteBuffer body) {
this.sender = (ActorRef) from;
this.binBody = body;
this.strBody = null;
}
public Builder(ActorRef super WebMessage> from) {
this(from, (String) null);
}
public Builder setSourceHost(String sourceAddress) {
this.sourceHost = sourceAddress;
return this;
}
public Builder setSourcePort(int sourcePort) {
this.sourcePort = sourcePort;
return this;
}
/**
* Sets the content type of the request being sent to the client.
*
* The given content type may include a character encoding
* specification, for example, {@code text/html;charset=UTF-8}.
*
* 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"}.
*
* 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 request 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();
// Normalize header names by their conversion to lower case
headers.put(name.toLowerCase(Locale.ENGLISH), value);
return this;
}
/**
* Adds the specified cookie to the request.
* 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;
}
public Builder setMethod(String method) {
this.method = method;
return this;
}
public Builder setScheme(String scheme) {
this.scheme = scheme;
return this;
}
public Builder setServer(String server) {
this.server = server;
return this;
}
public Builder setPort(int port) {
this.port = port;
return this;
}
public Builder setPath(String path) {
this.path = path;
return this;
}
/**
* Adds a request parameter with the given name and value.
*
* @param name the name of the parameter
* @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 addParam(final String name, final String value) {
if (params == null)
params = LinkedListMultimap.create();
params.put(name, value);
return this;
}
/**
* Instantiates a new immutable {@link HttpRequest} based on the values set in this builder.
*
* @return a new {@link HttpRequest}
*/
public HttpRequest build() {
return new SimpleHttpRequest(sender, this);
}
}
private static class SimpleHttpRequest extends HttpRequest {
private final String sourceHost;
private final int sourcePort;
private final ActorRef sender;
private final String contentType;
private final Charset charset;
private final String strBody;
private final ByteBuffer binBody;
private final Collection cookies;
private final ListMultimap headers;
private final Multimap params;
private final String method;
private final String scheme;
private final String server;
private final int port;
private final String uri;
/**
* Use when forwarding
*
* @param from
* @param httpRequest
*/
public SimpleHttpRequest(ActorRef super WebMessage> from, HttpRequest httpRequest) {
this.sourceHost = httpRequest.getSourceHost();
this.sourcePort = httpRequest.getSourcePort();
this.sender = (ActorRef) from;
this.contentType = httpRequest.getContentType();
this.charset = httpRequest.getCharacterEncoding();
this.strBody = httpRequest.getStringBody();
this.binBody = httpRequest.getByteBufferBody() != null ? httpRequest.getByteBufferBody().asReadOnlyBuffer() : null;
this.cookies = httpRequest.getCookies();
this.headers = httpRequest.getHeaders();
this.method = httpRequest.getMethod();
this.scheme = httpRequest.getScheme();
this.server = httpRequest.getServerName();
this.port = httpRequest.getServerPort();
this.params = httpRequest.getParameters();
this.uri = httpRequest.getRequestURI();
}
public SimpleHttpRequest(ActorRef super WebMessage> from, HttpRequest.Builder builder) {
this.sourceHost = builder.sourceHost;
this.sourcePort = builder.sourcePort;
this.sender = (ActorRef) from;
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.params = builder.params != null ? ImmutableListMultimap.copyOf(builder.params) : null;
this.method = builder.method;
this.scheme = builder.scheme;
this.server = builder.server;
this.port = builder.port;
this.uri = builder.path;
}
@Override
public String getSourceHost() {
return sourceHost;
}
@Override
public int getSourcePort() {
return sourcePort;
}
@Override
public ActorRef 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 getCookies() {
return cookies;
}
@Override
public ListMultimap getHeaders() {
return headers;
}
@Override
public Map getAttributes() {
return null;
}
@Override
public String getScheme() {
return scheme;
}
@Override
public String getMethod() {
return method;
}
@Override
public String getServerName() {
return server;
}
@Override
public int getServerPort() {
return port;
}
@Override
public Multimap getParameters() {
return params;
}
@Override
public String getRequestURI() {
return uri;
}
@Override
public String getQueryString() {
if(params == null)
return null;
StringBuilder sb = new StringBuilder();
for(Map.Entry entry : params.entries())
sb.append(entry.getKey()).append('=').append(entry.getValue()).append('&');
sb.delete(sb.length()-1, sb.length());
return sb.toString();
}
@Override
public String getPathInfo() {
throw new UnsupportedOperationException();
}
@Override
public String getContextPath() {
throw new UnsupportedOperationException();
}
}
/**
* The only date format permitted when generating HTTP headers.
*/
public static final String RFC1123_DATE = "EEE, dd MMM yyyy HH:mm:ss zzz";
private static final SimpleDateFormat format = new SimpleDateFormat(RFC1123_DATE, Locale.US);
/**
* The set of SimpleDateFormat formats to use in getDateHeader().
*/
private static final SimpleDateFormat formats[] = {
new SimpleDateFormat(RFC1123_DATE, Locale.US),
new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US)
};
private static final TimeZone gmtZone = TimeZone.getTimeZone("GMT");
/**
* All HTTP dates are on GMT
*/
static {
format.setTimeZone(gmtZone);
formats[0].setTimeZone(gmtZone);
formats[1].setTimeZone(gmtZone);
formats[2].setTimeZone(gmtZone);
}
private static long parseDate(String value) {
Date date = null;
for (int i = 0; (date == null) && (i < formats.length); i++) {
try {
date = formats[i].parse(value);
} catch (ParseException e) {
// Ignore
}
}
if (date == null)
return -1L;
return date.getTime();
}
}
================================================
FILE: comsat-actors-api/src/main/java/co/paralleluniverse/comsat/webactors/HttpResponse.java
================================================
/*
* 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} has been called on the request.
*/
public abstract class HttpResponse extends HttpMessage {
@Override
public abstract ActorRef 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 RFC 3986: Uniform Resource Identifier (URI): Generic Syntax, 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 sender;
private final HttpRequest request;
private final String strBody;
private final ByteBuffer binBody;
private String contentType;
private Charset charset;
private List cookies;
private ListMultimap 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) 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) 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.
*
* The given content type may include a character encoding
* specification, for example, {@code text/html;charset=UTF-8}.
*
* 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"}.
*
* 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.
*
*
* 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.
*
* 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 sender;
private final HttpRequest request;
private final String contentType;
private final Charset charset;
private final String strBody;
private final ByteBuffer binBody;
private final Collection cookies;
private final ListMultimap 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) 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) 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 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 getCookies() {
return cookies;
}
@Override
public ListMultimap 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;
}
}
}
================================================
FILE: comsat-actors-api/src/main/java/co/paralleluniverse/comsat/webactors/HttpStreamOpened.java
================================================
/*
* 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 co.paralleluniverse.actors.behaviors.IdMessage;
/**
* A message sent to a web actor indicating that a new stream has been opened.
* This message is sent as a result of the web actor replying to an {@link HttpRequest}
* with an {@link HttpResponse} whose {@link HttpResponse.Builder#startActor() startActor} method has been called.
* The actor sending this message writes all received {@link WebDataMessage} to the HTTP response stream that's been left
* open after the {@link HttpResponse}.
*
* This is usually used by SSE.
*/
public class HttpStreamOpened extends WebStreamOpened implements IdMessage {
private final HttpResponse response;
public HttpStreamOpened(ActorRef actor, HttpResponse response) {
super(actor);
this.response = response;
}
/**
* {@inheritDoc }
*
* Returns the {@link HttpResponse} passed to the constructor.
*/
@Override
public Object getId() {
return response;
}
}
================================================
FILE: comsat-actors-api/src/main/java/co/paralleluniverse/comsat/webactors/SSE.java
================================================
/*
* 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 co.paralleluniverse.strands.channels.Channels;
import co.paralleluniverse.strands.channels.SendPort;
import com.google.common.base.Function;
import java.nio.charset.Charset;
/**
* Utility classes for SSE (Server-Sent Events).
* To start an SSE stream in response to an {@link HttpRequest}, do the following:
*
* ```java
* request.getFrom().send(new HttpResponse(self(), SSE.startSSE(request)));
* ```
* This will result in a {@link HttpStreamOpened} message being sent to the web actor from a newly
* created actor that represents the SSE connection. To send SSE events, simply send {@link WebDataMessage}s
* to that actor:
*
* ```java
* // send events
* sseActor.send(new WebDataMessage(self(), SSE.event("this is an SSE event!")));
* ```
*
* You might want to consider wrapping the actor sending {@link HttpStreamOpened} with a
* {@link co.paralleluniverse.strands.channels.Channels#mapSend(co.paralleluniverse.strands.channels.SendPort, com.google.common.base.Function) mapping channel}
* to transform a specialized message class into {@link WebDataMessage} using the methods in this class.
*
* For a good tutorial on SSE, please see: Stream Updates with Server-Sent Events,
* by Eric Bidelman
*/
public final class SSE {
/*
*see http://www.html5rocks.com/en/tutorials/eventsource/basics/
*/
/**
* This method returns a new {@link HttpResponse HttpResponse} with
* its {@link HttpResponse.Builder#setContentType(String) content type}
* and {@link HttpResponse.Builder#setCharacterEncoding(java.nio.charset.Charset) character encoding} set
* in compliance with to the SSE spec, and an empty body.
*
* @param request the {@link HttpRequest} in response to which we wish to start an SSE stream.
* @return an {@link HttpResponse.Builder HttpResponse.Builder} (which can have other metadata, such as headers or cookies added to).
*/
public static HttpResponse.Builder startSSE(ActorRef super WebMessage> from, HttpRequest request) {
return new HttpResponse.Builder(from, request)
.setContentType("text/event-stream")
.setCharacterEncoding(Charset.forName("UTF-8"))
.startActor();
}
/**
* This method returns a new {@link HttpResponse HttpResponse} with
* its {@link HttpResponse.Builder#setContentType(String) content type}
* and {@link HttpResponse.Builder#setCharacterEncoding(java.nio.charset.Charset) character encoding} set
* in compliance with to the SSE spec, and a body encoding a {@link #reconnectTimeout(long) reconnection timeout} indication.
*
* @param request the {@link HttpRequest} in response to which we wish to start an SSE stream.
* @param reconnectTimeout the amount of time, in milliseconds, the client should wait before attempting to reconnect
* after the connection has closed (will be encoded in the message body as {@code retry: ...})
* @return an {@link HttpResponse.Builder HttpResponse.Builder} (which can have other metadata, such as headers or cookies added to).
*/
public static HttpResponse.Builder startSSE(ActorRef super WebMessage> from, HttpRequest request, long reconnectTimeout) {
return new HttpResponse.Builder(from, request, retryString(reconnectTimeout) + '\n')
.setContentType("text/event-stream")
.setCharacterEncoding(Charset.forName("UTF-8"))
.startActor();
}
/**
* Wrappes the whole string body
*/
public static SendPort wrapAsSSE(SendPort actor) {
return Channels.mapSend(actor, new Function() {
@Override
public WebDataMessage apply(WebDataMessage f) {
return new WebDataMessage(f.getFrom(), SSE.event(f.getStringBody()));
}
});
}
/**
* Returns the SSE last-event-id value from the request (the {@code Last-Event-ID} header).
*
* @param request the request
* @return the SSE last-event-id value from the request, or {@code -1} if not specified.
*/
public static long getLastEventId(HttpRequest request) {
String str = request.getHeader("Last-Event-ID");
if (str == null)
return -1;
return Long.parseLong(str);
}
/**
* Encodes a given payload as an SSE event message. The returned value can be used as the body of a {@link WebDataMessage}.
*
* @param id the SSE event id (will be encoded in the message as {@code id: ...})
* @param eventType the name of the type of the event (will be encoded in the message as {@code event: ...})
* @param payload the message payload (will be encoded in the message as {@code data: ...})
* @return the payload encoded as an SSE event
*/
public static String event(long id, String eventType, String payload) {
return idString(id) + eventString(eventType) + dataString(payload) + '\n';
}
/**
* Encodes a given payload as an SSE event message. The returned value can be used as the body of a {@link WebDataMessage}.
*
* @param eventType the name of the type of the event (will be encoded in the message as {@code event: ...})
* @param payload the message payload (will be encoded in the message as {@code data: ...})
* @return the payload encoded as an SSE event
*/
public static String event(String eventType, String payload) {
return dataString(payload) + '\n';
}
/**
* Encodes a given payload and id as an SSE event message. The returned value can be used as the body of a {@link WebDataMessage}.
*
* @param id the SSE event id (will be encoded in the message as {@code id: ...})
* @param payload the message payload (will be encoded in the message as {@code data: ...})
* @return the id and payload encoded as an SSE event
*/
public static String event(long id, String payload) {
return idString(id) + dataString(payload) + '\n';
}
/**
* Encodes a given payload as an SSE event message. The returned value can be used as the body of a {@link WebDataMessage}.
*
* @param payload the message payload the message payload (will be encoded in the message as {@code data: ...})
* @return the payload encoded as an SSE event
*/
public static String event(String payload) {
return dataString(payload) + '\n';
}
/**
* Encodes an indication to the client to attempt a reconnect if the connection is closed within the given time.
* This string may be concatenated ahead of a string encoding an SSE event, like so: {@code reconnectTimeout(t) + event(x)}).
*
* @param reconnectTimeout the amount of time, in milliseconds, the client should wait before attempting to reconnect
* after the connection has closed (will be encoded in the message as {@code retry: ...})
* @return a string encoding the reconnection timeout indication
*/
public static String reconnectTimeout(long reconnectTimeout) {
return retryString(reconnectTimeout);
}
private static String idString(long id) {
return "id: " + id + '\n';
}
private static String eventString(String eventName) {
return "event: " + eventName + '\n';
}
private static String retryString(long reconnectTimeout) {
return "retry: " + reconnectTimeout + '\n';
}
private static String dataString(String payload) {
String message = payload.trim();
if (message.charAt(message.length() - 1) == '\n')
message = message.substring(0, message.length() - 1);
message = message.replaceAll("\n", "\ndata: ");
message = "data: " + message + '\n';
return message;
}
private SSE() {
}
}
================================================
FILE: comsat-actors-api/src/main/java/co/paralleluniverse/comsat/webactors/WebActor.java
================================================
/*
* 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 java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A class annotated with this annotation will be automatically loaded by COMSAT
* and spawned as a web actor.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface WebActor {
/**
* The name of the WebActor.
*/
String name() default "";
/**
* Display name of this WebActor, if present.
*/
String displayName() default "";
/**
* Description of this WebActor, if present.
*/
String description() default "";
/**
* Array of HTTP URL patterns to which this WebActor applies.
* For example {@code /myservice}, or {@code /myservice/*}.
*/
String[] httpUrlPatterns() default {};
/**
* Array of WebSocket URI patterns to which this WebActor applies.
* For example {@code /myservice/ws}, or {@code /myservice/ws/*}.
*/
String[] webSocketUrlPatterns() default {};
/**
* A convenience property, to allow extremely simple annotation of a class -
* Array of HTTP URL patterns.
*
* @see #httpUrlPatterns()
*/
String[] value() default {};
}
================================================
FILE: comsat-actors-api/src/main/java/co/paralleluniverse/comsat/webactors/WebDataMessage.java
================================================
/*
* 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 java.nio.ByteBuffer;
/**
* A message that can be received from or sent to a web client, and contains only data (and no metadata like headers).
*/
public class WebDataMessage extends WebMessage {
private final ActorRef sender;
private final String string;
private final ByteBuffer byteBuffer;
/**
* Constructs a {@code WebDataMessage} with a text body.
* @param from the message sender
* @param body the message body
*/
public WebDataMessage(ActorRef super WebDataMessage> from, String body) {
this.sender = (ActorRef)from;
this.string = body;
this.byteBuffer = null;
}
/**
* Constructs a {@code WebDataMessage} with a binary body.
* @param from the message sender
* @param body the message body
*/
public WebDataMessage(ActorRef super WebDataMessage> from, ByteBuffer body) {
this.sender = (ActorRef)from;
this.string = null;
this.byteBuffer = body;
}
@Override
public ActorRef getFrom() {
return sender;
}
/**
* Whether this is a binary message or a text message.
* @return {@code true} if this is a binary message; {@code false} if this is a text mesasge.
*/
public boolean isBinary() {
return (byteBuffer != null);
}
@Override
public String getStringBody() {
return string;
}
@Override
public ByteBuffer getByteBufferBody() {
return byteBuffer;
}
@Override
protected String contentString() {
return super.contentString() +
" size: " + (string != null ? string.length() : byteBuffer != null ? byteBuffer.remaining() : 0) +
(isBinary() ? "" : " data: " + string);
}
}
================================================
FILE: comsat-actors-api/src/main/java/co/paralleluniverse/comsat/webactors/WebMessage.java
================================================
/*
* 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 co.paralleluniverse.actors.behaviors.ActorMessage;
import co.paralleluniverse.actors.behaviors.FromMessage;
import java.nio.ByteBuffer;
/**
* A message that is received from or can be set to a web client (via HTTP or WebSockets).
* The message has either a {@link #getStringBody() text body} or a {@link #getByteBufferBody() binary} body (but not both).
*/
public abstract class WebMessage extends ActorMessage implements FromMessage {
/**
* The actor that sent this message. This can be a virtual actor representing the web client.
*/
@Override
public abstract ActorRef extends WebMessage> getFrom();
/**
* The message's text body, if it has one; {@code null} otherwise.
*/
public abstract String getStringBody();
/**
* The message's binary body, if it has one; {@code null} otherwise.
*/
public abstract ByteBuffer getByteBufferBody();
@Override
protected String contentString() {
return super.contentString() + " from: " + getFrom();
}
}
================================================
FILE: comsat-actors-api/src/main/java/co/paralleluniverse/comsat/webactors/WebSocketOpened.java
================================================
/*
* 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;
/**
* A message sent to a web actor indicating that a new WebSocket has been opened by the client.
*/
public class WebSocketOpened extends WebStreamOpened {
public WebSocketOpened(ActorRef actor) {
super(actor);
}
}
================================================
FILE: comsat-actors-api/src/main/java/co/paralleluniverse/comsat/webactors/WebStreamOpened.java
================================================
/*
* 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 java.nio.ByteBuffer;
/**
* A message sent to a web actor indicating that a new stream has been opened by the client.
* The stream is usually a WebSocket or an SSE stream.
*/
public class WebStreamOpened extends WebMessage {
private final ActorRef actor;
public WebStreamOpened(ActorRef actor) {
this.actor = actor;
}
@Override
public ActorRef getFrom() {
return actor;
}
/**
* {@inheritDoc}
*
This method returns {@code null}, as it has no body.
*
* @return {@code null}
*/
@Override
public String getStringBody() {
return null;
}
/**
* {@inheritDoc}
*
This method returns {@code null}, as it has no body.
*
* @return {@code null}
*/
@Override
public ByteBuffer getByteBufferBody() {
return null;
}
}
================================================
FILE: comsat-actors-api/src/main/java/co/paralleluniverse/comsat/webactors/package-info.java
================================================
/*
* 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.
*/
/**
* The Web Actors API
*/
package co.paralleluniverse.comsat.webactors;
================================================
FILE: comsat-actors-netty/src/main/java/co/paralleluniverse/comsat/webactors/netty/AutoWebActorHandler.java
================================================
/*
* COMSAT
* Copyright (c) 2015-2016, 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.netty;
import co.paralleluniverse.actors.Actor;
import co.paralleluniverse.actors.ActorImpl;
import co.paralleluniverse.actors.ActorRef;
import co.paralleluniverse.actors.ActorSpec;
import co.paralleluniverse.common.reflection.AnnotationUtil;
import co.paralleluniverse.common.reflection.ClassLoaderUtil;
import co.paralleluniverse.common.util.Pair;
import co.paralleluniverse.comsat.webactors.WebActor;
import co.paralleluniverse.comsat.webactors.WebMessage;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
/**
* @author circlespainter
*/
public class AutoWebActorHandler extends WebActorHandler {
private static final InternalLogger log = InternalLoggerFactory.getInstance(AutoWebActorHandler.class);
private static final List> actorClasses = new ArrayList<>(32);
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
public AutoWebActorHandler() {
this(null, null, null, null);
}
public AutoWebActorHandler(List packagePrefixes) {
this(null, null, packagePrefixes, null);
}
public AutoWebActorHandler(String httpResponseEncoderName, List packagePrefixes) {
this(httpResponseEncoderName, null, packagePrefixes, null);
}
public AutoWebActorHandler(String httpResponseEncoderName) {
this(httpResponseEncoderName, null, null, null);
}
public AutoWebActorHandler(String httpResponseEncoderName, ClassLoader userClassLoader) {
this(httpResponseEncoderName, userClassLoader, null, null);
}
public AutoWebActorHandler(String httpResponseEncoderName, ClassLoader userClassLoader, List packagePrefixes) {
this(httpResponseEncoderName, userClassLoader, packagePrefixes, null);
}
public AutoWebActorHandler(String httpResponseEncoderName, Map, Object[]> actorParams) {
this(httpResponseEncoderName, null, null, actorParams);
}
public AutoWebActorHandler(String httpResponseEncoderName, List packagePrefixes, Map, Object[]> actorParams) {
this(httpResponseEncoderName, null, packagePrefixes, actorParams);
}
public AutoWebActorHandler(String httpResponseEncoderName, ClassLoader userClassLoader, List packagePrefixes, Map, Object[]> actorParams) {
super(null, httpResponseEncoderName);
super.contextProvider = newContextProvider(userClassLoader != null ? userClassLoader : ClassLoader.getSystemClassLoader(), packagePrefixes, actorParams);
}
public AutoWebActorHandler(String httpResponseEncoderName, AutoContextProvider prov) {
super(prov, httpResponseEncoderName);
}
protected AutoContextProvider newContextProvider(ClassLoader userClassLoader, List packagePrefixes, Map, Object[]> actorParams) {
return new AutoContextProvider(userClassLoader, packagePrefixes, actorParams);
}
public static class AutoContextProvider implements WebActorContextProvider {
private final ClassLoader userClassLoader;
private final List packagePrefixes;
private final Map, Object[]> actorParams;
private final Long defaultContextValidityMS;
public AutoContextProvider(ClassLoader userClassLoader, List packagePrefixes, Map, Object[]> actorParams) {
this(userClassLoader, packagePrefixes, actorParams, null);
}
public AutoContextProvider(ClassLoader userClassLoader, List packagePrefixes, Map, Object[]> actorParams, Long defaultContextValidityMS) {
this.userClassLoader = userClassLoader;
this.packagePrefixes = packagePrefixes;
this.actorParams = actorParams;
this.defaultContextValidityMS = defaultContextValidityMS;
}
@Override
public final Context get(final FullHttpRequest req) {
final String sessionId = getSessionId(req);
if (sessionId != null && sessionsEnabled()) {
final Context actorContext = sessions.get(sessionId);
if (actorContext != null) {
if (actorContext.renew())
return actorContext;
else
sessions.remove(sessionId); // Evict session
}
}
return newActorContext(req);
}
protected AutoContext newActorContext(FullHttpRequest req) {
final AutoContext c = new AutoContext(req, packagePrefixes, actorParams, userClassLoader);
if (defaultContextValidityMS != null)
c.setValidityMS(defaultContextValidityMS);
return c;
}
private String getSessionId(FullHttpRequest req) {
final Set cookies = NettyHttpRequest.getNettyCookies(req);
if (cookies != null) {
for (final Cookie c : cookies) {
if (c != null && SESSION_COOKIE_KEY.equals(c.name()))
return c.value();
}
}
return null;
}
}
private static class AutoContext extends DefaultContextImpl {
private String id;
private final List packagePrefixes;
private final Map, Object[]> actorParams;
private final ClassLoader userClassLoader;
private Class extends ActorImpl extends WebMessage>> actorClass;
private ActorRef extends WebMessage> actorRef;
public AutoContext(FullHttpRequest req, List packagePrefixes, Map, Object[]> actorParams, ClassLoader userClassLoader) {
this.packagePrefixes = packagePrefixes;
this.actorParams = actorParams;
this.userClassLoader = userClassLoader;
fillActor(req);
}
private void fillActor(FullHttpRequest req) {
final Pair, Class extends ActorImpl extends WebMessage>>> p = autoCreateActor(req);
if (p != null) {
actorRef = p.getFirst();
actorClass = p.getSecond();
}
}
@Override
public final String getId() {
return id != null ? id : (id = UUID.randomUUID().toString());
}
@Override
public final void restart(FullHttpRequest req) {
renewed = new Date().getTime();
fillActor(req);
}
@Override
public final ActorRef extends WebMessage> getWebActor() {
return actorRef;
}
@Override
public final boolean handlesWithHttp(String uri) {
return WebActorHandler.handlesWithHttp(uri, actorClass);
}
@Override
public final boolean handlesWithWebSocket(String uri) {
return WebActorHandler.handlesWithWebSocket(uri, actorClass);
}
@Override
public WatchPolicy watch() {
return WatchPolicy.DIE_IF_EXCEPTION_ELSE_RESTART;
}
@SuppressWarnings("unchecked")
private Pair, Class extends ActorImpl extends WebMessage>>> autoCreateActor(FullHttpRequest req) {
registerActorClasses();
final String uri = req.getUri();
for (final Class> c : actorClasses) {
if (WebActorHandler.handlesWithHttp(uri, c) || WebActorHandler.handlesWithWebSocket(uri, c))
return new Pair, Class extends ActorImpl extends WebMessage>>>(
Actor.newActor (
new ActorSpec(c, actorParams != null ? actorParams.get(c) : EMPTY_OBJECT_ARRAY)
).spawn(),
(Class extends ActorImpl extends WebMessage>>) c
);
}
return null;
}
private synchronized void registerActorClasses() {
if (actorClasses.isEmpty()) {
try {
final ClassLoader classLoader = userClassLoader != null ? userClassLoader : this.getClass().getClassLoader();
ClassLoaderUtil.accept((URLClassLoader) classLoader, new ClassLoaderUtil.Visitor() {
@Override
public final void visit(String resource, URL url, ClassLoader cl) {
if (packagePrefixes != null) {
boolean found = false;
for (final String packagePrefix : packagePrefixes) {
if (packagePrefix != null && resource.startsWith(packagePrefix.replace('.', '/'))) {
found = true;
break;
}
}
if (!found)
return;
}
if (!ClassLoaderUtil.isClassFile(resource))
return;
final String className = ClassLoaderUtil.resourceToClass(resource);
try (final InputStream is = cl.getResourceAsStream(resource)) {
if (AnnotationUtil.hasClassAnnotation(WebActor.class, is))
registerWebActor(cl.loadClass(className));
} catch (final IOException | ClassNotFoundException e) {
log.error("Exception while scanning class " + className + " for WebActor annotation", e);
throw new RuntimeException(e);
}
}
private void registerWebActor(Class> c) {
actorClasses.add(c);
}
});
} catch (final IOException e) {
log.error("IOException while scanning classes for WebActor annotation", e);
}
}
}
}
}
================================================
FILE: comsat-actors-netty/src/main/java/co/paralleluniverse/comsat/webactors/netty/NettyHttpRequest.java
================================================
/*
* COMSAT
* Copyright (c) 2015-2016, 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.netty;
import co.paralleluniverse.actors.ActorRef;
import co.paralleluniverse.comsat.webactors.Cookie;
import co.paralleluniverse.comsat.webactors.HttpRequest;
import co.paralleluniverse.comsat.webactors.HttpResponse;
import co.paralleluniverse.comsat.webactors.WebMessage;
import com.google.common.collect.*;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.UnsupportedCharsetException;
import java.util.*;
import static io.netty.handler.codec.http.HttpHeaders.Names.*;
/**
* @author circlespainter
*/
public final class NettyHttpRequest extends HttpRequest {
public static final String CHARSET_MARKER_STRING = "charset=";
final ActorRef super HttpResponse> actorRef;
final FullHttpRequest req;
final ChannelHandlerContext ctx;
final String sessionId;
private static final Set EMPTY_SET = new HashSet<>();
private final ByteBuf reqContent;
private InetSocketAddress sourceAddress;
private ImmutableMultimap params;
private URI uri;
private Collection cookies;
private ListMultimap heads;
private ByteBuffer byteBufferBody;
private String stringBody;
private Charset encoding;
private String contentType;
public NettyHttpRequest(ActorRef super HttpResponse> actorRef, ChannelHandlerContext ctx, FullHttpRequest req, String sessionId) {
this.actorRef = actorRef;
this.ctx = ctx;
this.req = req;
this.sessionId = sessionId;
reqContent = Unpooled.copiedBuffer(req.content());
}
@Override
public final String getSourceHost() {
fillSourceAddress();
return sourceAddress != null ? sourceAddress.getHostString() : null;
}
@Override
public final int getSourcePort() {
fillSourceAddress();
return sourceAddress != null ? sourceAddress.getPort() : -1;
}
private void fillSourceAddress() {
final SocketAddress remoteAddress = ctx.channel().remoteAddress();
if (sourceAddress == null && remoteAddress instanceof InetSocketAddress) {
sourceAddress = (InetSocketAddress) remoteAddress;
}
}
@Override
public final Multimap getParameters() {
QueryStringDecoder queryStringDecoder;
if (params == null) {
queryStringDecoder = new QueryStringDecoder(req.getUri());
final ImmutableMultimap.Builder builder = ImmutableMultimap.builder();
final Map> parameters = queryStringDecoder.parameters();
for (final String k : parameters.keySet())
builder.putAll(k, parameters.get(k));
params = builder.build();
}
return params;
}
@Override
public final Map getAttributes() {
return ImmutableMap.of(); // No attributes in Netty; Guava's impl. will return a pre-built instance
}
@Override
public final String getScheme() {
initUri();
return uri.getScheme();
}
private void initUri() {
if (uri == null) {
try {
uri = new URI(req.getUri());
} catch (final URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
@Override
public final String getMethod() {
return req.getMethod().name();
}
@Override
public final String getPathInfo() {
initUri();
return uri.getPath();
}
@Override
public final String getContextPath() {
return "/"; // Context path makes sense only for servlets
}
@Override
public final String getQueryString() {
initUri();
return uri.getQuery();
}
@Override
public final String getRequestURI() {
return req.getUri();
}
@Override
public final String getServerName() {
initUri();
return uri.getHost();
}
@Override
public final int getServerPort() {
initUri();
return uri.getPort();
}
@SuppressWarnings("unchecked")
@Override
public final ActorRef getFrom() {
return (ActorRef) actorRef;
}
@Override
public final ListMultimap getHeaders() {
if (heads == null) {
heads = extractHeaders(req.headers());
}
return heads;
}
@Override
public final Collection getCookies() {
if (cookies == null) {
final ImmutableList.Builder builder = ImmutableList.builder();
for (io.netty.handler.codec.http.cookie.Cookie c : getNettyCookies(req)) {
builder.add(
Cookie.cookie(c.name(), c.value())
.setDomain(c.domain())
.setPath(c.path())
.setHttpOnly(c.isHttpOnly())
.setMaxAge((int) c.maxAge())
.setSecure(c.isSecure())
.build()
);
}
cookies = builder.build();
}
return cookies;
}
static Set getNettyCookies(FullHttpRequest req) {
final HttpHeaders heads = req.headers();
final String head = heads != null ? heads.get(HttpHeaders.Names.COOKIE) : null;
if (head != null)
return ServerCookieDecoder.LAX.decode(head);
else
return EMPTY_SET;
}
@Override
public final int getContentLength() {
final String stringBody = getStringBody();
if (stringBody != null)
return stringBody.length();
final ByteBuffer bufferBody = getByteBufferBody();
if (bufferBody != null)
return bufferBody.remaining();
return 0;
}
@Override
public final Charset getCharacterEncoding() {
if (encoding == null)
encoding = extractCharacterEncoding(getHeaders());
return encoding;
}
@Override
public final String getContentType() {
if (contentType == null) {
getHeaders();
if (heads != null) {
final List cts = heads.get(CONTENT_TYPE);
if (cts != null && cts.size() > 0)
contentType = cts.get(0);
}
}
return null;
}
@Override
public final String getStringBody() {
if (stringBody == null) {
if (byteBufferBody != null)
return null;
decodeStringBody();
}
return stringBody;
}
@Override
public final ByteBuffer getByteBufferBody() {
if (byteBufferBody == null) {
if (stringBody != null)
return null;
if (reqContent != null)
byteBufferBody = reqContent.nioBuffer();
}
return byteBufferBody;
}
public final String getSessionId() {
return sessionId;
}
public ChannelHandlerContext getContext() {
return ctx;
}
public FullHttpRequest getRequest() {
return req;
}
private String decodeStringBody() {
if (reqContent != null) {
try {
stringBody =
getCharacterEncodingOrDefault()
.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(reqContent.nioBuffer())
.toString();
} catch (CharacterCodingException ignored) {
}
}
return stringBody;
}
Charset getCharacterEncodingOrDefault() {
return getCharacterEncodingOrDefault(getCharacterEncoding());
}
static Charset extractCharacterEncodingOrDefault(HttpHeaders headers) {
return getCharacterEncodingOrDefault(extractCharacterEncoding(extractHeaders(headers)));
}
static Charset extractCharacterEncoding(ListMultimap heads) {
if (heads != null) {
final List cts = heads.get(CONTENT_TYPE);
if (cts != null && cts.size() > 0) {
final String ct = cts.get(0).trim().toLowerCase();
if (ct.contains(CHARSET_MARKER_STRING)) {
try {
return Charset.forName(ct.substring(ct.indexOf(CHARSET_MARKER_STRING) + CHARSET_MARKER_STRING.length()).trim());
} catch (UnsupportedCharsetException ignored) {
}
}
}
}
return null;
}
static ImmutableListMultimap extractHeaders(HttpHeaders headers) {
if (headers != null) {
final ImmutableListMultimap.Builder builder = ImmutableListMultimap.builder();
for (final String n : headers.names())
// Normalize header names by their conversion to lower case
builder.putAll(n.toLowerCase(Locale.ENGLISH), headers.getAll(n));
return builder.build();
}
return null;
}
private static Charset getCharacterEncodingOrDefault(Charset characterEncoding) {
if (characterEncoding == null)
return Charset.defaultCharset();
return characterEncoding;
}
}
================================================
FILE: comsat-actors-netty/src/main/java/co/paralleluniverse/comsat/webactors/netty/WebActorHandler.java
================================================
/*
/*
* COMSAT
* Copyright (c) 2015-2016, 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.netty;
import co.paralleluniverse.actors.*;
import co.paralleluniverse.common.util.Pair;
import co.paralleluniverse.common.util.SystemProperties;
import co.paralleluniverse.comsat.webactors.*;
import co.paralleluniverse.comsat.webactors.Cookie;
import co.paralleluniverse.comsat.webactors.HttpRequest;
import co.paralleluniverse.comsat.webactors.HttpResponse;
import co.paralleluniverse.fibers.SuspendExecution;
import co.paralleluniverse.fibers.Suspendable;
import co.paralleluniverse.strands.Timeout;
import co.paralleluniverse.strands.channels.SendPort;
import co.paralleluniverse.strands.concurrent.CountDownLatch;
import co.paralleluniverse.strands.concurrent.ReentrantLock;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpHeaders.Names.*;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author circlespainter
*/
public class WebActorHandler extends SimpleChannelInboundHandler