Silken is the easiest way to drop in Google Closure Templates into your Java web application.
Silken wraps Google Closure Templates (Soy Templates) in a managed servlet environment simplifying template use in push-MVC environments. Silken encourages convention over configuration, and promotes a set standard structure for template management.
Google Closure Templates (aka Soy Templates) is a fantastic language neutral templating system. It has an advanced syntax, great localization support, enforces good practice such as parameter documentation, and allows the same templates to be used on both client and server.
However...
Google Closure Templates does not provide a clear set of standards for organizing and structuring your project. For example it provides little guidance on managing namepsaces, shared resource, file naming conventions, caching and methods of integrating with existing tools. Silken wraps Google Closure Templates in a nice consumable form.
Silken was initially developed to compliment the HtmlEasy project however is not tied to HtmlEasy in any way and can be used from any push-MVC environment/framework or your own Servlet controller code.
Silken is a zero-dependency JAR (other than Closure Templates of course) and can be quickly integrated into a new or existing Java web project via a simple servlet mapping. See the installation section to find out more about manual setup or using via Maven/Ivy.
- Loose Coupling: Clear separation between controller code, models and template rendering (views).
- Simple API: Render soy templates from your controller code with a simple Servlet forward/dispatch.
- Convention over Configuration: Clear conventions for file placement and naming conventions.
- Managed namespaces: Both isolated and shared namespaces.
- Smart Caching: Compiled templates are cached while JVM memory is available.
- Auto Publish as JavaScript: Expose selected templates as JavaScript (i.e. use the same templates on the client as on the server).
- Edit->Refresh Development: Turn off caching to speed on-the-fly template editing.
- Runtime Management: Precompile templates on startup, and flush caches or recompile remotely via management URLs.
- Globals: Conventions for defining both compile-time and run-time globals.
- Translation: Conventions for message bundle/file management.
- Simple Setup: Simple Servlet configuration. Optional Maven/Ivy support.
Before reading this section you should already be familiar with Google Closure Template's documentation and the concept of template namespaces.
Silken is a simple servlet and should be deployed on a /soy
context (or
equivalent). After adding the required dependencies to your project (see
installing), add the following to your web.xml
file:
<servlet>
<servlet-name>soy</servlet-name>
<servlet-class>com.papercut.silken.SilkenServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>soy</servlet-name>
<url-pattern>/soy/*</url-pattern>
</servlet-mapping>
Note: Although any URL prefix is supported, /soy
is recommended and the
remainder of the documentation assumes the servlet is hosted here.
Silken expects your soy templates to exist either under your web app root in
/templates
or /WEB-INF/templates
, or as resources on the classpath.
All soy templates have a namespace and just like a Java Class file, should be
stored under a directory structure matching the namespace. There is a special
namespace called shared
. By default a template can call all other
templates in its own namespace as well as templates contained in the
shared
namespace.
For example your project may look like:
templates/shared/footer.soy
templates/shared/header.soy
templates/products/summaryViews.soy
templates/products/boats/boats.soy
templates/products/boats/powerBoatView.soy
templates/products/boats/sailingBoatView.soy
templates/users/signup.soy
templates/users/cart.soy
templates/com/myorg/fully/qualified/misc.soy
Alternatively you may choose to put your *.soy
files as a resource on the
classpath in the corresponding namespace. This approach allows you to locate
*.soy
files relative to its supporting server-side code. The merits of
classpath vs. web root is a personal choice - Silken will search both
locations. When in doubt, choose the web root - soy files contain
presentation/views only, and the choice of web root makes this distinction
clear, and the location is analogous to *.jsp
files.
Unlike Classes in *.java
files, each *.soy
file may contain more
than one template. The suggest practice is to place large templates (e.g.
complete pages) in their own file, while smaller related templates (e.g. "a
partial view" or section templates) in a single file together.
Example: templates/products/summaryViews.soy
{namespace products}
/**
* Product template with stock remaining.
* @param productName The product name.
* @param qty The quantity remaining as an int.
*/
{template .remaining}
<tr>
<td>{$productName}</td>
{if $qty == 0}
<td class="nostock">{$qty}</td>
{else}
<td>{$qty}</td>
{/if}
</tr>
Templates are rendered by forwarding (dispatching) the request from your controller code across to the Silken Servlet. The target/forwarded URL will be in the format:
/soy/[namespace.templateName]
For example:
/soy/products.boats.sailingBoatView
/soy/com.myorg.fully.qualified.logoutPage
The general approach is as follows:
- The requst/browser hits your controller code (e.g. servlet code, or an MVC framework like HtmlEasy, or SpringMVC ).
- Your controller code generates the data parameters that will be consumed by
the template by constructing a "model". e.g. it may query a database
and make a
Boat()
POJO/Bean class. - The model is set as a Servlet Request Attribute under a name
model
. - Finally, the controller code forwards/dispatches the request across to Silken and the template is rendered (returned to the browser).
Data parameters (the model) are passed to the templates via a request attribute. The model may either be:
- A map of key-value pairs (
Map<String, ?>
) - A [POJO](http://en.wikipedia.org/wiki/Plain_Old_Java_Object] or Bean (nested POJOs are supported - see Referencing Model Data below)
- An instance of SoyMapData (if you wish to couple your controller logic with Soy)
Examples:
Using a simple servlet:
public class SimpleBoatServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String sailNumber = req.getParameter("sailNumber");
// Validate and fetch the SailBoat POJO from data store
// ...
SailBoat boatPojo = datastore.fetchSailingBoat(sailNumber);
// Push the model into the request attribute under key "model".
req.setAttribute("model", boatPojo);
// Forward across to Silken - the path denotes the template to render.
RequestDispatcher rd = getServletContext().getRequestDispatcher("/soy/products.boat.sailingBoatView");
rd.forward(req, resp);
}
}
Using HtmlEasy (recommended) in a nice-URL JAX-RS RESTful style:
public class BoatDisplayController {
@Path("/boats/sailing/{sailNumber}")
@View("/soy/products.boat.sailingBoatView")
public SailBoat showSailingBoat(@PathParam("sailNumber") String sailNumber) {
// Validate and fetch the SailBoat POJO from data store
// ...
return datastore.fetchSailingBoat(sailNumber);
}
}
In the example above the model (data passed to the template) is a POJO/Bean. The template parameters are populated with data from the matching POJO getter names.
For example the template:
{namespace products.boat}
/**
* Page template for sail boats.
* @param name The boat's name.
* @param lengthOverAll The boat's LOA.
*/
{template .sailingBoatView}
{call shared.header /}
<h1>Boat Details</h1>
<table>
<tr>
<td>Name:</td>
<td>{$name}</td>
</tr>
<tr>
<td>Length Over All (LOA):</td>
<td>{$lengthOverAll}m</td>
</tr>
</table>
{call shared.footer /}
{/template}
Could be rendered my passing an instance of the POJO:
class SailBoat {
private String name;
private int lengthOverAll;
public String getName() {
return name;
}
public int getLengthOverAll() {
return lengthOverAll;
}
// Setters ...
}
Or by passing a Map<String,?>
like:
ImmutableMap.of("name", "Australia II", "lengthOverAll", 19);
Note: There is no relationship between the dispatch URL used to render a
template, and the name or path of the *.soy
file. Although, like with
public Classes and *.java
files, there is often a one-to-one relationship
between a template and a *.soy
file, this is not always the case.
Multiple templates can exist in the one *.soy
file.
On large projects you may need to consider translation/localization of messages, global variables (both run time and compile time) and pre-compiling your key templates on startup. These advanced features and others are discussed in detail below.
Silken enhances Soy by supporting POJOs (aka simple Java Beans) as model data.
POJOs are automatically converted to Maps
before being passed to the
template. POJOs may also be nested (referenced or as List
elements).
For example, model data constructed like:
Employee manager = new Employee()
manager.setFirstName("Mary");
Employee projectLead = new Employee();
projectLead.setFirstName("John");
projectLead.setNickNames(Lists.newArrayList("Johnny", "Jack", "Johno"));
projectLead.setManager(manager);
Map<String, ?> model = ImmutableMap.of(
"project", "Project X",
"lead", projectLead);
can be accessed with template syntax:
Project: {$project}
Lead: {$lead.firstName}
Lead's Manager: {$lead.manager.firstName}
Lead is also known as:
{foreach $nick in $lead.nickNames}
{$nick},
{/foreach})
See Soy Expressions for more information supported types and how to reference deep/nested data and list elements.
One of Google Closure Templates most powerful features is it's first-class support for message translations. Silken offers a set of conventions to help with message file management. Message files should confirm to the following conventions:
-
Confirm to the file nameing convention
*.[JavaLocaleString].xlf
(e.g.messages.pt_BR.xlf
,messages.fr_FR.xlf
) -
Reside in the same namespace (directory) as the corresponding templates. Templates only have access to message files within its own namespace and the
shared
namespace.
For example:
templates/shared/footer.soy
templates/shared/header.soy
templates/shared/messages.pt_BR.xlf
templates/shared/messages.de.xlf
templates/products/summaryViews.soy
templates/products/messages.fr_FR.xlf
templates/products/messages.de_DE.xlf
Again a single messages file per namespace/locale is a recommendation only.
Silken will endeavor to source all *.[locale].xlf
files located within
the namespace. If it can't match a file using the full language_COUNTRY
format it will revert to searching at the wider language-only level
(e.g. pt_BR will match messages-pt.xlf
if message-pt_BR.xlf
does
not exist).
By default, Silken selects the locale based on the Accept-Language header.
ServletRequest.getLocale().
You can change this behaviour by pointing the localeResolver
servlet init
parameter to a new class that implements
com.papercut.silken.LocaleResolver
. (See Advanced Servlet configuration
options below.)
Silken exposes the following management URLs that can assist with development and debugging:
/soy/_precompile/[namespace]
-
Pre-compiles all templates in the given [namespace] and returns 200 OK on success.
/soy/_flush/[namespace]
-
Flushes any cached compiled templates in the given [namespace] forcing a recompile on next access.
/soy/_flushAll
-
Flushes all cached compiled templates from all namespaces.
Compile-time global variables are compiled into the template on first use and always remain fixed. Reasons for using compile-time globals include:
- A deployment version number
- A domain name (e.g. for absolute paths)
- A variable for cache busting assets
By default globals are sourced from a *.globals
file located in the
"shared" namespace. This file should be in key = value
format as
outlined in the Closure Templates
documentation
. You may define globals in code by setting the
compileTimeGlobalsProvider
servlet init parameter to the fully qualified
name of your class that implements com.papercut.silken.CompileTimeGlobalsProvider
.
Run-time globals are available as Soy
Injected Data
($ij.foo
).
Reasons for using run-time globals include:
- Passing in a user name so it's available in the header on every page.
- Session data that may be useful across many pages.
Runtime globals can be set in one of two ways:
- By setting servlet request attribute under the key "globals".
- In code by implementing
com.papercut.silken.RuntimeGlobalsResolver
.
A logical place to set your runtime globals would in a Servlet Filter or a JAX-RS PreProcessInterceptors.
public class SetGlobalsFilter extends Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// All templates need access to the logged in user ($ij.username)
String username = request.getUserPrincipal().getName()
// Let's make our user agent a global as well.
String userAgent = request.getHeader("User-Agent");
Map<String, ?> globals = ImmutableMap.of("username", username,
"userAgent", userAgent);
request.setAttribute("globals", globals);
}
}
Runtime globals can also be defined in code by implementing
com.papercut.silken.RuntimeGlobalsResolver
. This interface gives you
access to the HTTPServletRequest
. To define an implementation, set the
runtimeGlobalsResolver
servlet init parameter to a fully qualified name
of your class that implements com.papercut.silken.RuntimeGlobalsResolver
.
It is also possible to construct and inject an alternate GlobalsResolver
at runtime. See Injecting Configuration and Resolvers at Runtime below.
Closure Templates offer a unique advantage where the same template language can
be used on both the client and the server. Advanced web development
technologies such as ajax, push state, and pjax make the re-use of the same
template on both the client and the server very attractive. Templates defined
in a file with a js.soy
extension (as apposed to just soy
are
published as compiled JavaScript so they can included/consumed as a JavaScript
resources on the client. The URL to request the compiled templates (templates
in js.soy
files) for a given namespace is:
/soy/js/[serial]/[locale]/[namespace].js
Where:
[serial]
- a mandatory component that can be used for cache busting. For example this may be a date or version number that increments every time a new version is deployed.[locale]
- an optional component that denotes the locale (in Java string format like pt_BR, en, de, etc.) used to compile the template. If [locale] is not defined, the locale is selected using the accept-header or as implemented by thelocaleResolver
(see below).[namespace]
- the namespace. All templates defined injs.soy
files will be rendered into the request.
An example URL: http://myserver.com/soy/js/20120108/de/myproject.mytemplates.js
Note: JavaScript files are served up with a cache-control header setting the
cache time to 30-days. Using the cache busting [serial]
is the best way
to ensure browsers always pick up the latest version of your templates.
To speed up template development and editing, Silken may be run with caching disabled ensuring templates are recompiled on every request. Caching may be disabled with one of two ways:
- By setting a system variable
silken.disableCaching
For example, by adding-Dsilken.disableCaching
as a VM argument in your IDE launcher. - By setting the servlet init-parameter
disableCaching
.
Note: For obvious reasons, it's not a good idea to run in this mode in production!
Silken is a single zero-dependency JAR (other than Closure Templates of course) and can be quickly integrated into a new or existing Java web project via a servlet mapping. To install:
Add the
soy-[version].jar
and the silken-[version].jar
file onto your project's class path. The
latest version of Silken is:
Silken (and its dependency Google Closure Templates) are hosted in a Maven repository.
Repository:
<repository>
<id>codedance on Github</id>
<url>https://github.com/codedance/maven-repository/raw/master</url>
</repository>
Artifact:
<groupId>com.papercut.silken</groupId>
<artifactId>silken</artifactId>
<version>2013-03-05</version>
Note: Please check the repository for the latest version ID.
Silken has the following servlet init parameters to support advanced
configuration. They are usually defined in your web.xml
file as follows,
however may also be set in code.
<servlet>
<servlet-name>soy</servlet-name>
<servlet-class>com.papercut.silken.SilkenServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>sharedNamespaces </param-name>
<param-value>share,com.myapp.shared</param-value>
</init-param>
<init-param>
<param-name>precompileNamespaces</param-name>
<param-value>com.myapp.home,com.myapp.settings</param-value>
</init-param>
</servlet>
showStackTracesInErrors
- Set to "true" to show stack traces in the
browser/response. Default: false
disableCaching
- Set to "true" to turn off caching. Helps when authoring
templates (i.e. live refresh). Default: false
sharedNamespaces
- A comma separated list of namespaces shared
(available) to all templates. Default: shared
localeResolver
- Customize the locale resolver. Set to a fully qualified
class name pointing to an implementation of LocaleResolver. Default:
AcceptHeaderLocaleResolver
modelResolver
- Customize the model resolver. Set to a fully qualified
class name pointing to an implementation of ModelResolver
. Default:
RequestAttributeModelResolver
fileSetResolver
- Customize the model resolver. Set to a fully qualified
class name pointing to an implementation of FileSetResolver
. Default:
WebAppFileSetResolver
compileTimeGlobalsProvider
- Provide a custom map of Soy Template compile
time globals. Default: none
runtimeGlobalsResolver
- Provide a custom map of runtime globals passed
into every template render. Default: none
precompileNamespaces
- a comma separated list of namespaces to
pre-compile.
searchPath
- Advanced: Modify the default search path used to locate
*.soy
and associated files. Value is a colon separated path that may
contain/reference $CLASSPATH
and $WEBROOT
. Default:
$CLASSPATH:$WEBROOT/templates:$WEBROOT/WEB-INF/templates
In addition to using Servlet Init Parameters, configuration can be modified in
code at runtime. This includes modifying config options and also injecting
alternate provider implementation. To access the configuration at runtime,
grab a reference to the Silken Config class via the silken.config
servlet
context attribute. Example code:
import com.papercut.silken.Config;
// ...
Config config = (Config) getServletContext().getAttribute("silken.config");
config.setLocaleResolver(new MyLocaleResolver());
A logical place to perform this initalization would be in a startup servlet's
init method. It is also possible to access the Config
class using the
convenience method: SilkenServlet.getConfig()
.
It's possible for non-servlet request code to access Silken's template
rendering service. For example you may have a service that generates template
emails. By using Silken's TemplateRenderer
service, your template email
generation code can benefit from the same template structure, caching layer,
etc. Example usage:
Map<String, String> model = ImmutableMap.of("username", username
"password", password);
TemplateRenderer renderer = (TemplateRenderer) context.getAttribute("silken.templateRenderer");
String emailText = renderer.render("email.lostPassword", model);
myEmailService.sendEmail(emailAddress, emailText);
It is also possible to access the TemplateRenderer
using the convenience
method: SilkenServlet.getTemplateRenderer()
.
Silken will run in any standard Servlet hosting environment, including Google App Engine. Silken is written to work with the latest version of Soy Templates.
To get a quick fee for Silken it is recommended that you download and play with the Htmleasy Playground Project. This is a pre-assembled Google App Engine Eclipse project demonstrating Htmleasy and includes some Silken examples.
Q: What is the difference between Plovr and Silken?
A: Polvr dynamtically compiles JavaScript. Silken is designed to assist with the development of both the server-side Java and client-side JavaScript Closure templates.
Silken's development is supported by PaperCut Software (makers of print management software) and is use in production. It's been actively developed. If you have any ideas for features please submit them as issues. A few ideas:
- An implicit or explicit import system so it's easy to define dependencies between namespaces.
- Options to define deligate templates (via a resolver).
- Maybe a way of publishing multiple namespaces into one JavaScript file.
- Lock down management URLs to set client IPs.
Google Closure Templates is also referred to as Soy Templates. You'll find references to Soy and Tofu throughout the project's class names. Silken is a type of smooth fine Tofu.
2011-12-20
- Initial public release.
2012-01-05
- BUGFIX: Explicitly set the output character encoding to UTF-8.
2012-02-23
- Nested POJO support. Globals are now set as
$ij
Injected Date.
2012-02-27
- Runtime globals are now no longer also put into the Model. They are only
available via
$ij
Injected Data. - Improved documentation of injecting provider implementations at runtime.
- Fixed at potential NPE that may occur if the model is null and a custom runtime globals provider is implemented.
2012-03-04
- Global
$ij
data may now be defined by setting a request attribute "globals". - Experimental: The TemplateRenderer is now set as a servlet context attribute under "silken.templateRenderer". This allow other code to potentially use the template rendering layer in a raw form from outside web request code.
- Renamed RuntimeGlobalsProvider to RuntimeGlobalsResolver to make it consistent with the other request based resolvers. Sites using a custom implementation will need to rename their class.
- A few changes to areas of code that may not have been thread-safe.
2012-05-14
- Fixed a potential thread-safety issue that may occur when
running with
disableCaching
set to true (i.e. development mode).
2013-03-05
- Updated POM dependencies to bring in latest Closure/Soy version.
- Code changes to leverage later version of Guava (e.g. Loading Cache)
- Minor performance work: POJOs are now parsed using java.beans.Introspector to benefit from its caching.
- Documentation improvements
(c) Copyright 2011-2013 PaperCut Software Int. Pty. Ltd. http://www.papercut.com/
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.