/*
* The MIT License
*
* Copyright 2015 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.google.inject.name.Named;
import com.mastfrog.acteur.errors.ResponseException;
import com.mastfrog.acteur.headers.Headers;
import static com.mastfrog.acteur.server.ServerModule.DELAY_EXECUTOR;
import com.mastfrog.acteur.util.CacheControl;
import com.mastfrog.acteur.util.RequestID;
import com.mastfrog.acteurbase.ArrayChain;
import com.mastfrog.acteurbase.ChainCallback;
import com.mastfrog.acteurbase.ChainRunner;
import com.mastfrog.acteurbase.ChainsRunner;
import com.mastfrog.giulius.Dependencies;
import com.mastfrog.guicy.scope.ReentrantScope;
import com.mastfrog.settings.Settings;
import com.mastfrog.url.Path;
import com.mastfrog.util.Exceptions;
import com.mastfrog.util.collections.CollectionUtils;
import com.mastfrog.util.collections.Converter;
import com.mastfrog.util.thread.QuietAutoCloseable;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.inject.Inject;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.netbeans.validation.api.InvalidInputException;
/**
*
* @author Tim Boudreau
*/
class PagesImpl2 {
private final Application application;
private final ScheduledExecutorService scheduler;
private final ChainsRunner ch;
private final boolean debug;
@Inject
PagesImpl2(Application application, Settings settings, @Named(DELAY_EXECUTOR) ScheduledExecutorService scheduler) {
this.application = application;
this.scheduler = scheduler;
debug = settings.getBoolean("acteur.debug", false);
ChainRunner chr = new ChainRunner(application.getWorkerThreadPool(), application.getRequestScope());
ch = new ChainsRunner(application.getWorkerThreadPool(), application.getRequestScope(), chr);
}
public CountDownLatch onEvent(RequestID id, Event<?> event, Channel channel) {
CountDownLatch latch = new CountDownLatch(1);
Closables clos = new Closables(channel, application.control());
ChainToPageConverter chainConverter = new ChainToPageConverter(id, event, clos);
Iterator<Page> pageIterator = new ScopeWrapIterator<Page>(application.getRequestScope(), application.iterator(), id, event, channel, clos);
Iterable<PageChain> pagesIterable
= CollectionUtils.toIterable(CollectionUtils.convertedIterator(chainConverter, pageIterator));
CB callback = new CB(id, event, latch, channel);
CancelOnChannelClose closer = new CancelOnChannelClose();
channel.closeFuture().addListener(closer);
ch.submit(pagesIterable, callback, closer.cancelled, id, event, clos);
return latch;
}
static class CancelOnChannelClose implements ChannelFutureListener {
final AtomicBoolean cancelled = new AtomicBoolean();
@Override
public void operationComplete(ChannelFuture f) throws Exception {
cancelled.set(true);
}
}
class CB implements ChainCallback<Acteur, com.mastfrog.acteur.State, PageChain, Response, ResponseImpl>, ResponseSender {
private final Event<?> event;
private final CountDownLatch latch;
private final Channel channel;
private final RequestID id;
CB(RequestID id, Event<?> event, CountDownLatch latch, Channel channel) {
this.event = event;
this.latch = latch;
this.channel = channel;
this.id = id;
}
@Override
public void onBeforeRunOne(PageChain chain) {
Page.set(chain.page);
}
@Override
public void onAfterRunOne(PageChain chain, Acteur acteur) {
if (Page.get() == chain.page) {
Page.clear();
}
}
@Override
public void onDone(com.mastfrog.acteur.State state, List<ResponseImpl> responses) {
ResponseImpl finalR = new ResponseImpl();
// Coalesce the responses generated by individual acteurs
for (ResponseImpl r : responses) {
finalR.merge(r);
}
receive(state.getActeur(), state, finalR);
latch.countDown();
}
@Override
public void onRejected(com.mastfrog.acteur.State state) {
throw new UnsupportedOperationException("Should not ever be called from ChainsRunner");
}
@Override
public void onNoResponse() {
application.send404(id, event, channel);
latch.countDown();
}
@Override
public void onFailure(Throwable ex) {
uncaughtException(Thread.currentThread(), ex);
latch.countDown();
}
@Override
public void receive(final Acteur acteur, final com.mastfrog.acteur.State state, final ResponseImpl response) {
if (response.isModified() && response.status != null) {
// Actually send the response
try (QuietAutoCloseable clos = Page.set(application.getDependencies().getInstance(Page.class))){
// Abort if the client disconnected
if (!channel.isOpen()) {
latch.countDown();
return;
}
Charset charset = application.getDependencies().getInstance(Charset.class);
application.onBeforeSendResponse(response.status, event, response, state.getActeur(), state.getLockedPage());
// Create a netty response
HttpResponse httpResponse = response.toResponse(event, charset);
// Allow the application to add headers
httpResponse = application._decorateResponse(event, state.getLockedPage(), acteur, httpResponse);
// Allow the page to add headers
state.getLockedPage().decorateResponse(event, acteur, httpResponse);
// Abort if the client disconnected
if (!channel.isOpen()) {
latch.countDown();
return;
}
final HttpResponse resp = httpResponse;
try {
Callable<ChannelFuture> c = new ResponseTrigger(response, resp, state, acteur);
Duration delay = response.getDelay();
if (delay == null) {
c.call();
} else {
if (debug) {
System.err.println("Response will be delayed for " + delay);
}
final ScheduledFuture<?> s = scheduler.schedule(c, response.getDelay().getMillis(), TimeUnit.MILLISECONDS);
// Ensure the task is discarded if the connection is broken
channel.closeFuture().addListener(new CancelOnClose(s));
}
} finally {
latch.countDown();
}
} catch (ThreadDeath | OutOfMemoryError ee) {
Exceptions.chuck(ee);
} catch (Exception | Error e) {
uncaughtException(Thread.currentThread(), e);
}
} else {
onNoResponse();
}
}
boolean inUncaughtException = false;
@Override
public void uncaughtException(Thread thread, Throwable thrwbl) {
try {
if (inUncaughtException) {
// We have recursed - something was thrown from the ErrorActeur -
// bail out and we'll be caught by the catch block below
// and write a plain response
throw thrwbl;
}
// Certain things we just bail out on
if (thrwbl instanceof ThreadDeath || thrwbl instanceof OutOfMemoryError) {
Exceptions.chuck(thrwbl);
}
// These should not be logged - they can be thrown when validating input
if (!(thrwbl instanceof ResponseException && !(thrwbl instanceof InvalidInputException))) {
thrwbl.printStackTrace();
application.internalOnError(thrwbl);
}
// V1.6 - we no longer have access to the page where the exception was
// thrown
ErrorPage pg = application.getDependencies().getInstance(ErrorPage.class);
pg.setApplication(application);
// Build up a fake context for ErrorActeur to operate in
try (AutoCloseable ac = Page.set(pg)) {
inUncaughtException = true;
try (AutoCloseable ac2 = application.getRequestScope().enter(id, event, channel)) {
Acteur err = Acteur.error(null, pg, thrwbl,
application.getDependencies().getInstance(HttpEvent.class), true);
receive(err, err.getState(), err.getResponse());
}
} finally {
inUncaughtException = false;
}
} catch (Throwable ex) {
try {
if (channel.isOpen()) {
ByteBuf buf = channel.alloc().buffer();
try (PrintStream ps = new PrintStream(new ByteBufOutputStream(buf))) {
ex.printStackTrace(ps);
}
DefaultFullHttpResponse resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR, buf);
Headers.write(Headers.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8, resp);
Headers.write(Headers.CONTENT_LENGTH, (long) buf.writerIndex(), resp);
Headers.write(Headers.CONTENT_LANGUAGE, Locale.ENGLISH, resp);
Headers.write(Headers.CACHE_CONTROL, CacheControl.PRIVATE_NO_CACHE_NO_STORE, resp);
Headers.write(Headers.DATE, new DateTime(), resp);
channel.writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE);
}
} finally {
application.internalOnError(ex);
}
}
}
private class ResponseTrigger implements Callable<ChannelFuture> {
private final ResponseImpl response;
private final HttpResponse resp;
private final State state;
private final Acteur acteur;
public ResponseTrigger(ResponseImpl response, HttpResponse resp, State state, Acteur acteur) {
this.response = response;
this.resp = resp;
this.state = state;
this.acteur = acteur;
}
public ChannelFuture call() throws Exception {
// Give the application a last chance to do something
application.onBeforeRespond(id, event, response.getResponseCode());
// Send the headers
ChannelFuture fut = channel.writeAndFlush(resp);
final Page pg = state.getLockedPage();
fut = response.sendMessage(event, fut, resp);
application.onAfterRespond(id, event, acteur, pg, state, HttpResponseStatus.OK, resp);
return fut;
}
}
}
static class CancelOnClose implements ChannelFutureListener {
private final ScheduledFuture future;
public CancelOnClose(ScheduledFuture future) {
this.future = future;
}
@Override
public void operationComplete(ChannelFuture f) throws Exception {
future.cancel(true);
}
}
// A fake page for use with errors
static class ErrorPage extends Page {
}
class ChainToPageConverter implements Converter<PageChain, Page> {
private final RequestID id;
private final Event<?> event;
private final Closables clos;
private ChainToPageConverter(RequestID id, Event<?> event, Closables clos) {
this.id = id;
this.event = event;
this.clos = clos;
}
@Override
public PageChain convert(Page r) {
r.setApplication(application);
if (event instanceof HttpEvent) {
Path pth = ((HttpEvent) event).getPath();
Thread.currentThread().setName(pth + " for " + r.getClass().getName());
}
PageChain result = new PageChain(application.getDependencies(), application.getRequestScope(), Acteur.class, r, r, id, event, clos);
return result;
}
@Override
public Page unconvert(PageChain t) {
return t.page;
}
}
static class PageChain extends ArrayChain<Acteur> {
private final Page page;
private final Object[] ctx;
private AtomicBoolean first = new AtomicBoolean(true);
private final ReentrantScope scope;
private static final Object[] EMPTY = new Object[0];
public PageChain(Dependencies deps, ReentrantScope scope, Class<? super Acteur> type, Page page, Object... ctx) {
super(deps, type, page.acteurs());
this.page = page;
this.ctx = ctx;
this.scope = scope;
}
@Override
public Object[] getContextContribution() {
// First round we need to wrap the callable in the scope with
// these objects; they will already be in scope when it is
// wrapped for a subsequent call
if (first.compareAndSet(true, false)) {
return ctx;
} else {
return EMPTY;
}
}
public String toString() {
return "Chain for " + page;
}
}
static class ScopeWrapIterator<T> implements Iterator<T> {
private final ReentrantScope scope;
private final Iterator<T> delegate;
private final Object[] ctx;
public ScopeWrapIterator(ReentrantScope scope, Iterator<T> delegate, Object... ctx) {
this.scope = scope;
this.delegate = delegate;
this.ctx = ctx;
}
@Override
public boolean hasNext() {
return delegate.hasNext();
}
@Override
public T next() {
try (QuietAutoCloseable clos = scope.enter(ctx)) {
return delegate.next();
}
}
@Override
public void remove() {
delegate.remove();
}
//
// @Override
// public void forEachRemaining(Consumer<? super T> cnsmr) {
// delegate.forEachRemaining(cnsmr);
// }
}
}