Introduction

Implementation of Spring's MVC View Resolver using Google Closure (soy)

It is highly customizable via pluggable interfaces, for which a default implementation is provided. It distinguishes itself from other frameworks, which are often "black boxes" and do not allow easy customisation. It was created due to specific requirements, which no other framework at that time could provide.

Features

Google Closure Soy

Google Closure Soy is an implementation of logic-less templates by Google. Templates can be used on the server side and on the client side using soy to JavaScript compiler.

Example:
{namespace soy.example}

/**
 * @param words The words to display
 */
{template .clientWords}
  <ul>
    {foreach $word in $words}
      <li>{$word}</li>
    {/foreach}
  </ul>
{/template}

User's Guide

To use Spring-Soy View in "white-box" mode one has to manually wire beans. Conversely to use library in "back-box" mode all you have to do is to import pertinent @Configuration objects with bean definitions.

First we will look at "black-box" configuration as it is easier to begin with. Available @Configuration in library itself:

It is important to notice that if you decide to import SpringSoyViewAjaxConfig then you will automatically import SpringSoyViewBaseConfig since there is a dependency on it. It makes sense to use SpringSoyViewAjaxConfig only if you are interested in ajax compiler, which is effectively a spring controller compiles soy templates to javascript at runtime.

Example import usage: (Black-Box Mode)

@Configuration
@Import(SpringSoyViewAjaxConfig.class)
@PropertySource("classpath:spring-soy-view-example.properties")
@EnableWebMvc
@ComponentScan(basePackages = {"pl.matisoft.soy.example"})
public class SoyConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(final ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
    }

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    @Bean
    @Primary
    public ViewResolver contentNegotiatingViewResolver(final ViewResolver soyViewResolver) throws Exception {
        final ContentNegotiatingViewResolver contentNegotiatingViewResolver = new ContentNegotiatingViewResolver();
        contentNegotiatingViewResolver.setViewResolvers(Lists.newArrayList(soyViewResolver));
        contentNegotiatingViewResolver.setDefaultViews(Lists.<View>newArrayList(new MappingJacksonJsonView()));

        return contentNegotiatingViewResolver;
    }

}

Configurable Parameters (Black-Box Mode) - SpringSoyViewBaseConfig - core

Configurable Parameters (Black-Box Mode) - SpringSoyViewBaseConfig - ajax

As you can see, we are using @Bean @Primary annotation to override the implementation which is imported from the file. In case of negotiatingViewResolver, we need to do that because ViewResolver is already provided within a SpringSoyViewAjaxConfig inside a library. The ViewResolver which is passed as a function argument is in fact a soyViewResolver thanks to Spring dependency resolution.

In white-box mode the configuration may seem to be verbose but the advantage is that a developer is in full control which beans get instiantiated and wired up together. It is recommended that beginner users start with black-box approach and progress to white-box approach if they wish to be in full control over instantiated beans.

Available maven modules

Maven configuration

All artefacts have been pushed to maven central repository:

Maven's pom.xml


        <dependency>
            <groupId>pl.matisoft</groupId>
            <artifactId>spring-soy-view</artifactId>
            <version>1.25.6</version>
        </dependency>

        <dependency>
            <groupId>pl.matisoft</groupId>
            <artifactId>spring-soy-view-ajax-compiler</artifactId>
            <version>1.25.6</version>
        </dependency>

XML Configuration Example (White-Box Mode)

To use spring-soy-view via XML in Spring it is recommended to take this template as an example and adjust it accordingly. This example is taken from a real project and adjusted for the purposes of this guide.

Gist: spring-soy-view.xml (updated for: version 1.25.1 )

Java Bean Config Example (White-Box Mode)

In the same way it is possible to use XML wiring a better and more modern way to include soy-spring-view library is to use @Configuration annotations from spring.

Gist: SoyConfiguration.java (updated for: version 1.25.1 )

Templates location

By default a DefaultTemplatesFileResolver will look for *.soy templates stored inside /WEB-INF/templates folder, it will recursively resolve them, the location of files, an extension can be configured. At any given point in time it is also possible provide own implementation of TemplatesFileResolver if a default one doesn't meet our needs.

