Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Create A Full Stack App Using Java & Couchbase Server

TwitterFacebookRedditLinkedInHacker News

Full stack development is all the rage right now. Knowing how all the bits and pieces of an application works is a necessity in modern development. Previously I demonstrated how easy it was to create a full stack application using the Couchbase, Express, AngularJS, and Node.js (CEAN) stack, but this time we’re going to be swapping out Node.js for Java.

We’re going to look at creating a full stack application where Java and Couchbase Server acts as our back-end and AngularJS, HTML, and CSS acts as our front-end.

The goal here is to create a simple user management system that has a back-end accessibly via HTTP requests issued, in this case, by AngularJS. This allows us to use an API First approach to development, keeping the door open for further front-ends beyond AngularJS or even desktop computing.

The Prerequisites

We’ll be doing a lot of configuration throughout this guide, but this will give you a good sense of the tools we’ll be using and their versions.

  • Couchbase Server 4.1+
  • Maven
  • AngularJS
  • Bootstrap

Installing Couchbase Server

Let’s start with installing our database, Couchbase Server. In case you’re unfamiliar, Couchbase is an open source NoSQL document database. Head over to the downloads section of the Couchbase website and get the latest installation for your computer, whether it be Mac, Windows, or Linux.

Install the file that you downloaded and launch Couchbase Server. Upon first launch it should take you through a configuration wizard. The wizard takes about two minutes to complete.

Creating A Bucket And Index

Couchbase stores all NoSQL documents in what’s called a bucket. We’ll need to create one called restful-sample in order to continue in the application. From the Data Buckets tab of your Couchbase administrative dashboard, choose to Create New Data Bucket and name it appropriately.

Couchbase Dashboard Create Bucket

With the bucket created, we need to create a primary index on it. This will allow us to run N1QL queries against the data. You’ll see what they are and why they are beautiful deeper in this tutorial.

Run the Couchbase Query (CBQ) client from the Terminal or Command Prompt. On a Mac, it is found at the following path:

./Applications/Couchbase Server.app/Contents/Resources/couchbase-core/bin/cbq

On a Windows computer it is found at the following path:

C:/Program Files/Couchbase/Server/bin/cbq.exe

With CBQ open, run the following statement to create a primary index:

CREATE PRIMARY INDEX ON `restful-sample` USING GSI;

Couchbase Server and our bucket are now ready to be used!

The Project Back-End

Before jumping into any code, we need to draw out the project structure for our back-end. In a new folder, maybe on your Desktop, create the following files and directories:

src
    main
        java
            couchbase
                Application.java
                Database.java
        resources
            application.properties
pom.xml

To explain, all our Maven dependencies will be placed in our pom.xml file. Couchbase configurations such as database host and bucket will end up in our project’s src/main/resources/application.properties file and all application code will go into the Java classes.

The src/main/java/couchbase/Application.java file will hold all our API endpoints and the src/main/java/couchbase/Database.java file will hold all our database queries or interactions with Couchbase.

Our Maven Dependencies

Let’s start with the pom.xml file. Open it and add the following 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>com.couchbase.fullstack</groupId>
    <artifactId>java-fullstack</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.1.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.couchbase.client</groupId>
            <artifactId>java-client</artifactId>
            <version>2.2.3</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>couchbase</id>
            <name>couchbase repo</name>
            <url>http://files.couchbase.com/maven2</url>
            <snapshots><enabled>false</enabled></snapshots>
        </repository>
    </repositories>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Time to break it down.

<groupId>com.couchbase.fullstack</groupId>
<artifactId>java-fullstack</artifactId>
<version>1.0-SNAPSHOT</version>

This is our application package name and version. You can name it whatever you want as long as it exists.

We’ll be using Spring Boot which is why we’re including the org.springframework.boot dependency and we’re going to use the Java SDK for Couchbase which is why we’re including the com.couchbase.client dependency.

Maven won’t know where to obtain the Couchbase dependency which is why we include a particular repository to search in.

Our Couchbase Configuration

Next up we want to take a look at the src/main/resources/application.properties file we created. Open it and include the following three lines:

hostname=127.0.0.1
bucket=restful-sample
password=

Nothing special here, only the details to our server and bucket.

Our Database Class

This is where the bulk of our code will go. Open our project’s src/main/java/couchbase/Database.java file and include the following skeleton code:

package couchbase;

import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.query.*;
import com.couchbase.client.java.query.consistency.ScanConsistency;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.UUID;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class Database {

    private Database() { }

    public static List<Map<String, Object>> getAll(final Bucket bucket) {

    }

    public static List<Map<String, Object>> getByDocumentId(final Bucket bucket, String documentId) {

    }

    public static List<Map<String, Object>> delete(final Bucket bucket, String documentId) {

    }

    public static List<Map<String, Object>> save(final Bucket bucket, JsonObject data) {

    }

    private static List<Map<String, Object>> extractResultOrThrow(N1qlQueryResult result) {

    }

}

Every time an endpoint is hit, it will call a different database function in this Database class. The first function we want to define is the extractResultOrThrow because every other function makes use of it.

private static List<Map<String, Object>> extractResultOrThrow(N1qlQueryResult result) {
    if (!result.finalSuccess()) {
        throw new DataRetrievalFailureException("Query error: " + result.errors());
    }
    List<Map<String, Object>> content = new ArrayList<Map<String, Object>>();
    for (N1qlQueryRow row : result) {
        content.add(row.value().toMap());
    }
    return content;
}

This function takes a result from a Couchbase N1QL query, and loops over the result set while parsing it into a List of Map objects. This data type is something Spring Boot can return to the client as a JSON response.

Next up let’s look at the function responsible for returning all documents in the particular bucket. I’m talking about the getAll function:

public static List<Map<String, Object>> getAll(final Bucket bucket) {
    String queryStr = "SELECT META(users).id, firstname, lastname, email " +
                   "FROM `" + bucket.name() + "` AS users";
    N1qlQueryResult queryResult = bucket.query(N1qlQuery.simple(queryStr, N1qlParams.build().consistency(ScanConsistency.REQUEST_PLUS)));
    return extractResultOrThrow(queryResult);
}

Here we’re using a SQL-like N1QL query, passing the result into the extractResultOrThrow function, and returning the data back to the parent function that called it.

When it comes to working with a particular document we do things a bit different. In the getByDocumentId function we have the following:

public static List<Map<String, Object>> getByDocumentId(final Bucket bucket, String documentId) {
    String queryStr = "SELECT firstname, lastname, email " +
                   "FROM `" + bucket.name() + "` AS users " +
                   "WHERE META(users).id = $1";
    ParameterizedN1qlQuery query = ParameterizedN1qlQuery.parameterized(queryStr, JsonArray.create().add(documentId));
    N1qlQueryResult queryResult = bucket.query(query);
    return extractResultOrThrow(queryResult);
}

This time we use a parameterized query. We do this because we don’t want to fall victim to a SQL injection attack. Parameterized queries sanitize user input to prevent this, thus the use of the $1 in the query itself.

Starting in Couchbase 4.1 you can use full DML making DELETE queries a possibility. Here is our delete function:

public static List<Map<String, Object>> delete(final Bucket bucket, String documentId) {
    String queryStr = "DELETE " +
            "FROM `" + bucket.name() + "` AS users " +
            "WHERE META(users).id = $1";
    ParameterizedN1qlQuery query = ParameterizedN1qlQuery.parameterized(queryStr, JsonArray.create().add(documentId));
    N1qlQueryResult queryResult = bucket.query(query);
    return extractResultOrThrow(queryResult);
}

Again it uses a parameterized query to prevent an injection attack.

This brings us to the save function which is probably the more complicated in our entire project.

public static List<Map<String, Object>> save(final Bucket bucket, JsonObject data) {
    String documentId = !data.getString("document_id").equals("") ? data.getString("document_id") : UUID.randomUUID().toString();
    String queryStr = "UPSERT INTO `" + bucket.name() + "` (KEY, VALUE) VALUES " +
            "($1, {'firstname': $2, 'lastname': $3, 'email': $4})";
    JsonArray parameters = JsonArray.create()
            .add(documentId)
            .add(data.getString("firstname"))
            .add(data.getString("lastname"))
            .add(data.getString("email"));
    ParameterizedN1qlQuery query = ParameterizedN1qlQuery.parameterized(queryStr, parameters);
    N1qlQueryResult queryResult = bucket.query(query);
    return extractResultOrThrow(queryResult);
}

