Now we're getting to the real meat of the tutorial. First,
uncomment the BeerServlet and its corresponding
tags inside the web.xml. We'll make use of a
view to list all beers and make them easily searchable. We'll also
provide a form to create and/or edit beers and finally delete
them.
Here is the barebones structure of our
BeerServlet, which will be filled with live
data soon (again, comments and imports are removed).
package com.couchbase.beersample; public class BeerServlet extends HttpServlet { final CouchbaseClient client = ConnectionManager.getInstance(); final Gson gson = new Gson(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { if(request.getPathInfo() == null) { handleIndex(request, response); } else if(request.getPathInfo().startsWith("/show")) { handleShow(request, response); } else if(request.getPathInfo().startsWith("/delete")) { handleDelete(request, response); } else if(request.getPathInfo().startsWith("/edit")) { handleEdit(request, response); } else if(request.getPathInfo().startsWith("/search")) { handleSearch(request, response); } } catch (InterruptedException ex) { Logger.getLogger(BeerServlet.class.getName()).log( Level.SEVERE, null, ex); } catch (ExecutionException ex) { Logger.getLogger(BeerServlet.class.getName()).log( Level.SEVERE, null, ex); } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } private void handleIndex(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { } private void handleShow(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { } private void handleDelete(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException, InterruptedException, ExecutionException { } private void handleEdit(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } private void handleSearch(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { } }
Since our web.xml uses wildcards
(*) to route every
/beer-related to this servlet, we need to
inspect the path through getPathInfo() and
dispatch the request to a helper method that does the actual work.
The doPost() method will be used to analyze and
store the results of the web-form to edit and create beers (since
the form is sent through a POST request).
The first functionality we'll implement is to list the top 20
beers in a table. We can use the beer/by_name
view we've created at the beginning to get a sorted list of all
beers. The following code belongs to the
handleIndex method:
// Fetch the View View view = client.getView("beer", "by_name"); // Set up the Query object Query query = new Query(); // We the full documents and only the top 20 query.setIncludeDocs(true).setLimit(20); // Query the Cluster ViewResponse result = client.query(view, query); // This ArrayList will contain all found beers ArrayList<HashMap<String, String>> beers = new ArrayList<HashMap<String, String>>(); // Iterate over the found documents for(ViewRow row : result) { // Use Google GSON to parse the JSON into a HashMap HashMap<String, String> parsedDoc = gson.fromJson((String)row.getDocument(), HashMap.class); // Create a HashMap which will be stored in the beers list. HashMap<String, String> beer = new HashMap<String, String>(); beer.put("id", row.getId()); beer.put("name", parsedDoc.get("name")); beer.put("brewery", parsedDoc.get("brewery_id")); beers.add(beer); } // Pass all found beers to the JSP layer request.setAttribute("beers", beers); // Render the index.jsp template request.getRequestDispatcher("/WEB-INF/beers/index.jsp") .forward(request, response);
The index action queries the view, parses the results with GSON
into a HashMap and eventually forwards the
ArrayList to the JSP layer. We'll now implement
the index.jsp template which will iterate over
the ArrayList and print it out in a
nicely-formatted table:
<%@taglib prefix="t" tagdir="/WEB-INF/tags" %> <%@page contentType="text/html" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <t:layout> <jsp:body> <h3>Browse Beers</h3> <form class="navbar-search pull-left"> <input id="beer-search" type="text" class="search-query" placeholder="Search for Beers"> </form> <table id="beer-table" class="table table-striped"> <thead> <tr> <th>Name</th> <th>Brewery</th> <th></th> </tr> </thead> <tbody> <c:forEach items="${beers}" var="beer"> <tr> <td><a href="/beers/show/${beer.id}">${beer.name}</a></td> <td><a href="/breweries/show/${beer.brewery}">To Brewery</a></td> <td> <a class="btn btn-small btn-warning" href="/beers/edit/${beer.id}">Edit</a> <a class="btn btn-small btn-danger" href="/beers/delete/${beer.id}">Delete</a> </td> </tr> </c:forEach> </tbody> </table> </jsp:body> </t:layout>
We're using
JSP
tags to iterate over the beers and use their properties
(name and id) to fill the
rows in the table. On the website you should now see a table with
a list of beers with Edit and
Delete buttons on the right. There is also a
link to the associated brewery that you can click on. Let's
implement the delete action next, since its very easy to do with
Couchbase:
private void handleDelete(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException, InterruptedException, ExecutionException { // Split the Request-Path and get the Beer ID out of it String beerId = request.getPathInfo().split("/")[2]; // Try to delete the document and store the OperationFuture OperationFuture<Boolean> delete = client.delete(beerId); // If the Future succeeded (returned true), redirect to /beers if(delete.get()) { response.sendRedirect("/beers"); } }
The delete method deletes a document from the cluster based on the
given document key. Here, we wait on the
OperationFuture to return (through the
get() method) and if the delete was successful
(when true is returned), we redirect to the
index action.
Now that we can delete a document, it makes sense to also be able
to edit it. The edit action is very similar to the delete action,
but it reads the document based on the given ID
instead of deleting it. We also need to parse the String
representation of the JSON document into a Java structure, so we
can use it in the template. We again make use of the excellent
Google GSON library to handle this for us.
private void handleEdit(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Extract the Beer ID from the URL String[] beerId = request.getPathInfo().split("/"); // If there is a Beer ID if(beerId.length > 2) { // Read the Document (as a JSON string) String document = (String) client.get(beerId[2]); HashMap<String, String> beer = null; if(document != null) { // Convert the String into a HashMap beer = gson.fromJson(document, HashMap.class); beer.put("id", beerId[2]); // Forward the beer to the view request.setAttribute("beer", beer); } request.setAttribute("title", "Modify Beer \"" + beer.get("name") + "\""); } else { request.setAttribute("title", "Create a new beer"); } request.getRequestDispatcher("/WEB-INF/beers/edit.jsp").forward(request, response); }
If the document could be successfully loaded, it gets parsed into
a HashMap and then forwarded to the edit.jsp
template. Also, we define a title variable that is used inside the
template to determine if we want to edit a document or create a
new one (that is when no Beer ID passed to the edit method). Here
is the corresponding edit.jsp template:
<%@taglib prefix="t" tagdir="/WEB-INF/tags" %> <%@page contentType="text/html" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <t:layout> <jsp:body> <h3>${title}</h3> <form method="post" action="/beers/edit/${beer.id}"> <fieldset> <legend>General Info</legend> <div class="span12"> <div class="span6"> <label>Name</label> <input type="text" name="beer_name" placeholder="The name of the beer." value="${beer.name}"> <label>Description</label> <input type="text" name="beer_description" placeholder="A short description." value="${beer.description}"> </div> <div class="span6"> <label>Style</label> <input type="text" name="beer_style" placeholder="Bitter? Sweet? Hoppy?" value="${beer.style}"> <label>Category</label> <input type="text" name="beer_category" placeholder="Ale? Stout? Lager?" value="${beer.category}"> </div> </div> </fieldset> <fieldset> <legend>Details</legend> <div class="span12"> <div class="span6"> <label>Alcohol (ABV)</label> <input type="text" name="beer_abv" placeholder="The beer's ABV" value="${beer.abv}"> <label>Biterness (IBU)</label> <input type="text" name="beer_ibu" placeholder="The beer's IBU" value="${beer.ibu}"> </div> <div class="span6"> <label>Beer Color (SRM)</label> <input type="text" name="beer_srm" placeholder="The beer's SRM" value="${beer.srm}"> <label>Universal Product Code (UPC)</label> <input type="text" name="beer_upc" placeholder="The beer's UPC" value="${beer.upc}"> </div> </div> </fieldset> <fieldset> <legend>Brewery</legend> <div class="span12"> <div class="span6"> <label>Brewery</label> <input type="text" name="beer_brewery_id" placeholder="The brewery" value="${beer.brewery_id}"> </div> </div> </fieldset> <div class="form-actions"> <button type="submit" class="btn btn-primary">Save changes</button> </div> </form> </jsp:body> </t:layout>
It's a little bit longer, but just because we have lots of fields on our beer documents. Note how the beer attributes are used inside the value attributes of the HTML input fields. The unique ID is also used in the form method to dispatch it to the correct URL on submit.
The last thing we need to implement to make the form submission
work is the actual form parsing and storing itself. Since the form
submission happens through a POST request, we need to implement
the doPost() method on our servlet.
@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Parse the Beer ID String beerId = request.getPathInfo().split("/")[2]; HashMap<String, String> beer = beer = new HashMap<String, String>(); Enumeration<String> params = request.getParameterNames(); // Iterate over all POST params while(params.hasMoreElements()) { String key = params.nextElement(); if(!key.startsWith("beer_")) { continue; } String value = request.getParameter(key); // Store them in a HashMap with key and value beer.put(key.substring(5), value); } // Add two more fields beer.put("type", "beer"); beer.put("updated", new Date().toString()); // Set (add or override) the document (converted to JSON with GSON) client.set(beerId, 0, gson.toJson(beer)); // Redirect to the show page response.sendRedirect("/beers/show/" + beerId); }
The code iterates over all POST fields and stores them in a
HashMap. We then use the set command to store
the Document inside the cluster and use Google GSON to translate
out HashMap to a JSON string. In this case, we
could also wait for a OperationFuture response
and for example return an error if the set failed.
The last line redirects to a show method, which just shows all fields of the document. Since the patterns are the same as before, here is the show method without any further ado:
private void handleShow(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // Extract the Beer ID String beerId = request.getPathInfo().split("/")[2]; String document = (String) client.get(beerId); if(document != null) { // Parse the JSON and set it for the template if a document was found HashMap<String, String> beer = gson.fromJson(document, HashMap.class); request.setAttribute("beer", beer); } // render the show.jsp template request.getRequestDispatcher("/WEB-INF/beers/show.jsp") .forward(request, response); }
The ID is again extracted and if a document is found (get returns null when it can't find a document for the given ID), it gets parsed into a HashMap and forwarded to the show.jsp template. The templat then just prints out all keys and values in a table:
<%@taglib prefix="t" tagdir="/WEB-INF/tags" %> <%@page contentType="text/html" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <t:layout> <jsp:body> <h3>Show Details for Beer "${beer.name}"</h3> <table class="table table-striped"> <tbody> <c:forEach items="${beer}" var="item"> <tr> <td><strong>${item.key}</strong></td> <td>${item.value}</td> </tr> </c:forEach> </tbody> </table> </jsp:body> </t:layout>
In the index.jsp template, you may have noticed the search box at the top. We can use to dynamically filter our table based on the user input. We'll use nearly the same code for it as in the index method, aside from the fact that we make use of range queries to define a beginning and end to search for.
Before we implement the actual Java method, we need to put the
following snippet inside the js/beersample.js
file (if you haven't already at the beginning of the tutorial) to
listen on searchbox changes and update the table with the
resulting JSON (which will be returned from the search method):
$("#beer-search").keyup(function() { var content = $("#beer-search").val(); if(content.length >= 0) { $.getJSON("/beers/search", {"value": content}, function(data) { $("#beer-table tbody tr").remove(); for(var i=0;i<data.length;i++) { var html = "<tr>"; html += "<td><a href=\"/beers/show/"+data[i].id+"\">"+data[i].name+"</a></td>"; html += "<td><a href=\"/breweries/show/"+data[i].brewery+"\">To Brewery</a></td>"; html += "<td>"; html += "<a class=\"btn btn-small btn-warning\" href=\"/beers/edit/"+data[i].id+"\">Edit</a>\n"; html += "<a class=\"btn btn-small btn-danger\" href=\"/beers/delete/"+data[i].id+"\">Delete</a>"; html += "</td>"; html += "</tr>"; $("#beer-table tbody").append(html); } }); } });
The code waits for keyup events on the search field and if they happen does a AJAX query to the search method on the servlet. The servlet computes the result and sends it back as JSON. The JavaScript then clears the table, iterates over the result and creates new rows. The search method looks like this:
private void handleSearch(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // Exctract the searched value String startKey = request.getParameter("value").toLowerCase(); // Prepare a query against the by_name view View view = client.getView("beer", "by_name"); Query query = new Query(); // Define the query params query.setIncludeDocs(true) // include the full documents .setLimit(20) // only show 20 results .setRangeStart(ComplexKey.of(startKey)) // Start the search at the given search value .setRangeEnd(ComplexKey.of(startKey + "\uefff")); // End the search at the given search plus the unicode "end" // Query the view ViewResponse result = client.query(view, query); ArrayList<HashMap<String, String>> beers = new ArrayList<HashMap<String, String>>(); // Iterate over the results for(ViewRow row : result) { // Parse the Document to a HashMap HashMap<String, String> parsedDoc = gson.fromJson((String)row.getDocument(), HashMap.class); // Create a new Beer out of it HashMap<String, String> beer = new HashMap<String, String>(); beer.put("id", row.getId()); beer.put("name", parsedDoc.get("name")); beer.put("brewery", parsedDoc.get("brewery_id")); beers.add(beer); } // Return a JSON representation of all Beers response.setContentType("application/json"); PrintWriter out = response.getWriter(); out.print(gson.toJson(beers)); out.flush(); }
You can use the setRangeStart() and
setRangeEnd() methods to define which key range
from the index should be returned. If we've just provded the start
range key, then we'd get all documents starting from our search
value. Since we want only those beginning with the search value,
we can use the special "\uefff" UTF-8 character
at the end which means "end here". You need to get used to it in
the first place, but its very fast and efficient when accessing
the view.