© Peter Späth 2021
P. SpäthBeginning Java MVC 1.0https://doi.org/10.1007/978-1-4842-6280-1_12

12. A Java MVC Example Application

Peter Späth1 
(1)
Leipzig, Sachsen, Germany
 

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:
cd [GLASSFISH_INST]
cd javadb/bin
# start the DB client
./ij
ij> connect 'jdbc:derby://localhost:1527/booklubb;
create=true;user=bk';
ij> call SYSCS_UTIL.SYSCS_CREATE_USER('bk','pw715');
Note

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:
plugins {
    id 'war'
}
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}
repositories {
    jcenter()
}
dependencies {
  testImplementation 'junit:junit:4.12'
  implementation 'javax:javaee-api:8.0'
  implementation 'javax.mvc:javax.mvc-api:1.0.0'
  implementation 'org.eclipse.krazo:krazo-jersey:1.1.0-M1'
  implementation 'jstl:jstl:1.2'
}
task localDeploy(dependsOn: war,
             description:">>> Local deploy task") {
  doLast {
    def FS = File.separator
    def glassfish =
        project.properties['glassfish.inst.dir']
    def user = project.properties['glassfish.user']
    def passwd = project.properties['glassfish.passwd']
    File temp = File.createTempFile("asadmin-passwd",
        ".tmp")
    temp << "AS_ADMIN_${user}=${passwd} "
    def sout = new StringBuilder()
    def serr = new StringBuilder()
    def libsDir =
      "${project.projectDir}${FS}build${FS}libs"
    def proc = """${glassfish}${FS}bin${FS}asadmin
        --user ${user} --passwordfile ${temp.absolutePath}
        deploy --force=true
         ${libsDir}/${project.name}.war""".execute()
    proc.waitForProcessOutput(sout, serr)
    println "out> ${sout}"
    if(serr.toString()) System.err.println(serr)
    temp.delete()
  }
}
task localUndeploy(
             description:">>> Local undeploy task") {
  doLast {
    def FS = File.separator
    def glassfish =
        project.properties['glassfish.inst.dir']
    def user = project.properties['glassfish.user']
    def passwd = project.properties['glassfish.passwd']
    File temp = File.createTempFile("asadmin-passwd",
        ".tmp")
    temp << "AS_ADMIN_${user}=${passwd} "
    def sout = new StringBuilder()
    def serr = new StringBuilder()
    def proc = """${glassfish}${FS}bin${FS}asadmin
      --user ${user} --passwordfile ${temp.absolutePath}
      undeploy ${project.name}""".execute()
    proc.waitForProcessOutput(sout, serr)
    println "out> ${sout}"
    if(serr.toString()) System.err.println(serr)
    temp.delete()
  }
}

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
 */
@WebFilter(urlPatterns = "/")
public class RootRedirector extends HttpFilter {
  private static final long serialVersionUID =
      7332909156163673868L;
  @Override
  protected void doFilter(final HttpServletRequest req,
        final HttpServletResponse res,
        final FilterChain chain) throws IOException {
    res.sendRedirect("mvc/bk");
  }
}

Configuring BooKlubb Database Access

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:
<persistence
    xmlns:="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation=
      "http://java.sun.com/xml/ns/persistence
       persistence_1_0.xsd"
    version="1.0">
<persistence-unit name="default" transaction-type="JTA">
    <jta-data-source>jdbc/BooKlubb</jta-data-source>
    <exclude-unlisted-classes>
      false
    </exclude-unlisted-classes>
    <properties />
</persistence-unit>
</persistence>

This file’s main responsibility is to describe which database to use for the application.

The BooKlubb Internationalization

