|
|
@ -25,6 +25,7 @@ import io.netty.channel.ChannelFuture; |
|
|
|
import io.netty.channel.ChannelFutureListener; |
|
|
|
import io.netty.channel.ChannelHandler.Sharable; |
|
|
|
import io.netty.channel.ChannelHandlerContext; |
|
|
|
import io.netty.channel.ChannelInboundHandlerAdapter; |
|
|
|
import io.netty.handler.codec.http.DefaultHttpResponse; |
|
|
|
import io.netty.handler.codec.http.FullHttpRequest; |
|
|
|
import io.netty.handler.codec.http.HttpHeaders; |
|
|
@ -35,8 +36,6 @@ import io.netty.handler.codec.http.QueryStringDecoder; |
|
|
|
import io.netty.handler.stream.ChunkedStream; |
|
|
|
import io.netty.handler.stream.ChunkedWriteHandler; |
|
|
|
import io.netty.util.CharsetUtil; |
|
|
|
import io.netty.util.concurrent.Future; |
|
|
|
import io.netty.util.concurrent.GenericFutureListener; |
|
|
|
|
|
|
|
import java.io.InputStream; |
|
|
|
import java.net.URL; |
|
|
@ -56,10 +55,12 @@ import javax.activation.MimetypesFileTypeMap; |
|
|
|
import org.slf4j.Logger; |
|
|
|
import org.slf4j.LoggerFactory; |
|
|
|
|
|
|
|
import com.corundumstudio.socketio.SocketIOChannelInitializer; |
|
|
|
|
|
|
|
@Sharable |
|
|
|
public class ResourceHandler extends ChunkedWriteHandler { |
|
|
|
|
|
|
|
private final Logger log = LoggerFactory.getLogger(getClass()); |
|
|
|
public class ResourceHandler extends ChannelInboundHandlerAdapter { |
|
|
|
|
|
|
|
private final Logger log = LoggerFactory.getLogger(getClass()); |
|
|
|
|
|
|
|
public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; |
|
|
|
public static final String HTTP_DATE_GMT_TIMEZONE = "GMT"; |
|
|
@ -73,68 +74,60 @@ public class ResourceHandler extends ChunkedWriteHandler { |
|
|
|
} |
|
|
|
|
|
|
|
public void addResource(String pathPart, String resourcePath) { |
|
|
|
|
|
|
|
URL resUrl = getClass().getResource(resourcePath); |
|
|
|
if (resUrl == null) { |
|
|
|
log.error("The specified resource was not found: " + resourcePath); |
|
|
|
return; |
|
|
|
} |
|
|
|
resources.put(pathPart, resUrl); |
|
|
|
URL resUrl = getClass().getResource(resourcePath); |
|
|
|
if (resUrl == null) { |
|
|
|
log.error("The specified resource was not found: " + resourcePath); |
|
|
|
return; |
|
|
|
} |
|
|
|
resources.put(pathPart, resUrl); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { |
|
|
|
if (msg instanceof FullHttpRequest) { |
|
|
|
FullHttpRequest req = (FullHttpRequest) msg; |
|
|
|
QueryStringDecoder queryDecoder = new QueryStringDecoder(req.getUri()); |
|
|
|
URL resUrl = resources.get(queryDecoder.path()); |
|
|
|
if (resUrl != null) { |
|
|
|
URLConnection fileUrl = resUrl.openConnection(); |
|
|
|
long lastModified = fileUrl.getLastModified(); |
|
|
|
// check if file has been modified since last request |
|
|
|
if (isNotModified(req, lastModified)) { |
|
|
|
sendNotModified(ctx); |
|
|
|
req.release(); |
|
|
|
return; |
|
|
|
} |
|
|
|
// create resource input-stream and check existence |
|
|
|
final InputStream is = fileUrl.getInputStream(); |
|
|
|
if (is == null) { |
|
|
|
sendError(ctx, NOT_FOUND); |
|
|
|
return; |
|
|
|
} |
|
|
|
// create ok response |
|
|
|
HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK); |
|
|
|
// set Content-Length header |
|
|
|
setContentLength(res, fileUrl.getContentLengthLong()); |
|
|
|
// set Content-Type header |
|
|
|
setContentTypeHeader(res, fileUrl); |
|
|
|
// set Date, Expires, Cache-Control and Last-Modified headers |
|
|
|
setDateAndCacheHeaders(res, lastModified); |
|
|
|
// write initial response header |
|
|
|
ctx.write(res); |
|
|
|
|
|
|
|
// write the content stream |
|
|
|
ctx.pipeline().addBefore(SocketIOChannelInitializer.RESOURCE_HANDLER, "chunkedWriter", new ChunkedWriteHandler()); |
|
|
|
ChannelFuture writeFuture = ctx.channel().write(new ChunkedStream(is, fileUrl.getContentLength())); |
|
|
|
// add operation complete listener so we can close the channel and the input stream |
|
|
|
writeFuture.addListener(ChannelFutureListener.CLOSE); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
ctx.fireChannelRead(msg); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception { |
|
|
|
|
|
|
|
if (msg instanceof FullHttpRequest) { |
|
|
|
FullHttpRequest req = (FullHttpRequest) msg; |
|
|
|
QueryStringDecoder queryDecoder = new QueryStringDecoder(req.getUri()); |
|
|
|
URL resUrl = resources.get(queryDecoder.path()); |
|
|
|
if (resUrl != null) { |
|
|
|
URLConnection fileUrl = resUrl.openConnection(); |
|
|
|
long lastModified = fileUrl.getLastModified(); |
|
|
|
// check if file has been modified since last request |
|
|
|
if (isNotModified(req, lastModified)) { |
|
|
|
sendNotModified(ctx); |
|
|
|
req.release(); |
|
|
|
return; |
|
|
|
} |
|
|
|
// create resource input-stream and check existence |
|
|
|
final InputStream is = fileUrl.getInputStream(); |
|
|
|
if (is == null) { |
|
|
|
sendError(ctx, NOT_FOUND); |
|
|
|
return; |
|
|
|
} |
|
|
|
// create ok response |
|
|
|
HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK); |
|
|
|
// set Content-Length header |
|
|
|
setContentLength(res, fileUrl.getContentLengthLong()); |
|
|
|
// set Content-Type header |
|
|
|
setContentTypeHeader(res, fileUrl); |
|
|
|
// set Date, Expires, Cache-Control and Last-Modified headers |
|
|
|
setDateAndCacheHeaders(res, lastModified); |
|
|
|
// write initial response header |
|
|
|
ctx.write(res); |
|
|
|
// write the content stream |
|
|
|
ChannelFuture writeFuture = ctx.channel().write(new ChunkedStream(is, fileUrl.getContentLength())); |
|
|
|
// add operation complete listener so we can close the channel and the input stream |
|
|
|
writeFuture.addListener(new GenericFutureListener<Future<? super Void>>() { |
|
|
|
|
|
|
|
@Override |
|
|
|
public void operationComplete(Future<? super Void> future) throws Exception { |
|
|
|
// close the channel and insput stream on finish |
|
|
|
ctx.channel().close(); |
|
|
|
is.close(); |
|
|
|
} |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
ctx.fireChannelRead(msg); |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
* Checks if the content has been modified sicne the date provided by the IF_MODIFIED_SINCE http header |
|
|
|
* */ |
|
|
|
/* |
|
|
|
* Checks if the content has been modified sicne the date provided by the IF_MODIFIED_SINCE http header |
|
|
|
* */ |
|
|
|
private boolean isNotModified(HttpRequest request, long lastModified) throws ParseException { |
|
|
|
String ifModifiedSince = request.headers().get(HttpHeaders.Names.IF_MODIFIED_SINCE); |
|
|
|
if (ifModifiedSince != null && !ifModifiedSince.equals("")) { |
|
|
@ -152,7 +145,7 @@ public class ResourceHandler extends ChunkedWriteHandler { |
|
|
|
|
|
|
|
/* |
|
|
|
* Sends a Not Modified response to the client |
|
|
|
* |
|
|
|
* |
|
|
|
* */ |
|
|
|
private void sendNotModified(ChannelHandlerContext ctx) { |
|
|
|
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.NOT_MODIFIED); |
|
|
@ -179,8 +172,8 @@ public class ResourceHandler extends ChunkedWriteHandler { |
|
|
|
|
|
|
|
/** |
|
|
|
* Sends an Error response with status message |
|
|
|
* |
|
|
|
* @param ctx |
|
|
|
* |
|
|
|
* @param ctx |
|
|
|
* @param status |
|
|
|
*/ |
|
|
|
private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { |
|
|
|