A playful angle to web development

After JavaScript revolution that Node.js started, new frameworks and libraries have been born almost everyday. In the Java world pace has been a lot slower but Node.js has been giving enough pressure to make sure Java people are not just standing on their feet. IDEs have been bringing support for new libraries and tools like Grunt and Bower which indicates that enough many people use those, otherwise there wouldn't be any reason for IDEs to add support for them.

Java 6 has been the de facto in Java world for many years but that is changing. According to a recent study by ZeroTurnaround 35% developers use Java 7 on Java EE and already 65% on Java SE. Around 7% are already using Java 8 on Java SE. However, one PITA in Java development has been long deployment times - especially painful when developing frontend stuff. We have been using ZeroTurnaround's fabulous JRebel for many years and couldn't think of developing Java applications without it. Of course OSGi with its component model is giving another way out of long deployment times. Still many people are longing for a more lean way of developing, like Django and Ruby on Rails provide. Could that be possible in Java world?

One answer to that question is Play Framework. It provides a similar development path as Django and RoR for Java and Scala developers. Play 1.0 was built on Java but Play 2.0 is already fully built using Scala. Of course still providing Java compatibility. Play promises to be light, stateless and web-friendly framework. Also, its future seems solid as Typesafe has picked it as their Reactive Platform. One of the founding members of Typesafe is Martin Odersky, known for developing Scala language.

We have been using Play in some of our projects and so far experience has been positive. Together with Play we have also used AngularJS to build the UI. Angular is a very popular MVW(model-view-whatever) JavaScript framework. I want to demonstrate how easy and fast it is to build a web application using these two technologies. One good reason for this pairing is that Play treats JSON as a first class citizen and this provides en easy communication channel between Play and Angular.

The basic idea is that Play handles the backend and provides JSON responses for Angular. Angular handles all the UI things and perhaps does some own queries to other 3rd party services. Since WebJars are making their way into everyday use, I decided to use them for frontend dependencies. The application is a showcase of movie posters including detailed information about movies such as their ratings. It's also a responsive design making some clever use of Masonry (JavaScript based cascading grid layout).

First I created a new play project:

$ play new movies-example

and started planning the API through routes. So I opened conf/routes file and added the following routes to it:

GET        /                     controllers.Application.index
GET        /movies               controllers.Application.movies(year: Option[Int])
GET        /movies/:id           controllers.Application.movie(id: Long)

