Across primer part 1: our first application

About the Across primer series


In this series we try to give a quick overview of how to build web applications using Across and its Standard Modules.  Across is a framework focused on building application modules on top of Spring framework.  A single Across module packs a Spring ApplicationContext configuration, along with all web controllers, templates and static resources required to run it.  Across is very much developer focused, aiming at Java developers already proficient with Spring framework.
Across is not OSGi.  Although a running Across context consists of several modules, like most Spring and Spring Boot applications, these are not dynamically loaded but bootstrapped at JVM startup time.


Apart from Across core, there are also a number of Across Standard Modules.  These are separate modules that can be plugged into an Across context.  Where Across itself is no Web CMS, some of the Standard Modules do provide cms-related functionality.  

In this series we'll be focusing on building a web application mostly using the Standard Modules, explaining some general Across concepts along the way.   We'll be building a catalog browsing application.  On our site you can select a category and view the items that are part of that category.  

Throughout the series we'll be:

  • setting up the web application and creating the public user interface
  • building a database persisted domain model for categories and items
  • creating the administration user interface for managing our domain entities

Before you get started

Across is built entirely on top of Spring framework.  While we'll be focusing on introducing you to the concepts of Across, you should already be proficient with the following:

  • Spring framework
    • configuring the ApplicationContext using Java instead of XML
    • Spring profiles
    • Spring MVC
  • JUnit and Spring Mock MVC
  • Maven
  • Spring Boot (basic knowledge should suffice)

What we'll do in this article

In the first article of the primer series we will:

  • set up our simple web application using Spring MVC, Thymeleaf and Across
  • illustrate default settings and conventions regarding web resources
  • explain development mode and see how it improves productivity with instant reloading

Source code

All source code can be found on our Bitbucket repository: https://bitbucket.org/beforeach/across-primer/branch/part-1

Most steps in this article correspond with a single commit, you can checkout out to any of those revisions and start building from there on.

Create a web application with a first controller

So let's get started with creating our new application.  We'll create what looks very much like a basic Spring MVC application, with minimal configuration using Spring Boot.

Creating our new web application

Let's create a new Across application.  Our application will come with an embedded tomcat and provides a simple homepage controller.  When calling the homepage, a Thymeleaf template will be rendered and the output returned.

$ git checkout c99d441

To setup our new project we create a Maven pom.xml.  Here we are using the Across platform-bom as the parent for our project.  The platform-bom provides a curated set of dependencies of Across and its Standard Modules.  It extends the Spring platform BOM so many dependencies are defined.  Our own dependency section is very limited and only has a dependency on across-web for the AcrossWebModule, and spring-boot-starter-web for the embedded Tomcat we'll be using.  All other dependencies will be added transitively.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>across.demo.catalog</groupId>
    <artifactId>catalog-application</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>catalog-application</name>
    <description>Across primer demo project</description>

    <repositories>
        <repository>
            <id>foreach</id>
            <name>Foreach</name>
            <url>https://repository.foreach.be/nexus/repository/public/</url>
        </repository>
    </repositories>

    <parent>
        <groupId>com.foreach.across</groupId>
        <artifactId>platform-bom</artifactId>
        <version>2.0.0-SNAPSHOT</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.foreach.across</groupId>
            <artifactId>across-web</artifactId>
        </dependency>
        <!-- Add embedded Tomcat -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

Next we need the application class itself.   We create our CatalogApplication with the following content:

across.demo.catalog.CatalogApplication
@AcrossApplication(modules = AcrossWebModule.NAME)
public class CatalogApplication {

    public static void main(String[] args) {
        SpringApplication.run(CatalogApplication.class, args);
    }
}

The @AcrossApplication annotation defines the class as the root for an Across context application.  Doing this means a couple of things:

  • the class itself is a configuration class for the application itself
  • a java package called application relative to the package containing the CatalogApplication will be considered the root for the module containing the actual application model
  • Spring application properties will be loaded and a DispatcherServlet automatically created if in a web environment (much like Spring Boot)

The "modules" attribute on the annotation determines which modules should be added to our application.  In our case we only require AcrossWebModule as that will configure the default web support.

The main function containing the SpringApplication is the entry point for starting up our application, and is Spring Boot specific. 

At this point we could actually start up our application already, and it would start an embedded Tomcat on port 8080 with the AcrossWebModule bootstrapped.  But all requests would return a 404 as we have no controllers yet, so let's add one.

Adding the HomepageController

Let's add a very simple HomepageController:

