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 retriev/list all books use case, the corresponding column in the HTML table is filled with the names of publishers, 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 requires string or number values to populate forms. In our case, the property publisher
of the Book
class is an object reference. JSF allows converting objects to strings and vice versa by means of converters, which are classes annotated with @FacesConverter
and implementing the javax.faces.convert.Converter
interface:
@FacesConverter( value="pl.m.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; } }
A converter defines two methods: getAsObject
and getAsString
, which are responsible for the two conversions. For being able to retrieve an object from a database we need an EntityManager
, which can be obtained from a controller (e.g., the managed bean publisherCtrl
). The controller can be obtained via the FacesContext
singleton:
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 a h:selectOneMenu
element. The create.xhtml
file in the folder WebContent/views/books/
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.m.converter.PublisherConverter" /> </h:selectOneMenu> <h:message for="publisher" errorClass="error" /> </h:panelGrid> <h:commandButton value="Create" action="#{bookCtrl.create( book.isbn, book.title, book.year, book.publisher.name)}" /> </h:form> </ui:define> </ui:composition>
The converter is specified by using the f:converter
element and its associated @converterId
attribute. The value of this attribute must be the same as the one specified by the value
attribute of the annotation @FacesConverter
used when the converter class is defined, e.g.,
@FacesConverter( value = "pl.m.converter.PublisherConverter")
In general, the converter's local class name can be used (e.g., PublisherConverter
), or the fully qualified class name, if there is a risk of naming conflicts (e.g., pl.m.converter.PublisherConverter
). We recommend using the fully qualified name in all cases.
The #{publisherCtrl.publishers}
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.retrieveAll( em); }
The creation of the book is obtained by using the h:commandButton
element. It results in the invocation of the create
method of the BookController
class:
public String create( String isbn, String title, Integer year, Publisher publisher) { try { Book.create( em, ut, isbn, title, year, publisher); // Enforce clearing the form after creating the Book row 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.create
method for creating a new row in the books
table.
The code for the update book use case is very similar, the only important difference is the addition of the select
element (i.e., using the h:selectOneMenu
JSF element) for allowing to select the 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.create( book.isbn, book.title, book.year, book.publisher.name)}" /> </h:form> </ui:define> </ui:composition>