Bootiful CMS part 1 - Standalone Editor

A standalone editor with Spring Boot and AngularJS.

In this first blog post, we will build a very simple standalone post editor (think WordPress ultralight), using Spring Boot and AngularJS. It will have an H2 embedded database for now, and to keep things as simple as possible, the frontend will be limited to displaying a list of posts. The application will be secured using Spring Security and basic auth. And because no one likes to write XML, we will use Gradle to build it.

Frontend

The frontend is an AngularJS single page app. We will build it using Gulp and manage dependencies using Bower, so we’ll need a packages.json for NPM and a bower.json for Bower. A big thanks to Brecht Billiet for sharing his infinite knowledge of AngularJS!

Building with Gulp

editor/post/post-frontend/packages.json
{
  "name": "post-frontend",
  "version": "1.0.0",
  "description": "Post Frontend",
  "main": "/",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "/"
  },
  "author": "Jurgen Lust",
  "license": "/",
  "dependencies": {
    "del": "^1.1.1",
    "gulp": "^3.8.11",
    "gulp-angular-templatecache": "^1.5.0",
    "gulp-concat": "^2.5.2",
    "gulp-connect": "^2.2.0",
    "gulp-minify-css": "^1.0.0",
    "gulp-open": "^0.3.2",
    "gulp-sourcemaps": "^1.5.0",
    "gulp-uglify": "^1.1.0",
    "gulp-uncache": "^0.3.6",
    "gulp-util": "^3.0.4",
    "gulp-bower": "^0.0.10",
    "karma": "^0.12.31",
    "q": "^1.2.0",
    "run-sequence": "^1.0.2"
  }
}
editor/post/post-frontend/bower.json
{
  "name": "post-frontend",
  "version": "1.0.0",
  "homepage": "/",
  "description": "post-frontend",
  "main": "/",
  "authors": [
    "Jurgen Lust"
  ],
  "license": "/",
  "dependencies": {
    "angular": "~1.3.14",
    "angular-route": "~1.3.14",
    "angular-mocks": "~1.3.14",
    "bootstrap": "~3.3.4",
    "toastr": "~2.1.1",
    "lodash": "~3.5.0",
    "fontawesome": "~4.3.0"
  }
}

If you’re used to frontend development, there’s nothing new here. The packages file basically contains your development dependencies (build tools and libraries), while the bower file contains the dependencies you’ll use in your pages.

Note

Note that these 2 files already demonstrate why I don’t like it when build tools pretend to be more than build tools, by requiring metadata about your project that they don’t need. We already had to specify the name, version and description of this project twice, an if we would use Maven, 3 times!

Next we need a Gulp build file.

