io.higgs.http.server.protocol.HttpHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of http-s3 Show documentation
Show all versions of http-s3 Show documentation
Higgs HTTP S3 (Single Site Server)
package io.higgs.http.server.protocol;
import io.higgs.core.FixedSortedList;
import io.higgs.core.InvokableMethod;
import io.higgs.core.MessageHandler;
import io.higgs.core.ResolvedFile;
import io.higgs.core.reflect.dependency.DependencyProvider;
import io.higgs.core.reflect.dependency.Injector;
import io.higgs.http.server.HttpRequest;
import io.higgs.http.server.HttpResponse;
import io.higgs.http.server.HttpStatus;
import io.higgs.http.server.MessagePusher;
import io.higgs.http.server.ParamInjector;
import io.higgs.http.server.StaticFileMethod;
import io.higgs.http.server.WrappedResponse;
import io.higgs.http.server.config.HttpConfig;
import io.higgs.http.server.protocol.mediaTypeDecoders.FormUrlEncodedDecoder;
import io.higgs.http.server.protocol.mediaTypeDecoders.JsonDecoder;
import io.higgs.http.server.transformers.ResponseTransformer;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.WebApplicationException;
import java.net.SocketAddress;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaders.getHeader;
import static io.netty.handler.codec.http.HttpHeaders.setContentLength;
/**
* A stateful {@link MessageHandler} which processes HttpRequests.
* There will be 1 instance of this class per Http request.
*
* @author Courtney Robinson
*/
public class HttpHandler extends MessageHandler {
protected static final Class methodClass = HttpMethod.class;
protected final Queue mediaTypeDecoders = new ConcurrentLinkedDeque<>();
protected final HttpConfig httpConfig;
/**
* The current HTTP request
*/
protected HttpRequest request;
protected HttpResponse res;
/**
* The current HTTP method which matches the current {@link #request}.
* If no method matches this will be null
*/
protected HttpMethod method;
protected ParamInjector injector;
protected HttpProtocolConfiguration protocolConfig;
protected boolean replied;
protected MediaTypeDecoder decoder;
private Logger requestLogger = LoggerFactory.getLogger("request_logger");
public HttpHandler(HttpProtocolConfiguration config) {
super(config.getServer().getConfig());
httpConfig = config.getServer().getConfig();
protocolConfig = config;
injector = config.getInjector();
mediaTypeDecoders.addAll(config.getMediaTypeDecoders());
}
public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof LastHttpContent && !(msg instanceof FullHttpRequest) && replied) {
return; //can happen if exception was thrown before last http content received
}
replied = false;
if (msg instanceof HttpRequest || msg instanceof FullHttpRequest) {
if (msg instanceof HttpRequest) {
request = (HttpRequest) msg;
} else {
request = new HttpRequest((FullHttpRequest) msg);
}
res = new HttpResponse(Unpooled.buffer());
//apply transcriptions
protocolConfig.getTranscriber().transcribe(request);
//must always set protocol config before anything uses the request
request.setConfig(protocolConfig);
//initialize request, setting cookies, media types etc
request.init(ctx);
method = findMethod(request.getUri(), ctx, request, methodClass);
if (method == null) {
//404
throw new WebApplicationException(HttpStatus.NOT_FOUND.code());
}
if (isEntityRequest()) {
if (httpConfig.add_form_url_decoder) {
mediaTypeDecoders.add(new FormUrlEncodedDecoder(request));
}
if (httpConfig.add_json_decoder) {
mediaTypeDecoders.add(new JsonDecoder(request));
}
}
}
if (request == null || method == null) {
log.warn(String.format("Method or request is null \n method \n%s \n request \n%s",
method, request));
throw new WebApplicationException(HttpStatus.INTERNAL_SERVER_ERROR.code());
}
//we have a request and it matches a registered method
if (!isEntityRequest()) {
if (msg instanceof LastHttpContent) {
//only post and put requests are allowed to send form data so everything else just returns
invoke(ctx);
}
} else {
if (decoder == null) {
for (MediaTypeDecoder d : mediaTypeDecoders) {
if (d.canDecode(request.getContentType())) {
decoder = d;
break;
}
}
if (decoder == null) {
throw new WebApplicationException(HttpResponseStatus.NOT_ACCEPTABLE.code());
}
}
//decoder is created if it doesn't exist, can decode all if entire message received
request.setChunked(HttpHeaders.isTransferEncodingChunked(request));
if (msg instanceof HttpContent) {
// New chunk is received
HttpContent chunk = (HttpContent) msg;
decoder.offer(chunk);
if (chunk instanceof LastHttpContent) {
decoder.finished(ctx);
invoke(ctx);
}
}
}
}
public M findMethod(String path, ChannelHandlerContext ctx, Object msg,
Class methodClass) {
M m = super.findMethod(path, ctx, msg, methodClass);
if (m == null && config.add_static_resource_filter) {
StaticFileMethod fileMethod = new StaticFileMethod(protocolConfig.getServer().getFactories(),
protocolConfig);
if (fileMethod.matches(path, ctx, msg)) {
return (M) fileMethod;
}
}
return m;
}
/**
* @return true if post or put request, i.e. requests that have a body/entity
*/
private boolean isEntityRequest() {
return io.netty.handler.codec.http.HttpMethod.POST.name().equalsIgnoreCase(request.getMethod().name()) ||
io.netty.handler.codec.http.HttpMethod.PUT.name().equalsIgnoreCase(request.getMethod().name());
}
protected void invoke(final ChannelHandlerContext ctx) {
MessagePusher pusher = new MessagePusher() {
@Override
public ChannelFuture push(Object message) {
//http methods can return null or void and still have the response injected and modified
//so null messages are allowed here
Object wrappedRes = message != null && message instanceof WrappedResponse ?
((WrappedResponse) message).data() : null;
if (wrappedRes != null) {
message = wrappedRes;
}
Queue transformers = protocolConfig.getTransformers();
return writeResponse(ctx, message, transformers);
}
@Override
public ChannelHandlerContext ctx() {
return ctx;
}
};
//inject globally available provider
DependencyProvider provider = decoder == null ? DependencyProvider.from() : decoder.provider();
//take all objects in the global provider
provider.take(DependencyProvider.global());
provider.add(ctx, ctx.channel(), ctx.executor(), request, res,
request.getFormFiles(), request.getFormParam(), request.getCookies(), request.getSubject(),
request.getSubject().getSession(), protocolConfig.getSecurityManager(), request.getQueryParams(),
pusher, request.getPath());
Object[] params = Injector.inject(method.method().getParameterTypes(), new Object[0], provider);
//inject request specific provider
injector.injectParams(method, request, res, ctx, params);
try {
Object response = method.invoke(ctx, request.getUri(), method, params, provider);
pusher.push(response);
} catch (WebApplicationException wae) {
throw wae; //just re-throw for it to be handled in exceptionCaught handler
} catch (Throwable t) {
if (t.getCause() instanceof WebApplicationException) {
throw (WebApplicationException) t.getCause();
} else {
logDetailedFailMessage(true, params, t, method.method());
throw new WebApplicationException(HttpStatus.INTERNAL_SERVER_ERROR.code());
}
}
}
protected ChannelFuture writeResponse(ChannelHandlerContext ctx, Object response, Queue t) {
if (res.isRedirect()) {
return doWrite(ctx);
}
if (response instanceof HttpResponse) {
res = (HttpResponse) response;
return doWrite(ctx);
}
List ts = new FixedSortedList<>(t);
boolean notAcceptable = false;
for (ResponseTransformer transformer : ts) {
if (transformer.canTransform(response, request, request.getMatchedMediaType(), method, ctx)) {
transformer.transform(response, request, res, request.getMatchedMediaType(),
method, ctx);
notAcceptable = false;
break;
}
notAcceptable = true;
}
if (notAcceptable) {
res.setStatus(HttpStatus.NOT_ACCEPTABLE);
}
return doWrite(ctx);
}
protected ChannelFuture doWrite(ChannelHandlerContext ctx) {
long responseSize = getHeader(res, HttpHeaders.Names.CONTENT_LENGTH) == null ?
res.content().writerIndex() : HttpHeaders.getContentLength(res);
//apply request cookies to response, this includes the session id
res.finalizeCustomHeaders(request);
// Decide whether to close the connection or not.
boolean close = HttpHeaders.Values.CLOSE.equalsIgnoreCase(request.headers().get(CONNECTION))
|| request.getProtocolVersion().equals(HttpVersion.HTTP_1_0)
&& !HttpHeaders.Values.KEEP_ALIVE.equalsIgnoreCase(request.headers().get(CONNECTION));
if (!close && res.getManagedWriter() == null) {
setContentLength(res, res.content().readableBytes());
}
ChannelFuture future;
if (res.getManagedWriter() == null) {
//if no post write op is set then the handler flushes the response
future = ctx.writeAndFlush(res);
} else {
//if there is write manager it'll do all the write/flush
future = res.doManagedWrite();
ResolvedFile f = res.getManagedWriter().getFile();
if (f != null) {
responseSize = f.size();
}
}
// Close the connection after the write operation is done if necessary.
if (close || !config.enable_keep_alive_requests) {
future.addListener(ChannelFutureListener.CLOSE);
}
if (config.log_requests) {
SocketAddress address = ctx.channel().remoteAddress();
//going with the Apache format
//194.116.215.20 - [14/Nov/2005:22:28:57 +0000] “GET / HTTP/1.0″ 200 16440
requestLogger.info(String.format("%s - [%s] \"%s %s %s\" %s %s",
address,
HttpHeaders.getDate(request, request.getCreatedAt().toDate()),
request.getMethod().name(),
request.getUri(),
request.getProtocolVersion(),
res.getStatus().code(),
responseSize
));
}
//clean up and prep for next request. if keep-alive browsers like chrome will
//make multiple requests on the same channel
request = null;
res = null;
decoder = null;
replied = true;
return future;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
try {
if (cause instanceof WebApplicationException) {
writeResponse(ctx, cause, protocolConfig.getTransformers());
} else {
log.warn(String.format("Error while processing request %s", request), cause);
writeResponse(ctx, new WebApplicationException(HttpStatus.INTERNAL_SERVER_ERROR.code()),
protocolConfig.getTransformers());
}
} catch (Throwable t) {
//at this point if an exception occurs, just log and return internal server error
//internal server error
log.warn(String.format("Uncaught error while processing request %s", request), cause);
res.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
doWrite(ctx);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy