The user interface (UI) consists of a start page for navigating to the data management UI
pages and one UI page for each object type and data management use case. All these UI pages are
defined in the form of JSF view files in subfolders of the WebContent/views/
folder.
We create the Main Menu page index.xhtml
in the
subfolder WebContent/views/app
with the following
content:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="..." xmlns:h="..." xmlns:f="..."> <ui:composition template="/WEB-INF/templates/page.xhtml"> <ui:define name="content"> <h2>Public Library</h2> <h:button value="Manage publishers" outcome="/views/publishers/index" /> <h:button value="Manage books" outcome="/views/books/index" /> <h:form> <h:commandButton value="Clear database" action="#{appCtrl.clearData()}" /> <h:commandButton value="Create test data" action="#{appCtrl.createTestData()}" /> </h:form> </ui:define> </ui:composition> </html>
It creates the menu buttons which provide the redirects to corresponding views for each of
the management pages. Additionally, we need to create a corresponding AppController class which
is responsible for the creation and deletion of the test data. The controller is used to combine
code of the Publisher.createTestData
and Publisher.clearData
methods
with Book.createTestData
and Book.clearData
methods as
follows:
@ManagedBean( name="appCtrl") @SessionScoped public class AppController { @PersistenceContext( unitName="UnidirAssocApp") private EntityManager em; @Resource() UserTransaction ut; public String clearData() { try { Book.clearData( em, ut); Publisher.clearData( em, ut); } catch ( Exception e) { e.printStackTrace(); } return "index"; } public String createTestData() { try { Publisher.createTestData( em, ut); Book.createTestData( em, ut); } catch ( Exception e) { e.printStackTrace(); } return "index"; } }
The deletion of Book
and Publisher
data must be done in a
particular order for avoiding referential integrity violations (books have to be deleted first,
before their publishers are deleted).
Since the code runs in a Tomcat container, the initialization is made internally by the container.
In our example we have only one reference property, Book::publisher
, which is
functional. For showing information about the optional publisher of a book in the list books use case, the corresponding cell in the HTML table is
filled with the name of the publisher, if there is any:
<ui:composition template="/WEB-INF/templates/page.xhtml"> <ui:define name="content"> <h:dataTable value="#{bookCtrl.books}" var="b"> ... <h:column> <f:facet name="header">Publisher</f:facet> #{b.publisher.name} </h:column> </h:dataTable> <h:button value="Main menu" outcome="index" /> </ui:define> </ui:composition>
Notice the cascade call used in the #{b.publisher.name}
JSF
expression: accessing the property name
of the publisher
which is a property of the book instance b
.
In this section, we discuss how to create and update an object which has reference
properties, such as a Book
object with the reference property
publisher
.
HTML require string or number values to populate forms. In our case, the property
publisher
of the Book
class is an object reference. JSF allows the
mapping from object and string and back by using face
converters, which are classes annotated with @FacesConverter
and
implementing the javax.faces.convert.Converter
interface:
@FacesConverter( value="pl.model.converter.PublisherConverter") public class PublisherConverter implements Converter { @Override public Object getAsObject( FacesContext context, UIComponent component, String value) { PublisherController ac = FacesContext .getCurrentInstance() .getApplication() .evaluateExpressionGet( context, "#{publisherCtrl}", PublisherController.class); EntityManager em = ac.getEntityManager(); if (value == null) return null; else return em.find( Publisher.class, value); } @Override public String getAsString( FacesContext context, UIComponent component, Object value) { if (value == null) return null; else if (value instanceof Publisher) { return ((Publisher) value).getName(); } else return null; } }
It defines two methods, getAsObject
and getAsString
, responsible
for the two mappings. It allows to define custom representations which can be used not only in
connection with HTML forms but also allows serialization which can be used with display views.
For being able to extract an object from database we need an EntityManager
which
can be obtained from our controller class instances (e.g., the managed bean
publisherCtrl
). The instance of the controller can be obtained by using the
FacesContext
singletone as shown
above:
PublisherController ac = FacesContext.getCurrentInstance()
.getApplication()
.evaluateExpressionGet( context, "#{publisherCtrl}",
PublisherController.class);
In addition, we need to add a getEntityManager
method in the controller class
as
follows:
@ManagedBean( name="publisherCtrl") @SessionScoped public class PublisherController { @PersistenceContext( unitName="UnidirAssocApp") private EntityManager em; @Resource() UserTransaction ut; public EntityManager getEntityManager() { return this.em; } ... }
JSF needs to compare two publisher instances, when the publisher list has to auto-select
the current publisher, in the update book use case. For
this reason, the Publisher
model class needs to implement the
equals
method. In our case, two publishers are "one and the same", if the
values of their name
property are
equal:
@Override public boolean equals( Object obj) { if (obj instanceof Publisher) { Publisher publisher = (Publisher) obj; return ( this.name.equals( publisher.name)); } else return false; }
To allow selection of objects which have to be associated with the currently edited object
from a list in the create and update use cases, an HTML selection list (a
select
element) is populated with the instances of the
associated object type with the help of JSF h:selectOneMenu
element. The WebContent/views/books/create.xhtml
file is updated as
follows:
<ui:composition template="/WEB-INF/templates/page.xhtml"> <ui:define name="content"> <h:form id="createBookForm" styleClass="pure-form pure-form-aligned"> <h:panelGrid columns="3"> ... <h:outputLabel for="publisher" value="Publisher:" /> <h:selectOneMenu id="publisher" value="#{book.publisher}"> <f:selectItem itemValue="" itemLabel="---" /> <f:selectItems value="#{publisherCtrl.publishers}" var="p" itemLabel="#{p.name}" itemValue="#{p}" /> <f:converter converterId="pl.model.converter.PublisherConverter" /> </h:selectOneMenu> <h:message for="publisher" errorClass="error" /> </h:panelGrid> <h:commandButton value="Create" action="#{bookCtrl.add( book.isbn, book.title, book.year, book.publisher.name)}" /> </h:form> </ui:define> </ui:composition>
The object-to-string and back converter is specified by using the f:converter
JSF element and its associated @converterId attribute
. The value of this attribute
must be the same with the one specified by the value
property of the annotation
@FacesConverter
used when the converter class is defined, e.g.,
@FacesConverter( value="pl.model.converter.PublisherConverter")
. In general, the
converter simple class name should be used (e.g., PublisherConverter
), or the
complete fully qualified name, if there is a risk of naming conflicts (e.g.,
pl.model.converter.PublisherConverter
). We recommend to use the fully qualified
name in all cases.
The #{publisherCtrl.publishers}
JSF expression results in calling the
getPublishers
method of the PublisherController
class, which is
responsible to return the list of available
publishers:
public List<Publisher> getPublishers() { return Publisher.getAllObjectsAll( em); }
The creation of the book is obtained by using the h:commandButton
JSF
element. It results in the invocation of the add
method of the
BookController
class:
public String add( String isbn, String title, Integer year, Publisher publisher) { try { Book.add( em, ut, isbn, title, year, publisher); // Enforce clearing the form after creating the Book row. // Without this, the form will show the latest completed data FacesContext facesContext = FacesContext.getCurrentInstance(); facesContext.getExternalContext().getRequestMap().remove( "book"); } catch ( EntityExistsException e) { try { ut.rollback(); } catch ( Exception e1) { e1.printStackTrace(); } e.printStackTrace(); } catch ( Exception e) { e.printStackTrace(); } return "create"; }
It simply calls the Book.add
method, responsible with the creation of a new
row in the books table.
The code for the update book use case is very similar,
the only important modification being the addition of the select
element (i.e.,
using the h:selectOneMenu
JSF element), which allows to select which book to
update:
<ui:composition template="/WEB-INF/templates/page.xhtml"> <ui:define name="content"> <h:form id="createBookForm" styleClass="pure-form pure-form-aligned"> <h:panelGrid columns="3"> ... <h:outputLabel for="selectBook" value="Select book: " /> <h:selectOneMenu id="selectBook" value="#{book.isbn}"> <f:selectItem itemValue="" itemLabel="---" /> <f:selectItems value="#{bookCtrl.books}" var="b" itemValue="#{b.isbn}" itemLabel="#{b.title}" /> <f:ajax listener="#{bookCtrl.refreshObject( book)}" render="isbn title year publisher"/> </h:selectOneMenu> <h:message for="selectedBook"/> ... </h:panelGrid> <h:commandButton value="Create" action="#{bookCtrl.add( book.isbn, book.title, book.year, book.publisher.name)}" /> </h:form> </ui:define> </ui:composition>