com.sector91.wit.responders.FileResponder.java Source code

Java tutorial

Introduction

Here is the source code for com.sector91.wit.responders.FileResponder.java

Source

// ~~~~~~~~~~~~~~~~~~~~~~~~~~ //

////   ///   /// ///       
//////  ////   // ////  //// 
/// ////  ////  //  ////  //// 
///  //// /////  //        ///  
///  //// ///// //  //// ////// 
//   /// /////  //  ////  ////  
// //// ///// //  ////  ////   
/////////////  ////  ////   
////////////   ///   ///    
///// /////   ////  ////    
///// /////   //// ///// // 
////  ////    /// ///// //  
///// /////   ////////////   
////  ////     ////  ////    

// 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;
            }
        }
    }
}