across.demo.catalog.application.controllers.HomepageController
@Controller
public class HomepageController {

    @RequestMapping("/")
    public String showHomepage() {
        return "th/catalog/homepage";
    }
}

Our controller contains a single @RequestMapping that returns the name of the template that should be rendered.  AcrossWebModule by default enables support for Thymeleaf and all view names starting with th/ will be considered Thymeleaf templates and will be searched for on the classpath as views/th/*.thtml.  So our th/catalog/homepage would expect the template file to be views/th/catalog/homepage.thtml:

classpath:views/th/catalog/homepage.thtml
<html>
<head>
    <title>Across primer: catalog application</title>
</head>
<body>
    <h1>Hello world!</h1>
</body>
</html>

Why not simply put the templates in a template folder?

All resource files like templates and message sources are structured in module and type prefixed folders. This a convention that allows us to avoid module clashes and still easily support multiple strategies (eg. multiple view resolvers) and instant reloading in development mode. The first part of the path will be the type of resource: messages for message sources, views/th for Thymeleaf templates... The second part will be the module specific resource key.

In our case the module specific resource key is catalog. This key is automatically derived from the dynamic module that our CatalogApplication creates.

That's it, that's all the files we need to start our application and render some output on the homepage.  To verify, your project structure should look more or less like this:

Notice how we did not define any configuration XML or @Bean methods.  This is because by default all packages relative to the application package will automatically be scanned for Spring components.

Starting it up

To start the application simply run the CatalogApplication#main() method.  In the console output you should see something like the following:

What we see in the log is the output of the Across context being started.

A single Across module consists of a Spring ApplicationContext and as such has a lifecycle (start & stop).  An Across context is a number of Across modules thrown together, optionally defining dependencies between them.  The Across context itself is also represented by a single ApplicationContext that is the direct parent of the ApplicationContext of a module configured in the Across context.  When the Across context boostraps, it more or less means that it will start its own ApplicationContext and will start the ApplicationContext of each separate module.

This is not done at random but according to the module bootstrap order.  This order is determined by the dependencies between modules.

If we look at our example, we can see that 2 modules will be bootstrapped, first the AcrossWebModule and second the CatalogApplicationModule.  We explicitly defined the AcrossWebModule in our @AcrossApplication but we never added the CatalogApplicationModule anywhere.  Actually I've already hinted at this before.  When we are using @AcrossApplication the framework will automatically look for an application package relative to the package containing our application class.  If one is found, a dynamic module will be built from that package and added as the last module to our application.  That's actually exactly what the line "Adding package based APPLICATION" module is telling you.  You can also see the catalog resources key being printed there.

In multi-module projects, the bootstrap order of the modules is very important, as most problems you'll run into have something to do with this.  But don't worry about it too much right now, we'll talk more on this subject in later articles.

Once the application has started you can point your browser to http://localhost:8080 and should see output like the following:

Changing the webserver port

Because Across application builds on top of Spring Boot support classes, the application.properties file is also supported. Specifying the webserver port can be done by setting the server.port property.

Testing the application

So now we have our first Across based application, but we do not yet have any tests for it.  Writing actual unit tests has nothing to do with Across itself, but let's add an integration test for our code.

$ git checkout a6365d2

Start by adding the across-test dependency in the pom.  Across-test is a utility library that contains helpers for testing modules or contexts in both web and non-web configurations.  It is only useful for testing, hence the name and - more importantly - the dependency scope.

across-test dependency
<dependency>
    <groupId>com.foreach.across</groupId>
    <artifactId>across-test</artifactId>
    <scope>test</scope>
</dependency>

When across-test is being added, it will automatically add both spring-test and junit to your project.  Let's add general integration test that starts up our application and calls our controller using Spring MVC test framework.

across.demo.catalog.TestCatalogApplication
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(
        classes = {CatalogApplication.class, MockMvcConfiguration.class},
        initializers = MockAcrossServletContextInitializer.class
)
public class TestCatalogApplication {

    @Autowired
	private MockMvc mockMvc;

	@Test
	public void homepageShouldSayHello() throws Exception {
    	mockMvc.perform(get("/"))
        	    .andExpect(status().isOk())
            	.andExpect(content().string(containsString("Hello world!")));
	}
}

This will fully bootstrap our application but leave out the embedded webserver.  The entire servlet environment will be mocked out.

Our test is basically a Spring Boot application test where we add 2 important custom configuration classes:

  • MockAcrossServletContextInitializer creates a custom ServletContext mock that tracks dynamic registration of servlets and filters
  • MockMvcConfiguration provides a MockMvc bean that is initialized with the Across context and all filters registered through it

Now that we have a basic test, let's move on and add some localization to our application.

Using a message source for text

Our Thymeleaf template currently has everything hardcoded.  Let's move some text to property files and add support for multiple languages while we're at it. 

$ git checkout 11551f2

Adding a message source

Just like our Thymeleaf template is located in the module specific path views/th/catalog, our message source files will be in path messages/catalog.  Add a messages/catalog/default.properties with the following key:

classpath:messages/catalog/default.properties
welcome.message=Across bids you a warm welcome!

Update the homepage template and add the th:text attribute on our h1 tag:

<h1 th:text="#{welcome.message}">Hello world!</h1>

If you are unfamiliar with Thymeleaf, the th:text attribute will replace the static text at runtime.  The expression #{welcome.message} will look for the welcome.message key in the message source attached to the root ApplicationContext.

That's all there is to it.  If you restart the application you should see the text being replaced with the value from our properties file.

What happens is that Across auto-detects default message sources.  In this case our default.properties follows the convention and is registered without us having to add additional configuration.  Having a single message source however is not so useful, let's spice it up with some language switching.

Switching languages

First let's add a second default_nl.properties in exactly the same location as the first one:

classpath:messages/catalog/default_nl.properties
welcome.message=Across heet u van harte welkom!

Now we can configure some Spring classes that allow us to specify the language.  Make the CatalogApplication extend WebMvcConfigurerAdapter and add the following configuration:

across.demo.catalog.CatalogApplication
@AcrossApplication(modules = AcrossWebModule.NAME)
public class CatalogApplication extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // allow language to be changed by providing a request parameter 'language' with the locale string
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("language");
        localeChangeInterceptor.setIgnoreInvalidLocale(true);

        registry.addInterceptor(localeChangeInterceptor);
    }

    @Bean
    public CookieLocaleResolver localeResolver() {
        // read (and store) the selected locale from cookie
        CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
        cookieLocaleResolver.setDefaultLocale(Locale.ENGLISH);
        return cookieLocaleResolver;
    }

    public static void main(String[] args) {
        SpringApplication.run(CatalogApplication.class, args);
    }
}

First define a CookieLocaleResolver bean that allows storing the selected locale in a cookie.  If no locale has been selected, we fall-back to English.  Second we register a LocaleChangeInterceptor that allows us to change the locale using the language request parameter.  If the request parameter is specified, the locale will be changed and stored back into the cookie.

Notice how we add this configuration directly to the CatalogApplication and not create a separate configuration class inside our application module.   Even though it would be perfectly possible, we consider this to be application-level configuration and not relevant for the module itself.  Again, don't worry if this does not make much sense yet, it will by the time we reach the end of the primer series.

Updating our test

If we try executing our TestCatalogApplication after making the changes, you'll notice it fails.  We should update the test to support the localized messages, and let's add testing the actual locale request parameter as well:

Updated test methods for localized messages
@Test
public void homepageShouldDefaultToEnglish() throws Exception {
    mockMvc.perform(get("/"))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("Across bids you a warm welcome!")));
}

@Test
public void dutchHomepageShouldRenderLocalizedMessage() throws Exception {
    mockMvc.perform(get("/?language=nl"))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("Across heet u van harte welkom!")));
}

All is green again.  

Restart the application, call the homepage with an optional language parameter and see how the language is changed and remembered via the cookie.

Again, none of this is very specific to Across but simply using functionalities that Spring has been providing for many years.  The only specific thing is the auto-detection of the message sources.

Instant reloading of messages and templates using development mode

One of the major advantages of using conventions for Thymeleaf templates and static resources is that it allows for instant reloading, without having to restart the application.  Across does this out of the box if you enable development mode.  As the name suggests, this mode changes the configuration for enhanced productivity during development.

There is more than one way to activate development mode, but it is done automatically if you have an active Spring profile called "dev".  You add it by setting a spring.profiles.active system property, or if you use a decent IDE by editing the run configuration:

Start the application with development mode enabled.  You can verify this in the log output if you see the following statement:

[ost-startStop-1] c.f.a.c.d.AcrossDevelopmentMode          : Across development mode active: true

Further along in the log output you should see something like:

[ost-startStop-1] c.f.a.m.web.config.AcrossWebConfig       : Registering development Thymeleaf lookup for catalog with physical path ...
...
[ost-startStop-1] c.f.a.c.c.s.AcrossModuleMessageSource    : Mapping resource bundle paths classpath:/messages/catalog to physical file: ...

In development mode you can change that values in the message sources or modify the homepage template.  Changes should be reflected immediately if you simply refresh the page in the browser.  

Because we have a very simple application with a single Maven module, Across should be able to automatically detect the physical location of the resource files.  The default development resource loading depends on the working directory of the running application, in our example it is expected to be the root of the project.  The folder src/main/resources should be present in the working directory of the running application.  If that is not the case, then the default resource resolving will not work, but you can still manually provide the specific location where the physical files are present.  Ways to do that can be found in the documentation.  Configuring development mode is something you should do early on in a project, as it has a big impact on developer productivity.

To check: if you execute our integration test, you should notice in the console output that development mode is not active.

If you don't want to use a Spring profile to activate development mode, or you want to force it manually, you can do so by setting the across.development.active property to true or false.

Building our business domain

So far we have the most basic of controllers, but we've set us up for easy development.  We'll need a bit more to work with so let's build a bit of domain model and add some more pages to our website.

$ git checkout 7f7c154

Adding entities

Let's add both a Category and a CategoryItem class.  These are not beans but simple business classes that we consider part of our application model.   A single item has a name and a price and belongs to a single category.  A category has a name, a path and multiple items. Later on we will persist them in a database but for now we put a fixed static LIST field on the Category class.

The category path will be used for building the url to the category detail.

across.demo.catalog.application.business.CategoryItem
public class CategoryItem {
    private String name;
    private BigDecimal price;

    public CategoryItem() {
    }

    public CategoryItem(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }
}
across.demo.catalog.application.business.Category
public class Category {
    public static final List<Category> LIST = Collections.unmodifiableList(
            Arrays.asList(
                    new Category("tv", "TV",
                            Arrays.asList(
                                    new CategoryItem("Samsung", new BigDecimal("750")),
                                    new CategoryItem("Panasonic", new BigDecimal("800.99"))
                            )
                    ),
                    new Category("radio", "Radio",
                            Collections.singletonList(new CategoryItem("Onkyo", new BigDecimal("999.99")))
                    )
            )
    );

    private String path, name;
    private List<CategoryItem> items;

    public Category() {
    }

    public Category(String path, String name, List<CategoryItem> items) {
        this.path = path;
        this.name = name;
        this.items = items;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<CategoryItem> getItems() {
        return items;
    }

    public void setItems(List<CategoryItem> items) {
        this.items = items;
    }

    public static Category from(String path) {
        for (Category c : LIST) {
            if (StringUtils.equals(path, c.getPath())) {
                return c;
            }
        }
        return null;
    }
}

The from(String) method on Category is important as it will be automatically used by the Spring ConversionService when converting from a String to a Category. Something we require for our CategoryController.

Building the view layer

Start with adding some additional messages to our properties files.

classpath:messages/catalog/default.properties
welcome.message=Across bids you a warm welcome!
404.message=Oops, we could not find what you\'re looking for.

nav.home=Home
nav.browse=Browse

selectedCategory.title=Products for category: {0}
item.name=Product name
item.price=Price
classpath:messages/catalog/default_nl.properties
welcome.message=Across heet u van harte welkom!
404.message=We vonden niet wat je zocht :(

nav.home=Home
nav.browse=Bladeren

selectedCategory.title=Producten voor categorie: {0}
item.name=Productnaam
item.price=Prijs

To give our pages just a little bit of design, we'll be using some Bootstrap components.  I am no front-end guru, so let me apologise up front if your eyes get insulted.  We'll add a top menu on every page, with a Home and a Browse button.  When clicking on Browse, we'll jump to the CategoryController which will list us all the items of a specific category.  Because when clicking Browse we have not selected a category yet, we'll just hard-link it to the first category.

Update the HomepageController to add the list of categories to the view model.

Updated HomepageController
@RequestMapping("/")
public String showHomepage(Model model) {
    model.addAttribute("categories", Category.LIST);

    return "th/catalog/homepage";
}

Add the top navigation to the homepage template, and ensure the right menu item is selected.

classpath:views/th/catalog/homepage.thtml
<html>
<head>
    <title>Across primer: catalog application</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/>
</head>
<body>
<div class="container">
    <nav class="navbar navbar-default">
        <div class="navbar-header">
            <ul class="nav navbar-nav" th:inline="text">
                <li th:classappend="${selectedCategory == null} ? active"><a th:href="@{/}">[[#{nav.home}]]</a></li>
                <li th:classappend="${selectedCategory != null} ? active">
                    <a th:href="@{'/category/' + ${categories[0].path}}">[[#{nav.browse}]]</a>
                </li>
            </ul>
        </div>
    </nav>
    <div class="jumbotron">
        <h1 th:text="#{welcome.message}">Hello world!</h1>
    </div>
</div>
</body>
</html>

Notice we are checking for a selectedCategory value in our template, even though we did not mention it in our HomepageController. This is because we'll be including the same fragment for our category detail page, as you can see below.

If you restart the application, the homepage should now look something like the following:

Category detail page

Our category detail page lists all items in a category.  A category has a unique path that we use for building the url.  The url a category detail page will be http://localhost:8080/category/<<category path>>, eg http://localhost:8080/category/tv.  For the sake of this tutorial, we'll also throw a 404 (page not found) if a non-existing category path is requested.

Add the CategoryController that builds the category model and ensures a 404 is returned if the category does not exist.

across.demo.catalog.application.controllers.CategoryController
@Controller
public class CategoryController {

    @RequestMapping("/category/{categoryPath}")
    public String listCategoryItems(@PathVariable("categoryPath") Category selectedCategory, Model model) {
        if (selectedCategory == null) {
            throw new IllegalArgumentException("Illegal category requested.");
        }

        model.addAttribute("categories", Category.LIST);
        model.addAttribute("selectedCategory", selectedCategory);

        return "th/catalog/categoryItems";
    }

    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String categoryNotFound(Exception e, Model model) {
        model.addAttribute("feedback", e.getMessage());
        return "th/catalog/404";
    }
}

In our @RequestMapping method we map the categoryPath path variable directly on a Category instance. Behind the scenes the Spring ConversionService takes care of this because we have a static Category#from(String) method.

The category detail template categoryItems.thtml renders a simple table of the different items.  Notice the use of th:replace to reuse sections from the homepage template.  That is just default Thymeleaf functionality.  In the second part of this primer we'll see a different way that Across provides to do page templating.

classpath:views/th/catalog/categoryItems.thtml
<html>
<head th:replace="th/catalog/homepage :: head">
    <title>Across primer: catalog application</title>
</head>
<body>
<div class="container">
    <div th:replace="th/catalog/homepage :: nav">
        Here comes the navigation.
    </div>

    <div class="row">
        <div class="col-md-2">
            <ul class="nav nav-pills nav-stacked">
                <li th:each="category : ${categories}" th:classappend="${selectedCategory eq category} ? active">
                    <a th:href="@{'/category/' + ${category.path}}" href="#"
                       th:text="${category.name}">category name</a>
                </li>
            </ul>
        </div>
        <div class="col-md-10">
            <h1 th:text="#{selectedCategory.title(${selectedCategory.name})}">Items for selected category</h1>
            <table class="table table-striped">
                <thead th:inline="text">
                <tr>
                    <th>[[#{item.name}]]</th>
                    <th>[[#{item.price}]]</th>
                </tr>
                </thead>
                <tr th:each="item : ${selectedCategory.items}">
                    <td th:text="${item.name}">item name</td>
                    <td th:text="|$ ${#numbers.formatDecimal(item.price,1,2)}|">item price</td>
                </tr>
            </table>
        </div>
    </div>
</div>
</body>
</html>

After adding the page template and reloading the application, you should get a working category detail page when you hit the Browse link.

Illegal category

We already included the category not found functionality in the CategoryController, but we still need to add the template to get a customized 404 view:

classpath:views/th/catalog/404.thtml
<html>
<head th:replace="th/catalog/homepage :: head">
    <title>Across primer: catalog application</title>
</head>
<body th:inline="text">
<div class="container">

    <h2>[[#{404.message}]]</h2>
    <div class="alert alert-danger" th:text="${feedback}">
        Feedback reason.
    </div>
    <a th:href="@{/}" th:text="#{nav.home}">home</a>

</div>
</body>
</html>

If you now request a non-existing category, this is what you get:

You can check the network request, a HTTP status 404 should be returned.

Adding some tests

To wrap it up, let's extend our integration tests to test both valid categories and the 404 logic.

Update TestCatalogApplication
@Test
public void validCategoryShouldReturnItsOwnItems() throws Exception {
    mockMvc.perform(get("/category/tv"))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("Samsung")))
            .andExpect(content().string(not(containsString("Onkyo"))));

    mockMvc.perform(get("/category/radio"))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("Onkyo")));
}

@Test
public void illegalCategoryShouldGiveUs404() throws Exception {
    mockMvc.perform(get("/category/not-a-real-category"))
            .andExpect(status().isNotFound())
            .andExpect(content().string(containsString("Oops, we could not find what you&#39;re looking for.")));
}

Static resource versioning and caching

Now that we've built a very simple domain model and created a basic user interface, there's only one more thing before we're done with our introduction... we need a logo!  Adding a logo brings us neatly to the subject of how AcrossWebModule serves static resources like images and css.

$ git checkout 211c716

Just like with the message sources and the templates, Across has a conventional location for static resources that need to be served.  The type prefix is views/static, so our base path is views/static/catalog.

Download and add the Across logo as views/static/catalog/images/across-logo.svg.

We will put the logo in the left corner of the navigation bar.  Instead of adding an old-school image tag, I will demonstrate my elite CSS skills (ahum) to position the logo.

Add a second static resource, this time created views/static/catalog/css/catalog.css with the following content:

classpath:views/static/catalog/css/catalog.css
.logo {
    background: url("../images/across-logo.svg") no-repeat center top;
    background-size: 52px 52px;
    width: 52px;
    float: left;
    height: 50px;
}

All that's left to do is to add the logo div to the navigation, and include our css in the head section on the homepage.

<head>
    <title>Across primer: catalog application</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/>
	<!-- insert the custom css file -->
    <link rel="stylesheet" th:href="@{/across/resources/static/catalog/css/catalog.css}" />
</head>
...
<nav class="navbar navbar-default">
    <div class="navbar-header">
		<!-- add the logo -->
        <div class="logo">&nbsp;</div>
        <ul class="nav navbar-nav" th:inline="text">

Note that when linking to the css file we replace the views folder with /across/resources. The latter path is the default where AcrossWebModule publicly serves its static resources. Please refer to the documentation for more information if you want to customize.

Because we added a new type folder to our application (views/static), we need to restart the application before its get loaded.  If everything goes right, you should now get a nice logo in the navigation bar:

And because the head fragment is included in the category detail page, the logo appears there as well.

Default caching and versioning

Now that we have a fancy logo, let's take a loot at the source code of the HTML the homepage renders.  Assuming we are still in development mode, we should find something like the following in the head section:

<link rel="stylesheet" href="/across/resources/static/catalog/dev:8d788469-7892-4bc7-8a47-aeff7a95c594/css/catalog.css" />

AcrossWebModule has automatically inserted an additional path segment after our module resources key.  This is a version path and is done for cache busting purposes.  Actually you can also call the resource directly without the version path, simply try it by pointing your browser at /across/resources/static/catalog/css/catalog.css.  

AcrossWebModule does resource versioning by default for all static resources.  In development mode the version path will be different for every application start, ensuring you're never looking at client-side cached versions of resources.

You can control the version to be used in non-development mode by setting either a acrossWebModule.resources.versioning.version or build.number property.  A good strategy is to set a build.number property during your build process.  The property name is unfortunately a bit misleading as it can be pretty much any string.

Likewise if you look at the response headers of the css request itself, you should see a cache-control header that disables caching:

Cache-Control: max-age=0

As with default resource versioning, AcrossWebModule activates client-side caching of static resources for one year.  Except in development mode, were we force no client-side caching.

This is another example of how Across attempts to simplify your development workflow and improve your productivity.

So, let's put this to the test, let's add an application.properties file and specify a build.number:

classpath:application.properties
build.number=mybuild

The application.properties is automatically picked up in a Spring Boot application.  Restart the application but make sure you do so without the dev profile active.

Verify in the console output that development is not active:

c.f.a.c.d.AcrossDevelopmentMode          : Across development mode active: false

When looking at the HTML page source, you should now see:

<link rel="stylesheet" href="/across/resources/static/mybuild/catalog/css/catalog.css" />

And the response of the CSS request should have the following cache control header:

Cache-Control: max-age=31536000

Again, all this is not so much Across itself, but AcrossWebModule creating a sensible default configuration based on the functionality the Spring framework is packing.  This means you can still bypass it or fully customize it any way you'd like.  All details on how Across uses static resource versioning can be found in the reference documentation.

Wrapping up

That concludes the first part of our primer series.  We started by building a simple Spring MVC web application, explaining some basics of Across and showing how Across adds default configuration and convention on top of Spring framework.  We've built a very simple domain model and user interface that we'll be expanding throughout the series.

In the next part we'll be going over Across web templates and dynamic resource registration.