editor/post/post-frontend/gulpfile.js
(function() {
    'use strict';
    var gulp = require('gulp'),
        bower = require('gulp-bower'),
        concat = require('gulp-concat'),
        sourcemaps = require('gulp-sourcemaps'),
        templateCache = require('gulp-angular-templatecache'),
        connect = require('gulp-connect'),
        gOpen = require('gulp-open'),
        del = require('del'),
        Q = require('Q'),
        cssMinify = require('gulp-minify-css'),
        uglify = require('gulp-uglify'),
        util = require('gulp-util'),
        uncache = require('gulp-uncache'),
        runSequence = require('run-sequence');
    var environment = 'dev';
    var target = function(filename) {
        var dir = 'build/' + environment;
        if (filename) {
            return dir + '/' + filename;
        } else return dir;
    }
    var jsDependencies = [
            'bower_components/jquery/dist/jquery.js',
            'bower_components/angular/angular.js',
            'bower_components/angular-route/angular-route.js',
            'bower_components/bootstrap/dist/js/bootstrap.js',
            'bower_components/lodash/lodash.js',
            'bower_components/angular-spinner/angular-spinner.js',
            'bower_components/toastr/toastr.js'
        ],
        cssDependencies = [
            'bower_components/bootstrap/dist/css/bootstrap.css',
            'bower_components/toastr/toastr.css',
            'bower_components/fontawesome/css/font-awesome.css'
        ];

    /* install bower dependencies for this project */
    gulp.task('bower-install', function() {
        return bower();
    });


    /* compile AngularJS html templates to Javascript */
    gulp.task('template-cache', function() {
        return gulp.src('src/app/**/*.html')
            .pipe(templateCache({
                module: 'app.posts',
                root: 'app/'
            }))
            .pipe(concat('scripts.js'))
            .pipe(gulp.dest(target()));
    });


    /* bundle Javascript libraries and project scripts in 1 Javascript file */
    gulp.task('compile-javascript-libraries', ['bower-install'], function() {
        return gulp.src(jsDependencies)
            .pipe(sourcemaps.init())
            .pipe(concat('libs.js'))
            .pipe(sourcemaps.write('./'))
            .pipe(gulp.dest(target()));
    });


    /* bundle project scripts in 1 Javascript file */
    gulp.task('compile-javascript', ['template-cache', 'compile-javascript-libraries'], function() {
        return gulp.src(['src/app/**/*.js', target('scripts.js')])
            .pipe(sourcemaps.init())
            .pipe(concat('scripts.js'))
            .pipe(sourcemaps.write('./'))
            .pipe(gulp.dest(target()));
    });


    /* bundle library and project CSS files in 1 CSS file */
    gulp.task('compile-css', ['bower-install'], function() {
        return gulp.src(cssDependencies.concat(['src/style/screen.css']))
            .pipe(sourcemaps.init())
            .pipe(concat('screen.css'))
            .pipe(sourcemaps.write('./'))
            .pipe(gulp.dest(target()));
    });


    /* make sure the CSS and JS files are refreshed in the browser */
    gulp.task('uncache-index', function() {
        return gulp.src(['src/index.html'])
            .pipe(uncache({
                append: 'time'
            }))
            .pipe(gulp.dest(target()))
            .pipe(connect.reload());
    });


    gulp.task('copy-fonts', function() {
        return gulp.src('bower_components/fontawesome/fonts/*.*')
            .pipe(gulp.dest(target('fonts')));
    });


    gulp.task('copy-rest-sample-data', function() {
        return gulp.src('src/rest/*')
            .pipe(gulp.dest(target()))
            .pipe(connect.reload());
    })


    gulp.task('clear-dev', function() {
        var deferred = Q.defer();
        del([target('**/*.*'), target('fonts'), target('test')], function() {
            deferred.resolve();
        });
        return deferred.promise;
    });


    /* interaction */
    gulp.task('start-server', function() {
        connect.server({
            livereload: true,
            root: target()
        });
    });


    gulp.task('watch-changes', function() {
        gulp.watch(['src/app/*.js', 'src/app/**/*.js'], function() {
            runSequence('compile-javascript', 'uncache-index');
        });
        gulp.watch('src/app/**/*.html', function() {
            runSequence('compile-javascript', 'uncache-index');
        });
        gulp.watch('src/style/screen.css', function() {
            runSequence('compile-css', 'uncache-index');
        });
        gulp.watch('src/index.html', function() {
            runSequence('uncache-index');
        });
        gulp.watch('src/rest/*', function() {
            runSequence('copy-rest-sample-data');
        });
    });


    gulp.task('open-browser', function() {
        var options = {
            url: 'http://localhost:8080'
        };
        return gulp.src('./index.html')
            .pipe(gOpen('', options));
    });


    gulp.task('clean', ['clear-dev'], function() {});

    /* prepare the app for distribution */
    gulp.task('build', function() {
        environment = 'dist';
        runSequence('clear-dev', 'compile-javascript', 'compile-css', 'uncache-index', 'copy-fonts');
    });
    gulp.task('default', function() {
        runSequence('clear-dev', 'compile-javascript',
            'compile-css', 'uncache-index', 'copy-fonts', 'copy-rest-sample-data',
            function() {
                gulp.run('start-server');
                gulp.run('watch-changes');
                gulp.run('open-browser');
            });
    });

}());

This build file does quite a lot:

  • It installs Bower when it’s not yet available

  • It compiles the AngularJS HTML templates to Javascript

  • It concatenates Javascript libraries into one Javascript file

  • It concatenates the application Javascript and the Angular templates into one Javascript file

  • It concatenates all library and application CSS files into one CSS file

  • It puts all the resulting files into the build/dev (when using the default task) or build/dist directory (when running gulp build)

  • It starts a web server on localhost:8080 with the resulting web app

  • It watches all html, js and css source files for changes, and updates the running app immediately

AngularJS app