As Chapter 8 described, we use two classes, called BundleForEL and SetBundleFilter, for internationalization purposes:
package book.javamvc.bk.i18n;
import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;
import javax.servlet.http.HttpServletRequest;
public class BundleForEL extends ResourceBundle {
    private BundleForEL(Locale locale, String baseName) {
        setLocale(locale, baseName);
    }
    public static void setFor(HttpServletRequest request,
        String i18nAttributeName, String i18nBaseName) {
      if (request.getSession().
            getAttribute(i18nAttributeName) == null) {
          request.getSession().setAttribute(
            i18nAttributeName,
           new BundleForEL(request.getLocale(),
                           i18nBaseName));
      }
    }
    public void setLocale(Locale locale,
          String baseName) {
       if (parent == null ||
             !parent.getLocale().equals(locale)) {
           setParent(getBundle(baseName, locale));
       }
    }
    @Override
    public Enumeration<String> getKeys() {
        return parent.getKeys();
    }
    @Override
    protected Object handleGetObject(String key) {
        return parent.getObject(key);
    }
}
and
package book.javamvc.bk.i18n;
import java.io.IOException;
import java.util.Map;
import javax.inject.Inject;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Application;
@WebFilter("/*")
public class SetBundleFilter implements Filter {
    @Inject private Application appl;
    private String i18nAttributeName;
    private String i18nBaseName;
    @Override
    public void init(FilterConfig filterConfig)
          throws ServletException {
      Map<String,Object> applProps = appl.getProperties();
      i18nAttributeName = (String) applProps.get(
          "I18N_TEXT_ATTRIBUTE_NAME");
      i18nBaseName = (String) applProps.get(
          "I18N_TEXT_BASE_NAME");
   }
    @Override
    public void doFilter(ServletRequest request,
        ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
      BundleForEL.setFor((HttpServletRequest) request,
        i18nAttributeName, i18nBaseName);
      chain.doFilter(request, response);
    }
    @Override
    public void destroy() {
    }
}
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:
package book.javamvc.bk.db;
import java.util.Date;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "MEMBER")
@SequenceGenerator(name = "MEMBER_SEQ", initialValue = 1,
      allocationSize = 1)
public class Member {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY,
      generator = "MEMBER_SEQ")
  @Column(name = "id")
  private int id;
  @NotNull
  @Column(name = "first_name")
  private String firstName;
  @NotNull
  @Column(name = "last_name")
  private String lastName;
  @NotNull
  @Column(name = "birthday")
  private Date birthday;
  @NotNull
  @Column(name = "ssn")
  private String ssn;
  @JoinColumn(name = "MEMBER_ID")
  @OneToMany(cascade = CascadeType.ALL, orphanRemoval=true)
  private Set<Rental> rental;
  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;
  }
  public Set<Rental> getRental() {
    return rental;
  }
  public void setRental(Set<Rental> rental) {
    this.rental = rental;
  }
}
and
package book.javamvc.bk.db;
import java.util.Date;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "BOOK")
@SequenceGenerator(name = "BOOK_SEQ", initialValue = 1,
     allocationSize = 1)
public class Book {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY,
      generator = "BOOK_SEQ")
  @Column(name = "id")
  private int id;
  @NotNull
  @Column(name = "title")
  private String title;
  @NotNull
  @Column(name = "author_first_name")
  private String authorFirstName;
  @NotNull
  @Column(name = "author_last_name")
  private String authorLastName;
  @NotNull
  @Column(name = "make")
  private Date make;
  @NotNull
  @Column(name = "isbn")
  private String isbn;
  @OneToOne(cascade = CascadeType.ALL, orphanRemoval=true,
      mappedBy = "book")
  private Rental rental;
  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public String getTitle() {
    return title;
  }
  public void setTitle(String title) {
    this.title = title;
  }
  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 Date getMake() {
    return make;
  }
  public void setMake(Date make) {
    this.make = make;
  }
  public String getIsbn() {
    return isbn;
  }
  public void setIsbn(String isbn) {
    this.isbn = isbn;
  }
  public Rental getRental() {
    return rental;
  }
  public void setRental(Rental rental) {
    this.rental = rental;
  }
}
and
package book.javamvc.bk.db;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "RENTAL")
@SequenceGenerator(name = "RENTAL_SEQ", initialValue = 1,
    allocationSize = 1)
public class Rental {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY,
      generator = "RENTAL_SEQ")
  @Column(name = "id")
  private int id;
  @NotNull
  @Column(name = "member_id")
  private int memberId;
  @NotNull
  @JoinColumn(name = "book_id")
  @OneToOne
  private Book book;
  @NotNull
  @Column(name = "rental_day")
  private Date rentalDay;
  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public int getMemberId() {
    return memberId;
  }
  public void setMemberId(int memberId) {
    this.memberId = memberId;
  }
  public Book getBook() {
    return book;
  }
  public void setBook(Book book) {
    this.book = book;
  }
  public Date getRentalDay() {
    return rentalDay;
  }
  public void setRentalDay(Date rentalDay) {
    this.rentalDay = rentalDay;
  }
}

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:
package book.javamvc.bk;
import ...;
@Path("/bk")
@Controller
public class BooKlubbController {
  @Named
  @RequestScoped
  public static class ErrorMessages {
    private List<String> msgs = new ArrayList<>();
    public List<String> getMsgs() {
      return msgs;
    }
    public void setMsgs(List<String> msgs) {
      this.msgs = msgs;
    }
    public void addMessage(String msg) {
      msgs.add(msg);
    }
  }
  private @Inject ErrorMessages errorMessages;
  private @Inject BindingResult br;
  private @EJB MemberDAO memberDao;
  private @Inject MemberSearchResult memberSearchResult;
  private @EJB BookDAO bookDao;
  private @Inject BookSearchResult bookSearchResult;
  private @EJB RentalDAO rentalDao;
  private @Inject CurrentMember currentMember;
  // action methods...
}

