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.
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.
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.
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.
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!
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.
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.
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.
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!
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.
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.
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.
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
.
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.
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.
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.
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.
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.