Day 22: Developing Single Page Applications with Spring, MongoDB, and AngularJS - Archived

Today for my 30 day challenge, I decided to develop a single page web application using the Spring framework, MongoDB, and AngularJS. I have a good understanding of Spring and MongoDB but have never used AngularJS with the Spring framework.

We will develop a social bookmarking application like the one we developed with EmberJS a few days ago. I covered AngularJS basics on day 2 so please refer to the AngularJS app for more information.

We will use the latest version of the Spring framework i.e. 3.2.5.RELEASE and no XML (not even web.xml). We will configure everything using Spring annotation support. The Spring MVC(along with Spring framework) will create the RESTful backend. AngularJS will be used as the client side MVC framework to develop the frond-end of the application.

Application Usecase

In this blog post, we will develop a social bookmarking application which allows users to post and share links. You can view the live application running on OpenShift here. The application can do the following:

  • When a user goes to the ‘/’ url of the application, then the user will see a list of stories sorted in the application database. Behind the curtain, AngularJS makes a REST(/api/v1/stories) call to fetch all the stories.

GetBookMarks Home Page

GetBookmarks Story View

  • Finally, a user can submit a new story by navigating to http://getbookmarks-shekhargulati.rhcloud.com/#/stories/new. This will make a POST call to the RESTful backend and save the story in the MongoDB datastore. The user is only required to enter the url of the story. The application will fetch the title, main image, and excerpt of the story using Goose Extractor RESTful API developed on day 16.

GetBookmarks Story Submit

Prerequisite

  1. Basic Java knowledge is required. Install the latest Java Development Kit (JDK) on your operating system. You can either install OpenJDK 7 or Oracle JDK 7. OpenShift support OpenJDK 6 and 7.

  2. Basic Spring Knowledge is required.

  3. Sign up for an OpenShift Account. It is completely free and Red Hat gives every user three free Gears on which to run your applications. At the time of this writing, the combined resources allocated for each user is 1.5 GB of memory and 3 GB of disk space.

  4. Install the rhc client tool on your machine. RHC is a ruby gem so you need to have ruby 1.8.7 or above on your machine. To install rhc, just typesudo gem install rhc
    If you already have one, make sure it is the latest one. To update your rhc, execute the command shown below.sudo gem update rhc
    For additional assistance setting up the rhc command-line tool, see the following page: https://openshift.redhat.com/community/developers/rhc-client-tools-install

  5. Setup your OpenShift account using rhc setup command. This command will help you create a namespace and upload your ssh keys to OpenShift server.

Github Repository

The code for today’s demo application is available on github: day22-spring-angularjs-demo-app.

Step 1 : Create a Tomcat 7 application

We will start by creating a new application with a Tomcat 7 and a MongoDB cartridge.

$ rhc create-app getbookmarks tomcat-7 mongodb-2

This will create an application container for us, called a gear, and setup all of the required SELinux policies and cgroup configuration. OpenShift will also setup a private git repository for you and clone the repository to your local system. Finally, OpenShift will propagate the DNS to outside world. The application will be accessible at http://getbookmarks-{domain-name}.rhcloud.com/. Replace the {domain-name} with your own unique OpenShift domain name (also sometimes called a namespace).

Step 2 : Delete the template code

Next we will delete the template code created by OpenShift.