We use an inner class for the error messages, and we inject the various model classes and DAO EJBs needed to access the database.

The complete code reads as follows:
package book.javamvc.bk;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.mvc.Controller;
import javax.mvc.binding.BindingResult;
import javax.mvc.binding.MvcBinding;
import javax.mvc.binding.ParamError;
import javax.validation.constraints.Pattern;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import book.javamvc.bk.db.Book;
import book.javamvc.bk.db.BookDAO;
import book.javamvc.bk.db.Member;
import book.javamvc.bk.db.MemberDAO;
import book.javamvc.bk.db.RentalDAO;
import book.javamvc.bk.model.BookModel;
import book.javamvc.bk.model.BookSearchResult;
import book.javamvc.bk.model.CurrentMember;
import book.javamvc.bk.model.MemberSearchResult;
@Path("/bk")
@Controller
public class BooKlubbController {
  @Named
  @RequestScoped
  public static class ErrorMessages {
    private List<String> msgs = new ArrayList<>();
    public List<String> getMsgs() {
      return msgs;
    }
    public void setMsgs(List<String> msgs) {
      this.msgs = msgs;
    }
    public void addMessage(String msg) {
      msgs.add(msg);
    }
  }
  private @Inject ErrorMessages errorMessages;
  private @Inject BindingResult br;
  private @EJB MemberDAO memberDao;
  private @Inject MemberSearchResult memberSearchResult;
  private @EJB BookDAO bookDao;
  private @Inject BookSearchResult bookSearchResult;
  private @EJB RentalDAO rentalDao;
  private @Inject CurrentMember currentMember;
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:
@GET
@Path("/searchMemberSubmit")
public Response searchMemberSubmit(
    @MvcBinding @QueryParam("firstName")
        String firstName,
    @MvcBinding @QueryParam("lastName")
        String lastName,
    @MvcBinding @QueryParam("ssn")
        String ssn) {
  showErrors();
  String ssnNormal = ssn == null ?
      "" : ( ssn.replaceAll("\D", "") );
  List<Member> l = new ArrayList<>();
  if(!ssnNormal.isEmpty()) {
    memberDao.memberBySsn(ssnNormal).ifPresent(
        m1 -> { l.add(m1); });
    } else {
      l.addAll( memberDao.membersByName(
          firstName, lastName) );
    }
    memberSearchResult.addAll(l);
    return Response.ok("searchMemberResult.jsp").build();
  }
  @POST
  @Path("/newMemberSubmit")
  public Response newMemberSubmit(
      @MvcBinding @FormParam("firstName")
          String firstName,
      @MvcBinding @FormParam("lastName")
          String lastName,
      @MvcBinding @FormParam("birthday")
          @Pattern(regexp = "\d\d/\d\d/\d\d\d\d")
          String birthday,
      @MvcBinding @FormParam("ssn")
          String ssn) {
    showErrors();
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern(
          "MM/dd/yyyy");
    LocalDate ld = LocalDate.parse(birthday, dtf);
    Date date = Date.from(ld.atStartOfDay(
        ZoneId.systemDefault()).toInstant());
    memberDao.addMember(firstName, lastName, date, ssn);
    return Response.ok("newMemberResult.jsp").build();
  }
  @POST
  @Path("/deleteMember")
  public Response deleteMember(
      @MvcBinding @FormParam("memberId")
          int memberId) {
    showErrors();
    memberDao.deleteMember(memberId);
    return Response.ok("deleteMemberResult.jsp").build();
  }
  @POST
  @Path("/selectMember")
  public Response selectMember(
      @MvcBinding @FormParam("memberId")
          int memberId) {
    showErrors();
    Member m = memberDao.memberById(memberId);
    currentMember.setId(memberId);
    currentMember.setFirstName(m.getFirstName());
    currentMember.setLastName(m.getLastName());
    currentMember.setBirthday(m.getBirthday());
    currentMember.setSsn(m.getSsn());
    currentMember.setDefined(true);
    return Response.ok("index.jsp").build();
  }
  @POST
  @Path("/memberDetails")
  public Response memberDetails(
    @MvcBinding @FormParam("memberId")
        int memberId) {
    showErrors();
    Member m = memberDao.memberById(memberId);
    currentMember.setId(memberId);
    currentMember.setFirstName(m.getFirstName());
    currentMember.setLastName(m.getLastName());
    currentMember.setBirthday(m.getBirthday());
    currentMember.setSsn(m.getSsn());
    currentMember.setRentals(
      m.getRental().stream().map(r -> {
        Book b = r.getBook();
      return new BookModel(b.getId(),
          b.getAuthorFirstName(),
          b.getAuthorLastName(),
          b.getTitle(), b.getIsbn(), b.getMake());
      }).collect(Collectors.toSet())
    );
    currentMember.setDefined(true);
    return Response.ok("memberDetails.jsp").build();
  }
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:
@GET
@Path("/searchBookSubmit")
public Response searchBookSubmit(
    @MvcBinding @QueryParam("authorFirstName")
         String authorFirstName,
    @MvcBinding @QueryParam("authorLastName")
         String authorLastName,
    @MvcBinding @QueryParam("bookTitle")
         String bookTitle,
    @MvcBinding @QueryParam("isbn")
         String isbn) {
  showErrors();
  String isbnNormal = isbn == null ?
      "" : ( isbn.replaceAll("\D", "") );
  List<Book> l = new ArrayList<>();
  if(!isbnNormal.isEmpty()) {
    bookDao.bookByIsbn(isbnNormal).ifPresent(m1 -> {
        l.add(m1); });
  } else {
    l.addAll( bookDao.booksByName(authorFirstName,
        authorLastName, bookTitle) );
  }
  bookSearchResult.addAll(l);
  return Response.ok("searchBookResult.jsp").build();
}
@POST
@Path("/newBookSubmit")
public Response newBookSubmit(
    @MvcBinding @FormParam("authorFirstName")
        String authorFirstName,
    @MvcBinding @FormParam("authorLastName")
        String authorLastName,
    @MvcBinding @FormParam("title")
        String bookTitle,
    @MvcBinding @FormParam("make")
    @Pattern(regexp = "((\d\d/)?\d\d/)?\d\d\d\d")
        String make,
    @MvcBinding @FormParam("isbn")
        String isbn) {
  showErrors();
  String isbnNormal = isbn == null ?
      "" : ( isbn.replaceAll("\D", "") );
  String makeNormal = make == null ? "" : (
    make.matches("\d\d\d\d") ?
        "01/01/" + make :
        (make.matches("\d\d/\d\d\d\d") ?
          make.substring(0,2) + "/01" +
       make.substring(2) : make)
  );
  DateTimeFormatter dtf = DateTimeFormatter.ofPattern(
      "MM/dd/yyyy");
  LocalDate ld = LocalDate.parse(makeNormal, dtf);
  Date date = Date.from(ld.atStartOfDay(
      ZoneId.systemDefault()).toInstant());
  bookDao.addBook(authorFirstName, authorLastName,
      bookTitle, date, isbnNormal);
  return Response.ok("newBookResult.jsp").build();
}
@POST
@Path("/deleteBook")
public Response deleteBook(
    @MvcBinding @FormParam("bookId")
        int bookId) {
  showErrors();
  bookDao.deleteBook(bookId);
  return Response.ok("deleteBookResult.jsp").build();
}
@POST
@Path("/assignBook")
public Response assignBook(
    @MvcBinding @FormParam("bookId")
         int bookId,
    @MvcBinding @FormParam("userId")
         int userId) {
  showErrors();
  Book b = bookDao.bookById(bookId);
  Member m = memberDao.memberById(userId);
  Date now = new Date();
  rentalDao.rentBook(b, m, now);
  return Response.ok("index.jsp").build();
}
@POST
@Path("/unassignBook")
public Response unassignBook(
    @MvcBinding @FormParam("bookId")
         int bookId,
    @MvcBinding @FormParam("memberId")
         int userId) {
  showErrors();
  Book b = bookDao.bookById(bookId);
  Member m = memberDao.memberById(userId);
  rentalDao.unrentBook(b, m);
  currentMember.setRentals(
    m.getRental().stream().map(r -> {
      Book bb = r.getBook();
    return new BookModel(bb.getId(),
          bb.getAuthorFirstName(),
          bb.getAuthorLastName(),
          bb.getTitle(),
          bb.getIsbn(),
          bb.getMake());
    }).collect(Collectors.toSet())
    );
  return Response.ok("memberDetails.jsp").build();
}
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:
<%-- File: currentMember.jsp ******************** --%>
<%@ 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" %>
<div style="background-color:#AAA;margin-bottom:1em">
${msg.current_member}
<c:choose>
  <c:when test="${! currentMember.defined}">
    ----
  </c:when>
  <c:otherwise>
    <fmt:formatDate value="${currentMember.birthday}"
                      pattern="MM/dd/yyyy" var="cubd" />
    <span style="font-weight:bold">
      ${currentMember.firstName}
      ${currentMember.lastName}
      ${cubd} (${currentMember.ssn})
    </span>
  </c:otherwise>
</c:choose>
</div>
<%-- File: errors.jsp ******************************* --%>
<%@ 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" %>
<div style="color:red">
  <c:forEach var="e" items="${errorMessages.msgs}">
    ${e}
  </c:forEach>
</div>
<%-- File: mainMenu.jsp ***************************** --%>
<%@ 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" %>
<div style="width:30%; float:left;">
  <ul>
    <li><a href="${mvc.uriBuilder(
          'BooKlubbController#searchMember').build()}">
        ${msg.menu_search_member}</a></li>
    <li><a href="${mvc.uriBuilder(
          'BooKlubbController#newMember').build()}">
        ${msg.menu_new_member}</a></li>
    <li><a href="${mvc.uriBuilder(
          'BooKlubbController#searchBook').build()}">
        ${msg.menu_search_book}</a></li>
    <li><a href="${mvc.uriBuilder(
          'BooKlubbController#newBook').build()}">
        ${msg.menu_new_book}</a></li>
  </ul>
