/*
* 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.ImplementedBy;
import com.mastfrog.acteur.errors.ResponseException;
import com.mastfrog.acteur.headers.HeaderValueType;
import com.mastfrog.acteur.headers.Headers;
import com.mastfrog.acteur.preconditions.Description;
import com.mastfrog.acteur.preconditions.PageAnnotationHandler;
import com.mastfrog.acteur.util.CacheControl;
import com.mastfrog.giulius.Dependencies;
import com.mastfrog.util.Checks;
import com.mastfrog.util.Exceptions;
import com.mastfrog.util.collections.CollectionUtils;
import com.mastfrog.util.thread.AutoCloseThreadLocal;
import com.mastfrog.util.thread.QuietAutoCloseable;
import io.netty.handler.codec.http.HttpResponse;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* Really an aggregation of Acteurs and a place to set header values; in recent
* versions of Acteur it is rarely necessary to implement this - instead, simply
* annotate your entry-point Acteur with @HttpCall and one will be
* generated for you under-the-hood.
*
* To implement, simply subclass and add zero or more
* <code><a href="Acteur.html">Acteur</a></code> classes or instances using the
* <code>add()</code> method. Each Acteur is called in succession and can do one
* or more of:
* <ul>
* <li>Abort responding to the request so that the next Page in the application
* can have a chance to respond</li>
* <li>Leave whether or not to respond up to the next Acteur in the chain, but
* create some objects to inject into the next Acteur's constructor</li>
* <li>Accept responsibility for this page responding to the request (other
* pages will not be tried)</li>
* <li>Respond to the request, ending processing of it, by setting the response
* code and optionally adding a <code>ChannelFutureListener</code> which can
* start writing the response body once the headers are sent</li>
* </ul>
* The point is to break the logic of responding to requests into small,
* reusable chunks implemented as Acteurs.
*
* @author Tim Boudreau
*/
public abstract class Page implements Iterable<Acteur> {
private static final AutoCloseThreadLocal<Page> CURRENT_PAGE = new AutoCloseThreadLocal<>();
protected final ResponseHeaders responseHeaders = new ResponseHeaders();
private final List<Object> acteurs = new ArrayList<>(15);
volatile Application application;
protected Page() {
}
public final ResponseHeaders getResponseHeaders() {
return responseHeaders;
}
public final void add(Acteur action) {
acteurs.add(action);
}
protected String getDescription() {
Description desc = getClass().getAnnotation(Description.class);
return desc != null ? desc.value() : getClass().getSimpleName();
}
Iterable<Object> contents() {
List<Object> result = new ArrayList<Object>(annotations());
result.addAll(this.acteurs);
return result;
}
void describeYourself(Map<String, Object> into) {
Map<String, Object> m = new HashMap<>();
if (getClass().getAnnotation(Description.class) != null) {
m.put("description", getClass().getAnnotation(Description.class).value());
}
int count = countActeurs();
for (int i = 0; i < count; i++) {
try {
Acteur a = getActeur(i, false);
a.describeYourself(m);
} catch (Exception e) {
//ok
}
}
if (!m.isEmpty()) {
into.put(getClass().getSimpleName(), m);
}
}
static QuietAutoCloseable set(Page page) {
Checks.notNull("page", page);
QuietAutoCloseable result = CURRENT_PAGE.set(page);
assert CURRENT_PAGE.get() != null;
return result;
}
static Page get() {
return CURRENT_PAGE.get();
}
static void clear() {
CURRENT_PAGE.clear();
}
protected final void add(Class<? extends Acteur> action) {
if ((action.getModifiers() & Modifier.ABSTRACT) != 0) {
if (action.getAnnotation(ImplementedBy.class) == null) {
throw new IllegalArgumentException(action + " is abstract");
}
}
if (action.isLocalClass()) {
throw new IllegalArgumentException(action + " is not a top-level class");
}
if (!Acteur.class.isAssignableFrom(action)) {
throw new IllegalArgumentException(action + " is not a subclass of "
+ Acteur.class.getName());
}
assert Application.checkConstructor(action);
acteurs.add(action);
}
final Application getApplication() {
return application;
}
final void setApplication(Application app) {
this.application = app;
}
final int countActeurs() {
return acteurs.size();
}
final List<Object> getActeurs() {
return Collections.unmodifiableList(acteurs);
}
List<Object> acteurs() {
List<Acteur> annos = this.annotations();
List<Object> l = new ArrayList<>(annos.size() + acteurs.size());
l.addAll(this.annotations());
l.addAll(acteurs);
return l;
}
final Acteur getActeur(int ix) {
return getActeur(ix, true);
}
@SuppressWarnings("unchecked")
final Acteur getActeur(int ix, boolean logErrors) {
try (QuietAutoCloseable ac = Page.set(this)) {
Application app = getApplication();
if (app == null) {
throw new NullPointerException("Application is null - being called out of scope?");
}
Object o = acteurs.get(ix);
if (o instanceof Class<?>) {
Dependencies deps = app.getDependencies();
try {
Class<? extends Acteur> c = (Class<? extends Acteur>) o;
return deps.getInstance(c);
} catch (ThreadDeath | OutOfMemoryError e) {
return Exceptions.chuck(e);
} catch (final Exception t) {
return Acteur.error(null, this, t, deps.getInstance(HttpEvent.class), logErrors && !(t instanceof ResponseException));
} catch (final Error t) {
return Acteur.error(null, this, t, deps.getInstance(HttpEvent.class), logErrors);
}
} else if (o instanceof Acteur) {
return (Acteur) o;
} else {
throw new AssertionError("?: " + o + " in " + this);
}
}
}
protected void decorateResponse(Event<?> event, Acteur acteur, HttpResponse response) {
final ResponseHeaders properties = getResponseHeaders();
if (!properties.modified()) {
return;
}
List<HeaderValueType<?>> vary = new LinkedList<>();
properties.getVaryHeaders(vary);
if (!vary.isEmpty()) {
Headers.write(Headers.VARY, vary.toArray(new HeaderValueType<?>[vary.size()]), response);
}
Headers.writeIfNotNull(Headers.LAST_MODIFIED, properties.getLastModified(), response);
Headers.writeIfNotNull(Headers.TRANSFER_ENCODING, properties.getTransferEncoding(), response);
Headers.writeIfNotNull(Headers.CONTENT_ENCODING, properties.getContentEncoding(), response);
Headers.writeIfNotNull(Headers.ETAG, properties.getETag(), response);
Headers.writeIfNotNull(Headers.EXPIRES, properties.getExpires(), response);
CacheControl cacheControl = properties.getCacheControl();
if (cacheControl != null && !cacheControl.isEmpty()) {
Headers.write(Headers.CACHE_CONTROL, cacheControl, response);
}
Headers.writeIfNotNull(Headers.CONTENT_TYPE, properties.getContentType(), response);
Headers.writeIfNotNull(Headers.CONTENT_LANGUAGE, properties.getContentLanguage(), response);
Headers.writeIfNotNull(Headers.AGE, properties.getAge(), response);
Duration maxAge = properties.getMaxAge();
if (maxAge != null) {
Headers.write(Headers.EXPIRES, new DateTime().plus(maxAge), response);
}
Headers.writeIfNotNull(Headers.CONTENT_LOCATION, properties.getContentLocation(), response);
Headers.writeIfNotNull(Headers.LOCATION, properties.getLocation(), response);
Headers.writeIfNotNull(Headers.CONTENT_LENGTH, properties.getContentLength(), response);
}
@SuppressWarnings("deprecation")
private Iterator<Acteur> annotationActeurs() {
return annotations().iterator();
}
private List<Acteur> annotations() {
PageAnnotationHandler.Registry handler = getApplication().getDependencies().getInstance(PageAnnotationHandler.Registry.class);
List<Acteur> results = new LinkedList<>();
handler.processAnnotations(this, results);
return results;
}
@Override
@Deprecated
public Iterator<Acteur> iterator() {
assert getApplication() != null : "Application is null - called outside request?";
PageAnnotationHandler.Registry registry
= getApplication().getDependencies().getInstance(
PageAnnotationHandler.Registry.class);
if (registry.hasAnnotations(this)) {
return CollectionUtils.combine(annotationActeurs(), new I());
} else {
return new I();
}
}
/**
* An adaptor which instantiates the Acteurs of the page on demand and
* returns them one by one
*/
private final class I implements Iterator<Acteur> {
int ix = 0;
@Override
public boolean hasNext() {
return ix < countActeurs();
}
@Override
public Acteur next() {
return getActeur(ix++);
}
@Override
public void remove() {
throw new UnsupportedOperationException("Not supported");
}
}
}