Now that we can compile our frontend app, let’s actually create it. We’ll start with the entry point, the index.html.

editor/post/post-frontend/src/index.html
<!DOCTYPE html>
<html lang="en" ng-app="app">
    <head>
        <meta charset="UTF-8">
        <title>Post Editor Lite</title>
        <!--uncache(rename:false)-->
        <link href="screen.css" rel="stylesheet"/>
        <!--enduncache-->
    </head>
    <body ng-cloak>
        <nav class="navbar navbar-inverse navbar-fixed-top">
            <div class="container-fluid">
                <div class="navbar-header">
                    <a class="navbar-brand" href="#/">Standalone Posts</a>
                </div>
                <div id="navbar" class="navbar-collapse collapse">
                    <ul class="nav navbar-nav">
                        <li><a href="#/posts" title="Posts">Posts</a></li>
                    </ul>
                </div>
            </div>
        </nav>
        <div ng-view></div>
        <!--uncache(rename:false)-->
        <script src="libs.js" type="text/javascript"></script>
        <script src="scripts.js" type="text/javascript"></script>
        <!--enduncache-->
        <script type="text/javascript">
            (function(){
                'use strict';
                var app = angular.module('app', ['app.posts','ngRoute']);
                // register our dependencies
                app.constant('_', _);
                app.constant('toastr', toastr);
            }());
        </script>
    </body>
</html>

This is a very simple page with a menu bar on top and the Angular view (the ng-view div) below that. Note that the screen.css file we import on top is the result from the concatenation in Gulp. The same applies to the libs.js and scripts.js files. The block of Javascript in the bottom could have been put in a separate JS file as well. In fact, if we would only plan on building a standalone single page app, that’s what we should do, but we put this here because later we will split out single page app in multiple separate, independently deployable modules (while still retaining the single-page!).

Let’s add some very basic CSS, to make sure the menu bar doesn’t overlap with view area.

editor/post/post-frontend/src/css/screen.css
body{
    margin-top: 50px;
}
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
    display: none !important;
}
textarea {
    resize: none;
}

The ng-cloak stuff is there to prevent flickering when the application loads.

We are ready to build the application logic now. First, the configuration of the Posts module:

editor/post/post-frontend/src/app/module/posts/config.js
(function(){
    'use strict';
    var module = angular.module('app.posts', ['ngRoute']);
    function config($routeProvider){
        $routeProvider.when('/posts', {
            templateUrl: 'app/module/posts/view/index.html'
        });
    }
    config.$inject = ['$routeProvider'];
    module.config(config);
}());

This configures the route to the post view for the angular module app.posts. So when we click on the posts menu item, AngularJS shows us the post view.

Next, the model and the service:

editor/post/post-frontend/src/app/modules/posts/model/Post.js
(function() {
    'use strict';
    angular.module('app.posts').factory('Post', Implementation);

    function Implementation() {
        return Post;
        function Post(title, content) {
            var self = this;
            self.title = title;
            self.content = content;
        }
    }
}());
editor/post/post-frontend/src/app/modules/posts/service/PostService.js
(function() {
    'use strict';
    angular.module('app.posts').factory('PostService', Implementation);
    Implementation.$injector = ['Post', '$http'];
    function Implementation(Post, $http) {
        return {
            getPosts: getPosts
        };
        function getPosts() {
            return $http.get('/posts').then(function(response) {
                console.log(response.data);
                return response.data.map(function(post) {
                    return new Post(post.title, post.content);
                });
            });
        }
    }
}());

The service performs a GET request on the /posts resource and returns a list of Post instances.

The controller calls the service and puts the post list in the view model, so Angular can render it in the template:

editor/post/post-frontend/src/module/posts/controller/Post_indexController.js
(function(){
    'use strict';
    angular.module('app.posts').controller('Post_indexController', Constructor);
    Constructor.$injector = 'PostService';
    function Constructor(PostService){
        var vm = this;
        function initVm(){
            vm.refresh = function() {
                PostService.getPosts().then(function(data) { vm.posts = data });
            };
            vm.refresh();
        }
        initVm();
    }
}());

Finally the view just renders an unordered list:

editor/post/post-frontend/src/app/module/posts/view/index.html
<div class="container-fluid" ng-controller="Post_indexController as postVm">
    <h1>Posts</h1>
    <ul>
        <li ng-repeat="post in postVm.posts">{{post.title}} - {{post.content}}</li>
    </ul>
    <a ng-click="postVm.refresh()">refresh</a>