</div>

Landing Page

The landing page, called index.jsp (in the src/main/webapp/WEB-INF/views folder), includes the aforementioned fragments and otherwise shows no content:
<%@ 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.title}</h1>
  <%@ 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.
../images/499016_1_En_12_Chapter/499016_1_En_12_Fig1_HTML.jpg
Figure 12-1

BooKlubb landing page

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:
<%-- File newMember.jsp ***************************** --%>
<%@ 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_newMember}</h1>
  <%@ include file="../../fragments/currentMember.jsp" %>
  <div>
    <%@ include file="../../fragments/mainMenu.jsp" %>
    <div style="float:left">
      <form method="post"
          action="${mvc.uriBuilder(
              'BooKlubbController#newMemberSubmit').
              build()}">
    <table><tbody>
      <tr>
        <td>${msg.enter_memberFirstName}</td>
        <td><input type="text" name="firstName" /></td>
      </tr>
      <tr>
        <td>${msg.enter_memberLastName}</td>
        <td><input type="text" name="lastName" /></td>
      </tr>
      <tr>
        <td>${msg.enter_memberBirthday}</td>
        <td><input type="text" name="birthday" /></td>
      </tr>
      <tr>
        <td>${msg.enter_memberSsn}</td>
        <td><input type="text" name="ssn" /></td>
      </tr>
    </tbody></table>
    <input type="submit" value="${msg.btn_new}" />
  </form>
   </div>
   </div>