GET        /*file                controllers.Assets.at(path="/public", file)

This enables me to request all movies, filter the movies by year, get specific movie and get a list of years for which we have movies. So very simple API, no POST-methods or anything else relating to inserting data, just getting movie-objects from database.

Next I ran into Slick and it seemed like an awesome way to define data models and query the database using plain Scala. So no writing SQL involved. I created a Movies.scala file to app/models folder and modeled out a basetable for movie and a movies table. Our Movie-object has id, name, year, imdb id(for linking to imdb) and poster url(url to movie poster image file). Posters are from MoviePosterDB which allows everyone to download 300 pixels wide posters for free.

abstract class BaseTable[T](tag: Tag, name: String) extends Table[T](tag, name) {
  def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
}

case class Movie(id: Option[Long], name: String, year: Int, imdbID: String, posterURL: String)

class Movies(tag: Tag) extends BaseTable[Movie](tag, "movies") {
  def name: Column[String] = column[String]("name", O.NotNull)
  def year: Column[Int] = column[Int]("year", O.NotNull)
  def imdbID: Column[String] = column[String]("imdb_id", O.NotNull)
  def posterURL: Column[String] = column[String]("poster_url", O.NotNull)

  def * = {
    (id.?, name, year, imdbID, posterURL) <> (Movie.tupled, Movie.unapply)
  }
}

object Movies {
  val movies = TableQuery[Movies]
}

This is pretty basic definition, a similar example can be found in Slick's documentation. This enables me to actually add some data to database. I'm using the included H2 in-memory database here for making it simple. Here are the needed dependencies for build.sbt:

libraryDependencies ++= Seq(
  jdbc,
  cache,
  "com.typesafe.play" %% "play-slick" % "0.6.0.1",
  "com.typesafe.slick" %% "slick" % "2.0.2",
  "org.slf4j" % "slf4j-nop" % "1.6.4",
  "com.h2database" % "h2" % "1.3.175"
)

Next I will add some data using the Global.scala file in the app folder:

val movies: TableQuery[Movies] = TableQuery[Movies]
play.api.db.slick.DB.withSession{ implicit session =>
  movies.ddl.drop
  movies.ddl.create

  val moviesInsertResult: Option[Int] = movies ++= Seq (
    Movie(None, "How to Train Your Dragon 2", 2014, "tt1646971", "l_1646971_91766e57.jpg"),
    ...
  )

You can see how easy it is to add data. The call to movies.ddl.drop is just a safe measurement to clean the db and make sure it's always updated when doing development. Normally we would just need the movies.ddl.create.

Now that we have created the models and have added some data, let's configure some actions for the routes and see if we can get JSON out from our API. To be able to query our data we need some functions to do the queries so let's add those to app/models/Movies.scala inside the object Movies:

object Movies {
  val movies = TableQuery[Movies]
  def get(id: Long)(implicit s: Session): Option[Movie] = movies.where(_.id === id).firstOption
  def list()(implicit s: Session): List[Movie] = movies.list
  def findByYear(year: Int)(implicit s: Session): List[Movie] = {
    val query = for (m <- movies if m.year is year) yield m
    query.list
  }
}

These new functions enable us to get a single movie object, get all movies or get all movies for a specific year. Next I added the actions to controllers/Application.scala:

implicit val messageJsonWriter = Json.writes[Movie]

def movies(year: Option[Int]) = DBAction { implicit rs =>
  implicit val s = rs.dbSession
  val movies = year match {
    case Some(y) => Movies.findByYear(y)
    case None => Movies.list
  }
  Ok(Json.toJson(Map("movies" -> movies)))
}

def movie(id: Long) = DBAction { implicit rs =>
  implicit val s = rs.dbSession
  val movie =  Movies.get(id)
  Ok(Json.toJson(movie))
}

Now we can test how all these work by firing up play server play run. After making the first request to http://localhost:9000 we should get a question asking us to apply database evolutions, this will add all the movies into the database. Next, let's request movies using curl for demonstration purposes:

$ curl http://localhost:9000/movies
{"movies":[{"id":1,"name":"How to Train Your Dragon 2","year":2014,"imdbID":"tt1646971","posterURL":"l_1646971_91766e57.jpg"},
{"id":2,"name":"Star Trek: Into Darkness","year":2013,"imdbID":"tt1408101","posterURL":"l_1408101_ce007376.jpg"},
{"id":3,"name":"The Hunger Games: Catching Fire","year":2013,"imdbID":"tt1951264","posterURL":"l_1951264_8fb3bb2c.jpg"},...}

$ curl http://localhost:9000/movies/1
{"id":1,"name":"How to Train Your Dragon 2","year":2014,"imdbID":"tt1646971","posterURL":"l_1646971_91766e57.jpg"}

$ curl "http://localhost:9000/movies?year=2012"
{"movies":[{"id":18,"name":"Django Unchained","year":2012,"imdbID":"tt1853728","posterURL":"l_1853728_f77f8307.jpg"},
{"id":19,"name":"The Avengers","year":2012,"imdbID":"tt0848228","posterURL":"l_848228_6ed314dd.jpg"},
{"id":20,"name":"The Dark Knight Rises","year":2012,"imdbID":"tt1345836","posterURL":"l_1345836_a7e751aa.jpg"},...}

We can see our API is working as intended and providing list of all movies, movie by id and movies filtered by year. As the API is now in its initial working state we can move on to implementing the UI with Angular. For that we need to add some dependencies to our build.sbt file:

libraryDependencies ++= Seq(
  jdbc,
  cache,
  "com.typesafe.play" %% "play-slick" % "0.6.0.1",
  "com.typesafe.slick" %% "slick" % "2.0.2",
  "org.slf4j" % "slf4j-nop" % "1.6.4",
  "com.h2database" % "h2" % "1.3.175",
  "org.webjars" %% "webjars-play" % "2.2.1-2",
  "org.webjars" % "angularjs" % "1.2.16-2",
  "org.webjars" % "masonry" % "3.1.5",
  "org.webjars" % "foundation" % "5.2.2",
  "org.webjars" % "foundation-icon-fonts" % "d596a3cfb3"
)

And a new route for WebJars to first line of conf/routes:

GET        /webjars/*file        controllers.WebJarAssets.at(file)

I'm using WebJars to provide our frontend dependencies. Webjars-play will provide some helpers and integrate RequireJS support for our project. Then we have of course Angular and for making it look pretty, we have Masonry and Foundation. After this we need to setup views/index.scala.html for dependencies and angular wiring. Here's a snippet of the most important things:

<body>
    <div class="container">
        <div ng-view></div>
    </div>

    @Html(org.webjars.play.RequireJS.setup("js/app"))
</body>

The ng-view tells where are angular views are located and the @Html...-helper inserts RequireJS stuff to our page.

So let's get working. Below I've listed the files that are important for our angular implementation. There are also other files listed in the github-repository and those do contribute to the UI look but they don't define any functionality so I haven't listed them here.

├── public
│   ├── js
│   │   ├── app.js
│   │   ├── controllers.js
│   │   └── services.js
│   └── partials
│       ├── movie.html
│       └── movies.html

This follows pretty much the angular-seed project layout and more precisely the one made for Play. So in the app.js we have:

require(['angular', './controllers', './services', 'angular-route', './angular-masonry'],
    function(angular, controllers) {

        angular.module('movies', ['movies.services', 'wu.masonry', 'ngRoute']).
            config(['$routeProvider', function($routeProvider) {
                $routeProvider.when('/movies', {templateUrl: 'partials/movies.html', controller: controllers.Movies});
                $routeProvider.when('/movies/:id', {templateUrl: 'partials/movie.html', controller: controllers.Movie});
                $routeProvider.otherwise({redirectTo: '/movies'});
            }]);

        angular.bootstrap(document, ['movies']);
});

So here we use RequireJS to define which modules we need and then we bootstrap angular and define its modules. Followed by defining basic routing for our UI. We have /movies for listing all movies and /movies/:id for getting a page for a single movie. Ok, so next let's define a service for getting the JSON data from our API in the services.js file:

angular.module('movies.services', []).
    factory('moviesAPIservice', function($http) {
        var moviesAPI = {};

        moviesAPI.getMovies = function(year) {
            var url = "/movies";
            if (year) url += "?year=" + year;
            return $http({
                method: 'GET',
                url: url
            });
        };

        return moviesAPI;
    });
});

So we have a function for getting all movies or optionally requesting movies from a specific year. To use this data we need to define a controller that connects to a view. Let's do this in controllers.js:

define(['angular'], function(angular) {

var controllers = {};

controllers.Movies = function($scope, $routeParams, moviesAPIservice) {
    $scope.year = $routeParams.year;
    moviesAPIservice.getMovies($scope.year).success(function(response) {
        $scope.movies = response.movies;
    });
};

controllers.Movies.$inject = ["$scope", "$routeParams", "moviesAPIservice"];

return controllers;

});

Here we use angular's $routeParams to read the optional year-parameter and then call our moviesAPIservice. If the call is successful we provide the movies-list to our view. The $scope variable provides access between controllers and views and view will get all data from it.

To finish up let's edit the partials/movies.html to provide us viewable results.

<div ng-repeat="movie in movies">
    <img ng-src="/images/posters/{{movie.posterURL}}" alt="{{movie.name}}" />
    <span class="name">{{movie.name}}</span>
    Release year: {{movie.year}}
    <a ng-href="http://www.imdb.com/title/{{movie.imdbID}}/">IMDb</a>
</div>

Now, if you open your browser you should be able to see that http://localhost:9000/ redirects to /#/movies and shows you a list of movies. This of course looks ugly and needs fine tuning, but it should show you how fast you can build a JSON API using Scala and Play and providing user interface with Angular. There is only a tiny hint of Angular magic here, check for example how search is implemented and you will be surprised (hint: it's just a one input-element).

You can check a demo application that is hosted on Heroku. It might take a couple seconds to spin up as its hosted on the free tier which spins down every hour. The source code is available at github which includes all the rest code for single movie page and implementing search and year filters. You can find me also on Twitter if you have something to ask. Special thanks to Timo Hanhirova for helping me with the Slick stuff.

Katso viimeisimmät julkaisut
Lue lisää

Blogi

Liikenteen digitalisaatiota kiihdyttämässä Teslalla

Lue lisää

Blogi

Jfokus 2017 Developers Conference in Stockholm

I had the opportunity to visit Sweden's largest software developer conference Jfokus2017 on February 7-8. What was the conference like and what did I learn?

Lue lisää

Blogi

Opiskelijalle ongelmanratkaisun monitoimityökalut NASA:n Epic Challenge -koulutusohjelmasta

Sain ainutlaatuisen mahdollisuuden osallistua ensimmäisten joukossa Epic Challenge -koulutusohjelmaan ja pääsin työskentelemään yhdessä NASA:n asiantuntijoiden kanssa. Millainen kokemus Epic Challenge oli ja ennen kaikkea, mitä se opetti minulle?