Usage from Spring's Controller

To render templates, one has to use by default (can be customized) a special prefix, i.e. "soy:" followed by a logical template name.

This can be best illustrated by an example:

    @RequestMapping(value="/server-time")
    public String getServerTime(final Model model) {
        return "soy:soy.example.serverTime";
    }

As indicated in the above example, this will yield rendering of a template defined in soy.example.serverTime, mind you that it is a logical template name and it doesn't matter in which file it is stored as long as the file is resolveable by a TemplatesFileResolver. In the mentioned example a string with a template name is returned, as it is normal in spring mvc world that when a method returns a string, it indicates a path to a template.

Spring's Template resolvers chaining

In Spring MVC, templates resolvers can be chained in a sense of Chain of Responsibility pattern (GoF), this is the reason a "soy:" prefix for a template name was introduced, when SoyTemplateViewResolver cannot see that a template name have this prefix, it is skipping to next handler in the handler chain inside spring (ie. other template view resolvers).

Compile Time Global Parameters

There are parameters that are static in nature and they can be only changed upon restarts of the application. An example of such parameters could be: project directory, dev mode, google analytics token, etc. Normally these values are available via Spring's application properties. What you may notice is that from time to time you would like to reference this globally available data from your soy files, those parameters then are available in soy files in the form: {parameter.name}, note lack of $ sign.

e.g.

    <bean id="globalCompileVariablesProvider" class="pl.matisoft.soy.global.compile.DefaultCompileTimeGlobalModelResolver">
        <property name="data">
            <map>
                <entry key="app.global.siteUrl" value="http://www.mysite.com" />
            </map>
        </property>
    </bean>

Please note it is also possible to inject all spring properties via properties property. The data can be referenced in soy files in the following manner: {app.global.siteUrl}

Internationalization (i18n)

The library supports templates to be in many languages, all you need to do is to reference your language specific messages inside a soy file with a special {msg} tag.

Consider example:

<h3>{msg meaning="I18N.VEHICLE.Further_Technical_Data" desc=""}Further Technical Data{/msg}</h3>

Note that at the moment this library requires that you precompile all files that contain {msg} tags into so called xliff format, which is a standard format that describes internationalized messages. More information on: http://en.wikipedia.org/wiki/XLIFF

You can extract xliff messages from your soy file using a tool provided by google: https://developers.google.com/closure/templates/docs/translation

At the moment the library does not contain a maven-plugin that would extract i18n messages and create xliff file(s) for it, it also appears to be outside of it's core functionality.

The library contains a class: DefaultSoyMsgBundleResolver, which reads a xliff messages from classpath and makes it accessible to spring-soy-view library. By default a class will read messages stores in the classpath under: messages.xlf file, this can be configured to reflect project settings.

The library needs to lookup a locale and for this it uses an interface LocaleProvider, which comes with number of implementations. What is suggested and recommended is to use SpringLocaleProvider, which using spring resolver will lookup a current locale and load locale's xliff messages to be displayed for a user.

Developer and Production mode

In order to support hot-reloading of soy files a library supports a developer mode, which is controller via debugOn flag. A name of this property can be misleading but it is for historical reasons. Usually when this property is set to true on certain classes it means the caching will be disabled, note that you can set a debugOn property on some classes and always leave it set to true or false on others. This way you can control certain aspects of developer mode, some classes may continue to cache results others will constantly recompile or build pertinent objects.

Runtime Global Parameters

Google's Soy Templates support a notion of globally injected parameters, which are available under a special namespace: ${ij}, contrary to other spring's view resolver library this library makes use of this and that way this allows us to keep SoyVIew implementation clean. By default an implementation delegates to a DefaultGlobalModelResolver, which in turn contains a list of RuntimeResolvers Out of the box the library provides a resolution for the following runtime information:

Note that since by design a Soy does not allow any code from the templates to be executes in java (without writing a plugin for it), all of those implementation support only getting data without input parameters.