</body>
</html>
<%-- File newMemberResult.jsp *********************** --%>
<%@ 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.new_member_added}</h1>
  <%@ include file="../../fragments/currentMember.jsp" %>
  <div>
    <%@ include file="../../fragments/mainMenu.jsp" %>
    <div style="float:left">
    </div>
  </div>
</body>
</html>
The newMember.jsp JSP shows the input form for a new member. See Figure 12-2. The resultant page just shows a corresponding success message.
../images/499016_1_En_12_Chapter/499016_1_En_12_Fig2_HTML.jpg
Figure 12-2

BooKlubb New Member page

The code to search in the member database and the page showing the resultant list are as follows:
<%-- File searchMember.jsp ************************** --%>
<%@ 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_searchMember}</h1>
  <%@ include file="../../fragments/currentMember.jsp" %>
  <div>
    <%@ include file="../../fragments/mainMenu.jsp" %>
    <div style="float:left">
      <form method="get" action="${mvc.uriBuilder(
          'BooKlubbController#searchMemberSubmit').
          build()}">
        <table><tbody>
          <tr>
            <td>${msg.enter_memberFirstName}</td>
            <td><input type="text" name="firstName" /></td>
          </tr>
          <tr>
            <td>${msg.enter_memberLastName}</td>
            <td><input type="text" name="lastName" /> </td>
          </tr>
          <tr>
            <td>${msg.enter_memberSsn}</td>
            <td><input type="text" name="ssn" /> </td>
          </tr>
        </tbody></table>
        <input type="submit" value="${msg.btn_search}" />
      </form>
    </div>
  </div>