The save function accomplishes two things. It handles both creates and replacements of data, thus the use of the UPSERT query. We determine a create or replacement by looking to see if the document_id was passed in. If it was, we’re going to use it and know that our data will be replaced, otherwise we’re going to create a new UUID value for a create.

Like with the other queries we made that accept user data, we first parameterize them before executing.

All our queries are in place so now we just need to finalize the endpoints and then our back-end should be good to go!

Our Application Class

We’re going to start off with a skeleton class again. Open your project’s src/main/java/couchbase/Application.java file and add the following code:

package couchbase;

import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.CouchbaseCluster;
import com.couchbase.client.java.document.json.JsonObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@SpringBootApplication
@RestController
@RequestMapping("/api")
public class Application implements Filter {

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

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        chain.doFilter(req, res);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void destroy() {}

    @Value("${hostname}")
    private String hostname;

    @Value("${bucket}")
    private String bucket;

    @Value("${password}")
    private String password;

    public @Bean
    Cluster cluster() {
        return CouchbaseCluster.create(hostname);
    }

    public @Bean
    Bucket bucket() {
        return cluster().openBucket(bucket, password);
    }

    @RequestMapping(value="/getAll", method= RequestMethod.GET)
    public Object getAll() {

    }

    @RequestMapping(value="/get", method= RequestMethod.GET)
    public Object getByDocumentId(@RequestParam String document_id) {

    }

    @RequestMapping(value="/delete", method=RequestMethod.POST)
    public Object delete(@RequestBody String json) {

    }

    @RequestMapping(value="/save", method=RequestMethod.POST)
    public Object save(@RequestBody String json) {

    }

}

I lied, it isn’t completely a skeleton class. The line @RequestMapping("/api") means that our API endpoints will be found at http://example.com/api. We’re implementing the Filter class because we want to be able to accept cross origin requests, often referred to as CORS.

Skipping over all the Spring Boot configuration, let’s jump into the endpoint functions that I left blank. This is the stuff that matters the most.

Starting with the /api/getAll endpoint, we have:

@RequestMapping(value="/getAll", method= RequestMethod.GET)
public Object getAll() {
    return Database.getAll(bucket());
}

We pass in the bucket that we configured in earlier in the Application class and we get all the results to be returned to our client front-end.

In the same order we did our Database class, let’s look at the function for retrieving a single document. The getByDocumentId function looks like this:

@RequestMapping(value="/get", method= RequestMethod.GET)
public Object getByDocumentId(@RequestParam String document_id) {
    if(document_id.equals("")) {
        return new ResponseEntity<String>(JsonObject.create().put("message", "A document id is required").toString(), HttpStatus.BAD_REQUEST);
    }
    return Database.getByDocumentId(bucket(), document_id);
}

We are allowing and expecting a document_id in the request. If it does not exist we are creating a 400 response stating that the request was bad. If the document_id was found, pass it into the Database class and return the data.

The delete function follows the same concept, but this time uses a POST request:

@RequestMapping(value="/delete", method=RequestMethod.POST)
public Object delete(@RequestBody String json) {
    JsonObject jsonData = JsonObject.fromJson(json);
    if(jsonData.getString("document_id") == null || jsonData.getString("document_id").equals("")) {
        return new ResponseEntity<String>(JsonObject.create().put("message", "A document id is required").toString(), HttpStatus.BAD_REQUEST);
    }
    return Database.delete(bucket(), jsonData.getString("document_id"));
}

The POST body is a JSON string so we have to pick out properties. The property we care about is the document_id and if it is valid, pass it to the appropriate Database function.

Finally we have our save function. It is longer, but certainly not different from what we already saw:

@RequestMapping(value="/save", method=RequestMethod.POST)
public Object save(@RequestBody String json) {
    JsonObject jsonData = JsonObject.fromJson(json);
    if(jsonData.getString("firstname") == null || jsonData.getString("firstname").equals("")) {
        return new ResponseEntity<String>(JsonObject.create().put("message", "A firstname is required").toString(), HttpStatus.BAD_REQUEST);
    } else if(jsonData.getString("lastname") == null || jsonData.getString("lastname").equals("")) {
        return new ResponseEntity<String>(JsonObject.create().put("message", "A lastname is required").toString(), HttpStatus.BAD_REQUEST);
    } else if(jsonData.getString("email") == null || jsonData.getString("email").equals("")) {
        return new ResponseEntity<String>(JsonObject.create().put("message", "An email is required").toString(), HttpStatus.BAD_REQUEST);
    }
    return Database.save(bucket(), jsonData);
}

We just take in our POST body and make sure that the firstname, lastname, and email properties exist, otherwise return an error. If all is good, pass them into the appropriate Database function.

Our back-end is now complete. We now have a fully functional API that communicates to Couchbase Server. Now we just need a user interface.

The Project Front-End

Before diving into this, I should probably say that I’m using the same front-end source code I used in my full stack Node.js tutorial.

At the root of our project, we need to create another directory, this time called public. It is where all of our front-facing resources will end up. If you prefer to use the front-end code from the Node.js project, just clone the public directory into the root of this project. Otherwise, let’s go through it.

The front-end will be responsible for requesting data from our back-end (Java and Couchbase Server). It makes HTTP requests to the back-end allowing for better separation and flexibility between the front-end and back-end.

Including All Third-Party Libraries And Styles

The front-end we’re making is of course using AngularJS, but it is also using the AngularJS UI-Router library as well as Twitter Bootstrap. The first thing we want to do is download everything.

After downloading everything, place the minified (.min.js) files in your project’s public/js directory. Place all CSS files in your project’s public/css directory, and place all fonts in your project’s public/fonts directory.

Now everything can be included in your projects public/index.html file. Open it and include the following:

<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" href="css/bootstrap.min.css">
        <script src="js/angular.min.js"></script>
        <script src="js/angular-ui-router.min.js"></script>
        <script src="js/bootstrap.min.js"></script>
        <script src="js/app.js"></script>
    </head>
    <body ng-app="recordsapp">
        <div style="padding: 20px">
            <div ui-view></div>
        </div>
    </body>
</html>

You may be wondering what ng-app="recordsapp" is or what <div ui-view></div> is. Don’t worry, that is coming soon.

Creating The AngularJS Foundation

Inside your public/js directory you should have an app.js file. Go ahead and add the following foundation code:

angular.module("recordsapp", ["ui.router"])

.config(function($stateProvider, $urlRouterProvider) {

})

.controller("MainController", function($scope, $http, $state, $stateParams) {

});

You can see here that we’ve named our module recordsapp which relates to what we saw in the public/index.html file. We’ve also injected the ui.router and built functions for our .config and .controller.

Configuring The AngularJS UI-Router

If you’re unfamiliar with the AngularJS UI-Router, it is a wonderful library for adding multiple screens (views) to your front-end application. All configuration for these views will end up in our config function in the public/js/app.js file:

.config(function($stateProvider, $urlRouterProvider) {
    $stateProvider
        .state("list", {
            "url": "/list",
            "templateUrl": "templates/list.html",
            "controller": "MainController",
            "cache": false
        })
        .state("item", {
            "url": "/item/:documentId",
            "templateUrl": "templates/item.html",
            "controller": "MainController",
            "cache": false
        });
    $urlRouterProvider.otherwise("list");
})

Essentially we’re creating two views. We are creating a list view and a view for adding or editing items of the list. Each of the routes points to a particular controller and template file. Both routes point to the MainController that we’ll see soon, but each point to either a public/templates/list.html file or public/templates/item.html file.

By default, the list view will show through the $urlRouterProvider.otherwise("list"); line.

Designing Our View Routes

We know which template files we need to work on now as explained by the AngularJS UI-Router configuration. Starting with the public/templates/list.html file, add the following code:

<button class="btn btn-primary" ui-sref="item">New Item</button>
<br />
<table class="table table-striped" ng-init="fetchAll()">
    <thead>
        <tr>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Email</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <tr ng-repeat="(key, value) in items">
            <td>{{value.firstname}}</td>
            <td>{{value.lastname}}</td>
            <td>{{value.email}}</td>
            <td><a href="" ui-sref="item({documentId: key})">edit</a> | <a href="" ng-click="delete(key)">delete</a></td>
        </tr>
    </tbody>
</table>

Essentially we have a table that loops through each object within a particular object to populate the table. Functions like fetchAll and delete are still unknown at this point, but they are coming. If you take a look at the ui-sref items in the code you’ll notice they are referring to particular UI-Router states. One of which passes in an optional parameter containing the document key. You’ll see how it’s used soon.

The second template we want to look at is the public/templates/item.html file:

<form>
    <div class="form-group">
        <label for="firstname">First Name</label>
        <input type="text" class="form-control" id="firstname" placeholder="First Name" ng-model="inputForm.firstname">
    </div>
    <div class="form-group">
        <label for="lastname">Last Name</label>
        <input type="text" class="form-control" id="lastname" placeholder="Last Name" ng-model="inputForm.lastname">
    </div>
    <div class="form-group">
        <label for="email">Email</label>
        <input type="text" class="form-control" id="email" placeholder="Email" ng-model="inputForm.email">
    </div>
    <button type="button" class="btn btn-danger" ui-sref="list">Cancel</button>
    <button type="button" class="btn btn-success" ng-click="save(inputForm.firstname, inputForm.lastname, inputForm.email)">Save</button>
</form>

This file is nothing more than a form. When the success button is pressed, a save function is called. We haven’t created it yet, but it is coming.

Adding Our Controller Logic

The last part of our front-end is all of the application logic to fuel the visuals. Inside your project’s public/js/app.js file, add the following to your controller:

.controller("MainController", function($scope, $http, $state, $stateParams) {

    $scope.items = {};

    $scope.fetchAll = function() {
        $http(
            {
                method: "GET",
                url: "/api/getAll"
            }
        )
        .success(function(result) {
            for(var i = 0; i < result.length; i++) {
                $scope.items[result[i].id] = result[i];
            }
        })
        .error(function(error) {
            console.log(JSON.stringify(error));
        });
    }

    if($stateParams.documentId) {
        $http(
            {
                method: "GET",
                url: "/api/get",
                params: {
                    document_id: $stateParams.documentId
                }
            }
        )
        .success(function(result) {
            $scope.inputForm = result[0];
        })
        .error(function(error) {
            console.log(JSON.stringify(error));
        });
    }

    $scope.delete = function(documentId) {
        $http(
            {
                method: "POST",
                url: "/api/delete",
                data: {
                    document_id: documentId
                }
            }
        )
        .success(function(result) {
            delete $scope.items[documentId];
        })
        .error(function(error) {
            console.log(JSON.stringify(error));
        });
    }

    $scope.save = function(firstname, lastname, email) {
        $http(
            {
                method: "POST",
                url: "/api/save",
                data: {
                    firstname: firstname,
                    lastname: lastname,
                    email: email,
                    document_id: $stateParams.documentId
                }
            }
        )
        .success(function(result) {
            $state.go("list");
        })
        .error(function(error) {
            console.log(JSON.stringify(error));
        });
    }

});

That is a lot of $http requests, and that’s the point! The front-end of this stack is only suppose to request data from the back-end. It makes it very modular.

Seeing The Project In Action

We had Maven as one of the prerequisites to this application. With it installed, use the Command Prompt (Windows) or Terminal (Mac and Linux) to navigate to our project directory and execute the following:

mvn spring-boot:run

This command will download all our project dependencies such as the Couchbase Java SDK and Spring Boot. Then it will run the application.

With the application running, navigate to http://localhost:8080 from your web browser and it should load your application front-end. This front-end will communicate to the RESTful API endpoints that we created throughout the tutorial.

Conclusion

You just saw how to build a full stack Java application. Our code was short, clean, and very easy to understand thanks to the beauty of the Java SDK for Couchbase.

If you’re interested in the full source code to this project, it can be downloaded from the Couchbase Labs GitHub repository. Interested in the Node.js version of this article? Check it out here.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.