$ cd getbookmarks
$ git rm -rf src/main/webapp/*.jsp src/main/webapp/index.html src/main/webapp/images src/main/webapp/WEB-INF/web.xml  
$ git commit -am "deleted template files"

Please note that we also deleted web.xml.

Step 3 : Update the pom.xml

Next, update the application pom.xml with the one shown below.

<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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>getbookmarks</groupId>
    <artifactId>getbookmarks</artifactId>
    <packaging>war</packaging>
    <version>1.0</version>
    <name>getbookmarks</name>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>3.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>3.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-mongodb</artifactId>
            <version>1.3.2.RELEASE</version>
        </dependency>
 
        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-mapper-asl</artifactId>
            <version>1.9.13</version>
        </dependency>
 
 
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
 
    </dependencies>
    <profiles>
        <profile>
 
            <id>openshift</id>
            <build>
                <finalName>getbookmarks</finalName>
                <plugins>
                    <plugin>
                        <artifactId>maven-war-plugin</artifactId>
                        <version>2.4</version>
                        <configuration>
                            <failOnMissingWebXml>false</failOnMissingWebXml>
                            <outputDirectory>webapps</outputDirectory>
                            <warName>ROOT</warName>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
 
</project>

In the pom.xml shown above

  1. We added Maven dependencies for spring-webmvc, spring-mongodb, jackson, and latest servlet api.
  2. We updated the project to use JDK 7 instead of JDK 6.
  3. We updated the project to use latest version of Maven war plugin and added a configuration to avoid build failure when web.xml does not exist.

After making this change, make sure to update the maven project by Right Click > Maven > Update Project.

Step 4 : Write WebMvcConfig and AppConfig class

Create a new package com.getbookmarks.config and create a new class WebMvcConfig. Update the code with the shown below. The class shown below will enable the Spring Web MVC framework.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.json.MappingJacksonJsonView;
 
@EnableWebMvc
@ComponentScan(basePackageClasses = StoryResource.class)
@Configuration
public class WebMvcConfig{
 
}

Next we will write another configuration class AppConfig. Spring MongoDB has concept of repositories where in you implement interface and Spring will generate a proxy class with the implementation. This makes it very easy to write repository classes and removes lots of boiler plate code. The Spring MongoDB allows us to declaratively enable Mongo repositories by specifying @EnableMongoRepositories annotation.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.authentication.UserCredentials;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
 
import com.getbookmarks.repository.StoryRepository;
import com.mongodb.Mongo;
 
@Configuration
@EnableMongoRepositories(basePackageClasses = StoryRepository.class)
public class ApplicationConfig {
 
    @Bean
    public MongoTemplate mongoTemplate() throws Exception {
        String openshiftMongoDbHost = System.getenv("OPENSHIFT_MONGODB_DB_HOST");
        int openshiftMongoDbPort = Integer.parseInt(System.getenv("OPENSHIFT_MONGODB_DB_PORT"));
        String username = System.getenv("OPENSHIFT_MONGODB_DB_USERNAME");
        String password = System.getenv("OPENSHIFT_MONGODB_DB_PASSWORD");
        Mongo mongo = new Mongo(openshiftMongoDbHost, openshiftMongoDbPort);
        UserCredentials userCredentials = new UserCredentials(username, password);
        String databaseName = System.getenv("OPENSHIFT_APP_NAME");
        MongoDbFactory mongoDbFactory = new SimpleMongoDbFactory(mongo, databaseName, userCredentials);
        MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory);
        return mongoTemplate;
    }
 
}

The proxy repository classes internally use MongoTemplate to perform operations. We defined a MongoTemplate bean which uses OpenShift MongoDB credentials.

The code shown enables Spring MVC support in the application using @EnableWebMvc annotation.

Step 5: Write GetBookmarksWebApplicationInitializer class

With Servlet 3.0, the web.xml is optional. Normally, we configure Spring WebMVC dispatcher servlet in web.xml but now we can programmatically configure it using WebApplicationInitializer. From Spring 3.1, Spring provides an implementation of the ServletContainerInitializer interface called SpringServletContainerInitializer. TThe SpringServletContainerInitializer class delegates to an implementation of
org.springframework.web.WebApplicationInitializer that you provide. There is just one method that you need to implement: WebApplicationInitializer#onStartup(ServletContext). You are handed the ServletContext that you need to initialize.

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
 
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
 
public class GetBookmarksWebApplicationInitializer implements WebApplicationInitializer {
 
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
        webApplicationContext.register(ApplicationConfig.class, WebMvcConfig.class);
 
        Dynamic dynamc = servletContext.addServlet("dispatcherServlet", new DispatcherServlet(webApplicationContext));
        dynamc.addMapping("/api/v1/*");
        dynamc.setLoadOnStartup(1);
    }
 
}

Step 6 : Create Story domain class

In this application, we only have one domain class called Story.

@Document(collection = "stories")
public class Story {
 
    @Id
    private String id;
 
    private String title;
 
    private String text;
 
    private String url;
 
    private String fullname;
 
    private final Date submittedOn = new Date();
 
    private String image;
 
    public Story() {
    }
 
// Getter and Setter removed for brevity

Important things in the code snippet above are :

  1. @Document annotation identifies a domain object to be persisted in MongoDB. The stories specify the name of collection which will be created in MongoDB.
  2. @Id annotation marks this field as Id field which will be auto generated by MongoDB.

Step 7 : Create StoryRepository

As mentioned above, Spring MongoDB has concept of repositories wherein developer write an interface and Spring generates a proxy implementation class.Let us create the StoryRepository as shown below.

import java.util.List;
 
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
 
import com.getbookmarks.domain.Story;
 
@Repository
public interface StoryRepository extends CrudRepository<Story, String> {
 
    public List<Story> findAll();
}

The important things in the code snippet shown above are :

  1. StoryRepository interface extends CrudRepository interface which defines CRUD methods and finder methods. So, the proxy generated by Spring will have all those methods.
  2. @Repository annotation is a specialization of @Component annotation which indicates that class is a repository or DAO class. A class annotated with @Repository is eligible for Spring DataAccessException translation when used in conjunction with a PersistenceExceptionTranslationPostProcessor.

Step 8 : Write StoryResource

Next, we will write the REST JSON web service for performing create and read operations on Story. To do that, we will create a Spring MVC controller with methods shown below.

@Controller
@RequestMapping("/stories")
public class StoryResource {
 
    private StoryRepository storyRepository;
 
    @Autowired
    public StoryResource(StoryRepository storyRepository) {
        this.storyRepository = storyRepository;
    }
 
    @RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ResponseEntity<Void> submitStory(@RequestBody Story story) {
        Story storyWithExtractedInformation = decorateWithInformation(story);
        storyRepository.save(storyWithExtractedInformation);
        ResponseEntity<Void> responseEntity = new ResponseEntity<>(HttpStatus.CREATED);
        return responseEntity;
    }
 
    @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public List<Story> allStories() {
        return storyRepository.findAll();
    }
 
    @RequestMapping(value = "/{storyId}", produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Story showStory(@PathVariable("storyId") String storyId) {
        Story story = storyRepository.findOne(storyId);
        if (story == null) {
            throw new StoryNotFoundException(storyId);
        }
        return story;
    }
 
    private Story decorateWithInformation(Story story) {
        String url = story.getUrl();
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<Story> forEntity = restTemplate.getForEntity(
                "http://gooseextractor-t20.rhcloud.com/api/v1/extract?url=" + url, Story.class);
        if (forEntity.hasBody()) {
            return new Story(story, forEntity.getBody());
        }
        return story;
 
    }
 
}

Step 9 : Setup AngularJS and Twitter Bootstrap

Download the latest copy of AngularJS and Bootstrap from their respective official websites, or you can copy the resources from this project github repository.

Step 10: Create Index.html

Now we will write the view of the application.

<!DOCTYPE html>
<html ng-app="getbookmarks">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
    <link rel="stylesheet" href="css/bootstrap.css"/>
    <link rel="stylesheet" href="css/toastr.css"/>
    <style>
        body {
            padding-top: 60px;
        }
    </style>
    <title>GetBookmarks : Submit Story</title>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="navbar-inner">
        <div class="container">
            <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="brand" href="#">GetBookmarks</a>
 
        </div>
    </div>
</div>
 
<div class="container" ng-view>
 
</div>
 
<script src="js/jquery-1.9.1.js"></script>
<script src="js/bootstrap.js"></script>
<script src="js/angular.js"></script>
<script src="js/angular-resource.js"></script>
<script src="js/toastr.js"></script>
<script src="js/app.js"></script>
</body>
</html>

In the html shown above

  1. We imported all the required libraries. Our application code is in app.js.
  2. In Angular, you define the scope of the project using ng-app directive. We used ng-app on the html element but we can use it with any other element as well. Using the ng-app directive with html element means that AngularJS is available on the whole index.html. The ng-app directive can take a name. This name is the module name. I used getbookmarks as this application module name.

  3. The last interesting thing in the index.html is the use of ng-view directive. The ngView directive renders the template corresponding to the current route inside index.html. So, everytime you navigate to a route only the ng-view portion changes.

Step 11: Write AngularJS code

The app.js houses all the application specific JavaScript. All the application routes are defined inside it. In the code shown below, we have defined three routes and each has a corresponding Angular controller.

angular.module("getbookmarks.services", ["ngResource"]).
    factory('Story', function ($resource) {
        var Story = $resource('/api/v1/stories/:storyId', {storyId: '@id'});
        Story.prototype.isNew = function(){
            return (typeof(this.id) === 'undefined');
        }
        return Story;
    });
 
angular.module("getbookmarks", ["getbookmarks.services"]).
    config(function ($routeProvider) {
        $routeProvider
            .when('/', {templateUrl: 'views/stories/list.html', controller: StoryListController})
            .when('/stories/new', {templateUrl: 'views/stories/create.html', controller: StoryCreateController})
            .when('/stories/:storyId', {templateUrl: 'views/stories/detail.html', controller: StoryDetailController});
    });
 
function StoryListController($scope, Story) {
    $scope.stories = Story.query();
 
}
 
function StoryCreateController($scope, $routeParams, $location, Story) {
 
    $scope.story = new Story();
 
    $scope.save = function () {
        $scope.story.$save(function (story, headers) {
            toastr.success("Submitted New Story");
            $location.path('/');
        });
    };
}
 
 
function StoryDetailController($scope, $routeParams, $location, Story) {
    var storyId = $routeParams.storyId;
 
    $scope.story = Story.get({storyId: storyId});
 
}

Step 12 : Deploy the code

Finally, commit the code and push it to application gear.

$ git add .
$ git commit -am "application code"
$ git push

That’s it for today. Keep giving feedback.

Next Steps

Automatic Updates

Stay informed and learn more about OpenShift by receiving email updates.

Categories
MongoDB, OpenShift Online
Tags
,
Comments are closed.