Java tutorial
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ // //// /// /// /// ////// //// // //// //// /// //// //// // //// //// /// //// ///// // /// /// //// ///// // //// ////// // /// ///// // //// //// // //// ///// // //// //// ///////////// //// //// //////////// /// /// ///// ///// //// //// ///// ///// //// ///// // //// //// /// ///// // ///// ///// //////////// //// //// //// //// // The Web framework with class. // ~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Copyright (c) 2013 Adam R. Nelson // // 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. package com.sector91.wit.responders; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.zip.GZIPOutputStream; import org.simpleframework.http.Request; import org.simpleframework.http.Response; import org.simpleframework.http.Status; import com.esotericsoftware.minlog.Log; import com.google.common.base.Predicate; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.Weigher; import com.google.common.io.ByteStreams; import com.google.common.net.HttpHeaders; import com.sector91.wit.Responder; import com.sector91.wit.http.CompressionDecider; import com.sector91.wit.http.ContentTypes; import com.sector91.wit.http.HttpException; import com.sector91.wit.params.One; public class FileResponder implements Responder<One<String>> { private static final String TAG = "FileResponder"; public static final long DEFAULT_CACHE_CAPACITY = 4 * 1024 * 1024; // 4 MB public static final long DEFAULT_MAX_SIZE = 2 * 1024 * 1024; // 2 MB final Path root; long maxSize = DEFAULT_MAX_SIZE; private Cache<Path, FileEntry> cache; private CacheBuilder<? super Path, ? super FileEntry> cacheBuilder; Predicate<String> gzipIf = CompressionDecider.DEFAULT; public FileResponder(Path root) { this.root = root; this.cacheBuilder = CacheBuilder.newBuilder().maximumWeight(DEFAULT_CACHE_CAPACITY) .weigher(new FileSizeWeigher()); this.cache = cacheBuilder.build(); } public FileResponder gzipIf(Predicate<String> decider) { gzipIf = decider; return this; } public FileResponder cacheCapacity(long bytes) { cacheBuilder.maximumWeight(bytes); cache = cacheBuilder.build(); return this; } public FileResponder cacheMaxSize(long bytes) { maxSize = bytes; return this; } public FileResponder cacheExpiration(long quantity, TimeUnit unit) { cacheBuilder.expireAfterAccess(quantity, unit); cache = cacheBuilder.build(); return this; } private static void checkPathValidity(Path path) throws HttpException { if (!Files.exists(path)) throw new HttpException(Status.NOT_FOUND).debug("The path '" + path + "' does not exist."); if (Files.isDirectory(path)) throw new HttpException(Status.FORBIDDEN).debug("The path '" + path + "' is a directory."); if (!Files.isReadable(path)) throw new HttpException(Status.FORBIDDEN) .debug("The server does not have permission to access the path '" + path + "'."); } @Override public void respond(One<String> param, Request request, Response response) throws IOException, HttpException { // Read basic information from the request. String pathstr = param.first(); if (pathstr.startsWith("/")) pathstr = pathstr.substring(1); final Path path = root.resolve(pathstr); Log.debug(TAG, "Responding with file: " + path); final boolean gzipSupported = request.getValue(HttpHeaders.ACCEPT_ENCODING).contains("gzip"); // Get the file from the cache, loading it if necessary. FileEntry entry; while (true) { try { entry = cache.get(path, new FileLoader(path)); } catch (ExecutionException wrapper) { try { throw wrapper.getCause(); } catch (FileTooLargeForCacheException ex) { Log.debug(TAG, "File too large for cache: " + path); entry = ex.file; break; } catch (HttpException ex) { throw ex; } catch (Throwable ex) { throw new HttpException(Status.INTERNAL_SERVER_ERROR, ex); } } // Reload the cached file if it has been modified since it was cached. if (entry.data.length <= maxSize && Files.getLastModifiedTime(path).toMillis() > entry.timestamp) { Log.debug(TAG, "Reloading file " + path + " from cache."); cache.invalidate(path); } else { break; } } // Return a 304 Not Modified if the client already has a cached copy. response.setDate(HttpHeaders.LAST_MODIFIED, entry.timestamp); final long ifModifiedSince = request.getDate(HttpHeaders.IF_MODIFIED_SINCE); if (ifModifiedSince >= (entry.timestamp * 1000) / 1000) { Log.trace(TAG, "File not modified: " + path); response.setStatus(Status.NOT_MODIFIED); response.getOutputStream().close(); return; } // Set the HTTP headers. response.setDate(HttpHeaders.DATE, System.currentTimeMillis()); response.setContentType(entry.mimetype); // EDGE CASE: If we're caching a compressed version of the file, but the // client doesn't support compression, then don't bother caching, and just // read the file directly. if (!gzipSupported && entry.gzipped) { Log.debug(TAG, "File " + path + " is cached in gzipped form, but client does not support" + " gzip. Skipping cache."); checkPathValidity(path); try (final InputStream in = Files.newInputStream(path); final OutputStream out = response.getOutputStream()) { ByteStreams.copy(in, out); return; } } // Otherwise, write the response headers and data. if (entry.gzipped) response.setValue(HttpHeaders.CONTENT_ENCODING, "gzip"); response.setContentLength(entry.data.length); try (OutputStream out = response.getOutputStream()) { out.write(entry.data); } } class FileEntry { String mimetype; boolean gzipped; long timestamp; byte[] data; FileEntry(String mimetype, boolean gzipped, long timestamp, byte[] data) { this.mimetype = mimetype; this.gzipped = gzipped; this.timestamp = timestamp; this.data = data; } } class FileSizeWeigher implements Weigher<Path, FileEntry> { @Override public int weigh(Path key, FileEntry value) { return value.data.length; } } class FileTooLargeForCacheException extends Exception { private static final long serialVersionUID = 1L; final FileEntry file; FileTooLargeForCacheException(FileEntry file) { this.file = file; } } class FileLoader implements Callable<FileEntry> { private Path path; FileLoader(Path path) { this.path = path; } @Override public FileEntry call() throws HttpException, IOException, FileTooLargeForCacheException { Log.trace(TAG, "Reading file: " + path); checkPathValidity(path); try (final InputStream stream = Files.newInputStream(path)) { final long timestamp = Files.getLastModifiedTime(path).toMillis(); final String mimetype = ContentTypes.forPath(path.toString()); final boolean shouldCompress = gzipIf.apply(mimetype); final byte[] data; if (shouldCompress) { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (final GZIPOutputStream gzip = new GZIPOutputStream(baos)) { ByteStreams.copy(stream, gzip); } data = baos.toByteArray(); } else { data = ByteStreams.toByteArray(stream); } final FileEntry entry = new FileEntry(mimetype, shouldCompress, timestamp, data); if (data.length > maxSize) throw new FileTooLargeForCacheException(entry); return entry; } } } }