At Couchbase Connect 2015 we demonstrated an example application that uses N1QL to query data from a sample Couchbase bucket.
If you missed the conference, not a problem. We're going to go through how to reproduce this application and check out some of the highlights of Couchbase 4.0.
Prerequisites
- Apache Maven 3
- Java Development Kit (JDK) 1.7
- Couchbase Server 4.0
- IntelliJ IDEA 14.1+, Eclipse, or NetBeans. IntelliJ IDEA will be used in this example.
Creating a New Project
Open IntelliJ IDEA and choose to create a new Java project, making sure to use JDK 1.7 if asked. For purposes of this guide, let's call the project try-cb-java.
Now right-click try-cb-java in your project tree, then choose Add Frameworks Support and select Maven. This will add a pom.xml file to your project.
Setting up Maven
Inside the pom.xml file, start by giving the project a more appealing group name:
1 2 3 |
<groupId>com.couchbase.example</groupId> |
Then proceed to adding the rest of our dependencies to the file, that include Spring Boot, the Couchbase client, and the Spring security framework.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.2.3.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.0-dp</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> |
Creating a Run Profile
As of right now, if you try to run the application, it will error or nothing will happen because there is no profile currently configured.
From the toolbar, choose Run -> Edit Configurations and choose to add a new Maven configuration. You can name it whatever you want, but it is important have the following in the command line field:
1 2 3 |
spring-boot:run |
For this article we're going to name the configuration Spring Boot.
IntelliJ IDEA should now be ready for development.
Creating a Indices on the Couchbase Bucket
Because this tutorial uses N1QL queries, we must first add indices to our Couchbase Server bucket. Now this can easily be done through code, but for this example we're going to shortcut it and add them via the Couchbase Query (CBQ) client that gets automatically installed with a Mac OS and Windows installation of Couchbase 4+.
On Mac OS, launch CBQ found at /Applications/Couchbase Server.app/Contents/Resources/couchbase-core/bin/cbq and run the following:
1 2 3 |
CREATE PRIMARY INDEX def_primary ON `travel-sample` USING gsi; |
On Windows, launch CBQ found at C:/Program Files/Couchbase/Server/bin/cbq.exe and execute the same N1QL command as done on Mac OS.
Creating a Main Application Class
The main class for this project will be Application.java and can be created by right-clicking the trycb package from the project tree and choosing New -> Java Class.
Add the following to get the class in its most basic runnable state:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package trycb; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController @RequestMapping("/api") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } |
Make sure the project runs without errors by choosing Run -> Run 'Spring Boot' from the IntelliJ IDEA toolbar.
Handling Cross Origin Resource Sharing (CORS)
Since most of our testing will be done locally, we need to make sure CORS is enabled, otherwise the web browser is going to complain when trying to hit our API endpoints with JavaScript.
Make sure the Application class implements the Filter class and add the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@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() {} |
Configure Couchbase Cluster and Bucket Options
As of right now we essentially have a Spring Boot application with no Couchbase interation. We've included the Java client via Maven, so it is time to start using it.
Add the following to the Application class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@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); } |
We've not set up the hostname, bucket, and password variables, but they will be used to connect to a Couchbase cluster and a particular bucket.
Add Resource Variables
You saw that we were using hostname, bucket, and password, so now it is time to set them.
In the IntelliJ IDEA project tree, right-click src/main/resources and choose New -> File. Name the new file application.properties and add the following lines:
1 2 3 |
hostname=127.0.0.1 bucket=travel-sample password= |
Spring Boot will pick up this application.properties file for you. Further information on application related properties can be seen in the official Spring documentation.
Creating RESTful Endpoints
This application is going to be API based so certain endpoints need to be created:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@RequestMapping(value="/airport/findAll", method=RequestMethod.GET) public List<Map<String, Object>> airports(@RequestParam String search) { } @RequestMapping(value="/flightPath/findAll", method=RequestMethod.GET) public List<Map<String, Object>> all(@RequestParam String from, @RequestParam String to, @RequestParam String leave) throws Exception { } @RequestMapping(value="/user/login", method=RequestMethod.GET) public Object login(@RequestParam String user, @RequestParam String password) { } @RequestMapping(value="/user/login", method=RequestMethod.POST) public Object createLogin(@RequestBody String json) { } @RequestMapping(value="/user/flights", method=RequestMethod.POST) public Object book(@RequestBody String json) { } @RequestMapping(value="/user/flights", method=RequestMethod.GET) public Object booked(@RequestParam String username) { } |
Essentially we'll have endpoints for user registration and sign-ins, booking and finding flights, as well as searching flight information.
The logic behind these endpoints will appear in another class for cleanliness.
Creating a Database Class
We just set up the driving endpoints of our Spring Boot application, but it is now time to take a look at the logic behind interacting with the database.
The database class for this project will be Database.java and can be created by right-clicking the trycb package from the project tree of IntelliJ IDEA and choosing New -> Java Class.
Add the following to get the class for a nice skeleton of where we're going:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package trycb; public class Database { private Database() { } public static List<Map<String, Object>> findAllAirports(final Bucket bucket, final String params) { } public static List<Map<String, Object>> findAllFlightPaths(final Bucket bucket, String from, String to, Calendar leave) { } public static ResponseEntity<String> login(final Bucket bucket, final String username, final String password) { } public static ResponseEntity<String> createLogin(final Bucket bucket, final String username, final String password) { } private static List<Map<String, Object>> extractResultOrThrow(QueryResult result) { } } |
From here, we're going to complete each of these methods in the order they are likely to be interacted with by the user.
Creating a New User
When the user issues a POST to the /api/user/login, the following database function must be called:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public static ResponseEntity<String> createLogin(final Bucket bucket, final String username, final String password) { JsonObject data = JsonObject.create() .put("type", "user") .put("name", username) .put("password", BCrypt.hashpw(password, BCrypt.gensalt())); JsonDocument doc = JsonDocument.create("user::" + username, data); try { bucket.insert(doc); JsonObject responseData = JsonObject.create() .put("success", true) .put("data", data); return new ResponseEntity<String>(responseData.toString(), HttpStatus.OK); } catch (Exception e) { JsonObject responseData = JsonObject.empty() .put("success", false) .put("failure", "There was an error creating account") .put("exception", e.getMessage()); return new ResponseEntity<String>(responseData.toString(), HttpStatus.OK); } } |
The username and password included with the request will be added to a JSON object and then the password will be encrypted with the Spring BCrypt library. To keep track of the user data, new users will end up in documents titled user::{USERNAME_HERE}. Using bucket.insert(doc), an attempt to insert the data into the bucket is made. If there are no exceptions thrown, then it succeeded and a response is returned. If there is an exception, then inserting failed and the error will be returned.
Signing in as an Existing User
When the user issues a GET to the same /api/user/login endpoint, the following database function must be called:
1 2 3 4 5 6 7 8 9 10 11 12 |
public static ResponseEntity<String> login(final Bucket bucket, final String username, final String password) { JsonDocument doc = bucket.get("user::" + username); JsonObject responseContent; if(BCrypt.checkpw(password, doc.content().getString("password"))) { responseContent = JsonObject.create().put("success", true).put("data", doc.content()); } else { responseContent = JsonObject.empty().put("success", false).put("failure", "Bad Username or Password"); } return new ResponseEntity<String>(responseContent.toString(), HttpStatus.OK); } |
Using bucket.get(“user::” + username) with the provided username, the Java application gets the document from the bucket if it exists. The Spring BCrypt library has a great function to check whethor or not the provided password matches the encrypted password that is stored. If it does, then return a success object, otherwise return a login failed object.
Extracting the N1QL Result and Making it Java Readable
N1QL returns a QueryResult object that may be less desirable if returning data to a requesting front-end. What we really want to do is convert it into a List object.
1 2 3 4 5 6 7 8 9 10 11 12 |
private static List<Map<String, Object>> extractResultOrThrow(QueryResult result) { if (!result.finalSuccess()) { throw new DataRetrievalFailureException("Query error: " + result.errors()); } List<Map<String, Object>> content = new ArrayList<Map<String, Object>>(); for (QueryRow row : result) { content.add(row.value().toMap()); } return content; } |
This function will be called every time N1QL data is returned.
Finding All Airports
Now we're going to see some of the magic behind N1QL when it comes to searching for airports.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public static List<Map<String, Object>> findAllAirports(final Bucket bucket, final String params) { Statement query; AsPath prefix = select("airportname").from(i(bucket.name())); if (params.length() == 3) { query = prefix.where(x("faa").eq(s(params.toUpperCase()))); } else if (params.length() == 4 && (params.equals(params.toUpperCase()) || params.equals(params.toLowerCase()))) { query = prefix.where(x("icao").eq(s(params.toUpperCase()))); } else { query = prefix.where(i("airportname").like(s(params + "%"))); } QueryResult result = bucket.query(Query.simple(query)); return extractResultOrThrow(result); } |
You can see in the above code we are using the Fluent API with IntelliJ IDEA to create our N1QL query. Essentially, if you were to look at raw SQL, it would look like this:
1 2 3 |
SELECT airportname FROM `travel-sample` WHERE faa = {{PARAMS}} |
In the above, {{PARAMS}} represents an airport like LAX or similar. Of course that is provided the params length is three.
Finding All Flight Routes
Finally, we're left with the method responsible for finding flight routes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public static List<Map<String, Object>> findAllFlightPaths(final Bucket bucket, String from, String to, Calendar leave) { Statement query = select(x("faa").as("fromAirport")) .from(i(bucket.name())) .where(x("airportname").eq(s(from))) .union() .select(x("faa").as("toAirport")) .from(i(bucket.name())) .where(x("airportname").eq(s(to))); QueryResult result = bucket.query(Query.simple(query)); if (!result.finalSuccess()) { throw new DataRetrievalFailureException("Query error: " + result.errors()); } String fromAirport = null; String toAirport = null; for (QueryRow row : result) { if (row.value().containsKey("fromAirport")) { fromAirport = row.value().getString("fromAirport"); } if (row.value().containsKey("toAirport")) { toAirport = row.value().getString("toAirport"); } } Statement joinQuery = select("a.name", "s.flight", "s.utc", "r.sourceairport", "r.destinationairport", "r.equipment") .from(i(bucket.name()).as("r")) .unnest("r.schedule AS s") .join(i(bucket.name()).as("a") + " ON KEYS r.airlineid") .where(x("r.sourceairport").eq(s(fromAirport)).and(x("r.destinationairport").eq(s(toAirport))).and(x("s.day").eq(leave.get(Calendar.DAY_OF_MONTH)))) .orderBy(Sort.asc("a.name")); QueryResult otherResult = bucket.query(joinQuery); return extractResultOrThrow(otherResult); } |
We're doing two N1QL queries in this method. The first can easily be translated to the following:
1 2 3 |
SELECT faa AS fromAirport FROM `travel-sample` WHERE airportname = {{PARAMS.FROM}} UNION SELECT faa AS toAirport FROM `travel-sample` WHERE airportname = {{PARAMS.TO}} |
Of course {{PARAMS}} is whatever was passed to your endpoint. In the statement, we're combining the result sets of all the from airports and all the to airports.
After getting both the result sets, we are looping through them to make sure the to and from airports exist, otherwise we're defaulting them to NULL which will prevent the next query from being successful.
The second query can be translated into the following raw query:
1 2 3 |
SELECT a.name, s.flight, s.utc, r.sourceairport, r.destinationairport, r.equipment FROM `travel-sample` AS r UNNEST r.schedule AS s JOIN `travel-sample` AS a ON KEYS r.airlineid WHERE r.sourceairport = {{TO}} AND r.destinationairport = {{TO}} AND s.day = 3 ORDER BY a.name ASC |
We're getting schedule information about the flights by unnesting it from the JSON document and then joining on the now flattened key.
Wrapping Up The Application and Database Classes
We now have our endpoints and database methods, but they are not connected to each other. It is time to revisit the Application.java class and add some code to the functions we created previously:
1 2 3 4 5 6 7 8 9 10 11 12 |
@RequestMapping(value="/user/login", method= RequestMethod.GET) public Object login(@RequestParam String user, @RequestParam String password) { return Database.login(bucket(), user, password); } @RequestMapping(value="/user/login", method=RequestMethod.POST) public Object createLogin(@RequestBody String json) { JsonObject jsonData = JsonObject.fromJson(json); return Database.createLogin(bucket(), jsonData.getString("user"), jsonData.getString("password")); } |
You can see that the two static Database methods are called from each of the endpoints relating to user accounts. The same process can be done for the other endpoints that we've previously created:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@RequestMapping("/airport/findAll") public List<Map<String, Object>> airports(@RequestParam String search) { return Database.findAllAirports(bucket(), search); } @RequestMapping("/flightPath/findAll") public List<Map<String, Object>> all(@RequestParam String from, @RequestParam String to, @RequestParam String leave) throws Exception { Calendar calendar = Calendar.getInstance(Locale.US); calendar.setTime(DateFormat.getDateInstance(DateFormat.SHORT, Locale.US).parse(leave)); return Database.findAllFlightPaths(bucket(), from, to, calendar); } |
Testing The Sample Endpoints
There are a few ways to test the endpoints of the application. In this example we're going to use cURL, but you can certainly use Postman for Google Chrome or something similar.
With cURL installed, open a Terminal or Command Prompt and enter the following:
1 2 3 |
curl -X GET 'http://localhost:8080/api/airport/findAll?search=LAX' |
The above cURL command will hit the api/airport/findAll endpoint and pass a parameter of search=LAX. If successful, you should get a response of:
1 |
[{"airportname":"Los Angeles Intl"}] |
The same kind of testing can done for every other endpoint.
Conclusion
We just saw how to get a sample travel application setup that uses Couchbase Server and Spring Boot for Java. Although we didn't set up a front-end, it is very possible to add one using languages such as AngularJS, jQuery, or ReactJS.
This full project along with an AngularJS front-end can be obtained from the Couchbase Labs GitHub channel.