/*
* Copyright (c) xlightweb.org, 2008 - 2009. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Please refer to the LGPL license at: http://www.gnu.org/copyleft/lesser.txt
* The latest copy of this software may be found on http://www.xlightweb.org/
*/
package org.xlightweb;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xsocket.DataConverter;
import org.xsocket.Execution;
import org.xsocket.connection.IWriteCompletionHandler;
import org.xsocket.connection.IConnection.FlushMode;
/**
* Handler implementation to handle file requests. If the requested file does not exists, the
* handler will forward the request. If the handler is embedded within {@link RequestHandlerChain}
* the next handler will be called. If no successor exists, a 404 will be returned.
*
* If the flag <i>isShowDirectoryTree</i> is set (default is false) the directory tree will be
* printed, in case the request file does not exists. See example:
*
* <pre>
* String basePath = "public/files";
* IHttpRequestHandler handler = new FileServiceRequestHandler(basePath, true); // will show the directory tree, if a directory is requested
*
* IServer server = new HttpServer(8080, handler);
* server.start();
* </pre>
*
*
* @author grro@xlightweb.org
*/
public class FileServiceRequestHandler extends RequestHandlerBase {
private static final Logger LOG = Logger.getLogger(FileServiceRequestHandler.class.getName());
public static final boolean SHOW_DIRECTORY_TREE_DEFAULT = false;
private static final Map<String, String> MIME_TYPE_MAPPING = HttpUtils.getMimeTypeMapping();
private static final int TRANSFER_BYTE_BUFFER_MAX_MAP_SIZE = Integer.parseInt(System.getProperty("org.xsocket.connection.transfer.mappedbytebuffer.maxsize", "65536"));
private final File fileBase;
private final boolean isShowDirectoryTree;
private AtomicInteger pendingSendtransactions = new AtomicInteger(0);
private int countFound = 0;
private int countNotFound = 0;
/**
* constructor
*
* @param fileBasepath the base path
*
* @throws FileNotFoundException if the base path not exists
*/
public FileServiceRequestHandler(String fileBasepath) throws FileNotFoundException {
this(fileBasepath, SHOW_DIRECTORY_TREE_DEFAULT);
}
/**
* constructor
*
* @param fileBasepath the base path
* @param isShowDirectoryTree true, if the directory tree will been shown, if the requests file is a directory
*
* @throws FileNotFoundException if the base path not exists
*/
public FileServiceRequestHandler(String fileBasepath, boolean isShowDirectoryTree) throws FileNotFoundException {
this.fileBase = new File(fileBasepath);
this.isShowDirectoryTree = isShowDirectoryTree;
if (!new File(fileBasepath).exists()) {
throw new FileNotFoundException("base path "+ fileBasepath + "does not exits");
}
}
/**
* returns if the directory tree should be shown
*
* @return true, if the directory tree should be shown
*/
boolean isShowDirectoryTree() {
return isShowDirectoryTree;
}
/**
* return the base path
*
* @return the base path
*/
String getBasepath() {
return fileBase.getAbsolutePath();
}
/**
* returns the number of pending send transactions
* @return the number of pending send transactions
*/
int getPendingSendTransactions() {
return pendingSendtransactions.get();
}
/**
* returns the number of found
* @return the number of found
*/
int getCountFound() {
return countFound;
}
/**
* returns the number of not found
* @return the number of not found
*/
int getCountNotFound() {
return countNotFound;
}
/**
* {@inheritDoc}
*/
@Execution(Execution.MULTITHREADED)
@InvokeOn(InvokeOn.MESSAGE_RECEIVED)
public void onRequest(IHttpExchange exchange) throws IOException {
IHttpRequest request = exchange.getRequest();
// only GET or POST is supported by this handler
if (request.getMethod().equalsIgnoreCase("GET") || request.getMethod().equalsIgnoreCase("POST")) {
String requestURI = URLDecoder.decode(request.getRequestURI(), "UTF-8");
int ctxLength = request.getContextPath().length() + request.getRequestHandlerPath().length();
if (requestURI.length() > ctxLength) {
String filepath = requestURI.substring(ctxLength, requestURI.length());
// file defined?
if (filepath.length() > 0) {
// converting slash to file system's one
filepath = filepath.replaceAll("[/\\\\]+", "\\" + File.separator);
// create native path
String path = fileBase.getAbsolutePath() + filepath;
// removing tailing file separator
if (path.endsWith(File.separator)) {
path = path.substring(0, path.length() - 1);
}
File file = new File(path);
// does file exits?
if (file.exists()) {
// is file?
if (file.isFile()) {
long lastModified = (file.lastModified() / 1000) * 1000; // ignore millis
String ifModifiedSinceRequestHeader = request.getHeader("If-Modified-Since");
if (ifModifiedSinceRequestHeader != null) {
long ifModifedSince = DataConverter.toDate(ifModifiedSinceRequestHeader).getTime();
if (lastModified <= ifModifedSince) {
IHttpResponse response = new HttpResponse(304);
enhanceFoundResponseHeader(response.getResponseHeader(), lastModified, filepath);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine(filepath + " requested. returning not modified");
}
exchange.send(response);
return;
}
}
HttpResponseHeader responseHeader = new HttpResponseHeader(200);
enhanceFoundResponseHeader(responseHeader, lastModified, filepath);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine(filepath + " requested. returning data");
}
BodyDataSink outChannel = exchange.send(responseHeader, (int) file.length());
SendFileProcess sendFile = new SendFileProcess(exchange.getConnection().getId(), outChannel, file);
sendFile.start();
countFound++;
return;
// ... on, it is a directory
} else {
handleNotFound(exchange, request, file);
return;
}
// file does not exit
} else {
handleNotFound(exchange, request, file);
return;
}
}
// no file defined
} else {
exchange.sendError(404, request.getRequestURI() + " not found");
return;
}
}
handleNotFound(exchange, request, fileBase);
}
@Execution(Execution.MULTITHREADED)
private final class SendFileProcess implements IWriteCompletionHandler {
private final String id;
private final File file;
private final RandomAccessFile raf;
private final FileChannel fc;
private final BodyDataSink outChannel;
private long remaining = 0;
private long offset = 0;
private long length = 0;
private int bufferHashcode = 0;
public SendFileProcess(String id, BodyDataSink outChannel, File file) throws IOException {
this.id = id;
this.file = file;
this.raf = new RandomAccessFile(file, "r");
fc = raf.getChannel();
this.outChannel = outChannel;
outChannel.setFlushmode(FlushMode.ASYNC);
remaining = file.length();
}
public void start() throws IOException {
pendingSendtransactions.incrementAndGet();
write();
}
public void onWritten(int written) throws IOException {
if (LOG.isLoggable(Level.FINE)) {
if (remaining > 0) {
LOG.fine("[" + id + "] {" + bufferHashcode + "} data (size=" + written + " bytes) has been written. Writing next chunk");
} else {
LOG.fine("[" + id + "] {" + bufferHashcode + "} data (size=" + written + " bytes) has been written.");
}
}
write();
}
private void write() throws IOException {
// remaining data to write?
if (remaining > 0) {
// limit the buffer allocation size
if (remaining > TRANSFER_BYTE_BUFFER_MAX_MAP_SIZE) {
length = TRANSFER_BYTE_BUFFER_MAX_MAP_SIZE;
} else {
length = remaining;
}
MappedByteBuffer buffer = fc.map(MapMode.READ_ONLY, offset, length);
ByteBuffer[] bufs = new ByteBuffer[] { buffer };
bufferHashcode = bufs.hashCode();
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + id + "] {" + bufferHashcode + "} writing data (size=" + length + " bytes)");
}
outChannel.write(bufs, this);
offset += length;
remaining -= length;
// no, closing channel
} else {
closeFile();
outChannel.close();
}
}
public void onException(IOException ioe) {
closeFile();
outChannel.destroy();
}
private void closeFile() {
try {
pendingSendtransactions.decrementAndGet();
fc.close();
raf.close();
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by clsoing file channel " + file.getAbsolutePath());
}
}
}
}
private void enhanceFoundResponseHeader(IHttpResponseHeader responseHeader, long lastModified, String filepath) {
responseHeader.setDate(DataConverter.toFormatedRFC822Date(System.currentTimeMillis()));
responseHeader.setHeader("Last-Modified", DataConverter.toFormatedRFC822Date(lastModified));
int pos = filepath.lastIndexOf(".");
if (pos != -1) {
String extension = filepath.substring(pos + 1, filepath.length());
String mimeType = MIME_TYPE_MAPPING.get(extension);
if (mimeType == null) {
responseHeader.setContentType("application/octet-stream");
} else {
responseHeader.setContentType(mimeType);
}
}
}
private void handleNotFound(IHttpExchange exchange, IHttpRequest request, File file) throws IOException {
countNotFound++;
if ((isShowDirectoryTree) &&
(file.isDirectory() &&
(fileBase.getAbsolutePath().length() <= file.getAbsolutePath().length()))) {
String body = printDirectoryTree(request, file);
exchange.send(new HttpResponse(200, "text/html", body));
return;
}
exchange.forward(request, new HttpResponseHandler(exchange));
}
private static final class HttpResponseHandler implements IHttpResponseHandler {
private IHttpExchange exchange = null;
public HttpResponseHandler(IHttpExchange exchange) {
this.exchange = exchange;
}
public void onResponse(IHttpResponse response) throws IOException {
exchange.send(response);
}
public void onException(IOException ioe) {
exchange.sendError(500);
}
}
private String printDirectoryTree(IHttpRequest request, File directory) throws IOException {
StringBuilder sb = new StringBuilder();
String requestResource = directory.getAbsolutePath();
requestResource = requestResource.substring(fileBase.getAbsolutePath().length(), requestResource.length());
if (request.getRequestHandlerPath().length() > 0) {
requestResource = request.getRequestHandlerPath() + "/" + requestResource;
}
if (request.getContextPath().length() > 0) {
requestResource = request.getContextPath() + "/" + requestResource;
}
requestResource = requestResource.replace("\\", "/");
sb.append("<html>\r\n");
sb.append(" <!-- This page is auto-generated by xSocket-http (http://xsocket.org) -->\r\n");
sb.append(" <head>\r\n");
sb.append(" <title>Index of " + requestResource + "</title>\r\n");
sb.append(" </head>\r\n");
sb.append(" <body>\r\n");
sb.append(" <H1 style=\"color:#0a328c;font-size:1.5em;\">Index of " + requestResource + "</H1>\r\n");
sb.append(" <p style=\"font-size:0.8em;\">\r\n");
sb.append(" <table border=\"0\" style=\"color:#0a328c;font-size:1.0em;\">\r\n");
for (File file : directory.listFiles()) {
sb.append(" <tr>");
sb.append(" <td align=\"right\">");
if (file.isDirectory()) {
sb.append("[DIR]");
} else {
sb.append("[TXT]");
}
sb.append(" </td>\r\n");
sb.append(" <td>");
sb.append("<a href=");
String[] parts = requestResource.split("/");
if (parts.length > 0) {
sb.append(URLEncoder.encode(parts[parts.length - 1], "UTF-8") + "/");
}
sb.append(URLEncoder.encode(file.getName(), "UTF-8") + "> " + file.getName() + "</a>");
sb.append(" </td>\r\n");
sb.append(" <td>");
sb.append(DataConverter.toFormatedDate(file.lastModified()));
sb.append(" </td>\r\n");
sb.append(" <td align=\"right\">");
if (!file.isDirectory()) {
sb.append(DataConverter.toFormatedBytesSize(file.length()));
} else {
sb.append("-");
}
sb.append(" </td>\r\n");
sb.append(" </tr>");
}
sb.append(" </table>\r\n");
sb.append(" </p>\r\n");
sb.append(" <p style=\"font-size:0.8em;\">" + new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z").format(new Date()) + " xLightweb (" +
HttpUtils.getImplementationVersion() + ") at " + request.getServerName() +
" Port " + request.getServerPort() + "</p>\r\n");
sb.append(" </body>\r\n");
sb.append("</html>\r\n");
return sb.toString();
}
}