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

Java tutorial

Introduction

Here is the source code for com.sector91.wit.responders.CachedResponder.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.OutputStream;
import java.io.PrintStream;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.zip.Adler32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.Checksum;
import java.util.zip.GZIPOutputStream;

import org.simpleframework.http.Request;
import org.simpleframework.http.Response;
import org.simpleframework.http.ResponseWrapper;
import org.simpleframework.http.Status;

import com.esotericsoftware.minlog.Log;
import com.google.common.base.Predicate;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.Weigher;
import com.google.common.net.HttpHeaders;
import com.sector91.wit.Responder;
import com.sector91.wit.http.CompressionDecider;
import com.sector91.wit.http.HttpException;
import com.sector91.wit.params.Params;

public abstract class CachedResponder<P extends Params<?>> implements Responder<P> {

    public static final long DEFAULT_CACHE_CAPACITY = 4 * 1024 * 1024; // 4 MB
    public static final long DEFAULT_MAX_SIZE = 2 * 1024 * 1024; // 2 MB

    private final String TAG = getClass().getSimpleName();

    private final CachedResponseLoader loader = new CachedResponseLoader();
    private CacheBuilder<P, CachedResponse> cacheBuilder = CacheBuilder.newBuilder()
            .weigher(new ResponseSizeWeigher()).maximumWeight(DEFAULT_CACHE_CAPACITY);
    LoadingCache<P, CachedResponse> cache = cacheBuilder.build(loader);

    boolean useETags = false;

    Predicate<String> gzipIf = CompressionDecider.DEFAULT;
    long maxSize = DEFAULT_MAX_SIZE;

    ThreadLocal<Request> localRequest = new ThreadLocal<>();
    ThreadLocal<Response> localResponse = new ThreadLocal<>();

    public CachedResponder() {
    }

    public CachedResponder<P> gzipIf(Predicate<String> decider) {
        gzipIf = decider;
        return this;
    }

    public CachedResponder<P> cacheCapacity(long bytes) {
        cacheBuilder.maximumWeight(bytes);
        cache = cacheBuilder.build(loader);
        return this;
    }

    public CachedResponder<P> cacheMaxSize(long bytes) {
        maxSize = bytes;
        return this;
    }

    public CachedResponder<P> expireAfterAccess(long quantity, TimeUnit unit) {
        cacheBuilder.expireAfterAccess(quantity, unit);
        cache = cacheBuilder.build(loader);
        return this;
    }

    public CachedResponder<P> expireAfterWrite(long quantity, TimeUnit unit) {
        cacheBuilder.expireAfterWrite(quantity, unit);
        cache = cacheBuilder.build(loader);
        return this;
    }

    public CachedResponder<P> useETags() {
        useETags = true;
        return this;
    }

    @Override
    public final void respond(P params, Request request, Response response) throws HttpException, IOException {
        final CachedResponse cachedResponse;
        try {
            localRequest.set(request);
            localResponse.set(response);
            cachedResponse = cache.get(params);
        } catch (ExecutionException ex) {
            try {
                throw ex.getCause();
            } catch (TooLargeForCacheException ex2) {
                Log.debug(TAG, ex2.getMessage());
                return;
            } catch (HttpException | IOException | RuntimeException ex2) {
                throw ex2;
            } catch (Throwable ex2) {
                throw new HttpException(Status.INTERNAL_SERVER_ERROR, ex2);
            }
        } finally {
            localRequest.set(null);
            localResponse.set(null);
        }
        if (!response.isCommitted()) {
            if (useETags) {
                final List<String> etags = request.getValues(HttpHeaders.IF_NONE_MATCH);
                if (etags.contains(cachedResponse.etag)) {
                    Log.trace(TAG, "Response not modified for URL '" + request.getPath() + "', with params "
                            + params + ".");
                    response.setStatus(Status.NOT_MODIFIED);
                    response.setValue(HttpHeaders.ETAG, cachedResponse.etag);
                    response.close();
                    return;
                }
            } else {
                final long ifModifiedSince = request.getDate(HttpHeaders.IF_MODIFIED_SINCE);
                if (ifModifiedSince >= (cachedResponse.timestamp / 1000) * 1000) {
                    Log.trace(TAG, "Response not modified for URL '" + request.getPath() + "', with params "
                            + params + ".");
                    response.setStatus(Status.NOT_MODIFIED);
                    response.close();
                    return;
                }
            }
            cachedResponse.writeTo(response);
        }
    }

    public abstract void cacheMiss(P params, Request request, Response response) throws HttpException, IOException;

    public void uncache(P params) {
        cache.invalidate(params);
    }

    public void uncacheAll() {
        cache.invalidateAll();
    }

    private class CachedResponseLoader extends CacheLoader<P, CachedResponse> {
        CachedResponseLoader() {
        }

        @Override
        public CachedResponse load(P key) throws TooLargeForCacheException, HttpException, IOException {
            final Request request = localRequest.get();
            final CachingResponseWrapper wrapper = new CachingResponseWrapper(localResponse.get(),
                    request.getPath().toString(), key);
            try {
                cacheMiss(key, request, wrapper);
            } finally {
                if (wrapper.open)
                    wrapper.close();
            }
            return wrapper.getCacheEntry();
        }
    }

    static class TooLargeForCacheException extends Exception {
        private static final long serialVersionUID = 1L;

        TooLargeForCacheException(String message) {
            super(message);
        }
    }

    class CachedResponse {
        final int code;
        final String description;
        final String contentType;
        final long timestamp;
        final String etag;
        final String[][] headers;
        final byte[] body;