</div>

Now all we need to get this working standalone is a static version of the /posts resource:

editor/post/post-frontend/src/rest/posts
[{"id":1,"title":"sample post 1 from JSON file","content":"sample content 1"},{"id":2,"title":"sample post 2 from JSON file","content":"sample content 2"}]

The frontend of the post editor is complete, so let’s run it!

cd editor/post/post-frontend
npm install
gulp

Your browser should now open on http://localhost:8080/ and you should see this:

Post Editor v1

Nothing earth-shattering, but the functionality of the app is not important for this blog, it’s the way we build it.

Note

Does this seem like an awful lot of Javascript for a simple use case? I agree. In almost all circumstances, I would use Thymeleaf with Spring MVC for this type of application. I’m not a big fan of Single Page Applications, but AngularJS seems popular, so I decided to play with it for this blog.

Building the frontend with Gradle

As a lazy developer I don’t like to remember more than one command to build a project. In this case, I don’t want to bother with npm install every time I want to build the post editor. Also, I don’t want to install NodeJS and NPM on my build server. Luckily, someone in the Gradle community had the same problem and solved it already: enter the Gradle Gulp plugin. This plugin enables us to run gulp tasks from Gradle, first installing NodeJS, NPM and Gulp if necessary and update NPM dependencies.

The Gradle build file for our post editor front-end looks like this:

editor/post/post-frontend/build.gradle
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.moowork.gradle:gradle-gulp-plugin:0.10'
    }
}

apply plugin: 'java'
apply plugin: 'com.moowork.gulp'

node {
    //download NodeJS if we don't have it
    download=true
    // Version of node to use.
    version = '0.12.7'
    // Version of npm to use.
    npmVersion = '2.12.1'
}

installGulp.dependsOn npm_install
task('gulp_bower-install').dependsOn installGulp
gulp_build.dependsOn 'gulp_bower-install'
jar.dependsOn gulp_build
jar {
    from 'build/dist'
    eachFile { details ->
        details.path = details.path.startsWith('META-INF') ?: 'META-INF/resources/static/'+details.path
    }
    includeEmptyDirs = false
}
task cleanBower(type: Delete) {
    delete 'bower_components'
}
clean.dependsOn cleanBower
clean.dependsOn npm_cache_clean

First we apply the Java (we’re building a jar-file) and Gulp plugins. Then we tell the NPM plugin (which is a transitive dependency of the Gulp plugin) to download Node if needed, and which versions of Node and NPM to use. Then we set a number of task dependencies (bottom-up): The jar task needs the build/dist directory, so it depends on the Gulp build task. The Gulp build task needs Bower, so it depends on bower-install. All those Gulp tasks of course need Gulp to be installed, and in order to install Gulp we need NPM.

The jar task takes the output of the Gulp build and puts everything in the folder META-INF/resources/static/. We’re using this path so we can load these static resources from the jar file using Spring ResourceHandlers.

Finally the clean task also removes the NPM cache and the bower_components directory.

So building the frontend project from scratch can now be done with this command:

gradle clean build

Thanks to this Gradle build script we can now make the post editor backend depend on the frontend project.

Backend

Building with Gradle

As mentioned before, we want to make the frontend a dependency for the backend, so we’ll create a multi-project Gradle build.

In the editor/post directory, we’ll create 2 files for that:

editor/post/build.gradle
wrapper.gradleVersion = '2.5'
editor/post/settings.gradle
include 'post-frontend'
include 'post-backend'

Now we can create the post-backend project. This is the build file:

editor/post/post-backend
def vJavaLang = '1.8'
buildscript {
    ext.springRepo = 'http://repo.spring.io/libs-release'
    repositories {
        maven { url springRepo }
    }
    dependencies {
        classpath "org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE"
    }
}
apply plugin: 'java'
apply plugin: 'spring-boot'
targetCompatibility = vJavaLang
sourceCompatibility = vJavaLang
repositories {
    maven { url springRepo }
}
dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-actuator")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("com.h2database:h2")
    compile project(':post-frontend')
}

