We finish the book with a comprehensive example application covering many of the aspects we talked about in previous chapters. The application in question is a book club administration that we call BooKlubb. We limit the domain to books and members, which only to some small extent supersedes the various examples we already talked about, but nevertheless can serve as a blueprint for many applications. You’ll often encounter this kind of people-things combination.
The BooKlubb application concentrates on Java MVC capabilities; we do not spend much energy on frontend design and we also do not use AJAX, to keep the distraction at a minimum. Of course, you can work out the application to any extent you like.
The BooKlubb Database
We talked about using databases in Chapter 10. We use the same built-in Apache Derby database for BooKlubb. There are three tables: MEMBER for BooKlubb members, BOOK for the books, and BOOK_RENTAL for book rental information (assigning books to members).
Before you can use Apache Derby, remember you have to start it via bin/asadmin start-database from inside the GlassFish installation folder.
Next we connect to the new database via the ij client (use any other suitable DB client if you like), and add user credentials to it:
Next time you connect, you have to provide the password, as in connect '...;user=bk;password=pw715';
To create the tables and ID sequences, you enter the following:
CREATE TABLE MEMBER (
ID INT NOT NULL,
FIRST_NAME VARCHAR(128) NOT NULL,
LAST_NAME VARCHAR(128) NOT NULL,
BIRTHDAY DATE NOT NULL,
SSN VARCHAR(16) NOT NULL,
PRIMARY KEY (ID));
CREATE SEQUENCE MEMBER_SEQ start with 1 increment by 1;
CREATE TABLE BOOK (
ID INT NOT NULL,
TITLE VARCHAR(128) NOT NULL,
AUTHOR_FIRST_NAME VARCHAR(128) NOT NULL,
AUTHOR_LAST_NAME VARCHAR(128) NOT NULL,
MAKE DATE NOT NULL,
ISBN VARCHAR(24) NOT NULL,
PRIMARY KEY (ID));
CREATE SEQUENCE BOOK_SEQ start with 1 increment by 1;
CREATE TABLE RENTAL (
ID INT NOT NULL,
MEMBER_ID INT NOT NULL,
BOOK_ID INT NOT NULL,
RENTAL_DAY DATE NOT NULL,
PRIMARY KEY (ID));
CREATE SEQUENCE RENTAL_SEQ start with 1 increment by 1;
In the GlassFish server, we need to create resources for the database connection. We can use the asadmin tool to achieve that:
cd [GLASSFISH_INST]
cd bin
./asadmin create-jdbc-connection-pool
--datasourceclassname
org.apache.derby.jdbc.ClientXADataSource
--restype javax.sql.XADataSource
--property
portNumber=1527:password=pw715:user=bk:
serverName=localhost:databaseName=booklubb:
securityMechanism=3
BooKlubbPool
./asadmin create-jdbc-resource
--connectionpoolid BooKlubbPool jdbc/BooKlubb
(There should be no line break and no spaces after bk: and booklubb:.). Because of these resources, JPA knows how to connect to the database. JPA needs a datasource and the commands create exactly such a datasource.
Caution
Datasource creation is specific to the server. If you use a server other than GlassFish, you have to consult the manual in order to learn how to crate datasources.
The BooKlubb Eclipse Project
Open Eclipse and select any suitable workspace. For example, choose the same workspace as in the book’s examples.
Create a new Gradle project: choose File ➤ New ➤ Other... ➤ Gradle ➤ Gradle Project. Enter the name BooKlubb.
If a build path error appears (view Problems), right-click the project and choose Properties ➤ Java Build Path. Remove the false JRE System Library (marked unbound), then choose Add Library and select your Java 8 JDK. Click Apply and Close. Also see the section entitled “More About Gradle” in Chapter 3.
Replace the contents of the build.gradle file with the following:
This is the same build file described in Chapter 4. Choose Gradle ➤ Refresh Gradle Project to make sure the dependencies are transported to the Java build path.
As a configuration for deployment and “un-deployment,” add a gradle.properties file to the project, adapting the values according to your needs:
glassfish.inst.dir = /path/to/your/glassfish5.1
glassfish.user = admin
glassfish.passwd =
The BooKlubb Infrastructure Classes
Similar to the HelloWorld example in Chapter 4, we use the App and RootRedirector classes to tailor the context path and create the landing page:
package book.javamvc.bk;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/mvc")
public class App extends Application {
@PostConstruct
public void init() {
}
@Override
public Map<String, Object> getProperties() {
Map<String, Object> res = new HashMap<>();
res.put("I18N_TEXT_ATTRIBUTE_NAME",
"msg");
res.put("I18N_TEXT_BASE_NAME",
"book.javamvc.bk.messages.Messages");
return res;
}
}
and
package book.javamvc.bk;
import javax.servlet.FilterChain;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Redirecting http://localhost:8080/BooKlubb/
* This way we don't need a <welcome-file-list> in web.xml
The application uses JPA to access the database. As described in Chapter 10, we need a persistence.xml file in src/main/resources/META-INF, as follows:
In src/main/resources/book/javamvc/bk/messages/Messages.properties, we put a resources file with these contents:
title = BooKlubb
menu_search_member = Search Member
menu_new_member = New Member
menu_search_book = Search Book
menu_new_book = New Book
current_member = Current Member:
enter_memberFirstName = First Name:
enter_memberLastName = Last Name:
enter_memberBirthday = Birthday:
enter_memberSsn = SSN:
enter_authorFirstName = Author First Name:
enter_authorLastName = Author First Name:
enter_bookTitle = Title:
enter_bookMake = Make:
enter_isbn = ISBN:
hd_searchResult = Search Result
hd_searchMember = Search Member
hd_newMember = New Member
hd_searchBook = Search Book
hd_newBook = New Book
hd_memberDetails = Member Details
hd_booksAssigned = Books Assigned
tblhdr_id = ID
tblhdr_last_name = Last Name
tblhdr_first_name = First Name
tblhdr_birthday = Birthday
tblhdr_ssn = SSN
tblhdr_author_last_name = Last Name
tblhdr_author_first_name = First Name
tblhdr_book_title = Title
tblhdr_book_make = Make
tblhdr_isbn = ISBN
btn_search = Search
btn_new = New
btn_delete = Delete
btn_select = Select
btn_details = u2190
btn_assign = Assign
btn_unassign = Unassign
no_result = ---- No result ----
new_member_added = New Member Added
new_book_added = New Book Added
member_deleted = Member Deleted
book_deleted = Book Deleted
memb_id = ID:
memb_firstName = First Name:
memb_lastName = Last Name:
memb_birthday = Birthday:
memb_ssn = SSN:
These key-value pairs are used exclusively by the view pages only.
The BooKlubb Entity Classes
With the database table definitions at hand, we can immediately write the JPA entity classes. This is possible without having defined any functionalities, since entity classes don’t contain any programming logic. For BooKlubb, they read as follows:
These classes reflect the database table fields and the relationships via the @OneToOne and @OneToMany annotations. The idea behind the latter is that a member may have zero, one, or more books rented (@OneToMany), and a book may or may not be rented (@OneToOne, with “not rented” reflected as a null value).
BooKlubb Database Access via DAOs
The DAOs encapsulate handling database access and deal with the entity classes. The DAOs provide methods to create, update, and delete entities, and to search inside the database. We put them in the book.javamvc.bk.db package.
package book.javamvc.bk.db;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
@Stateless
public class MemberDAO {
@PersistenceContext
private EntityManager em;
public int addMember(String firstName, String lastName,
Date birthday, String ssn) {
// First check if there is already a member with the
// same SSN. Create a new entry only if none found.
List<?> l = em.createQuery("SELECT m FROM Member m "+
"WHERE m.ssn=:ssn").
setParameter("ssn", ssn).
getResultList();
int id = 0;
if(l.isEmpty()) {
Member member = new Member();
member.setFirstName(firstName);
member.setLastName(lastName);
member.setBirthday(birthday);
member.setSsn(ssn);
em.persist(member);
em.flush(); // needed to get the ID
id = member.getId();
} else {
id = ((Member)l.get(0)).getId();
}
return id;
}
public List<Member> allMembers() {
TypedQuery<Member> q = em.createQuery(
"SELECT m FROM Member m", Member.class);
List<Member> l = q.getResultList();
return l;
}
public Member memberById(int id) {
return em.find(Member.class, id);
}
public Optional<Member> memberBySsn(String ssn) {
List<?> l = em.createQuery("SELECT m FROM Member m "+
"WHERE m.ssn=:ssn").
setParameter("ssn", ssn).
getResultList();
if(l.isEmpty()) {
return Optional.empty();
} else {
return Optional.of((Member)l.get(0));
}
}
@SuppressWarnings("unchecked")
public List<Member> membersByName(String firstName,
String lastName) {
List<?> l = em.createQuery("SELECT m FROM Member m "+
"WHERE m.firstName LIKE :fn AND "+
"m.lastName LIKE :ln").
setParameter("fn", firstName.isEmpty() ?
"%" : "%" + firstName + "%").
setParameter("ln", lastName.isEmpty() ?
"%" : "%" + lastName + "%").
getResultList();
return (List<Member>) l;
}
public void deleteMember(int id) {
Member member = em.find(Member.class, id);
em.remove(member);
}
}
You can see that we inject an instance of EntityManager as an interface to JPA. From there, we can use its methods to access database tables. For example, in addMember(), we use the JPA Query Language (JQL) to search the member’s table using the SSN given as a method parameter, and if we can’t find one, we save a new entity via EntityManager.persist(). In memberById() instead we can directly use EntityManager.find(), since the argument is the entity class’ primary key ID.
The other class, called BookDAO, primarily addresses the book table. Its code reads as follows:
package book.javamvc.bk.db;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
@Stateless
public class BookDAO {
@PersistenceContext
private EntityManager em;
public int addBook(String authorFirstName,
String authorLastName, String title,
Date make, String isbn) {
// First check if there is already a book with the
// same ISBN in the database. Create a new entry
// only if none is found.
List<?> l = em.createQuery("SELECT b FROM Book b "+
"WHERE b.isbn=:isbn").
setParameter("isbn", isbn).
getResultList();
int id = 0;
if(l.isEmpty()) {
Book book = new Book();
book.setAuthorFirstName(authorFirstName);
book.setAuthorLastName(authorLastName);
book.setTitle(title);
book.setMake(make);
book.setIsbn(isbn);
em.persist(book);
em.flush(); // needed to get the ID
id = book.getId();
} else {
id = ((Book)l.get(0)).getId();
}
return id;
}
public List<Book> allBooks() {
TypedQuery<Book> q = em.createQuery(
"SELECT b FROM Book b", Book.class);
List<Book> l = q.getResultList();
return l;
}
public Book bookById(int id) {
return em.find(Book.class, id);
}
public Optional<Book> bookByIsbn(String isbn) {
List<?> l = em.createQuery("SELECT b FROM Book b "+
"WHERE b.isbn=:isbn").
setParameter("isbn", isbn).
getResultList();
if(l.isEmpty()) {
return Optional.empty();
} else {
return Optional.of((Book)l.get(0));
}
}
@SuppressWarnings("unchecked")
public List<Book> booksByName(String authorFirstName,
String authorLastName, String bookTitle) {
String afn = (authorFirstName == null ||
authorFirstName.isEmpty() ) ?
"%" : ("%"+authorFirstName+"%");
String aln = (authorLastName == null ||
authorLastName.isEmpty() ) ?
"%" : ("%"+authorLastName+"%");
String t = (bookTitle == null ||
bookTitle.isEmpty() ) ?
"%" : ("%"+bookTitle+"%");
List<?> l = em.createQuery("SELECT b FROM Book b "+
"WHERE b.title LIKE :title AND "+
"b.authorLastName LIKE :aln AND "+
"b.authorFirstName LIKE :afn").
setParameter("title", t).
setParameter("aln", aln).
setParameter("afn", afn).
getResultList();
return (List<Book>) l;
}
public void deleteBook(int id) {
Book book = em.find(Book.class, id);
em.remove(book);
}
}
The third DAO class, called RentalDAO, registers book rentals (assigns books to members):
package book.javamvc.bk.db;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Stateless
public class RentalDAO {
@PersistenceContext
private EntityManager em;
public void rentBook(Book b, Member m, Date day) {
Rental r = b.getRental();
if(r == null) {
r = new Rental();
}
// Update the BOOK table
r.setBook(b);
r.setMemberId(m.getId());
r.setRentalDay(day);
b.setRental(r);
em.merge(b);
// Update the MEMBER table
Set<Rental> rs = m.getRental();
if(rs.stream().allMatch(r1 -> {
return r1.getBook().getId() != b.getId(); })) {
rs.add(r);
m.setRental(rs);
em.merge(m);
}
}
public void unrentBook(Book b, Member m) {
Rental r = b.getRental();
if(r == null) return;
// Update the BOOK table
b.setRental(null);
em.merge(b);
// Update the MEMBER table
Set<Rental> newRental =
m.getRental().stream().filter(rr -> {
return rr.getBook().getId() != b.getId(); }).
collect(Collectors.toSet());
m.setRental(newRental);
em.merge(m);
}
}
The BooKlubb Model
The model part of the BooKlubb application (Java MVC model, not database model) consists of a couple of classes that transport data between the controller and the views:
MemberModel: Contains a club member. We need it only as an item type for a member search result list. Request scoped.
MemberSearchResult: A result list from a member search. Request scoped.
BookModel: Contains book information. We need it as an item type for a book search result list, and for the book rentals listed in the current member’s details view. Request scoped.
BookSearchResult: A result list from a book search. Request scoped.
CurrentMember: Contains information about the currently selected member. This is the only model bean that is session-scoped. We need this broader scope because a current member can be chosen from the member search result list and henceforth must be remembered in order to assign books to this member on a different page.
We put them all in the book.javamvc.bk.model package and the code reads as follows:
package book.javamvc.bk.model;
import java.util.Date;
public class MemberModel {
private int id;
private String firstName;
private String lastName;
private Date birthday;
private String ssn;
public MemberModel(int id, String firstName,
String lastName, Date birthday, String ssn) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.birthday = birthday;
this.ssn = ssn;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getSsn() {
return ssn;
}
public void setSsn(String ssn) {
this.ssn = ssn;
}
}
and
package book.javamvc.bk.model;
import java.util.ArrayList;
import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import book.javamvc.bk.db.Member;
@Named
@RequestScoped
public class MemberSearchResult extends
ArrayList<MemberModel>{
private static final long serialVersionUID =
-5926389915908884067L;
public void addAll(List<Member> l) {
l.forEach(m -> {
add(new MemberModel(
m.getId(),
m.getFirstName(),
m.getLastName(),
m.getBirthday(),
m.getSsn()
));
});
}
}
In this class, we added a convenience method called addAll( List < Member > l ) with the Member class from the database layer. Normally we don’t want to use database entities outside the DAOs, but Member is just a data holder and we don’t need any functionalities for it. So mixing of layers doesn’t impact the application architecture too much.
package book.javamvc.bk.model;
import java.util.Date;
public class BookModel {
private int id;
private String authorFirstName;
private String authorLastName;
private String title;
private String isbn;
private Date make;
public BookModel(int id, String authorFirstName,
String authorLastName, String title, String isbn,
Date make) {
this.id = id;
this.authorFirstName = authorFirstName;
this.authorLastName = authorLastName;
this.title = title;
this.isbn = isbn;
this.make = make;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAuthorFirstName() {
return authorFirstName;
}
public void setAuthorFirstName(String authorFirstName) {
this.authorFirstName = authorFirstName;
}
public String getAuthorLastName() {
return authorLastName;
}
public void setAuthorLastName(String authorLastName) {
this.authorLastName = authorLastName;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public Date getMake() {
return make;
}
public void setMake(Date make) {
this.make = make;
}
}
and
package book.javamvc.bk.model;
import java.util.ArrayList;
import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import book.javamvc.bk.db.Book;
@Named
@RequestScoped
public class BookSearchResult extends
ArrayList<BookModel>{
private static final long serialVersionUID =
-5926389915908884067L;
public void addAll(List<Book> l) {
l.forEach(b -> {
add(new BookModel(
b.getId(),
b.getAuthorFirstName(),
b.getAuthorLastName(),
b.getTitle(),
b.getIsbn(),
b.getMake()
));
});
}
}
and
package book.javamvc.bk.model;
import java.io.Serializable;
import java.util.Date;
import java.util.Set;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;
@Named
@SessionScoped
public class CurrentMember extends MemberModel
implements Serializable {
private static final long serialVersionUID =
-7855133427774616033L;
public CurrentMember(int id, String firstName,
String lastName, Date birthday, String ssn) {
super(id, firstName, lastName, birthday, ssn);
}
private boolean defined = false;
private Set<BookModel> rentals;
public boolean isDefined() {
return defined;
}
public void setDefined(boolean defined) {
this.defined = defined;
}
public void setRentals(Set<BookModel> rentals) {
this.rentals = rentals;
}
public Set<BookModel> getRentals() {
return rentals;
}
}
The BooKlubb Controller
The controller is responsible for receiving all POST and GET actions from the views. In Java MVC and for the BooKlubb application, it looks like this:
We add a couple of methods that use @GET to retrieve pages without user input:
@GET
public String showIndex() {
return "index.jsp";
}
@GET
@Path("/searchMember")
public Response searchMember() {
return Response.ok("searchMember.jsp").build();
}
@GET
@Path("/newMember")
public Response newMember() {
return Response.ok("newMember.jsp").build();
}
@GET
@Path("/searchBook")
public Response searchBook() {
return Response.ok("searchBook.jsp").build();
}
@GET
@Path("/newBook")
public Response newBook() {
return Response.ok("newBook.jsp").build();
}
The following are methods that relate to members: showing a list of searched-for members, reacting to creating a new member, deleting a member, showing member details, and selecting a member:
We just need to add the book-related methods, which includes reacting to searching for books, adding or deleting a book, and assigning or “unassigning” a book:
We add one private method, which transports errors detected by Java MVC, and then close the class:
private void showErrors() {
if(br.isFailed()) {
br.getAllErrors().stream().forEach(
(ParamError pe) -> {
errorMessages.addMessage(pe.getParamName() +
": " + pe.getMessage());
});
}
}
} // closing the class
The BooKlubb View
As we did in the other Java MVC applications in this book, we add an empty file called beans.xml to src/main/webapp/WEB-INF. Also, add the usual glassfish-web.xml to the same folder:
<?xml version="1.0" encoding="UTF-8"?>
<glassfish-web-app error-url="">
<class-loader delegate="true"/>
</glassfish-web-app>
Furthermore, download a jQuery distribution and put it in the src/main/webapp/js folder.
In the following section, we describe the view-related JSP files needed for BooKlubb.
Fragment Files
These elements are shown on every web page—a main menu, the currently selected member, and any error information. We therefore extract them as fragments to be included via the <%@ include ... %> directive.
The fragments are placed in the src/main/webapp/fragments folder; the code reads as follows:
The landing page, called index.jsp (in the src/main/webapp/WEB-INF/views folder), includes the aforementioned fragments and otherwise shows no content:
<%@ include file="../../fragments/currentMember.jsp" %>
<div>
<%@ include file="../../fragments/mainMenu.jsp" %>
<div style="float:left">
</div>
</div>
</body>
</html>
Caution
Make sure you enter the correct version of the jQuery distribution you downloaded. The same holds true for all JSP files presented in subsequent sections.
All JSP files use the same overall structure:
<div style="float:left">
</div>
This empty tag will serve as a container for the actual page contents. Figure 12-1 shows the browser page when you’re entering the application.
Member-Related View Files
To create a new member, delete a member, search for a member, and show member details (including books assigned)—as well as for the action result pages for most of these—we need a separate JSP page. They all reside in the src/main/webapp/WEB-INF/views folder.
The code to create a new member and the resultant page are as follows:
<%@ include file="../../fragments/currentMember.jsp" %>
<div>
<%@ include file="../../fragments/mainMenu.jsp" %>
<div style="float:left">
<c:choose>
<c:when test="${empty memberSearchResult}">
${msg.no_result}
</c:when>
<c:otherwise>
<table>
<thead>
<tr>
<th>${msg.tblhdr_id}</th>
<th>${msg.tblhdr_last_name}</th>
<th>${msg.tblhdr_first_name}</th>
<th>${msg.tblhdr_birthday}</th>
<th>${msg.tblhdr_ssn}</th>
<th></th>
<th></th>
</tr>
<thead>
<tbody>
<c:forEach items="${memberSearchResult}"
var="itm">
<tr id="itm-${itm.id}">
<td>${itm.id}</td>
<td>${itm.lastName}</td>
<td>${itm.firstName}</td>
<fmt:formatDate value="${itm.birthday}"
pattern="MM/dd/yyyy"
var="d1" />
<td>${d1}</td>
<td>${itm.ssn}</td>
<td><button onclick="deleteItm(${itm.id})">
${msg.btn_delete}</button></td>
<td><button onclick="selectMember(${itm.id})">
${msg.btn_select}</button></td>
<td><button onclick="showDetails(${itm.id})">
${msg.btn_details}</button></td>
</tr>
</c:forEach>
</tbody>
</table>
</c:otherwise>
</c:choose>
<script type="text/javascript">
function deleteItm(id) {
jQuery('#memberIdForDelete').val(id);
jQuery('#deleteForm').submit();
}
function selectMember(id) {
jQuery('#memberIdForSelect').val(id);
jQuery('#selectForm').submit();
}
function showDetails(id) {
jQuery('#memberIdForDetails').val(id);
jQuery('#detailsForm').submit();
}
</script>
<form id="deleteForm" method="post"
action="${mvc.uriBuilder(
'BooKlubbController#deleteMember').
build()}">
<input id="memberIdForDelete" type="hidden"
name="memberId" />
</form>
<form id="selectForm" method="post"
action="${mvc.uriBuilder(
'BooKlubbController#selectMember').
build()}">
<input id="memberIdForSelect" type="hidden"
name="memberId" />
</form>
<form id="detailsForm" method="post"
action="${mvc.uriBuilder(
'BooKlubbController#memberDetails').
build()}">
<input id="memberIdForDetails" type="hidden"
name="memberId" />
</form>
</div>
</div>
</body>
</html>
The searchMember.jsp file shows an input form for a member search; see Figure 12-3. The resultant page shows the corresponding member list, as shown in Figure 12-4.
You can see that each member item in the list has three buttons—one for deleting the member, one for making it the current member, one for showing member details. We use JavaScript to forward button clicks to one of the invisible forms added near the end of the file.
After member deletion, we just show a success message, which is defined in the deleteMemberResult.jsp file:
<%@ page contentType="text/html;charset=UTF-8"
language="java" %>
<%@ taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt"
uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<meta charset="UTF-8">
<title>Member Search</title>
</head>
<body>
<%@ include file="../../fragments/errors.jsp" %>
<h1>${msg.member_deleted}</h1>
<%@ include file="../../fragments/currentMember.jsp" %>
<div>
<%@ include file="../../fragments/mainMenu.jsp" %>
<div style="float:left">
</div>
</div>
</body>
</html>
On the details page, we show the member information and the books assigned. This is defined by the memberDetails.jsp file:
<%@ page contentType="text/html;charset=UTF-8"
language="java" %>
<%@ taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt"
uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<meta charset="UTF-8">
<script type="text/javascript"
src="${mvc.basePath}/../js/jquery-3.5.1.min.js">
</script>
<title>${msg.title}</title>
</head>
<body>
<%@ include file="../../fragments/errors.jsp" %>
<h1>${msg.hd_memberDetails}</h1>
<%@ include file="../../fragments/currentMember.jsp" %>
<div>
<%@ include file="../../fragments/mainMenu.jsp" %>
<div style="float:left">
<table>
<tbody>
<tr>
<td>${msg.memb_id}</td>
<td>${currentMember.id}</td>
</tr>
<tr>
<td>${msg.memb_firstName}</td>
<td>${currentMember.firstName}</td>
</tr>
<tr>
<td>${msg.memb_lastName}</td>
<td>${currentMember.lastName}</td>
</tr>
<fmt:formatDate value="${currentMember.birthday}"
pattern="MM/dd/yyyy"
var="bd" />
<tr>
<td>${msg.memb_birthday}</td>
<td>${bd}</td>
</tr>
<tr>
<td>${msg.memb_ssn}</td>
<td>${currentMember.ssn}</td>
</tr>
</tbody>
</table>
<h2>${msg.hd_booksAssigned}</h2>
<c:choose>
<c:when test="${empty currentMember.rentals}">
----
</c:when>
<c:otherwise>
<table>
<tbody>
<c:forEach items="${currentMember.rentals}"
var="r">
<tr>
<td>${r.authorFirstName}
${r.authorLastName}</td>
<td>${r.title}</td>
<fmt:formatDate value="${r.make}"
pattern="MM/dd/yyyy"
var="makeDay" />
<td>${makeDay}</td>
<td>
<button onclick="unassign(
${currentMember.id},${r.id})">
${msg.btn_unassign}
</button>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</c:otherwise>
</c:choose>
<script type="text/javascript">
function unassign(memberId,bookId) {
jQuery('#memberIdForUnassign').val(memberId);
jQuery('#bookIdForUnassign').val(bookId);
jQuery('#unassignForm').submit();
}
</script>
<form id="unassignForm" method="post"
action="${mvc.uriBuilder(
'BooKlubbController#unassignBook').build()}">
<input id="memberIdForUnassign" type="hidden"
name="memberId" />
<input id="bookIdForUnassign" type="hidden"
name="bookId" />
</form>
</div>
</div>
</body>
</html>
In the books assigned list, we again use buttons to unassign books, and JavaScript to submit an invisible form. Figure 12-5 shows a details page example. Assigning books to members happens in the book search result list, discussed in a later section.
Book-Related View Files
For books, we identify the following use cases: create a new book record, delete a book record, search for a book, and assign a book to a member (rental). We have JSP pages to create a book and to search for a book, plus action result pages. Just as with the members, they all reside in the src/main/webapp/WEB-INF/views folder. Book record deletion and assignment to the current member happens from inside the book search result list.
The code to create a book record and its corresponding submit result page is as follows:
<%@ include file="../../fragments/currentMember.jsp" %>
<div>
<%@ include file="../../fragments/mainMenu.jsp" %>
<div style="float:left">
</div>
</div>
</body>
</html>
The new book page is a form for entering the author’s name, the book title, make, and the ISBN number. See Figure 12-6. The resultant page just shows an info message.
To search the database and present the search result list, the following two files are used:
<%@ include file="../../fragments/currentMember.jsp" %>
<div>
<%@ include file="../../fragments/mainMenu.jsp" %>
<div style="float:left">
<c:choose>
<c:when test="${empty bookSearchResult}">
${msg.no_result}
</c:when>
<c:otherwise>
<table>
<thead>
<tr>
<th>${msg.tblhdr_id}</th>
<th>${msg.tblhdr_author_last_name}</th>
<th>${msg.tblhdr_author_first_name}</th>
<th>${msg.tblhdr_book_title}</th>
<th>${msg.tblhdr_book_make}</th>
<th>${msg.tblhdr_isbn}</th>
<th></th>
<th></th>
</tr>
<thead>
<tbody>
<c:forEach items="${bookSearchResult}"
var="itm">
<tr id="itm-${itm.id}">
<td>${itm.id}</td>
<td>${itm.authorLastName}</td>
<td>${itm.authorFirstName}</td>
<td>${itm.title}</td>
<fmt:formatDate value="${itm.make}"
pattern="MM/dd/yyyy"
var="d1" />
<td>${d1}</td>
<td>${itm.isbn}</td>
<td><button onclick="deleteItm(${itm.id})">
${msg.btn_delete}
</button>
</td>
<td><button onclick="assignItm(${itm.id},
${currentMember.id})">
${msg.btn_assign}
</button>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</c:otherwise>
</c:choose>
<script type="text/javascript">
function deleteItm(id) {
jQuery('#bookIdForDelete').val(id);
jQuery('#deleteForm').submit();
}
function assignItm(bookId, userId) {
jQuery('#bookIdForAssign').val(bookId);
jQuery('#userIdForAssign').val(userId);
jQuery('#assignForm').submit();
}
</script>
<form id="deleteForm" method="post"
action="${mvc.uriBuilder(
'BooKlubbController#deleteBook').build()}">
<input id="bookIdForDelete" type="hidden"
name="bookId" />
</form>
<form id="assignForm" method="post"
action="${mvc.uriBuilder(
'BooKlubbController#assignBook').build()}">
<input id="bookIdForAssign" type="hidden"
name="bookId" />
<input id="userIdForAssign" type="hidden"
name="userId" />
</form>
</div>
</div>
</body>
</html>
The book search result list is depicted in Figure 12-7. For each list item, we provide a Delete and an Assign button. JavaScript code takes care of forwarding button presses to one of the two invisible forms added near the end of the code.
After clicking one of the Delete buttons, a simple success message is shown. The deleteBookResult.jsp file takes care of that:
<%@ page contentType="text/html;charset=UTF-8"
language="java" %>
<%@ taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt"
uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<meta charset="UTF-8">
<title>Book Search</title>
</head>
<body>
<%@ include file="../../fragments/errors.jsp" %>
<h1>${msg.book_deleted}</h1>
<%@ include file="../../fragments/currentMember.jsp" %>
<div>
<%@ include file="../../fragments/mainMenu.jsp" %>
<div style="float:left">
</div>
</div>
</body>
</html>
Deploying and Testing BooKlubb
To build and deploy the BooKlubb application, you enter the following inside the console:
./gradlew localDeploy
# or, if you need to specify a certain JDK
JAVA_HOME=/path/to/jdk ./gradlew localDeploy
For this to work, the GlassFish server must be running and the gradle.properties file must contain the correct connection properties for the GlassFish server. The WAR file that’s built during this process is copied into the build/libs folder.
If everything works correctly, you can point your browser to the following URL to enter the application: