Sunday, February 28, 2016

Spring Boot WebJar

Spring Boot WebJar


When creating an end to end application that includes client side code and server side code, we need a full application solution that can be easily built and deployed.
The client side may be written in different frameworks – angular, backbone. Spring Boot supports all of these solutions in multiple ways.

Static Resources

The strait forward way is to copy all resources from the client code (js, html, css...) to the static folder user the resources folder (see https://spring.io/blog/2013/12/19/serving-static-web-content-with-spring-boot).
The problem with this solution is that we are tightly couples between the client and server code. In order to compile the server code we need to copy the client compiled code to our resources folder.
To use this solution we need to create a Jenkins job that takes the client code, compiles it (using grunt or other tools) and then takes the folder that was created and copies it to the resources folder and then compile the server code.
This is definitely not the maven way. The solution that we would want is for the client code to create an artifact for us to add as a dependency in our pom file. The solution that has become a standard is what is called a webjar (www.webjar.com).

WebJar

A webjar is a zip file with the following folder structure:
META-INF\resources\webjars\artifactId\version\
When the client requests a resource from the server, Spring Boot looks in all its resource locations for the resource. The resource locations are: resource\static, resource\public, webjars.
So far the solution looks like it works nicely. The next issue is that webjar technology is not just a server side technology. Webjars come to solve another issue on the client side. When the client needs global functionality the solution for creating modules is to use webjars. What this means is that when the client needs a resource from the server, it needs to specify the artifactId and the version so that the server can retrieve it from the webjar.
For example:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head> 
    <title>Getting Started: Serving Web Content</title> 
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <script src="webjars/jquery/2.0.3/jquery.min.js"></script>
    <script type="text/javascript">
        $(document).ready(function() {
            $('p').animate({
                fontSize: '48px'
            }, "slow");
        });
    </script>
</head>
<body>
    <p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>

As you can see the client code references a version of the modules it wants to use: "webjars/jquery/2.0.3/jquery.min.js". This might be good in most cases, but the problem is that in a lot of cases our client side is a single page solution and does not necessarily need the webjar solution (with the full artifcatId and version). What then happens is that the client requests a resource but the server cannot find it since it needs the artifactid and version to find it within the webjar. We would like the client to request a js file or html file without the prefix of the webjar and for the server to find it and return it to the client.

ResourceHandlers

To do this we need to add our own bean that will supply the lookup for resources for spring.
We are actually going to solve here two other issues that arise with single page applications and Spring Boot.
In addition to the issue that we don’t want the client code to need to reference its own webjar, we would like to have the option to have all the client resources out of our jar, so that we can upgrade the client code without recompiling the server code.
Another issue with single page applications is that the client side navigation uses the url path to do this. So if the client side moves to an admin page, it will usually add to the url “/admin”. The problem with this is that if the use now refreshes his browser, the server will get a url request with “/admin” that the server does not have.
To solve this, we need to create another configuration bean that will inherit WebMvcConfigurerAdapter.  
The default implementation of spring is to search all webjars in the application (see https://spring.io/blog/2014/01/03/utilizing-webjars-in-spring-boot):
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!registry.hasMappingForPattern("/webjars/**")) {
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars/");
    }
    if (!registry.hasMappingForPattern("/**")) {
        registry.addResourceHandler("/**").addResourceLocations(
                RESOURCE_LOCATIONS);
    }
}

We will change this and override the addResourceHandler as follows:
override def addResourceHandlers(registry: ResourceHandlerRegistry): Unit = {

  if (!registry.hasMappingForPattern("/**")) {

    registry.addResourceHandler("/**").addResourceLocations(

      classpathResourceLocations: _*)

  }

}

This will now allow us to define a string array that will tell spring were to search for resources.
Our basic resource location array will be as follows:
private var classpathResourceLocations = List(

  "classpath:/META-INF/resources/", "classpath:/resources/",

  "classpath:/static/", "classpath:/public/")

This means, for spring it will first search in the resources folder of the classpath, and the in static folder, and lastly in the public folder (standard locations for resources).
But what we want is to add is the option to have a resource folder out of our jar and also to add the location of the webjar within our application without the client having to specific the full path.
classpathResourceLocations = (externalWebsite :: classpathResourceLocations) :+ s"classpath:/META-INF/resources/webjars/my.web-jar/$infoBuildVersion/"

The order that spring boot searches for resources is by the order of the string array for the locations. So the first entry will be a location to an external folder that can be specified in the application.properties in the format of a url (file:\...).
The second entry will be my web-jar with the fullpath of the resources inside the webjar. By doing this the client does not need to send the path, since we have added all the internal resources without the prefix.

Client Side Paths

 The last issue I wish to address is client side paths. The client side is written in angular so navigation in the java script is done by urls. So the client can have a url of : www.server.com\domain\method.
The “method” is not a endpoint in the server side but an endpoint in the client side. All this works fine until the user clicks on the page refresh. What then happens is that the URL is sent to the server, where the serve does not have the endpoint and returns a page not found error.
To fix this error we need to add View Controllers path in our WebMvcConfigurerAdapter as follows:
override def addViewControllers(registry: ViewControllerRegistry): Unit = {

  registry.addViewController("/").setViewName("forward:/index.html")

  registry.addViewController("/administration").setViewName("forward:/index.html")

  registry.addViewController("/license").setViewName("forward:/index.html")

  registry.addViewController("/404").setViewName("forward:/index.html")

  registry.addViewController("/download").setViewName("forward:/index.html")

}

In this method we will add all client side URL’s with a forward to the main page of the client indext.html. This will allow the server to accept the client endpoint and the redirect it back to the client.

Error endpoint

This all works fine unless the client endpoint is /error. Since this is a standard endpoint spring automatically implements this endpoint for us. Since we do not want this we must override spring’s default implementation as follows. In you configuration bean you need to add:
@Bean(name = Array("error"))

def defaultErrorView() : View  = {

  new RedirectView("index.html")

}

This implements the error bean of spring and tells spring to not use the spring implementation but in our case to redirect the error endpoint back to the client.