        CachedResponse(int code, String description, String contentType, long timestamp, String etag,
                String[][] headers, byte[] body) {
            this.code = code;
            this.description = description;
            this.contentType = contentType;
            this.timestamp = timestamp;
            this.etag = etag;
            this.headers = headers;
            this.body = body;
        }

        void writeTo(Response response) throws IOException {
            response.setCode(code);
            response.setDescription(description);
            response.setContentType(contentType);
            response.setContentLength(body.length);
            if (useETags && etag != null) {
                response.setValue(HttpHeaders.ETAG, etag);
            } else {
                response.setDate(HttpHeaders.LAST_MODIFIED, timestamp);
            }
            for (String[] header : headers) {
                response.setValue(header[0], header[1]);
            }
            try (final OutputStream out = response.getOutputStream(body.length)) {
                out.write(body);
            }
        }
    }

    private class CachingResponseWrapper extends ResponseWrapper {

        final String url;
        final P params;
        final long timestamp = System.currentTimeMillis();

        boolean gzip = true;
        boolean direct = false;
        boolean open = false;

        Checksum checksum;
        ByteArrayOutputStream cacheOutput;
        OutputStream responseOutput;
        private DelegateOutputStream delegateOutput;
        private OutputStream output;

        public CachingResponseWrapper(Response response, String url, P params) {
            super(response);
            this.url = url;
            this.params = params;
        }

        @Override
        public void setContentType(String type) {
            gzip = gzipIf.apply(type);
            super.setContentType(type);
        }

        @Override
        public void setContentLength(long length) {
            direct = length > maxSize;
            super.setContentLength(length);
        }

        private void open(int size) throws IOException {
            if (gzip && !isCommitted()) {
                setValue(HttpHeaders.CONTENT_ENCODING, "gzip");
            }
            if (!direct) {
                cacheOutput = new ByteArrayOutputStream();
                if (useETags)
                    checksum = new Adler32();
            }
            responseOutput = (size > 0) ? super.getOutputStream(size) : super.getOutputStream();
            delegateOutput = new DelegateOutputStream();
            output = gzip ? new GZIPOutputStream(delegateOutput) : delegateOutput;
            open = true;
        }

        @Override
        public synchronized OutputStream getOutputStream() throws IOException {
            if (!open)
                open(-1);
            return output;
        }

        @Override
        public synchronized OutputStream getOutputStream(int size) throws IOException {
            if (!open)
                open(size);
            return output;
        }

        @Override
        public PrintStream getPrintStream() throws IOException {
            return new PrintStream(getOutputStream());
        }

        @Override
        public PrintStream getPrintStream(int size) throws IOException {
            return new PrintStream(getOutputStream(size));
        }

        @Override
        public WritableByteChannel getByteChannel() throws IOException {
            return Channels.newChannel(getOutputStream());
        }

        @Override
        public WritableByteChannel getByteChannel(int size) throws IOException {
            return Channels.newChannel(getOutputStream(size));
        }

        CachedResponse getCacheEntry() throws TooLargeForCacheException {
            if (direct) {
                throw new TooLargeForCacheException(
                        "Response for URL '" + url + "', with params " + params + ", is too large for cache.");
            } else {
                final List<String[]> headers = new ArrayList<>();
                for (String name : getNames())
                    headers.add(new String[] { name, getValue(name) });
                return new CachedResponse(getCode(), getDescription(), getContentType().toString(), timestamp,
                        useETags ? Long.toHexString(checksum.getValue()) : null,
                        headers.toArray(new String[headers.size()][]), cacheOutput.toByteArray());
            }
        }

        @Override
        public void commit() throws IOException {
            if (gzip && !open && !isCommitted()) {
                setValue(HttpHeaders.CONTENT_ENCODING, "gzip");
            }
            super.commit();
        }

        @Override
        public void close() throws IOException {
            open = false;
            output.close();
            super.close();
        }

        private class DelegateOutputStream extends OutputStream {

            long size = 0;
            boolean closed = false;
            private OutputStream delegate;

            DelegateOutputStream() {
                this.delegate = direct ? responseOutput
                        : (useETags ? new CheckedOutputStream(cacheOutput, checksum) : cacheOutput);
            }

            private void checkSize() throws IOException {
                if (!direct && size > maxSize) {
                    delegateTo(responseOutput);
                    direct = true;
                    write(cacheOutput.toByteArray());
                    cacheOutput = null;
                }
            }

            @Override
            public synchronized void write(int c) throws IOException {
                size += 1;
                checkSize();
                delegate.write(c);
            }

            @Override
            public synchronized void write(byte[] b) throws IOException {
                size += b.length;
                checkSize();
                delegate.write(b);
            }

            @Override
            public synchronized void write(byte[] b, int off, int len) throws IOException {
                size += len;
                checkSize();
                delegate.write(b, off, len);
            }

            synchronized void delegateTo(OutputStream delegate) throws IOException {
                this.delegate.close();
                this.delegate = delegate;
            }

            @Override
            public synchronized void close() throws IOException {
                if (closed)
                    return;
                if (!direct) {
                    if (useETags) {
                        setValue(HttpHeaders.ETAG, Long.toHexString(checksum.getValue()));
                    } else {
                        setDate(HttpHeaders.LAST_MODIFIED, timestamp);
                    }
                    responseOutput.write(cacheOutput.toByteArray());
                }
                closed = true;
                CachingResponseWrapper.this.close();
            }
        }
    }

    private class ResponseSizeWeigher implements Weigher<P, CachedResponse> {
        ResponseSizeWeigher() {
        }

        @Override
        public int weigh(P key, CachedResponse value) {
            return value.body.length;
        }
    }
}