As you can see, we’re compiling against Java 8, and we’re using Spring Boot 1.2.5. All the Spring Boot magic happens thanks to the dependencies: the 4 starter dependencies will result in a Spring MVC web application that can be started locally with gradle bootRun, it will have endpoints for health monitoring, metrics and some other information, it will have basic security applied, and it will have database access to a relational database through JPA. By default, this will be an in-memory H2 database. Thanks to the post-frontend dependency, the frontend jar file will be on the class path of the web app.

The REST Service

First, we need an Application class to have Spring Boot initialize our web app.

editor/post/post-backend/src/main/java/…​/Application.java
package be.beeworks.editor.post;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class Application extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }
}

This is enough to have a working web application. But before we run it locally, let’s configure a couple of things:

editor/post/post-backend/src/main/resources/application.yml
server:
  port: 8081
spring:
  application:
    name: backend-post
security:
  user:
    password: posts
    role: VIEW_POST

Now, go ahead and run the application:

gradle bootRun

This will first build the post-frontend project, and then build and run the post-backend project in a Tomcat container on port 8081. To verify, go to http://localhost:8081/health and you should get some JSON similar to this:

{"status":"UP","diskSpace":{"status":"UP","free":290690367488,"threshold":10485760},"db":{"status":"UP","database":"H2","hello":1}}

So far so good. New let’s create the REST post resource. Start with the model:

editor/post/post-backend/src/main/java/be/beeworks/editor/post/model/Post.java
package be.beeworks.editor.post.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Post {
    @Id
    @GeneratedValue
    private Long id;
    private String title;
    private String content;
    public Post() {
    }
    public Post(String title) {
        this.title = title;
    }
    public Post(String title, String content) {
        this.title = title;
        this.content = content;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
}

Now the repository. With Spring Data, we can have a CRUD repository generated on the fly by just extending the CrudRepository interface. Additional finder methods will be generated if we add methods to the interface and follow the relevant naming conventions (check the Spring Data reference documentation).

editor/post/post-backend/src/main/java/be/beeworks/editor/post/repository/PostRepository.java
package be.beeworks.editor.post.repository;
import be.beeworks.editor.post.model.Post;
import org.springframework.data.repository.CrudRepository;
import java.util.List;

public interface PostRepository extends CrudRepository<Post,Long> {
    List<Post> findByTitle(String title);
}

And finally the controller:

editor/post/post-backend/src/main/java/be/beeworks/editor/post/controller/PostController.java
package be.beeworks.editor.post.controller;
import be.beeworks.editor.post.model.Post;
import be.beeworks.editor.post.repository.PostRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import javax.transaction.Transactional;
import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("/posts")
public class PostController {

    private static final Logger logger = LoggerFactory.getLogger(PostController.class);

    @Autowired
    private PostRepository postRepository;

    @RequestMapping(value = "", method = RequestMethod.GET)
    @Secured({"ROLE_VIEW_POST"})
    public Iterable<Post> getPosts() {
        logger.info("requesting posts");
        return postRepository.findAll();
    }

    @PostConstruct
    public void createSamplePosts() {
        postRepository.save(new Post("sample post 1 from database", "sample content 1"));
        postRepository.save(new Post("sample post 2 from database", "sample content 2"));
    }
}

Note that I’ve been extremely lazy here: rather than add endpoints for creating posts, I just added a init-method to the controller that inserts 2 records in the database for me. Other than that it’s a pretty standard Spring MVC REST controller, with a GET request mapped to /posts that returns a list of posts from the repository. The endpoint is secured with basic auth and only users with the VIEW_POST role are allowed to access it.

The final step to getting the frontend working with the /posts endpoint from the backend is telling Spring where to find our frontend resources, and forwarding the root context to /index.html:

editor/post/post-backend/src/main/java/be/beeworks/editor/post/ResourceConfiguration.java
package be.beeworks.editor.post;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class ResourceConfiguration extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/static/");
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("forward:/index.html");
    }
}

Start the application again with gradle bootRun, and direct your browser to http://localhost:8081/. A basic auth dialog will pop up, so log in with username user and password posts, click on the Posts menu item and you should see this:

Post Editor v2

And there you have it, our standalone post editor.

Coming up

In the next blog post, we will add an API gateway in front of this post editor application, and introduce distributed sessions. Finally we will add a second web application next to the post editor, which will be integrated in the single-page app.