org.wisdom.engine.wrapper.ContextFromNetty Maven / Gradle / Ivy
/*
* #%L
* Wisdom-Framework
* %%
* Copyright (C) 2013 - 2014 Wisdom Framework
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package org.wisdom.engine.wrapper;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.util.CharsetUtil;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wisdom.api.content.BodyParser;
import org.wisdom.api.cookies.Cookie;
import org.wisdom.api.cookies.Cookies;
import org.wisdom.api.cookies.FlashCookie;
import org.wisdom.api.cookies.SessionCookie;
import org.wisdom.api.http.Context;
import org.wisdom.api.http.FileItem;
import org.wisdom.api.http.MimeTypes;
import org.wisdom.api.http.Request;
import org.wisdom.api.router.Route;
import org.wisdom.engine.server.ServiceAccessor;
import org.wisdom.engine.wrapper.cookies.CookieHelper;
import org.wisdom.engine.wrapper.cookies.FlashCookieImpl;
import org.wisdom.engine.wrapper.cookies.SessionCookieImpl;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
/**
* An implementation from the Wisdom HTTP context based on servlet objects.
* Not Thread Safe !
*/
public class ContextFromNetty implements Context {
private static AtomicLong ids = new AtomicLong();
private final long id;
private final HttpRequest httpRequest;
private final ServiceAccessor services;
private final FlashCookie flashCookie;
private final SessionCookie sessionCookie;
private final QueryStringDecoder queryStringDecoder;
private /*not final*/ Route route;
/**
* the request object, created lazily.
*/
private RequestFromNetty request;
/**
* Attribute from the body.
*/
private Map> attributes = Maps.newHashMap();
/**
* List of uploaded files.
*/
private List files = Lists.newArrayList();
/**
* The raw body.
*/
private String raw;
/**
* The logger.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(ContextFromNetty.class);
/**
* Creates a new context.
*
* @param accessor a structure containing the used services.
* @param ctxt the channel handler context.
* @param req the incoming HTTP Request.
*/
public ContextFromNetty(ServiceAccessor accessor, ChannelHandlerContext ctxt, HttpRequest req) {
id = ids.getAndIncrement();
httpRequest = req;
services = accessor;
queryStringDecoder = new QueryStringDecoder(httpRequest.getUri());
request = new RequestFromNetty(this, ctxt, httpRequest);
flashCookie = new FlashCookieImpl(accessor.getConfiguration());
sessionCookie = new SessionCookieImpl(accessor.getCrypto(), accessor.getConfiguration());
sessionCookie.init(this);
flashCookie.init(this);
}
/**
* A http content type should contain a character set like
* "application/json; charset=utf-8".
*
* If you only want to get "application/json" you can use this method.
*
* See also: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
*
* @param rawContentType "application/json; charset=utf-8" or "application/json"
* @return only the contentType without charset. Eg "application/json"
*/
public static String getContentTypeFromContentTypeAndCharacterSetting(String rawContentType) {
if (rawContentType.contains(";")) {
return rawContentType.split(";")[0];
} else {
return rawContentType;
}
}
/**
* Decodes the content of the request. Notice that the content can be split in several chunk.
*
* @param req the request
* @param content the content
* @param decoder the decoder.
*/
public void decodeContent(HttpRequest req, HttpContent content, HttpPostRequestDecoder decoder) {
// Determine whether the content is chunked.
boolean readingChunks = HttpHeaders.isTransferEncodingChunked(req);
// Offer the content to the decoder.
if (readingChunks) {
// If needed, read content chunk by chunk.
decoder.offer(content);
readHttpDataChunkByChunk(decoder);
} else {
// Else, read content.
if (content.content().isReadable()) {
// We may have the content in different HTTP message, check if we already have a content.
// Issue #257.
// To avoid we run out of memory we cut the read body to 100Kb. This can be configured using the
// "request.body.max.size" property.
boolean exceeded = raw != null
&& raw.length() >=
services.getConfiguration().getIntegerWithDefault("request.body.max.size", 100 * 1024);
if (!exceeded) {
if (this.raw == null) {
this.raw = content.content().toString(CharsetUtil.UTF_8);
} else {
this.raw += content.content().toString(CharsetUtil.UTF_8);
}
}
}
decoder.offer(content);
try {
for (InterfaceHttpData data : decoder.getBodyHttpDatas()) {
readAttributeOrFile(data);
}
} catch (HttpPostRequestDecoder.NotEnoughDataDecoderException e) {
LOGGER.debug("Error when decoding content, not enough data", e);
}
}
}
/**
* Reads request by chunk and getting values from chunk to chunk.
*/
private void readHttpDataChunkByChunk(HttpPostRequestDecoder decoder) {
try {
while (decoder.hasNext()) {
InterfaceHttpData data = decoder.next();
if (data != null) {
try {
// new value
readAttributeOrFile(data);
} finally {
// Do not release the data if it's a file, we released it once everything is done.
if (data.getHttpDataType() != InterfaceHttpData.HttpDataType.FileUpload) {
data.release();
}
}
}
}
} catch (HttpPostRequestDecoder.EndOfDataDecoderException e) {
LOGGER.debug("Error when decoding content, end of data reached", e);
}
}
private void readAttributeOrFile(InterfaceHttpData data) {
if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) {
Attribute attribute = (Attribute) data;
String value;
try {
String name = attribute.getName();
value = attribute.getValue();
List values = attributes.get(name);
if (values == null) {
values = new ArrayList<>();
attributes.put(name, values);
}
values.add(value);
} catch (IOException e) {
LOGGER.warn("Error while reading attributes", e);
}
} else {
if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.FileUpload) {
FileUpload fileUpload = (FileUpload) data;
if (fileUpload.isCompleted()) {
files.add(new FileItemFromNetty(fileUpload));
} else {
LOGGER.warn("Un-complete file upload");
}
}
}
}
/**
* The context id (unique).
*/
@Override
public Long id() {
return id;
}
/**
* Returns the current request.
*/
@Override
public Request request() {
return request;
}
/**
* Returns the path that the controller should act upon.
*
* For instance in servlets you could have something like a context prefix.
* /myContext/app
*
* If your route only defines /app it will work as the requestpath will
* return only "/app". A context path is not returned.
*
* It does NOT decode any parts of the url.
*
* Interesting reads: -
* http://www.lunatech-research.com/archives/2009/02/03/
* what-every-web-developer-must-know-about-url-encoding -
* http://stackoverflow
* .com/questions/966077/java-reading-undecoded-url-from-servlet
*
* @return The the path as seen by the server. Does exclude any container
* set context prefixes. Not decoded.
*/
@Override
public String path() {
return request().path();
}
/**
* Returns the flash cookie. Flash cookies only live for one request. Good
* uses are error messages to display. Almost everything else is bad use of
* Flash Cookies.
*
* A FlashCookie is usually not signed. Don't trust the content.
*
* @return the flash cookie of that request.
*/
@Override
public FlashCookie flash() {
return flashCookie;
}
/**
* Returns the client side session. It is a cookie. Therefore you cannot
* store a lot of information inside the cookie. This is by intention.
*
* If you have the feeling that the session cookie is too small for what you
* want to achieve thing again. Most likely your design is wrong.
*
* @return the Session of that request / response cycle.
*/
@Override
public SessionCookie session() {
return sessionCookie;
}
/**
* Get cookie from context.
*
* @param cookieName Name of the cookie to retrieve
* @return the cookie with that name or null.
*/
@Override
public Cookie cookie(String cookieName) {
return request().cookie(cookieName);
}
/**
* Checks whether the context contains a given cookie.
*
* @param cookieName Name of the cookie to check for
* @return {@code true} if the context has a cookie with that name.
*/
@Override
public boolean hasCookie(String cookieName) {
return request().cookie(cookieName) != null;
}
/**
* Get all cookies from the context.
*
* @return the cookie with that name or null.
*/
@Override
public Cookies cookies() {
return request().cookies();
}
/**
* Get the context path on which the application is running.
*
* @return the context-path with a leading "/" or "" if running on root
*/
@Override
public String contextPath() {
return "";
}
/**
* Get the parameter with the given key from the request. The parameter may
* either be a query parameter, or in the case of form submissions, may be a
* form parameter.
*
* When the parameter is multivalued, returns the first value.
*
* The parameter is decoded by default.
*
* @param name The key of the parameter
* @return The value, or null if no parameter was found.
* @see #parameterMultipleValues
*/
@Override
public String parameter(String name) {
Map> parameters = queryStringDecoder.parameters();
if (parameters != null && parameters.containsKey(name)) {
// Return only the first one.
return parameters.get(name).get(0);
}
return null;
}
@Override
public Map> attributes() {
return attributes;
}
/**
* Get the parameter with the given key from the request. The parameter may
* either be a query parameter, or in the case of form submissions, may be a
* form parameter.
*
* The parameter is decoded by default.
*
* @param name The key of the parameter
* @return The values, possibly an empty list.
*/
@Override
public List parameterMultipleValues(String name) {
Map> parameters = queryStringDecoder.parameters();
if (parameters != null && parameters.containsKey(name)) {
return parameters.get(name);
}
return new ArrayList<>();
}
/**
* Same like {@link #parameter(String)}, but returns given defaultValue
* instead of null in case parameter cannot be found.
*
* The parameter is decoded by default.
*
* @param name The name of the parameter
* @param defaultValue A default value if parameter not found.
* @return The value of the parameter of the defaultValue if not found.
*/
@Override
public String parameter(String name, String defaultValue) {
String parameter = parameter(name);
if (parameter == null) {
return defaultValue;
}
return parameter;
}
/**
* Same like {@link #parameter(String)}, but converts the parameter to
* Integer if found.
*
* The parameter is decoded by default.
*
* @param name The name of the parameter
* @return The value of the parameter or null if not found.
*/
@Override
public Integer parameterAsInteger(String name) {
String parameter = parameter(name);
try {
return Integer.parseInt(parameter);
} catch (Exception e) { //NOSONAR
return null;
}
}
/**
* Same like {@link #parameter(String, String)}, but converts the
* parameter to Integer if found.
*
* The parameter is decoded by default.
*
* @param name The name of the parameter
* @param defaultValue A default value if parameter not found.
* @return The value of the parameter of the defaultValue if not found.
*/
@Override
public Integer parameterAsInteger(String name, Integer defaultValue) {
Integer parameter = parameterAsInteger(name);
if (parameter == null) {
return defaultValue;
}
return parameter;
}
/**
* Same like {@link #parameter(String)}, but converts the
* parameter to Boolean if found.
*
* The parameter is decoded by default.
*
* @param name The name parameter
* @return The value of the parameter of the defaultValue if not found.
*/
@Override
public Boolean parameterAsBoolean(String name) {
String parameter = parameter(name);
try {
return Boolean.parseBoolean(parameter);
} catch (Exception e) { //NOSONAR
return null;
}
}
/**
* Same like {@link #parameter(String, String)}, but converts the
* parameter to Boolean if found.
*
* The parameter is decoded by default.
*
* @param name The name of the parameter
* @param defaultValue A default value if parameter not found.
* @return The value of the parameter of the defaultValue if not found.
*/
@Override
public Boolean parameterAsBoolean(String name, boolean defaultValue) {
// We have to check if the map contains the key, as the retrieval method returns false on missing key.
if (!parameters().containsKey(name)) {
return defaultValue;
}
Boolean parameter = parameterAsBoolean(name);
if (parameter == null) {
return defaultValue;
}
return parameter;
}
/**
* Get the path parameter for the given key.
*
* The parameter will be decoded based on the RFCs.
*
* Check out http://docs.oracle.com/javase/6/docs/api/java/net/URI.html for
* more information.
*
* @param name The name of the path parameter in a route. Eg
* /{myName}/rest/of/url
* @return The decoded path parameter, or null if no such path parameter was
* found.
*/
@Override
public String parameterFromPath(String name) {
String encodedParameter = route.getPathParametersEncoded(
path()).get(name);
if (encodedParameter == null) {
return null;
} else {
return URI.create(encodedParameter).getPath();
}
}
/**
* Get the path parameter for the given key.
*
* Returns the raw path part. That means you can get stuff like:
* blue%2Fred%3Fand+green
*
* @param name The name of the path parameter in a route. Eg
* /{myName}/rest/of/url
* @return The encoded (!) path parameter, or null if no such path parameter
* was found.
*/
@Override
public String parameterFromPathEncoded(String name) {
return route.getPathParametersEncoded(path()).get(name);
}
/**
* Get the path parameter for the given key and convert it to Integer.
*
* The parameter will be decoded based on the RFCs.
*
* Check out http://docs.oracle.com/javase/6/docs/api/java/net/URI.html for
* more information.
*
* @param key the key of the path parameter
* @return the numeric path parameter, or null of no such path parameter is
* defined, or if it cannot be parsed to int
*/
@Override
public Integer parameterFromPathAsInteger(String key) {
String parameter = parameterFromPath(key);
if (parameter == null) {
return null;
} else {
return Integer.parseInt(parameter);
}
}
/**
* Get all the parameters from the request.
* This method does not check the attributes.
*
* @return The parameters
*/
@Override
public Map> parameters() {
return queryStringDecoder.parameters();
}
/**
* Get the (first) request header with the given name.
*
* @return The header value
*/
@Override
public String header(String name) {
return httpRequest.headers().get(name);
}
/**
* Get all the request headers with the given name.
*
* @return the header values
*/
@Override
public List headers(String name) {
return httpRequest.headers().getAll(name);
}
/**
* Get all the headers from the request.
*
* @return The headers
*/
@Override
public Map> headers() {
return request.headers();
}
/**
* Get the cookie value from the request, if defined.
*
* @param name The name of the cookie
* @return The cookie value, or null if the cookie was not found
*/
@Override
public String cookieValue(String name) {
return CookieHelper.getCookieValue(name, request().cookies());
}
/**
* This will give you the request body nicely parsed. You can register your
* own parsers depending on the request type.
*
*
* @param classOfT The class of the result.
* @return The parsed request or null if something went wrong.
*/
@Override
public T body(Class classOfT) {
String rawContentType = request().contentType();
// If the Content-type: xxx header is not set we return null.
// we cannot parse that request.
if (rawContentType == null) {
return null;
}
// If Content-type is application/json; charset=utf-8 we split away the charset
// application/json
String contentTypeOnly = getContentTypeFromContentTypeAndCharacterSetting(
rawContentType);
BodyParser parser = services.getContentEngines().getBodyParserEngineForContentType(contentTypeOnly);
if (parser == null) {
return null;
}
return parser.invoke(this, classOfT);
}
/**
* Retrieves the request body as a String. If the request has no body, {@code null} is returned.
*
* @return the body as String
*/
public String body() {
return raw;
}
/**
* Get the reader to read the request.
*
* @return The reader
*/
@Override
public BufferedReader reader() throws IOException {
if (raw != null) {
return IOUtils.toBufferedReader(new StringReader(raw));
}
return null;
}
/**
* Get the route for this context.
*
* @return The route
*/
@Override
public Route route() {
return route;
}
/**
* Sets the route associated with the current context.
*
* @param route the route
*/
public void route(Route route) {
// Can be called only once, with a non null route.
Preconditions.checkState(this.route == null);
Preconditions.checkNotNull(route);
this.route = route;
}
/**
* Check if request is of type multipart. Important when you want to process
* uploads for instance.
*
* Also check out: http://commons.apache.org/fileupload/streaming.html
*
* @return true if request is of type multipart.
*/
@Override
public boolean isMultipart() {
return MimeTypes.MULTIPART.equals(request().contentType());
}
/**
* Gets the collection of uploaded files.
*
* @return the collection of files, {@literal empty} if no files.
*/
@Override
public Collection extends FileItem> files() {
return files;
}
/**
* Gets the uploaded file having a form's field matching the given name.
*
* @param name the name of the field of the form that have uploaded the file
* @return the file object, {@literal null} if there are no file with this name
*/
@Override
public FileItem file(String name) {
for (FileItem item : files) {
if (item.field().equals(name)) {
return item;
}
}
return null;
}
/**
* Releases uploaded files.
*/
public void cleanup() {
for (FileItemFromNetty file : files) {
file.upload().release();
}
request().data().clear();
}
}