Java tutorial
/* * The MIT License * * Copyright 2013 Tim Boudreau. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.mastfrog.acteur; import com.google.inject.Key; import com.google.inject.name.Names; import com.mastfrog.acteur.ResponseWriter.AbstractOutput; import com.mastfrog.acteur.ResponseWriter.Output; import com.mastfrog.acteur.ResponseWriter.Status; import com.mastfrog.acteur.headers.HeaderValueType; import com.mastfrog.acteur.headers.Headers; import com.mastfrog.acteur.headers.Method; import com.mastfrog.acteur.server.ServerModule; import com.mastfrog.giulius.Dependencies; import com.mastfrog.guicy.scope.ReentrantScope; import com.mastfrog.util.Checks; import com.mastfrog.util.Codec; import com.mastfrog.util.Exceptions; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import static io.netty.channel.ChannelFutureListener.CLOSE; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_MODIFIED; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; import org.joda.time.Duration; /** * Aggregates the set of headers and a body writer which is used to respond to * an HTTP request. * * @author Tim Boudreau */ final class ResponseImpl extends Response { private volatile boolean modified; HttpResponseStatus status; private final List<Entry<?>> headers = Collections.synchronizedList(new ArrayList<Entry<?>>()); private String message; ChannelFutureListener listener; private boolean chunked; private Duration delay; ResponseImpl() { } boolean isModified() { return modified; } void modify() { this.modified = true; } void merge(ResponseImpl other) { this.modified |= other.modified; if (other.modified) { for (Entry<?> e : other.headers) { addEntry(e); } if (other.status != null) { setResponseCode(other.status); } if (other.message != null) { setMessage(other.message); } if (other.chunked) { setChunked(true); } if (other.listener != null) { setBodyWriter(other.listener); } if (other.delay != null) { this.delay = other.delay; } } } private <T> void addEntry(Entry<T> e) { add(e.decorator, e.value); } public void setMessage(String message) { modify(); this.message = message; } public void setDelay(Duration delay) { modify(); this.delay = delay; } public void setResponseCode(HttpResponseStatus status) { modify(); this.status = status; } public HttpResponseStatus getResponseCode() { return status == null ? HttpResponseStatus.OK : status; } @Override public void setBodyWriter(ResponseWriter writer) { Page p = Page.get(); Application app = p.getApplication(); Dependencies deps = app.getDependencies(); HttpEvent evt = deps.getInstance(HttpEvent.class); Charset charset = deps.getInstance(Charset.class); ByteBufAllocator allocator = deps.getInstance(ByteBufAllocator.class); Codec mapper = deps.getInstance(Codec.class); Key<ExecutorService> key = Key.get(ExecutorService.class, Names.named(ServerModule.BACKGROUND_THREAD_POOL_NAME)); ExecutorService svc = deps.getInstance(key); setWriter(writer, charset, allocator, mapper, evt, svc); } Duration getDelay() { return delay; } static class HackHttpHeaders extends HttpHeaders { private final HttpHeaders orig; public HackHttpHeaders(HttpHeaders orig, boolean chunked) { this.orig = orig; if (chunked) { orig.set(Names.TRANSFER_ENCODING, Values.CHUNKED); orig.remove(Names.CONTENT_LENGTH); } else { orig.remove(Names.TRANSFER_ENCODING); } } @Override public String get(String name) { return orig.get(name); } @Override public List<String> getAll(String name) { return orig.getAll(name); } @Override public List<Map.Entry<String, String>> entries() { return orig.entries(); } @Override public boolean contains(String name) { return orig.contains(name); } @Override public boolean isEmpty() { return orig.isEmpty(); } @Override public Set<String> names() { return orig.names(); } @Override public HttpHeaders add(String name, Object value) { if (Names.TRANSFER_ENCODING.equals(name)) { return this; } return orig.add(name, value); } @Override public HttpHeaders add(String name, Iterable<?> values) { if (Names.TRANSFER_ENCODING.equals(name)) { return this; } if (Names.CONTENT_LENGTH.equals(name)) { // System.out.println("ATTEMPT TO SET LENGTH TO " + values); return this; } return orig.add(name, values); } @Override public HttpHeaders set(String name, Object value) { if (Names.TRANSFER_ENCODING.equals(name)) { return this; } if (Names.CONTENT_LENGTH.equals(name)) { // System.out.println("ATTEMPT TO SET LENGTH TO " + value); return this; } if (Names.CONTENT_ENCODING.equals(name)) { // System.out.println("ATTEMPT TO SET CONTENT ENCODING TO " + value); return this; } return orig.set(name, value); } @Override public HttpHeaders set(String name, Iterable<?> values) { if (Names.TRANSFER_ENCODING.equals(name)) { return this; } if (Names.CONTENT_LENGTH.equals(name)) { // System.out.println("ATTEMPT TO SET LENGTH"); return this; } if (Names.CONTENT_ENCODING.equals(name)) { // System.out.println("ATTEMPT TO SET TO "); return this; } return orig.set(name, values); } @Override public HttpHeaders remove(String name) { if (Names.TRANSFER_ENCODING.equals(name)) { return this; } if (Names.CONTENT_LENGTH.equals(name)) { // System.out.println("ATTEMPT TO REMOVE LENGTH"); return this; } if (Names.CONTENT_ENCODING.equals(name)) { // System.out.println("ATTEMPT TO REMOVE TO "); return this; } return orig.remove(name); } @Override public HttpHeaders clear() { return orig.clear(); } @Override public Iterator<Map.Entry<String, String>> iterator() { return orig.iterator(); } } private static class HackHttpResponse extends DefaultHttpResponse { private final HttpHeaders hdrs; // Workaround for https://github.com/netty/netty/issues/1326 HackHttpResponse(HttpResponseStatus status, boolean chunked) { super(HttpVersion.HTTP_1_1, status); hdrs = new HackHttpHeaders(super.headers(), chunked); } @Override public HttpHeaders headers() { return hdrs; } } public <T> void add(HeaderValueType<T> decorator, T value) { List<Entry<?>> old = new LinkedList<>(); // XXX set cookie! for (Iterator<Entry<?>> it = headers.iterator(); it.hasNext();) { Entry<?> e = it.next(); if (e.decorator.equals(Headers.SET_COOKIE)) { continue; } if (e.match(decorator) != null) { old.add(e); it.remove(); } } Entry<?> e = new Entry<>(decorator, value); // For now, special handling for Allow: // Longer term, should HeaderValueType.isArray() and a way to // coalesce if (!old.isEmpty() && decorator == Headers.ALLOW) { old.add(e); Set<Method> all = new HashSet<>(); for (Entry<?> en : old) { Method[] m = (Method[]) en.value; all.addAll(Arrays.asList(m)); } value = (T) all.toArray(new Method[0]); e = new Entry<>(decorator, value); } headers.add(e); modify(); } public <T> T get(HeaderValueType<T> decorator) { for (Entry<?> e : headers) { HeaderValueType<T> d = e.match(decorator); if (d != null) { return d.type().cast(e.value); } } return null; } public void setChunked(boolean chunked) { this.chunked = chunked; modify(); } <T extends ResponseWriter> void setWriter(T w, Dependencies deps, HttpEvent evt) { // setChunked(true); Charset charset = deps.getInstance(Charset.class); ByteBufAllocator allocator = deps.getInstance(ByteBufAllocator.class); Codec mapper = deps.getInstance(Codec.class); Key<ExecutorService> key = Key.get(ExecutorService.class, Names.named(ServerModule.BACKGROUND_THREAD_POOL_NAME)); ExecutorService svc = deps.getInstance(key); setWriter(w, charset, allocator, mapper, evt, svc); } <T extends ResponseWriter> void setWriter(Class<T> w, Dependencies deps, HttpEvent evt) { // setChunked(true); Charset charset = deps.getInstance(Charset.class); ByteBufAllocator allocator = deps.getInstance(ByteBufAllocator.class); Key<ExecutorService> key = Key.get(ExecutorService.class, Names.named(ServerModule.BACKGROUND_THREAD_POOL_NAME)); ExecutorService svc = deps.getInstance(key); Codec mapper = deps.getInstance(Codec.class); setWriter(new DynResponseWriter(w, deps), charset, allocator, mapper, evt, svc); } static class DynResponseWriter extends ResponseWriter { private final AtomicReference<ResponseWriter> actual = new AtomicReference<>(); private final Callable<ResponseWriter> resp; public DynResponseWriter(final Class<? extends ResponseWriter> type, final Dependencies deps) { ReentrantScope scope = deps.getInstance(ReentrantScope.class); assert scope.inScope(); resp = scope.wrap(new Callable<ResponseWriter>() { @Override public ResponseWriter call() throws Exception { ResponseWriter w = actual.get(); if (w == null) { actual.set(w = deps.getInstance(type)); } return w; } }); } @Override public ResponseWriter.Status write(Event<?> evt, Output out) throws Exception { ResponseWriter actual = resp.call(); return actual.write(evt, out); } @Override public Status write(Event<?> evt, Output out, int iteration) throws Exception { ResponseWriter actual = resp.call(); return actual.write(evt, out, iteration); } } static boolean isKeepAlive(Event<?> evt) { return evt instanceof HttpEvent ? ((HttpEvent) evt).isKeepAlive() : false; } void setWriter(ResponseWriter w, Charset charset, ByteBufAllocator allocator, Codec mapper, Event<?> evt, ExecutorService svc) { setBodyWriter( new ResponseWriterListener(evt, w, charset, allocator, mapper, chunked, !isKeepAlive(evt), svc)); } private static final class ResponseWriterListener extends AbstractOutput implements ChannelFutureListener { private volatile ChannelFuture future; private volatile int callCount = 0; private final boolean chunked; private final ResponseWriter writer; private final boolean shouldClose; private final Event<?> evt; private final ExecutorService svc; public ResponseWriterListener(Event<?> evt, ResponseWriter writer, Charset charset, ByteBufAllocator allocator, Codec mapper, boolean chunked, boolean shouldClose, ExecutorService svc) { super(charset, allocator, mapper); this.chunked = chunked; this.writer = writer; this.shouldClose = shouldClose; this.evt = evt; this.svc = svc; } public Channel channel() { if (future == null) { throw new IllegalStateException("No future -> no channel"); } return future.channel(); } @Override public Output write(ByteBuf buf) throws IOException { assert future != null; if (chunked) { future = future.channel().writeAndFlush(new DefaultHttpContent(buf)); } else { future = future.channel().writeAndFlush(buf); } return this; } volatile boolean inOperationComplete; volatile int entryCount = 0; @Override public void operationComplete(final ChannelFuture future) throws Exception { try { // See https://github.com/netty/netty/issues/2415 for why this is needed if (entryCount > 0) { svc.submit(new Runnable() { @Override public void run() { try { operationComplete(future); } catch (Exception ex) { Exceptions.chuck(ex); } } }); return; } entryCount++; Callable<Void> c = new Callable<Void>() { @Override public Void call() throws Exception { inOperationComplete = true; try { ResponseWriterListener.this.future = future; ResponseWriter.Status status = writer.write(evt, ResponseWriterListener.this, callCount++); if (status.isCallback()) { ResponseWriterListener.this.future = ResponseWriterListener.this.future .addListener(ResponseWriterListener.this); } else if (status == Status.DONE) { if (chunked) { ResponseWriterListener.this.future = ResponseWriterListener.this.future .channel().writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); } if (shouldClose) { ResponseWriterListener.this.future = ResponseWriterListener.this.future .addListener(CLOSE); } } } finally { inOperationComplete = false; } return null; } }; if (!inOperationComplete) { c.call(); } else { svc.submit(c); } } finally { entryCount--; } } @Override public ChannelFuture future() { return future; } } /** * Set a ChannelFutureListener which will be called after headers are * written and flushed to the socket; prefer * <code>setResponseWriter()</code> to this method unless you are not using * chunked encoding and want to stream your response (in which case, be sure * to setChunked(false) or you will have encoding errors). * * @param listener */ public void setBodyWriter(ChannelFutureListener listener) { if (this.listener != null) { throw new IllegalStateException("Listener already set to " + this.listener); } this.listener = listener; } public String getMessage() { return message; } final boolean canHaveBody(HttpResponseStatus status) { switch (status.code()) { case 204: case 205: case 304: return false; default: return true; } } public HttpResponse toResponse(Event<?> evt, Charset charset) { if (!canHaveBody(getResponseCode()) && (message != null || listener != null)) { if (listener != ChannelFutureListener.CLOSE) { System.err.println(evt + " attempts to attach a body to " + getResponseCode() + " which cannot have one: " + message + " - " + listener); } } String msg = getMessage(); HttpResponse resp; if (msg != null) { ByteBuf buf = Unpooled.copiedBuffer(msg, charset); long size = buf.readableBytes(); add(Headers.CONTENT_LENGTH, size); DefaultFullHttpResponse r = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, getResponseCode(), buf); resp = r; } else { resp = new HackHttpResponse(getResponseCode(), this.status == NOT_MODIFIED ? false : chunked); } for (Entry<?> e : headers) { // Remove things which cause problems for non-modified responses - // browsers will hold the connection open regardless if (this.status == NOT_MODIFIED) { if (e.decorator == Headers.CONTENT_LENGTH) { continue; } else if (HttpHeaders.Names.CONTENT_ENCODING.equals(e.decorator.name())) { continue; } else if ("Transfer-Encoding".equals(e.decorator.name())) { continue; } } e.write(resp); } return resp; } ChannelFuture sendMessage(Event<?> evt, ChannelFuture future, HttpMessage resp) { if (listener != null) { future = future.addListener(listener); return future; } else if (!isKeepAlive(evt)) { future = future.addListener(ChannelFutureListener.CLOSE); } return future; } @Override public String toString() { return "Response{" + "modified=" + modified + ", status=" + status + ", headers=" + headers + ", message=" + message + ", listener=" + listener + ", chunked=" + chunked + " has listener " + (this.listener != null) + '}'; } private static final class Entry<T> { private final HeaderValueType<T> decorator; private final T value; Entry(HeaderValueType<T> decorator, T value) { Checks.notNull("decorator", decorator); Checks.notNull(decorator.name().toString(), value); // assert value == null || decorator.type().isInstance(value) : // value + " of type " + value.getClass() + " is not a " + decorator.type(); this.decorator = decorator; this.value = value; } public void decorate(HttpMessage msg) { msg.headers().set(decorator.name(), value); } public void write(HttpMessage msg) { Headers.write(decorator, value, msg); } @Override public String toString() { return decorator.name() + ": " + decorator.toString(value); } @Override public int hashCode() { return decorator.name().hashCode(); } @Override public boolean equals(Object o) { return o instanceof Entry<?> && ((Entry<?>) o).decorator.name().equals(decorator.name()); } @SuppressWarnings({ "unchecked" }) public <R> HeaderValueType<R> match(HeaderValueType<R> decorator) { if (decorator == this.decorator) { return (HeaderValueType<R>) this.decorator; } if (this.decorator.name().equals(decorator.name()) && this.decorator.type().equals(decorator.type())) { return decorator; } return null; } } }