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