</body>
</html>
<%-- File searchMemberResult.jsp ******************** --%>
<%@ 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>Member Search</title>
</head>
<body>
  <%@ include file="../../fragments/errors.jsp" %>
  <h1>${msg.hd_searchResult}</h1>
  <%@ 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.
../images/499016_1_En_12_Chapter/499016_1_En_12_Fig3_HTML.jpg
Figure 12-3

BooKlubb Search Member page

../images/499016_1_En_12_Chapter/499016_1_En_12_Fig4_HTML.jpg
Figure 12-4

BooKlubb search member result page

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.
../images/499016_1_En_12_Chapter/499016_1_En_12_Fig5_HTML.jpg
Figure 12-5

BooKlubb Member Details page

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:
<%-- File newBook.jsp ******************************** --%>
<%@ 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_newBook}</h1>
  <%@ include file="../../fragments/currentMember.jsp" %>
  <div>
    <%@ include file="../../fragments/mainMenu.jsp" %>
     <div style="float:left">
       <form method="post"
           action="${mvc.uriBuilder(
          'BooKlubbController#newBookSubmit').build()}">
       <table><tbody>
         <tr>
           <td>${msg.enter_authorFirstName}</td>
           <td>
             <input type="text" name="authorFirstName" />
           </td>
           </tr>
           <tr>
             <td>${msg.enter_authorLastName}</td>
             <td>
               <input type="text" name="authorLastName" />
             </td>
           </tr>
           <tr>
             <td>${msg.enter_bookTitle}</td>
             <td>
               <input type="text" name="title" />
             </td>
           </tr>
           <tr>
             <td>${msg.enter_bookMake}</td>
             <td>
               <input type="text" name="make" />
             </td>
           </tr>
           <tr>
             <td>${msg.enter_isbn}</td>
             <td>
               <input type="text" name="isbn" />
             </td>
           </tr>
         </tbody></table>
      <input type="submit" value="${msg.btn_new}" />
      </form>
    </div>
  </div>
</body>
</html>
<%-- File newBookResult.jsp ************************** --%>
<%@ 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.new_book_added}</h1>
  <%@ 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.
../images/499016_1_En_12_Chapter/499016_1_En_12_Fig6_HTML.jpg
Figure 12-6

BooKlubb New Book entry

To search the database and present the search result list, the following two files are used:
<%-- File searchBook.jsp **************************** --%>
<%@ 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_searchBook}</h1>
  <%@ include file="../../fragments/currentMember.jsp" %>
  <div>
    <%@ include file="../../fragments/mainMenu.jsp" %>
    <div style="float:left">
    <form method="get"
        action="${mvc.uriBuilder(
        'BooKlubbController#searchBookSubmit').build()}">
    <table><tbody>
      <tr>
        <td>${msg.enter_authorFirstName}</td>
        <td>
          <input type="text" name="authorFirstName" />
        </td>
      </tr>
      <tr>
        <td>${msg.enter_authorLastName}</td>
        <td>
           <input type="text" name="authorLastName" />
        </td>
      </tr>
      <tr>
        <td>${msg.enter_bookTitle}</td>
        <td>
           <input type="text" name="bookTitle"/>
        </td>
      </tr>
      <tr>
        <td>${msg.enter_isbn}</td>
        <td>
          <input type="text" name="isbn"/>
        </td>
      </tr>
    </tbody></table>
    <input type="submit" value="${msg.btn_search}" />
    </form>
    </div>
  </div>
</body>
</html>
<%-- File searchBookResult.jsp ********************** --%>
<%@ 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>Book Search</title>
</head>
<body>
  <%@ include file="../../fragments/errors.jsp" %>
  <h1>${msg.hd_searchResult}</h1>
  <%@ 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.
../images/499016_1_En_12_Chapter/499016_1_En_12_Fig7_HTML.jpg
Figure 12-7

BooKlubb book search result

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:
http://localhost:8080/BooKlubb

See Figure 12-1.

Summary

This chapter concluded the book with a comprehensive example application called BooKlubb, which illustrates many Java MVC features.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.71.237