/*
* Copyright (c) 2013, 2017 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.grizzly.http.server;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.glassfish.grizzly.Buffer;
import org.glassfish.grizzly.Grizzly;
import org.glassfish.grizzly.WriteHandler;
import org.glassfish.grizzly.filterchain.Filter;
import org.glassfish.grizzly.filterchain.FilterChain;
import org.glassfish.grizzly.filterchain.FilterChainContext;
import org.glassfish.grizzly.http.server.filecache.FileCache;
import org.glassfish.grizzly.http.io.NIOOutputStream;
import org.glassfish.grizzly.http.io.OutputBuffer;
import org.glassfish.grizzly.http.util.MimeType;
import org.glassfish.grizzly.http.util.Header;
import org.glassfish.grizzly.http.util.HttpStatus;
import org.glassfish.grizzly.memory.Buffers;
import org.glassfish.grizzly.memory.MemoryManager;
The basic class for HttpHandler
implementations, which processes requests to a static resources. Author: Jeanfrancois Arcand, Alexey Stashok
/**
* The basic class for {@link HttpHandler} implementations,
* which processes requests to a static resources.
*
* @author Jeanfrancois Arcand
* @author Alexey Stashok
*/
public abstract class StaticHttpHandlerBase extends HttpHandler {
private static final Logger LOGGER = Grizzly.logger(StaticHttpHandlerBase.class);
private volatile int fileCacheFilterIdx = -1;
private volatile boolean isFileCacheEnabled = true;
Returns true if this StaticHttpHandler has been
configured to use file cache to serve static resources,
or false otherwise. Please note, even though this StaticHttpHandler might be configured to use file cache, file cache itself might be disabled FileCache.isEnabled()
. In this case StaticHttpHandler will operate as if file cache was disabled. Returns: true if this StaticHttpHandler has been
configured to use file cache to serve static resources,
or false otherwise.
/**
* Returns <tt>true</tt> if this <tt>StaticHttpHandler</tt> has been
* configured to use file cache to serve static resources,
* or <tt>false</tt> otherwise.
*
* Please note, even though this StaticHttpHandler might be configured
* to use file cache, file cache itself might be disabled
* {@link FileCache#isEnabled()}. In this case StaticHttpHandler will operate
* as if file cache was disabled.
*
* @return <tt>true</tt> if this <tt>StaticHttpHandler</tt> has been
* configured to use file cache to serve static resources,
* or <tt>false</tt> otherwise.
*/
@SuppressWarnings("UnusedDeclaration")
public boolean isFileCacheEnabled() {
return isFileCacheEnabled;
}
Set true to configure this StaticHttpHandler
to use file cache to serve static resources, or false otherwise. Please note, even though this StaticHttpHandler might be configured to use file cache, file cache itself might be disabled FileCache.isEnabled()
. In this case StaticHttpHandler will operate as if file cache was disabled. Params: - isFileCacheEnabled – true to configure this
StaticHttpHandler to use file cache to serve static resources,
or false otherwise.
/**
* Set <tt>true</tt> to configure this <tt>StaticHttpHandler</tt>
* to use file cache to serve static resources, or <tt>false</tt> otherwise.
*
* Please note, even though this StaticHttpHandler might be configured
* to use file cache, file cache itself might be disabled
* {@link FileCache#isEnabled()}. In this case StaticHttpHandler will operate
* as if file cache was disabled.
*
* @param isFileCacheEnabled <tt>true</tt> to configure this
* <tt>StaticHttpHandler</tt> to use file cache to serve static resources,
* or <tt>false</tt> otherwise.
*/
@SuppressWarnings("UnusedDeclaration")
public void setFileCacheEnabled(boolean isFileCacheEnabled) {
this.isFileCacheEnabled = isFileCacheEnabled;
}
public static void sendFile(final Response response, final File file)
throws IOException {
response.setStatus(HttpStatus.OK_200);
// In case this sendFile(...) is called directly by user - pickup the content-type
pickupContentType(response, file.getPath());
final long length = file.length();
response.setContentLengthLong(length);
response.addDateHeader(Header.Date, System.currentTimeMillis());
if (!response.isSendFileEnabled() || response.getRequest().isSecure()) {
sendUsingBuffers(response, file);
} else {
sendZeroCopy(response, file);
}
}
private static void sendUsingBuffers(final Response response, final File file)
throws FileNotFoundException, IOException {
final int chunkSize = 8192;
response.suspend();
final NIOOutputStream outputStream = response.getNIOOutputStream();
outputStream.notifyCanWrite(
new NonBlockingDownloadHandler(response, outputStream,
file, chunkSize));
}
private static void sendZeroCopy(final Response response, final File file)
throws IOException {
final OutputBuffer outputBuffer = response.getOutputBuffer();
outputBuffer.sendfile(file, null);
}
public final boolean addToFileCache(final Request req,
final Response res,
final File resource) {
if (isFileCacheEnabled) {
final FilterChainContext fcContext = req.getContext();
final FileCacheFilter fileCacheFilter = lookupFileCache(fcContext);
if (fileCacheFilter != null) {
final FileCache fileCache = fileCacheFilter.getFileCache();
if (fileCache.isEnabled()) {
if (res != null) {
addCachingHeaders(res, resource);
}
fileCache.add(req.getRequest(), resource);
return true;
}
}
}
return false;
}
// ------------------------------------------------ Methods from HttpHandler
Based on the Request
URI, try to map the file from the getDocRoots()
, and send it back to a client. Params: Throws:
/**
* Based on the {@link Request} URI, try to map the file from the
* {@link #getDocRoots()}, and send it back to a client.
* @param request the {@link Request}
* @param response the {@link Response}
* @throws Exception
*/
@Override
public void service(final Request request, final Response response)
throws Exception {
final String uri = getRelativeURI(request);
if (uri == null || !handle(uri, request, response)) {
onMissingResource(request, response);
}
}
// ------------------------------------------------------- Protected Methods
protected String getRelativeURI(final Request request) throws Exception {
String uri = request.getDecodedRequestURI();
if (uri.contains("..")) {
return null;
}
final String resourcesContextPath = request.getContextPath();
if (resourcesContextPath != null && !resourcesContextPath.isEmpty()) {
if (!uri.startsWith(resourcesContextPath)) {
return null;
}
uri = uri.substring(resourcesContextPath.length());
}
return uri;
}
The method will be called, if the static resource requested by the Request
wasn't found, so StaticHttpHandler
implementation may try to workaround this situation. The default implementation - sends a 404 response page by calling customizedErrorPage(Request, Response)
. Params: Throws:
/**
* The method will be called, if the static resource requested by the {@link Request}
* wasn't found, so {@link StaticHttpHandler} implementation may try to
* workaround this situation.
* The default implementation - sends a 404 response page by calling {@link #customizedErrorPage(Request, Response)}.
*
* @param request the {@link Request}
* @param response the {@link Response}
* @throws Exception
*/
protected void onMissingResource(final Request request, final Response response)
throws Exception {
response.sendError(404);
}
Lookup a resource based on the request URI, and process it.
Params: Throws: Returns: true, if the static resource has been found and processed,
or false, if the static resource hasn't been found
/**
* Lookup a resource based on the request URI, and process it.
*
* @param uri The request URI
* @param request the {@link Request}
* @param response the {@link Response}
* @return <tt>true</tt>, if the static resource has been found and processed,
* or <tt>false</tt>, if the static resource hasn't been found
* @throws Exception
*/
protected abstract boolean handle(final String uri,
final Request request,
final Response response) throws Exception;
// --------------------------------------------------------- Private Methods
protected FileCacheFilter lookupFileCache(final FilterChainContext fcContext) {
final FilterChain fc = fcContext.getFilterChain();
final int lastFileCacheIdx = fileCacheFilterIdx;
if (lastFileCacheIdx != -1 && lastFileCacheIdx < fc.size()) {
final Filter filter = fc.get(lastFileCacheIdx);
if (filter instanceof FileCacheFilter) {
return (FileCacheFilter) filter;
}
}
final int size = fc.size();
for (int i = 0; i < size; i++) {
final Filter filter = fc.get(i);
if (filter instanceof FileCacheFilter) {
fileCacheFilterIdx = i;
return (FileCacheFilter) filter;
}
}
fileCacheFilterIdx = -1;
return null;
}
protected static void pickupContentType(final Response response,
final String path) {
if (!response.getResponse().isContentTypeSet()) {
int dot = path.lastIndexOf('.');
if (dot > 0) {
String ext = path.substring(dot + 1);
String ct = MimeType.get(ext);
if (ct != null) {
response.setContentType(ct);
}
} else {
response.setContentType(MimeType.get("html"));
}
}
}
protected static void addCachingHeaders(final Response response,
final File file) {
final StringBuilder sb = new StringBuilder();
final long fileLength = file.length();
final long lastModified = file.lastModified();
if ((fileLength >= 0) || (lastModified >= 0)) {
sb.append('"').append(fileLength).append('-').
append(lastModified).append('"');
response.setHeader(Header.ETag, sb.toString());
}
response.addDateHeader(Header.LastModified, lastModified);
}
private static class NonBlockingDownloadHandler implements WriteHandler {
// keep the remaining size
private volatile long size;
private final Response response;
private final NIOOutputStream outputStream;
private final FileChannel fileChannel;
private final MemoryManager mm;
private final int chunkSize;
NonBlockingDownloadHandler(final Response response,
final NIOOutputStream outputStream, final File file,
final int chunkSize) {
try {
fileChannel = new FileInputStream(file).getChannel();
} catch (FileNotFoundException e) {
throw new IllegalStateException("File should have existed", e);
}
size = file.length();
this.response = response;
this.outputStream = outputStream;
mm = response.getRequest().getContext().getMemoryManager();
this.chunkSize = chunkSize;
}
@Override
public void onWritePossible() throws Exception {
LOGGER.log(Level.FINE, "[onWritePossible]");
// send CHUNK of data
final boolean isWriteMore = sendChunk();
if (isWriteMore) {
// if there are more bytes to be sent - reregister this WriteHandler
outputStream.notifyCanWrite(this);
}
}
@Override
public void onError(Throwable t) {
LOGGER.log(Level.FINE, "[onError] ", t);
response.setStatus(500, t.getMessage());
complete(true);
}
Send next CHUNK_SIZE of file
/**
* Send next CHUNK_SIZE of file
*/
private boolean sendChunk() throws IOException {
// allocate Buffer
final Buffer buffer = mm.allocate(chunkSize);
// mark it available for disposal after content is written
buffer.allowBufferDispose(true);
// read file to the Buffer
final int justReadBytes = (int) Buffers.readFromFileChannel(
fileChannel, buffer);
if (justReadBytes <= 0) {
complete(false);
return false;
}
// prepare buffer to be written
buffer.trim();
// write the Buffer
outputStream.write(buffer);
size -= justReadBytes;
// check the remaining size here to avoid extra onWritePossible() invocation
if (size <= 0) {
complete(false);
return false;
}
return true;
}
Complete the download
/**
* Complete the download
*/
private void complete(final boolean isError) {
try {
fileChannel.close();
} catch (IOException e) {
if (!isError) {
response.setStatus(500, e.getMessage());
}
}
try {
outputStream.close();
} catch (IOException e) {
if (!isError) {
response.setStatus(500, e.getMessage());
}
}
if (response.isSuspended()) {
response.resume();
} else {
response.finish();
}
}
}
}