package com.google.sitebricks; import com.google.common.collect.HashBiMap; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.inject.Binding; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Stage; import com.google.sitebricks.compiler.Compilers; import com.google.sitebricks.compiler.TemplateCompileException; import com.google.sitebricks.headless.Service; import com.google.sitebricks.rendering.Decorated; import com.google.sitebricks.rendering.EmbedAs; import com.google.sitebricks.rendering.Templates; import com.google.sitebricks.rendering.With; import com.google.sitebricks.rendering.control.WidgetRegistry; import com.google.sitebricks.rendering.resource.ResourcesService; import com.google.sitebricks.routing.PageBook; import com.google.sitebricks.routing.PageBook.Page; import com.google.sitebricks.routing.SystemMetrics; import java.lang.annotation.Annotation; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import static com.google.inject.matcher.Matchers.annotatedWith; import static com.google.sitebricks.SitebricksModule.BindingKind.ACTION; import static com.google.sitebricks.SitebricksModule.BindingKind.EMBEDDED; import static com.google.sitebricks.SitebricksModule.BindingKind.PAGE; import static com.google.sitebricks.SitebricksModule.BindingKind.SERVICE; import static com.google.sitebricks.SitebricksModule.BindingKind.STATIC_RESOURCE; /** * @author Dhanji R. Prasanna (dhanji@gmail.com) */ class ScanAndCompileBootstrapper implements Bootstrapper { private final PageBook pageBook; private final List<Package> packages; private final ResourcesService resourcesService; private final WidgetRegistry registry; private final SystemMetrics metrics; private final Compilers compilers; @Inject private final Templates templates = null; @Inject @Bricks private final List<SitebricksModule.LinkingBinder> bindings = null; @Inject @Bricks private final Map<String, Class<? extends Annotation>> methodMap = null; @Inject private final Stage currentStage = null; @Inject private final Injector injector = null; private final Logger log = Logger.getLogger(ScanAndCompileBootstrapper.class.getName()); @Inject public ScanAndCompileBootstrapper(PageBook pageBook, @Bricks List<Package> packages, ResourcesService resourcesService, WidgetRegistry registry, SystemMetrics metrics, Compilers compilers) { this.pageBook = pageBook; this.packages = packages; this.resourcesService = resourcesService; this.registry = registry; this.metrics = metrics; this.compilers = compilers; } public void start() { Set<Class<?>> set = Sets.newHashSet(); for (Package pkg : packages) { //look for any classes annotated with @At, @EmbedAs and @With set.addAll(Classes.matching( annotatedWith(At.class).or( annotatedWith(EmbedAs.class)).or( annotatedWith(With.class)).or( annotatedWith(Show.class)) ).in(pkg)); } //we need to scan all the pages first (do not collapse into the next loop) Set<PageBook.Page> pagesToCompile = scanPagesToCompile(set); collectBindings(bindings, pagesToCompile); extendedPages(pagesToCompile); // Compile templates for scanned classes (except in dev mode, where faster startup // time is more important and compiles are amortized across visits to each page). // TODO make this configurable separately to stage for GAE if (Stage.DEVELOPMENT != currentStage) { compilePages(pagesToCompile); } // Start all services. List<Binding<Aware>> bindings = injector.findBindingsByType(AWARE_TYPE); for (Binding<Aware> binding : bindings) { binding.getProvider().get().startup(); } //set application mode to started (now debug mechanics can kick in) metrics.activate(); } private void extendedPages(Set<Page> pagesToCompile) { Set<Page> pageExtensions = Sets.newHashSet(); for (Page page : pagesToCompile) { if (page.pageClass().isAnnotationPresent(Decorated.class)) { // recursively add extension pages analyseExtension(pageExtensions, page.pageClass()); } } pagesToCompile.addAll(pageExtensions); } //processes all explicit bindings, including static resources. private void collectBindings(List<SitebricksModule.LinkingBinder> bindings, Set<PageBook.Page> pagesToCompile) { // Reverse the method map for easy lookup of HTTP method annotations. Map<Class<? extends Annotation>, String> methodSet = null; //go thru bindings and obtain pages from them. for (SitebricksModule.LinkingBinder binding : bindings) { if (EMBEDDED == binding.bindingKind) { if (null == binding.embedAs) { // This can happen if embed() is not followed by an .as(..) throw new IllegalStateException("embed() missing .as() clause: " + binding.pageClass); } registry.addEmbed(binding.embedAs); pagesToCompile.add(pageBook.embedAs(binding.pageClass, binding.embedAs)); } else if (PAGE == binding.bindingKind) { pagesToCompile.add(pageBook.at(binding.uri, binding.pageClass)); } else if (STATIC_RESOURCE == binding.bindingKind) { //localize the resource to the SitebricksModule's package. resourcesService.add(SitebricksModule.class, binding.getResource()); } else if (SERVICE == binding.bindingKind) { pagesToCompile.add(pageBook.serviceAt(binding.uri, binding.pageClass)); } else if (ACTION == binding.bindingKind) { // Lazy create this inverse lookup map, once. if (null == methodSet) { methodSet = HashBiMap.create(methodMap).inverse(); } pageBook.at(binding.uri, binding.actionDescriptors, methodSet); } } } //goes through the set of scanned classes and builds pages out of them. private Set<PageBook.Page> scanPagesToCompile(Set<Class<?>> set) { Set<Templates.Descriptor> templates = Sets.newHashSet(); Set<PageBook.Page> pagesToCompile = Sets.newHashSet(); for (Class<?> pageClass : set) { EmbedAs embedAs = pageClass.getAnnotation(EmbedAs.class); if (null != embedAs) { final String embedName = embedAs.value(); //is this a text rendering or embedding-style widget? if (Renderable.class.isAssignableFrom(pageClass)) { @SuppressWarnings("unchecked") Class<? extends Renderable> renderable = (Class<? extends Renderable>) pageClass; registry.add(embedName, renderable); } else { pagesToCompile.add(embed(embedName, pageClass)); } } At at = pageClass.getAnnotation(At.class); if (null != at) { if (pageClass.isAnnotationPresent(Service.class)) { pagesToCompile.add(pageBook.serviceAt(at.value(), pageClass)); } else if (pageClass.isAnnotationPresent(Export.class)) { //localize the resource to the SitebricksModule's package. resourcesService.add(SitebricksModule.class, pageClass.getAnnotation(Export.class)); } else { pagesToCompile.add(pageBook.at(at.value(), pageClass)); } } if (pageClass.isAnnotationPresent(Show.class)) { // This has a template associated with it. templates.add(new Templates.Descriptor(pageClass, pageClass.getAnnotation(Show.class).value())); } } // Eagerly load all detected templates in production mode. if (Stage.DEVELOPMENT != currentStage) { this.templates.loadAll(templates); } return pagesToCompile; } private void analyseExtension(Set<PageBook.Page> pagesToCompile, final Class<?> extendClassArgument) { // store the page with a special page name used by ExtendWidget Class<?> extendClass = extendClassArgument; pagesToCompile.add(pageBook.decorate(extendClass)); // recursively analyse super class while (extendClass != Object.class) { extendClass = extendClass.getSuperclass(); if (extendClass.isAnnotationPresent(Decorated.class)) { analyseExtension(pagesToCompile, extendClass); } else if (extendClass.isAnnotationPresent(Show.class)) { // there is a @Show with no @Extension so this is the outer template return; } } throw new IllegalStateException("Could not find super class annotated with @Show on parent of class: " + extendClassArgument); } private void compilePages(Set<PageBook.Page> pagesToCompile) { final List<TemplateCompileException> failures = Lists.newArrayList(); //perform a compilation pass over all the pages and their templates for (PageBook.Page page : pagesToCompile) { Class<?> pageClass = page.pageClass(); // Headless web services need to be analyzed but not page-compiled. if (page.isHeadless()) { // TODO(dhanji): Feedback errors as return rather than throwing. compilers.analyze(pageClass); continue; } if (log.isLoggable(Level.FINEST)) { log.finest("Compiling template for page " + pageClass.getName()); } try { compilers.compilePage(page); compilers.analyze(pageClass); } catch (TemplateCompileException e) { failures.add(e); } } //log failures if any (we don't abort the app startup) if (!failures.isEmpty()) { logFailures(failures); } } private PageBook.Page embed(String embedAs, Class<?> page) { //store custom page wrapped as an embed widget registry.addEmbed(embedAs); //store argument name(s) wrapped as an Argument (multiple aliases allowed) if (page.isAnnotationPresent(With.class)) { for (String callWith : page.getAnnotation(With.class).value()) { registry.addArgument(callWith); } } //...add as an unbound (to URI) page return pageBook.embedAs(page, embedAs); } private void logFailures(List<TemplateCompileException> failures) { StringBuilder builder = new StringBuilder(); for (TemplateCompileException failure : failures) { builder.append(failure.getMessage()); builder.append("\n\n"); } log.severe(builder.toString()); } }