It is expected that if you need your own domain specific runtime data resolver, you simply write a new class and implement the interface pl.matisoft.soy.global.runtime.resolvers.RuntimeResolver, once done wire this via configuration of DefaultGlobalModelResolver

public interface RuntimeDataResolver {

   void resolveData(HttpServletRequest request, HttpServletResponse response, Map<String, ? extends Object> model, SoyMapData root);

}

Model adjuster (ModelAdjuster)

This concept has been created because Spring MVC will often wrap your real domain object inside own model. Soy needs to access a path to your domain object compiled to soy compatible data structures, not internal spring model. To use it, just wire SpringModelAdjuster and specify your model property (typically 'model').

POJO Data Conversion (ToSoyDataConverter)

A library by default will convert your POJO domain objects to soy compatible data structures. A default implementation of ToSoyDataConverter that will recursively inspect a passed in model and build a nested structure of SoyMapData objects, which consist only of primitives supported by Soy and thus can be rendered.

Warning: be careful when your domain objects perform an expensive calls, e.g. hibernate db load. ToSoyDataConverter will recursively invoke getters and is (for booleans) methods automatically to build SoyMapData object structure.

Ajax JavaScript compilation

This is an optional module, in which compilation of soy files can be done via a Spring MVC controller. Typically, however, some project may choose to use a maven plugin or grunt task that compiles soy files to javascript or even not compilation to JavaScript at all (not recommended - usage allows performance tweaks and lazy loading)

To use an ajax compiler it is necessary to wire or include SoyAjaxController in application's configuration files:

<bean id="ajaxSoyController" class="pl.matisoft.soy.ajax.SoyAjaxController">
        <property name="localeProvider" ref="localeProvider" />
        <property name="soyMsgBundleResolver" ref="soyMsgBundleResolver" />
        <property name="templateFilesResolver" ref="templateFileResolver" />
        <property name="tofuCompiler" ref="tofuCompiler" />
        <property name="hotReloadMode">
            <bean class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
                <property name="targetBeanName" value="soyViewConfig" />
                <property name="propertyPath" value="hotReloadMode" />
            </bean>
        </property>
        <property name="cacheControl" value="public, max-age=86400" /> <!-- one year -->
        <property name="expiresHeaders" value="Mon, 16 May 2050 20:00:00 GMT" />
        <property name="outputProcessors">
            <list>
                <bean class="pl.matisoft.soy.ajax.process.google.GoogleClosureOutputProcessor" />
            </list>
        </property>
        <property name="authManager">
            <bean class="pl.matisoft.soy.ajax.auth.ConfigurableAuthManager">
                <property name="allowedTemplates">
                    <list>
                        <value>ajax_search_makes</value>
                        <value>ajax_search_models</value>
                        <value>ajax_check_distance</value>
                        <value>ajax_adnav</value>
                        <value>ajax_similar_ads</value>
                        <value>ajax_google_ad_info</value>
                    </list>
                </property>
            </bean>
        </property>
</bean>

An example from a html document:

<script type="text/javascript" src="../bower_components/soyutils/soyutils.js"></script>
<script type="text/javascript" src="soy/compileJs?file=templates/client-words.soy&amp;file=templates/server-time.soy"></script>

SoyAjaxController supports the following endpoints:

/soy/compileJs

with the following parameters:

Google groups forum

If you find issues, cannot understand something, please post a question to a Google Groups forum, I will try to help you with the problem.

Google groups forum

Git pull requests

Git pull requests are welcome and I am happy to discuss and integrate them to the project

ChangeLog

1.13.0

1.13.1

1.13.2

1.13.3

1.14.0

1.20.0

1.25.1

1.25.2

1.25.3

1.25.4 - 22.06.2014

1.25.5 - 24.09.2014

1.25.6 - 07.12.2014

1.25.7 - 28.05.2015

2.0.0 - 02.07.2015

License

Apache License Version 2.0

Known issues:

Example project (shows Black-Box mode in action)

author: Mateusz Szczap