Java EE Servlet tutorial : Adding create, update and delete to the bookstore listing
From Resin 4.0 Wiki
This tutorial is part of Java EE Tutorial covering JSP_2.2, and Servlets 3.0.
Java EE Servlet Tutorial: Implementing a basic CRUD listing
We left off with a basic listing. BookListServlet
used a BookRepository
object (DAO) to load a list of books and then delegated to book-list.jsp
to render
the book listing.
In this step, we want to add a link to the book listing so when the user clicks it a form gets rendered so that they can edit the details of book. Also we want an add link so that the end user can add a new book to the listing.
Adding a link to the book listing to edit a book
Change the code that looks like this in book-list.jsp:
... <c:forEach var="book" items="${books}"> <tr> <td>${book.title}</td> ...
To this
Adding an edit book link to book-list.jsp listing
... <c:forEach var="book" items="${books}"> <tr> <td><a href="${pageContext.request.contextPath}/book?id=${book.id}">${book.title}</a></td> ...
The EL expression ${pageContext.request.contextPath}
refers to the URI you have the war file mapped to in the
Servlet container. The default URI for a webapp is its war file name so our war file name is bookstore so a rendered
URL will look like this:
... <tr> <td><a href="/bookstore/book?id=0">War and Peace</a></td> ... <tr> <td><a href="/bookstore/book?id=1">Pride and Prejudice</a></td> ...
The link /bookstore/book/
(with slash) loads the book listing. The link /bookstore/book?id=1
loads
the form page to edit an existing book. To add a new book, we will use the link /bookstore/book
.
Adding a link to the book listing to add a book
Adding an add book link to book-list.jsp listing
Before the table add this HTML code to add a new book:
... <a href="${pageContext.request.contextPath}/book">Add Book</a> ...
Now we have to links going to the URI /book, we need a Servlet that handles the links.
The BookEditorServlet
handles both add and the edit book functions.
Servlet doGet to load a Book form
BookEditorServlet.java doGet
package com.bookstore.web; ... @WebServlet("/book") public class BookEditorServlet extends HttpServlet { @Inject private BookRepository bookRepo; private SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); ... /** Prepare the book form before we display it. */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String id = request.getParameter("id"); if (id != null && !id.isEmpty()) { Book book = bookRepo.lookupBookById(id); request.setAttribute("book", book); request.setAttribute("bookPubDate", dateFormat.format(book.getPubDate())); } /* Redirect to book-form. */ getServletContext().getRequestDispatcher("/WEB-INF/pages/book-form.jsp").forward( request, response); } }
Notice we use @WebServlet("/book")
to map BookEditorServlet
to the URI /book.
It is common to load a form from a doGet
method, and to handle the form submission via doPost
.
Following REST and HTTP principles GET operations reads data, and POST data modifies data.
The doGet
method uses the id being empty or not to determine if this is a load
"Add Employee Form" or load "Update Employee Form" operation. The doGet method also converts
the date into a format that is easy for the end user to modify and it also maps the book into request scope as follows:
BookEditorServlet.java doGet() delegate to book-form.jsp page
Book book = bookRepo.lookupBookById(id); request.setAttribute("book", book); request.setAttribute("bookPubDate", dateFormat.format(book.getPubDate()));
In the model 2 architecture, the Servlet tier prepares the form data before a form loads to be rendered and edited correctly.
To render the HTML form, the servlet delegates to book-form.jsp as follows:
BookEditorServlet.java doGet() delegate to book-form.jsp page
/* Redirect to book-form. */ getServletContext().getRequestDispatcher("/WEB-INF/pages/book-form.jsp").forward( request, response);
Rendering the book form HTML
The book-form.jsp has the HTML code to render a form as follows:
book-form.jsp Renders form to update or add a Book
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <!DOCTYPE HTML> <html> <head> <title>Book Form</title> </head> <body> <h1>Book Form</h1> <form method="post" action="${pageContext.request.contextPath}/book"> <fieldset> <legend> <c:choose> <c:when test="${not empty book.id }"> Updating Book </c:when> <c:otherwise> Adding Book </c:otherwise> </c:choose> </legend> <div> <label for="title">Title</label> <input type="text" name="title" id="title" value="${book.title}" /> </div> <div> <label for="description">Description</label> <textarea name="description" id="description" rows="2" cols="60">${book.description}</textarea> </div> <div> <label for="price">Price $</label> <input name="price" id="price" value="${book.price}" /> </div> <div> <label for="pubDate">Publication Date</label> <input name="pubDate" id="pubDate" value="${bookPubDate}" /> <label class="after">(MM/DD/YYYY)</label> </div> <c:if test="${not empty book.id}"> <input type="hidden" name="id" value="${book.id}" /> </c:if> </fieldset> <div class="button-row"> <a href="${pageContext.request.contextPath}/book/">Cancel</a> or <input type="submit" value="Submit" /> </div> </form> </body> </html>
The book-form.jsp
uses JSTL c:choose, c:otherwise to display whether we are adding a new book or updating a book as follows:
book-form.jsp using JSTL c:choose to display update or add status
... <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> ... <legend> <c:choose> <c:when test="${not empty book.id }"> Updating Book </c:when> <c:otherwise> Adding Book </c:otherwise> </c:choose> </legend> ...
If the book.id
property is present the test "Updating Book" is rendered.
Also, the form renders a hidden id property if it is doing an edit/update operation as follows:
<c:if test="${not empty book.id}"> <input type="hidden" name="id" value="${book.id}" /> </c:if>
The doPost
method of BookEditorServlet
handles the form submission as follows:
Creating a doPost method to handle the form submission
BookEditorServlet.java doPost
package com.bookstore.web; ... @WebServlet("/book") public class BookEditorServlet extends HttpServlet { @Inject private BookRepository bookRepo; ... /** * Handles posting an HTML book form. If the id is null then it is an * "add book", if the id is set then it is an "update book" */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String title = request.getParameter("title"); String description = request.getParameter("description"); String price = request.getParameter("price"); String pubDate = request.getParameter("pubDate"); String id = request.getParameter("id"); if (id == null || id.isEmpty()) { bookRepo.addBook(title, description, price, pubDate); } else { bookRepo.updateBook(id, title, description, price, pubDate); } response.sendRedirect(request.getContextPath() + "/book/"); } ... }
Notice that if the id is null then BookEditorServlet.doPost
calls bookRepo.addBook
, otherwise it calls bookRepo.updateBook
.
Once the form handling is done, doPost
redirects to /book/
. A redirect means an extra hit to the server, since we are basically
telling the browser to load another link. The astute reader may wonder why we don't just do a forward like we did in the doGet
.
The answer is bookmarking. The URL /book/
(ending in slash) represents a collection of books,
while /book (no slash) represents a single book (using REST style URL).
If we did a forward, then after the form submission the browser would show the listing, but under the URI /book instead of /book/.
If the ender user linked to /book, it would not take them back to the listing but back to a form, which not correct.
This is why we do a sendRedirect instead of a forward.
Now we have an add/edit/update/read/ and listing. It is everything you could want from a CRUD listing.
At this point, we don't do much validation. We will add this in a later lesson.
Quick review of what we have so far
We have created the following files for this bookstore example:
$ find . . ./WebContent/WEB-INF/pages/book-form.jsp Book Form ./WebContent/WEB-INF/pages/book-list.jsp Book Listing ./src/META-INF/beans.xml Needed for Java EE dependency injection (CDI) ./src/com/bookstore/Book.java Domain/model object ./src/com/bookstore/BookRepositoryImpl.java Repository implementation using Java collections (just for testing) ./src/com/bookstore/BookRepository.java Interface to Book Repository so we can swap it out with JDBC, JPA, JCache and MongoDB version later ./src/com/bookstore/web/BookEditorServlet.java Servlet that loads Book (doGet) form and handles Book form submissions (doPost). ./src/com/bookstore/web/BookListServlet.java Servlet that looks up a list of books and displays the listing
./WebContent/WEB-INF/pages/book-form.jsp full listing
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <!DOCTYPE HTML> <html> <head> <title>Book Form</title> </head> <body> <h1>Book Form</h1> <form method="post" action="${pageContext.request.contextPath}/book"> <fieldset> <legend> <c:choose> <c:when test="${not empty book.id }"> Updating Book </c:when> <c:otherwise> Adding Book </c:otherwise> </c:choose> </legend> <div> <label for="title">Title</label> <input type="text" name="title" id="title" value="${book.title}" /> </div> <div> <label for="description">Description</label> <textarea name="description" id="description" rows="2" cols="60">${book.description}</textarea> </div> <div> <label for="price">Price $</label> <input name="price" id="price" value="${book.price}" /> </div> <div> <label for="pubDate">Publication Date</label> <input name="pubDate" id="pubDate" value="${bookPubDate}" /> <label class="after">(MM/DD/YYYY)</label> </div> <c:if test="${not empty book.id}"> <input type="hidden" name="id" value="${book.id}" /> </c:if> </fieldset> <div class="button-row"> <a href="${pageContext.request.contextPath}/book/">Cancel</a> or <input type="submit" value="Submit" /> </div> </form> </body> </html>
./WebContent/WEB-INF/pages/book-list.jsp full listing
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix ="c" %> <!DOCTYPE HTML> <html> <head> <title>Book listing</title> </head> <body> <a href="${pageContext.request.contextPath}/book">Add Book</a> <table> <tr> <th>Title</th> <th>Description</th> <th>Price</th> <th>Publication Date</th> </tr> <c:forEach var="book" items="${books}"> <tr> <td><a href="${pageContext.request.contextPath}/book?id=${book.id}">${book.title}</a></td> <td>${book.description}</td> <td>${book.price}</td> <td>${book.pubDate}</td> </tr> </c:forEach> </table> </body> </html>
./src/META-INF/beans.xml full listing
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd"> </beans>
./src/com/bookstore/Book.java
package com.bookstore; import java.math.BigDecimal; import java.util.Date; public class Book implements Cloneable { private String title; private String description; private BigDecimal price; private Date pubDate; private String id; public Book(String id, String title, String description, BigDecimal price, Date pubDate) { this.id = id; this.title = title; this.description = description; this.price = price; this.pubDate = pubDate; } public Book () { } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; } public Date getPubDate() { return pubDate; } public void setPubDate(Date pubDate) { this.pubDate = pubDate; } public String getId() { return id; } public void setId(String id) { this.id = id; } public Book cloneMe() { try { return (Book) super.clone(); } catch (CloneNotSupportedException e) { return null; } } @Override public String toString() { return "Book [title=" + title + ", description=" + description + ", price=" + price + ", pubDate=" + pubDate + ", id=" + id + "]"; } }
./src/com/bookstore/BookRepositoryImpl.java full listing (testing only)
package com.bookstore; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.math.BigDecimal; import javax.enterprise.context.ApplicationScoped; @ApplicationScoped public class BookRepositoryImpl implements BookRepository { private SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); private int count; private Map<String, Book> idToBookMap = new HashMap<String, Book>(); public BookRepositoryImpl() { synchronized (this) { books(book("War and Peace", "blah blah blah", "5.50", "5/29/1970"), book("Pride and Prejudice", "blah blah blah", "5.50", "5/29/1960"), book("book1", "blah blah blah", "5.50", "5/29/1960"), book("book2", "blah blah blah", "5.50", "5/29/1960"), book("book3", "blah blah blah", "5.50", "5/29/1960"), book("book4", "blah blah blah", "5.50", "5/29/1960"), book("book5", "blah blah blah", "5.50", "5/29/1960"), book("book6", "blah blah blah", "5.50", "5/29/1960"), book("book7", "blah blah blah", "5.50", "5/29/1960"), book("book8", "blah blah blah", "5.50", "5/29/1960"), book("book9", "blah blah blah", "5.50", "5/29/1960"), book("Java for dummies", "blah blah blah", "1.99", "5/29/1960")); } } private Book book(String title, String description, String aPrice, String aPubDate) { Date pubDate = null; BigDecimal price = null; try { price = new BigDecimal(aPrice); }catch (Exception ex) { } try { pubDate = dateFormat.parse(aPubDate); }catch (Exception ex) { } return new Book("" + (count++), title, description, price, pubDate); } private void books(Book... books) { for (Book book : books) { doAddBook(book); } } private void doAddBook(Book book) { synchronized (this) { this.idToBookMap.put(book.getId(), book); } } @Override public Book lookupBookById(String id) { synchronized (this) { return this.idToBookMap.get(id).cloneMe(); } } @Override public void addBook(String title, String description, String price, String pubDate) { doAddBook(book(title, description, price, pubDate)); } @Override public void updateBook(String id, String title, String description, String price, String pubDate) { Book book = book(title, description, price, pubDate); synchronized (this) { book.setId(id); this.idToBookMap.put(id, book); } } private List<Book> doListBooks() { List<Book> books; synchronized (this) { books = new ArrayList<Book>(this.idToBookMap.size()); for (Book book : this.idToBookMap.values()) { books.add(book.cloneMe()); } } return books; } public List<Book> listBooks() { List<Book> books = doListBooks(); Collections.sort(books, new Comparator<Book>() { public int compare(Book bookA, Book bookB) { return bookA.getId().compareTo(bookB.getId()); } }); return books; } @Override public void removeBook(String id) { synchronized(this) { this.idToBookMap.remove(id); } } }
./src/com/bookstore/BookRepository.java full listing
package com.bookstore; import java.util.List; public interface BookRepository { Book lookupBookById(String id); void addBook(String title, String description, String price, String pubDate); void updateBook(String id, String title, String description, String price, String pubDate); void removeBook(String id); List<Book> listBooks(); }
./src/com/bookstore/web/BookEditorServlet.java
package com.bookstore.web; import java.io.IOException; import java.text.SimpleDateFormat; import javax.inject.Inject; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.bookstore.Book; import com.bookstore.BookRepository; /** * Servlet implementation class BookEditorServlet */ @WebServlet("/book") public class BookEditorServlet extends HttpServlet { @Inject private BookRepository bookRepo; private SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); /** Prepare the book form before we display it. */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String id = request.getParameter("id"); if (id != null && !id.isEmpty()) { Book book = bookRepo.lookupBookById(id); request.setAttribute("book", book); request.setAttribute("bookPubDate", dateFormat.format(book.getPubDate())); } /* Redirect to book-form. */ getServletContext().getRequestDispatcher("/WEB-INF/pages/book-form.jsp").forward( request, response); } /** * Handles posting an HTML book form. If the id is null then it is an * "add book", if the id is set then it is an "update book" */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String title = request.getParameter("title"); String description = request.getParameter("description"); String price = request.getParameter("price"); String pubDate = request.getParameter("pubDate"); String id = request.getParameter("id"); if (id == null || id.isEmpty()) { bookRepo.addBook(title, description, price, pubDate); } else { bookRepo.updateBook(id, title, description, price, pubDate); } response.sendRedirect(request.getContextPath() + "/book/"); } }
./src/com/bookstore/web/BookListServlet.java
package com.bookstore.web; import java.io.IOException; import javax.inject.Inject; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.bookstore.BookRepository; @WebServlet("/book/") public class BookListServlet extends HttpServlet { @Inject private BookRepository bookRepo; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setAttribute("books", bookRepo.listBooks()); getServletContext().getRequestDispatcher("/WEB-INF/pages/book-list.jsp").forward(request, response); } }
Technical debt
We have accumulated some technical debt. Our UI is just plain HTML, it has no styling. This is probably and oversight. There is no footer, or header. There probably should be some sort of header area that has a link to the home page, and a footer with a copyright notice or something. The book listing does not format the price of the date, it should.
We will address these issues and others in the coming lessons.
Cookbooks and Tutorials
- Building a simple listing in JSP: covers model 2, Servlets, JSP intro.
- Java EE Servlet tutorial : Adding create, update and delete to the bookstore listing: covers more interactions.
- Java EE Servlet tutorial : Using JSPs to create header, footer area, formatting, and basic CSS for bookstore.
- Java EE Servlet tutorial : Adding MySQL and JDBC to bookstore example.
- Java EE Servlet tutorial : Adding validation and JSP tag files to bookstore example.
- Java EE Servlet tutorial : Adding I18N support to bookstore example.
- Java EE Servlet tutorial : Load testing and health monitoring using bookstore example.
- Java EE Servlet tutorial : Setting up clustering and session replication.
- Java EE Servlet tutorial : Setting up security for bookstore example.
- Java EE Servlet tutorial : File uploads for bookstore example.
- Java EE Servlet tutorial : Using JPA for bookstore example.
- Java EE Servlet tutorial : Using JCache for bookstore example.