/* * 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.common.net.MediaType; import com.mastfrog.acteur.Acteur.BaseState; import com.mastfrog.acteur.errors.Err; import com.mastfrog.acteur.errors.ErrorRenderer; import com.mastfrog.acteur.errors.ErrorResponse; import com.mastfrog.acteur.headers.HeaderValueType; import com.mastfrog.acteur.headers.Headers; import com.mastfrog.acteurbase.AbstractActeur; import com.mastfrog.acteurbase.ActeurResponseFactory; import com.mastfrog.giulius.Dependencies; import com.mastfrog.guicy.scope.ReentrantScope; import com.mastfrog.settings.Settings; import com.mastfrog.util.Checks; import com.mastfrog.util.Codec; import com.mastfrog.util.Exceptions; import com.mastfrog.util.Invokable; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.handler.codec.http.HttpResponseStatus; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; import static io.netty.handler.codec.http.HttpResponseStatus.NO_CONTENT; import static io.netty.handler.codec.http.HttpResponseStatus.OK; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicReference; /** * A single piece of logic which can * <ul> * <li>Reject an HTTP request</li> * <li>Validate an HTTP request and allow the next Acteur in the chain to * process it</li> * <li>Initiate an HTTP response</li> * </ul> * Acteurs are aggregated into a list in a {@link Page}. All of an Acteur's work * happens either in its constructor, prior to a call to setState(), or in its * overridden getState() method. The state determines whether processing of the * current list of Acteurs will continue, or if not, what happens to it. * <p/> * Acteurs are constructed by Guice - in fact, what a Page has is usually just a * list of classes. Objects they need, such as the current request * {@link HttpEvent} can simply be constructor parameters if the constructor is * annotated with Guice's @Inject. * <p/> * An Acteur may construct some objects which will then be included in the set * of objects the next Acteur in the chain can request for injection in its * constructor parameters. * <p/> * A number of inner classes are provided which can be used as standard states. * <p/> * Acteurs may be - in fact, are likely to be - called asynchronously. For a * given page, they will always be called in the sequence that page lists them * in, but there is no guarantee that any two adjacent Acteurs will be called on * the same thread. Any shared state should take the form of objects put into * the context when the output State is created. * <p/> * This makes it possible to incrementally respond to a request, for example, * doing just enough computation to determine if a NOT MODIFIED response is * possible without computing the complete response (which in that case would be * thrown away). * <p/> * Their asynchronous nature means that many requests can be handled * simultaneously and run small bits of logic, interleaved, on fewer threads, * for maximum throughput. * * @author Tim Boudreau */ public abstract class Acteur extends AbstractActeur<Response, ResponseImpl, State> { class BaseState extends com.mastfrog.acteur.State { protected final Page page; public BaseState(Object... context) { super(context); page = Page.get(); if (page == null) { throw new IllegalStateException("Page not set"); } } public BaseState(boolean rejected) { super(rejected); page = Page.get(); if (page == null) { throw new IllegalStateException("Page not set"); } } @Override public final Acteur getActeur() { return Acteur.this; } } /** * Create an acteur. * * @param async If true, the framework should prefer to run the <i>next</i> * action asynchronously */ protected Acteur() { super(INSTANCE); } private static final RT INSTANCE = new RT(); protected com.mastfrog.acteur.State getState() { return super.getState(); } private static final Object[] EMPTY = new Object[0]; public Object[] getContextContribution() { return EMPTY; } final Throwable creationStackTrace() { return creationStackTrace; } static class RT extends ActeurResponseFactory<Response, ResponseImpl> { @Override protected ResponseImpl create() { return new ResponseImpl(); } @Override protected boolean isFinished(ResponseImpl obj) { return obj != null && obj.status != null; } @Override protected boolean isModified(ResponseImpl obj) { return obj != null && obj.isModified(); } } /** * If you write an acteur which delegates to another one, implement this so * that that other one's changes to the response will be picked up. This * pattern is sometimes used where a choice is made about which acteur to * call next. */ public interface Delegate { /** * Get the acteur being delegated to * * @return An acteur */ Acteur getDelegate(); } protected <T> Acteur add(HeaderValueType<T> decorator, T value) { response().add(decorator, value); return this; } protected ResponseImpl getResponse() { if (this instanceof Delegate) { return ((Delegate) this).getDelegate().getResponse(); } return super.getResponse(); } protected <T> T get(HeaderValueType<T> header) { return response().get(header); } public final Acteur setResponseCode(HttpResponseStatus status) { response().setResponseCode(status); return this; } public final Acteur setMessage(Object message) { response().setMessage(message); return this; } public final Acteur setChunked(boolean chunked) { response().setChunked(chunked); return this; } protected final Response response() { if (this instanceof Delegate) { return ((Delegate) this).getDelegate().response(); } return super.response(); } static Acteur error(Acteur errSource, Page page, Throwable t, HttpEvent evt, boolean log) { try { return new ErrorActeur(errSource, evt, page, t, true, log); } catch (IOException ex) { page.application.internalOnError(t); try { return new ErrorActeur(errSource, evt, page, t, true, log); } catch (IOException ex1) { return Exceptions.chuck(ex1); } } } public void describeYourself(Map<String, Object> into) { } protected final Acteur noContent() { setState(new RespondWith(NO_CONTENT)); return this; } protected final Acteur badRequest() { setState(new RespondWith(BAD_REQUEST)); return this; } protected final Acteur badRequest(Object msg) { setState(new RespondWith(BAD_REQUEST, msg)); return this; } protected final Acteur notFound() { setState(new RespondWith(NOT_FOUND)); return this; } protected final Acteur notFound(Object msg) { setState(new RespondWith(NOT_FOUND, msg)); return this; } protected final Acteur ok(Object msg) { setState(new RespondWith(OK, msg)); return this; } protected final Acteur ok() { setState(new RespondWith(OK)); return this; } protected final Acteur reply(HttpResponseStatus status) { setState(new RespondWith(status)); return this; } protected final Acteur reply(HttpResponseStatus status, Object msg) { setState(new RespondWith(status, msg)); return this; } protected final Acteur reply(Err err) { setState(new RespondWith(err)); return this; } protected final Acteur reject() { setState(new RejectedState()); return this; } protected final Acteur next(Object... context) { if (context == null || context.length == 0) { setState(new ConsumedState()); } else { setState(new ConsumedLockedState(context)); } return this; } /** * A shorthand state for responding with a particular http response code and * optional message, which if non-string, will be rendered as JSON. */ public class RespondWith extends BaseState { public RespondWith(int status) { this(HttpResponseStatus.valueOf(status)); } public RespondWith(HttpResponseStatus status) { this(status, null); } public RespondWith(int status, Object msg) { this(HttpResponseStatus.valueOf(status), msg); } public RespondWith(ErrorResponse err) { super(false); setResponseCode(err.status()); ErrorRenderer ren = getLockedPage().getApplication().getDependencies().getInstance(ErrorRenderer.class); Object message; try { message = ren.render(err, getLockedPage().getApplication().getDependencies().getInstance(HttpEvent.class)); setMessage(message); } catch (IOException ex) { Exceptions.chuck(ex); } } /** * Acteur.this; Response which uses JSON * * @param status * @param msg */ public RespondWith(HttpResponseStatus status, Object msg) { super(false); if (page == null) { IllegalStateException e = new IllegalStateException("Called outside ActionsImpl.onEvent"); e.printStackTrace(); throw e; } setResponseCode(status); if (msg != null) { setMessage(msg); } } @Override public String toString() { return "Respond with " + getResponse().getResponseCode() + " - " + super.toString() + " - " + getResponse().getMessage(); } } /** * A state indicating the acteur neither accepts nor definitively refuses a * request. */ protected class RejectedState extends BaseState { public RejectedState() { super(true); if (page == null) { throw new IllegalStateException("Called outside ActionsImpl.onEvent"); } } public RejectedState(HttpResponseStatus status) { super(true); setResponseCode(status); } } /** * State indicating that this acteur chain is taking responsibility for * responding to the request. It may optionally include objects which should * be available for injection into subsequent acteurs. */ protected class ConsumedState extends BaseState { private final Page page; private final Object[] context; public ConsumedState(Object... context) { super(false); page = Page.get(); if (page == null) { throw new IllegalStateException("Called outside ActionsImpl.onEvent"); } this.context = context; } } protected class ConsumedLockedState extends BaseState { private final Page page; public ConsumedLockedState(Object... context) { super(context); page = Page.get(); if (page == null) { throw new IllegalStateException("Called outside ActionsImpl.onEvent"); } } } /** * Set a response writer which can iteratively be called back until the * response is completed. The writer will be created dynamically but any * object currently in scope can be injected into it. * * @param <T> The type of writer * @param writerType The writer class */ protected final <T extends ResponseWriter> Acteur setResponseWriter(Class<T> writerType) { Page page = Page.get(); Dependencies deps = page.getApplication().getDependencies(); HttpEvent evt = deps.getInstance(HttpEvent.class); response(); getResponse().setWriter(writerType, deps, evt); return this; } /** * Set a response writer which can iteratively be called back until the * response is completed. * * @param <T> The type of writer * @param writer The writer */ protected final <T extends ResponseWriter> Acteur setResponseWriter(T writer) { Page page = Page.get(); Dependencies deps = page.getApplication().getDependencies(); HttpEvent evt = deps.getInstance(HttpEvent.class); response(); getResponse().setWriter(writer, deps, evt); return this; } /** * 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). * <p/> * This method will dynamically construct the passed listener type using * Guice, and including all of the contents of the scope in which this call * was made. * * @param <T> a type * @param type The type of listener */ protected final <T extends ChannelFutureListener> Acteur setResponseBodyWriter(final Class<T> type) { final Page page = Page.get(); final Dependencies deps = page.getApplication().getDependencies(); ReentrantScope scope = page.getApplication().getRequestScope(); final AtomicReference<ChannelFuture> fut = new AtomicReference<>(); // An object which can instantiate and run the listener class I extends Invokable<ChannelFuture, Void, Exception> { private ChannelFutureListener delegate; @Override public Void run(ChannelFuture argument) throws Exception { if (delegate == null) { delegate = deps.getInstance(type); } delegate.operationComplete(argument); return null; } @Override public String toString() { return "Delegate for " + type; } } // A runnable-like object which takes an argument, and which can // be wrapped by the scope in order to reconstitute the scope contents // as they are now before constructing the actual listener final Invokable<ChannelFuture, Void, Exception> listenerInvoker = scope.wrap(new I(), fut); // Wrap this in a dummy listener which will create the real one on // demand class C implements ChannelFutureListener { @Override public void operationComplete(ChannelFuture future) throws Exception { try (AutoCloseable cl = Page.set(page)) { fut.set(future); listenerInvoker.run(future); } } @Override public String toString() { return "Delegate for " + listenerInvoker; } } ChannelFutureListener l = new C(); setResponseBodyWriter(l); return this; } protected final Dependencies dependencies() { final Page p = Page.get(); final Application app = p.getApplication(); return app.getDependencies(); } /** * 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 final Acteur setResponseBodyWriter(final ChannelFutureListener listener) { if (listener == ChannelFutureListener.CLOSE || listener == ChannelFutureListener.CLOSE_ON_FAILURE) { response(); getResponse().setBodyWriter(listener); return this; } Page p = Page.get(); final Application app = p.getApplication(); class WL implements ChannelFutureListener, Callable<Void> { private ChannelFuture future; private Callable<Void> wrapper = app.getRequestScope().wrap(this); @Override public void operationComplete(ChannelFuture future) throws Exception { this.future = future; wrapper.call(); } @Override public Void call() throws Exception { listener.operationComplete(future); return null; } @Override public String toString() { return "Scope wrapper for " + listener; } } response().setBodyWriter(new WL()); return this; } // public <T extends State & com.mastfrog.acteur.State> State getState() { // return super.getState(); // } public static Acteur wrap(final Class<? extends Acteur> type, final Dependencies deps) { Checks.notNull("type", type); final Charset charset = deps.getInstance(Charset.class); return new WrapperActeur(deps, charset, type); } static class WrapperActeur extends Acteur implements Delegate { private final Dependencies deps; private final Charset charset; private final Class<? extends Acteur> type; public WrapperActeur(Dependencies deps, Charset charset, Class<? extends Acteur> type) { this.deps = deps; this.charset = charset; this.type = type; } Acteur acteur; public Class<? extends Acteur> type() { return type; } @Override public void describeYourself(Map<String, Object> into) { try { delegate().describeYourself(into); } catch (Exception e) { //ok - we may be called without an event to play with } } boolean inOnError; protected void onError(Throwable t) throws UnsupportedEncodingException { if (inOnError) { Exceptions.chuck(t); } inOnError = true; try { if (!Dependencies.isProductionMode(deps.getInstance(Settings.class))) { ByteArrayOutputStream out = new ByteArrayOutputStream(); t.printStackTrace(new PrintStream(out)); add(Headers.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.withCharset(charset)); this.setMessage(new String(out.toByteArray(), charset)); } this.setResponseCode(HttpResponseStatus.INTERNAL_SERVER_ERROR); } finally { inOnError = false; } } private com.mastfrog.acteur.State cachedState; Acteur delegate() { if (acteur == null) { try { acteur = deps.getInstance(type); } catch (Exception e) { try { onError(e); deps.getInstance(Application.class).internalOnError(e); } catch (UnsupportedEncodingException ex) { Exceptions.chuck(ex); } } } return acteur; } @Override public com.mastfrog.acteur.State getState() { return cachedState == null ? cachedState = delegate().getState() : cachedState; } @Override public String toString() { return "Wrapper [" + (acteur == null ? type + " (type)" : acteur) + " lastState=" + cachedState + "]"; } @Override public Acteur getDelegate() { return delegate(); } } }