/* * 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.mastfrog.acteur.headers.Headers; import com.google.common.net.MediaType; import com.google.inject.Inject; import com.google.inject.name.Named; import com.mastfrog.acteur.Acteur.WrapperActeur; import com.mastfrog.acteur.annotations.Concluders; import com.mastfrog.acteur.annotations.HttpCall; import com.mastfrog.acteur.annotations.Precursors; import com.mastfrog.acteur.preconditions.Description; import com.mastfrog.settings.SettingsBuilder; import com.mastfrog.giulius.Dependencies; import com.mastfrog.guicy.scope.ReentrantScope; import com.mastfrog.acteur.util.CacheControl; import com.mastfrog.acteur.util.CacheControlTypes; import com.mastfrog.acteur.server.ServerModule; import static com.mastfrog.acteur.server.ServerModule.SETTINGS_KEY_CORS_ALLOW_ORIGIN; import static com.mastfrog.acteur.server.ServerModule.SETTINGS_KEY_CORS_MAX_AGE_MINUTES; import com.mastfrog.acteur.util.ErrorInterceptor; import com.mastfrog.acteur.spi.ApplicationControl; import com.mastfrog.acteur.util.RequestID; import com.mastfrog.acteurbase.InstantiatingIterators; import com.mastfrog.parameters.Param; import com.mastfrog.parameters.Params; import com.mastfrog.util.ConfigurationError; import com.mastfrog.util.Checks; import com.mastfrog.util.perf.Benchmark; import com.mastfrog.util.perf.Benchmark.Kind; import com.mastfrog.util.thread.QuietAutoCloseable; import io.netty.buffer.ByteBuf; 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.HttpHeaders; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import org.joda.time.DateTime; import org.joda.time.Duration; import org.netbeans.validation.api.Validator; import org.netbeans.validation.api.builtin.stringvalidation.StringValidators; import org.openide.util.Exceptions; /** * A web application. Principally, the application is a collection of Page * types, which are instantiated per-request and offered the request, in the * order they are added, until one accepts it and takes responsibility for * responding. * * @author Tim Boudreau */ public class Application implements Iterable<Page> { private static final Set<String> checkedTypes = Collections.synchronizedSet(new HashSet<String>()); private final List<Object> pages = new ArrayList<>(); @Inject private Dependencies deps; @Inject @Named(ServerModule.BACKGROUND_THREAD_POOL_NAME) private ExecutorService exe; @Inject private RequestLogger logger; @Inject private ReentrantScope scope; private final Exception stackTrace = new Exception(); @Inject private PagesImpl2 runner; @Inject(optional = true) private ErrorInterceptor errorHandler; @Inject private Charset charset; @Inject(optional = true) @Named(SETTINGS_KEY_CORS_ALLOW_ORIGIN) String corsAllowOrigin = "*"; @Inject(optional = true) @Named("application.name") String name; @Inject(optional = true) @Named(SETTINGS_KEY_CORS_MAX_AGE_MINUTES) long corsMaxAgeMinutes = 5; @Inject(optional = true) @Named("acteur.debug") private boolean debug = false; private final RequestID.Factory ids = new RequestID.Factory(); /** * Create an application, optionally passing in an array of page types (you * can also call <code>add()</code> to add them). * * @param types */ @SuppressWarnings("unchecked") protected Application(Class<?>... types) { this(); for (Class<?> type : types) { add((Class<? extends Page>) type); } } protected Application() { Help help = getClass().getAnnotation(Help.class); if (help != null) { add(helpPageType()); } } List<Object> rawPages() { return this.pages; } /** * Get the type of the built in help page class, which uses * Acteur.describeYourself() to generate a JSON description of all URLs the * application responnds to * * @return A page type */ public static Class<? extends Page> helpPageType() { return HelpPage.class; } private boolean corsEnabled; public final boolean isDefaultCorsHandlingEnabled() { return corsEnabled; } protected final void enableDefaultCorsHandling() { if (!corsEnabled) { corsEnabled = true; pages.add(0, CORSResource.class); } } private final ApplicationControl control = new ApplicationControl() { @Override public void enableDefaultCorsHandling() { Application.this.enableDefaultCorsHandling(); } @Override public CountDownLatch onEvent(Event<?> event, Channel channel) { return Application.this.onEvent(event, channel); } @Override public void internalOnError(Throwable err) { Checks.notNull("err", err); Application.this.internalOnError(err); } }; ApplicationControl control() { return control; } /** * Create an application * * @param types * @return */ public static Application create(Class<?>... types) { return new Application(types); } /** * Get the <code>Scope</code> which is holds per-request state. * * @return */ public ReentrantScope getRequestScope() { return scope; } ExecutorService getWorkerThreadPool() { return exe; } private static String deConstantNameify(String name) { StringBuilder sb = new StringBuilder(); boolean capitalize = true; for (char c : name.toCharArray()) { if (c == '_') { sb.append(' '); } else { if (capitalize) { c = Character.toUpperCase(c); capitalize = false; } else { c = Character.toLowerCase(c); } sb.append(c); } } return sb.toString(); } private void introspectAnnotation(Annotation a, Map<String, Object> into) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (a instanceof HttpCall) { return; } if (a instanceof Precursors) { Precursors p = (Precursors) a; for (Class<?> t : p.value()) { for (Annotation anno : t.getAnnotations()) { introspectAnnotation(anno, into); } } } else if (a instanceof Concluders) { Concluders c = (Concluders) a; for (Class<?> t : c.value()) { for (Annotation anno : t.getAnnotations()) { introspectAnnotation(anno, into); } } } else if (a instanceof Params) { Params p = (Params) a; for (Param par : p.value()) { String name = par.value(); Map<String, Object> desc = new LinkedHashMap<>(); desc.put("type", par.type().toString()); if (!par.defaultValue().isEmpty()) { desc.put("Default value", par.defaultValue()); } if (!par.example().isEmpty()) { desc.put("Example", par.example()); } desc.put("required", par.required()); List<String> constraints = new LinkedList<>(); for (StringValidators validator : par.constraints()) { constraints.add(deConstantNameify(validator.name())); } for (Class<? extends Validator<String>> c : par.validators()) { Description des = c.getAnnotation(Description.class); if (des == null) { constraints.add(c.getSimpleName()); } else { constraints.add(des.value()); } } if (!constraints.isEmpty()) { desc.put("constraints", constraints); } into.put(name, desc); } } else { Class<? extends Annotation> type = a.annotationType(); for (java.lang.reflect.Method m : type.getMethods()) { switch (m.getName()) { case "annotationType": case "toString": case "hashCode": break; default: if (m.getParameterTypes().length == 0 && m.getReturnType() != null) { into.put(m.getName(), m.invoke(a)); } } } if (type.getAnnotation(Description.class) != null) { Description d = type.getAnnotation(Description.class); into.put("Description", d.value()); } } } Map<String, Object> describeYourself() { Map<String, Object> m = new HashMap<>(); for (Object o : this.pages) { if (o instanceof Class<?>) { Class<?> type = (Class<?>) o; Map<String, Object> pageDescription = new HashMap<>(); String typeName = type.getName(); if (typeName.endsWith(HttpCall.GENERATED_SOURCE_SUFFIX)) { typeName = typeName.substring(0, typeName.length() - HttpCall.GENERATED_SOURCE_SUFFIX.length()); } pageDescription.put("type", type.getName()); String className = type.getSimpleName(); if (className.endsWith(HttpCall.GENERATED_SOURCE_SUFFIX)) { className = className.substring(0, className.length() - HttpCall.GENERATED_SOURCE_SUFFIX.length()); } m.put(className, pageDescription); Annotation[] l = type.getAnnotations(); for (Annotation a : l) { if (a instanceof HttpCall) { continue; } Map<String, Object> annoDescription = new HashMap<>(); pageDescription.put(a.annotationType().getSimpleName(), annoDescription); try { introspectAnnotation(a, annoDescription); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { Exceptions.printStackTrace(ex); } if (annoDescription.size() == 1 && "value".equals(annoDescription.keySet().iterator().next())) { pageDescription.put(a.annotationType().getSimpleName(), annoDescription.values().iterator().next()); } } try { Page p = (Page) deps.getInstance(type); for (Object acteur : p.contents()) { Class<?> at = null; if (acteur instanceof Acteur.WrapperActeur) { at = ((WrapperActeur) acteur).type(); } else if (acteur instanceof Class<?>) { at = (Class<?>) acteur; } if (at != null) { Map<String, Object> callFlow = new HashMap<>(); for (Annotation a1 : at.getAnnotations()) { introspectAnnotation(a1, callFlow); } if (!className.equals(at.getSimpleName())) { if (!callFlow.isEmpty()) { pageDescription.put(at.getSimpleName(), callFlow); } } } else if (acteur instanceof Acteur) { Map<String, Object> callFlow = new HashMap<>(); for (Annotation a1 : acteur.getClass().getAnnotations()) { introspectAnnotation(a1, callFlow); } ((Acteur) acteur).describeYourself(callFlow); if (!callFlow.isEmpty()) { pageDescription.put(acteur.toString(), callFlow); } } } } catch (Exception e) { // A page may legitiimately be uninstantiable } } else if (o instanceof Page) { ((Page) o).describeYourself(m); } } return m; } /** * Add a subtype of Page which should be instantiated on demand when * responding to requests * * @param page A page */ protected final void add(Class<? extends Page> page) { if ((page.getModifiers() & Modifier.ABSTRACT) != 0) { throw new ConfigurationError(page + " is abstract"); } if (page.isLocalClass()) { throw new ConfigurationError(page + " is not a top-level class"); } if (!Page.class.isAssignableFrom(page)) { throw new ConfigurationError(page + " is not a subclass of " + Page.class.getName()); } assert checkConstructor(page); pages.add(page); } protected final void add(Page page) { pages.add(page); } static boolean checkConstructor(Class<?> type) { Checks.notNull("type", type); if (checkedTypes.contains(type.getName())) { return true; } boolean found = true; for (Constructor c : type.getDeclaredConstructors()) { if (c.getParameterTypes() == null || c.getParameterTypes().length == 0) { found = true; } @SuppressWarnings("unchecked") Inject inj = (Inject) c.getAnnotation(Inject.class); if (inj != null) { found = true; } } if (!found) { throw new ConfigurationError(type + " does not have an injectable constructor"); } checkedTypes.add(type.getName()); return true; } public String getName() { return name == null ? getClass().getSimpleName() : name; } /** * Add any custom headers or other attributes - override to intercept all * requests. * * @param event * @param page * @param action * @param response * @deprecated Use onBeforeSendResponse instead * @return */ @Deprecated protected HttpResponse decorateResponse(Event<?> event, Page page, Acteur action, HttpResponse response) { return response; } HttpResponse _decorateResponse(Event<?> event, Page page, Acteur action, HttpResponse response) { Headers.write(Headers.SERVER, getName(), response); Headers.write(Headers.DATE, new DateTime(), response); if (debug) { String pth = event instanceof HttpEvent ? ((HttpEvent) event).getPath().toString() : ""; Headers.write(Headers.stringHeader("X-Req-Path"), pth, response); Headers.write(Headers.stringHeader("X-Acteur"), action.getClass().getName(), response); Headers.write(Headers.stringHeader("X-Page"), page.getClass().getName(), response); } if (corsEnabled && !response.headers().contains(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN)) { Headers.write(Headers.ACCESS_CONTROL_ALLOW_ORIGIN, corsAllowOrigin, response); if (!response.headers().contains(HttpHeaders.Names.ACCESS_CONTROL_MAX_AGE)) { Headers.write(Headers.ACCESS_CONTROL_MAX_AGE, new Duration(corsMaxAgeMinutes), response); } } return decorateResponse(event, page, action, response); } /** * Create a 404 response * * @param event * @return */ protected HttpResponse createNotFoundResponse(Event<?> event) { ByteBuf buf = event.getChannel().alloc().buffer(90); String msg = "<html><head>" + "<title>Not Found</title></head><body><h1>Not Found</h1>" + event + " was not found\n<body></html>\n"; buf.writeBytes(msg.getBytes(charset)); DefaultFullHttpResponse resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND, buf); Headers.write(Headers.CONTENT_TYPE, MediaType.HTML_UTF_8.withCharset(charset), resp); Headers.write(Headers.CONTENT_LENGTH, (long) buf.writerIndex(), resp); Headers.write(Headers.CONTENT_LANGUAGE, Locale.ENGLISH, resp); Headers.write(Headers.CACHE_CONTROL, new CacheControl(CacheControlTypes.no_cache), resp); Headers.write(Headers.DATE, new DateTime(), resp); if (debug) { String pth = event instanceof HttpEvent ? ((HttpEvent) event).getPath().toString() : ""; Headers.write(Headers.custom("X-Req-Path"), pth, resp); } return resp; } /** * Override to do any post-response tasks * * @param id The incrementing ID of the request, for logging purposes * @param event The event, usually HttpEvent * @param acteur The final acteur in the chain * @param page The page which took responsibility for answering the request * @param state The state produced by the last acteur * @param status The status code for the HTTP response * @param resp The HTTP response, from Netty's HTTP codec */ protected void onAfterRespond(RequestID id, Event<?> event, Acteur acteur, Page page, State state, HttpResponseStatus status, HttpResponse resp) { } /** * Called before the response is sent RequestID * * @param id * @param event * @param status */ protected void onBeforeRespond(RequestID id, Event<?> event, HttpResponseStatus status) { logger.onRespond(id, event, status); } /** * Called before an event is processed * * @param id The request id * @param event The event */ protected void onBeforeEvent(RequestID id, Event<?> event) { logger.onBeforeEvent(id, event); } /** * Called when an error is encountered * * @param err */ @Benchmark(value = "uncaughtExceptions", publish = Kind.CALL_COUNT) final void internalOnError(Throwable err) { Checks.notNull("err", err); try { if (errorHandler != null) { errorHandler.onError(err); } } finally { onError(err); } } /** * Called when an exception is thrown * * @param err */ public void onError(Throwable err) { Checks.notNull("err", err); err.printStackTrace(System.err); } /** * Called when an event occurs * * @param event * @param channel * @return */ @Benchmark(value = "httpEvents", publish = Kind.CALL_COUNT) private CountDownLatch onEvent(final Event<?> event, final Channel channel) { //XXX get rid of channel param? // Create a new incremented id for this request final RequestID id = ids.next(); // Enter request scope with the id and the event try (QuietAutoCloseable cl = scope.enter(event, id)) { onBeforeEvent(id, event); return runner.onEvent(id, event, channel); } catch (Exception e) { internalOnError(e); CountDownLatch latch = new CountDownLatch(1); latch.countDown(); return latch; } } @SuppressWarnings({"unchecked", "ThrowableInstanceNotThrown", "ThrowableInstanceNeverThrown"}) Dependencies getDependencies() { if (deps == null) { try { new IllegalArgumentException("Initializing dependencies backwards. This instance was not created by Guice? " + this, stackTrace).printStackTrace(); deps = new Dependencies(SettingsBuilder.createDefault().buildMutableSettings(), new ServerModule(getClass())); deps.getInjector().getMembersInjector(Application.class).injectMembers(this); } catch (IOException ex) { throw new ConfigurationError(ex); } } return deps; } /** * Get the set of page instances, constructing them dynamically. Note that * this should be called inside the application scope, with any objects * which need to be available for injection available in the scope. * * @return An iterator */ @Override public Iterator<Page> iterator() { return iterators.iterable(pages, Page.class).iterator(); } @Inject private InstantiatingIterators iterators; protected void send404(RequestID id, Event<?> event, Channel channel) { HttpResponse response = createNotFoundResponse(event); onBeforeRespond(id, event, response.getStatus()); ChannelFutureListener closer = !ResponseImpl.isKeepAlive(event) ? ChannelFutureListener.CLOSE : null; ChannelFuture fut = channel.writeAndFlush(response); if (closer != null) { fut.addListener(closer); } } protected void onBeforeSendResponse(HttpResponseStatus status, Event<?> event, Response response, Acteur acteur, Page page) { // do nothing } }