diff --git a/README.md b/README.md index 67bf329..4c64d53 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Our design goals are... * To provide the convenience of PHP-style save-and-refresh development in Java * To make web development simple for beginners by having simple, expressive APIs + * To provide core HTTP functionality while giving developers complete freedom in choosing + how to manage sessions, users, groups, auth, and templating. This framework was written for use in a course I taught at Rice University. @@ -13,7 +15,7 @@ This framework was written for use in a course I taught at Rice University. - Powerful type-safe HTTP abstractions - Built-in annotation-based request routing (w/ wildcards and parameters) - - Built-in templating with Freemarker + - Built-in templating (with Freemarker by default, but extendable to any engine) - Built-in email support (via SMTP) - Built-in SSL support (w/ option to redirect insecure requests) - Built-in MySQL support (w/ connection pooling, transactions) @@ -602,6 +604,18 @@ By default, Lightning will attempt to utilize the MySQL drivers if you have conf TODO: MORE FLEXIBLE OPTIONS COMING SOON! +### Templates + +By default, Lightning will use FreeMarker templates (via `lightning.templates.FreeMarkerTemplateEngine`). + +If you wish to use a different template system, you must: + 1. Implement your own engine (`lightning.templates.TemplateEngine`) OR use an alternate engine provided by Lightning. + 2. Pass your engine to `lightning.Lightning::launch` by configuring dependency-injection on an instance of your engine + for the class `TemplateEngine.class`. + +Lightning currently ships with the following template engines: + - `lightning.templates.FreeMarkerTemplateEngine` (DEFAULT) + ### Debug Mode Lightning features a powerful debug mode. Debug mode can be enabled by setting `lightning.config.Config`'s `enableDebugMode` property. diff --git a/src/main/java/lightning/mvc/HandlerContext.java b/src/main/java/lightning/mvc/HandlerContext.java index 6f93665..cb77c86 100644 --- a/src/main/java/lightning/mvc/HandlerContext.java +++ b/src/main/java/lightning/mvc/HandlerContext.java @@ -50,6 +50,7 @@ import lightning.sessions.Session; import lightning.sessions.Session.SessionException; import lightning.sessions.drivers.MySQLSessionDriver; +import lightning.templates.TemplateEngine; import lightning.users.User; import lightning.users.Users; import lightning.users.Users.UsersException; @@ -65,8 +66,6 @@ import com.google.gson.JsonIOException; import com.google.gson.JsonSyntaxException; -import freemarker.template.Configuration; - /** * A controller is a class that is used to process a single HTTP request and should be sub-classed. * Each controller lives only on a single thread to service a single request. Instances are @@ -78,7 +77,7 @@ public class HandlerContext implements AutoCloseable, MySQLDatabaseProvider { public final Request request; public final Response response; public final Config config; - public final Configuration templateEngine; + public final TemplateEngine templateEngine; public final SecureCookieManager cookies; public final URLGenerator url; public final Validator validator; @@ -96,7 +95,7 @@ public class HandlerContext implements AutoCloseable, MySQLDatabaseProvider { // TODO(mschurr): Add a user property, implement a proxy for it. // TODO(mschurr): Add a memory cache accessor, and implement the API for it. - public HandlerContext(Request rq, Response re, MySQLDatabaseProvider dbp, Config c, Configuration te, FileServer fs, @Nullable Mailer mailer) { + public HandlerContext(Request rq, Response re, MySQLDatabaseProvider dbp, Config c, TemplateEngine te, FileServer fs, @Nullable Mailer mailer) { isClosed = false; this.request = rq; this.response = re; @@ -767,7 +766,7 @@ public final ModelAndView modelAndView(String viewName, Object viewModel) { */ public final String renderToString(String viewName, Object viewModel) throws Exception { StringWriter stringWriter = new StringWriter(); - templateEngine.getTemplate(viewName).process(viewModel, stringWriter); + templateEngine.render(viewName, viewModel, stringWriter); return stringWriter.toString(); } @@ -786,7 +785,7 @@ public final String renderToString(ModelAndView modelAndView) throws Exception { * @throws Exception */ public final void render(String viewName, Object viewModel) throws Exception { - templateEngine.getTemplate(viewName).process(viewModel, response.raw().getWriter()); + templateEngine.render(viewName, viewModel, response.raw().getWriter()); } /** @@ -795,7 +794,7 @@ public final void render(String viewName, Object viewModel) throws Exception { */ public final void render(ModelAndView modelAndView) throws Exception { response.header(HTTPHeader.CONTENT_TYPE, "text/html; charset=UTF-8"); - templateEngine.getTemplate(modelAndView.viewName).process(modelAndView.viewModel, response.raw().getWriter()); + templateEngine.render(modelAndView.viewName, modelAndView.viewModel, response.raw().getWriter()); } /** diff --git a/src/main/java/lightning/server/Context.java b/src/main/java/lightning/server/Context.java index 2bbf963..667103b 100644 --- a/src/main/java/lightning/server/Context.java +++ b/src/main/java/lightning/server/Context.java @@ -38,6 +38,7 @@ import lightning.mvc.Validator.FieldValidator; import lightning.sessions.Session; import lightning.sessions.Session.SessionException; +import lightning.templates.TemplateEngine; import lightning.users.User; import lightning.users.Users; import lightning.users.Users.UsersException; @@ -46,8 +47,6 @@ import com.google.gson.JsonIOException; import com.google.gson.JsonSyntaxException; -import freemarker.template.Configuration; - /** * Provides a thread-specific context for incoming requests. * Controllers will want to import static Context.* and use these methods. @@ -109,7 +108,7 @@ public static final Config config() { return context().config; } - public static final Configuration templateEngine() { + public static final TemplateEngine templateEngine() { return context().templateEngine; } diff --git a/src/main/java/lightning/server/LightningHandler.java b/src/main/java/lightning/server/LightningHandler.java index 0ef8dc4..5ae46a0 100644 --- a/src/main/java/lightning/server/LightningHandler.java +++ b/src/main/java/lightning/server/LightningHandler.java @@ -15,9 +15,9 @@ import lightning.Lightning; import lightning.ann.Before; import lightning.ann.Befores; +import lightning.ann.ExceptionHandler; import lightning.ann.Filter; import lightning.ann.Filters; -import lightning.ann.ExceptionHandler; import lightning.ann.Json; import lightning.ann.Multipart; import lightning.ann.RequireAuth; @@ -68,9 +68,10 @@ import lightning.scanner.ScanResult; import lightning.scanner.Scanner; import lightning.sessions.Session; +import lightning.templates.FreeMarkerTemplateEngine; +import lightning.templates.TemplateEngine; import lightning.users.User; import lightning.users.Users; -import lightning.util.Iterables; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.util.MultiException; @@ -81,7 +82,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import freemarker.template.Configuration; @@ -99,7 +99,7 @@ public class LightningHandler extends AbstractHandler { private Config config; private MySQLDatabaseProvider dbp; - private Configuration userTemplateConfig; + private TemplateEngine userTemplateConfig; private Configuration internalTemplateConfig; private ExceptionMapper exceptionHandlers; private Scanner scanner; @@ -128,23 +128,11 @@ public LightningHandler(Config config, MySQLDatabaseProvider dbp, InjectorModule TemplateExceptionHandler.HTML_DEBUG_HANDLER :*/ TemplateExceptionHandler.RETHROW_HANDLER); - this.userTemplateConfig = new Configuration(FREEMARKER_VERSION); - this.userTemplateConfig.setSharedVariable("__LIGHTNING_DEV", config.enableDebugMode); - if (config.server.templateFilesPath != null) { - File templatePath = Iterables.firstOr(Iterables.filter(ImmutableList.of( - new File("./src/main/java/" + config.server.templateFilesPath), - new File("./src/main/resources/" + config.server.templateFilesPath) - ), f -> f.exists()), new File(config.server.templateFilesPath)); - if (templatePath.exists() && config.enableDebugMode) { - this.userTemplateConfig.setDirectoryForTemplateLoading(templatePath); - } else { - this.userTemplateConfig.setClassForTemplateLoading(getClass(), "/" + config.server.templateFilesPath); - } + userTemplateConfig = userModule.getBindingForClass(TemplateEngine.class); + if (userTemplateConfig == null) { + // Use the default template engine. + userTemplateConfig = new FreeMarkerTemplateEngine(config); } - this.userTemplateConfig.setShowErrorTips(config.enableDebugMode); - this.userTemplateConfig.setTemplateExceptionHandler(/*config.enableDebugMode ? - TemplateExceptionHandler.HTML_DEBUG_HANDLER :*/ - TemplateExceptionHandler.RETHROW_HANDLER); this.exceptionHandlers = new ExceptionMapper<>(); this.scanner = new Scanner(config.autoReloadPrefixes, config.scanPrefixes, config.enableDebugMode); @@ -263,7 +251,6 @@ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, if (config.enableDebugMode) { rescan(); - userTemplateConfig.clearTemplateCache(); internalTemplateConfig.clearTemplateCache(); } diff --git a/src/main/java/lightning/templates/FreeMarkerTemplateEngine.java b/src/main/java/lightning/templates/FreeMarkerTemplateEngine.java new file mode 100644 index 0000000..f1ac5fe --- /dev/null +++ b/src/main/java/lightning/templates/FreeMarkerTemplateEngine.java @@ -0,0 +1,49 @@ +package lightning.templates; + +import java.io.File; +import java.io.Writer; + +import lightning.config.Config; +import lightning.util.Iterables; + +import com.google.common.collect.ImmutableList; + +import freemarker.template.Configuration; +import freemarker.template.TemplateExceptionHandler; +import freemarker.template.Version; + +public final class FreeMarkerTemplateEngine implements TemplateEngine { + private static final Version FREEMARKER_VERSION = new Version(2, 3, 20); + private final Config config; + private final Configuration configuration; + + public FreeMarkerTemplateEngine(Config config) throws Exception { + this.config = config; + this.configuration = new Configuration(FREEMARKER_VERSION); + this.configuration.setSharedVariable("__LIGHTNING_DEV", config.enableDebugMode); + this.configuration.setShowErrorTips(config.enableDebugMode); + this.configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + + if (config.server.templateFilesPath != null) { + File templatePath = Iterables.firstOr(Iterables.filter(ImmutableList.of( + new File("./src/main/java/" + config.server.templateFilesPath), + new File("./src/main/resources/" + config.server.templateFilesPath) + ), f -> f.exists()), new File(config.server.templateFilesPath)); + + if (templatePath.exists() && config.enableDebugMode) { + this.configuration.setDirectoryForTemplateLoading(templatePath); + } else { + this.configuration.setClassForTemplateLoading(getClass(), "/" + config.server.templateFilesPath); + } + } + } + + @Override + public void render(String templateName, Object viewModel, Writer outputStream) throws Exception { + if (config.enableDebugMode) { + configuration.clearTemplateCache(); + } + + configuration.getTemplate(templateName).process(viewModel, outputStream); + } +} diff --git a/src/main/java/lightning/templates/TemplateEngine.java b/src/main/java/lightning/templates/TemplateEngine.java new file mode 100644 index 0000000..26b023c --- /dev/null +++ b/src/main/java/lightning/templates/TemplateEngine.java @@ -0,0 +1,25 @@ +package lightning.templates; + +import java.io.Writer; + +/** + * An abstract interface for rendering templates. + * + * Implementors must respect the semantics of debug mode. In particular: + * - render(...) should not perform ANY caching in debug mode + * - Templates should be loaded from the FILESYSTEM in debug mode (by searching for the + * templateFilesPath in ./src/main/java, ./src/main/resources, and ./. + * - Templates should be loaded from the CLASSPATH in production mode. + * + * For reference implementation, see the provided FreeMarkerTemplateEngine. + */ +public interface TemplateEngine { + /** + * Renders the template with given name and view model to the given output stream. + * @param templateName A template file name. + * @param viewModel A view model. + * @param outputStream A stream to write output to. + * @throws Exception On failure. + */ + public void render(String templateName, Object viewModel, Writer outputStream) throws Exception; +}