In this example we implement the Book hierarchy shown in Figure 12.1. The Java Entity class model is derived from this design model.
Recall that we obtain a Java Entity class modelfrom an OO class model by (1) making all properties private, (2) using Java datatype classes, (3) adding public getters and setters, (4) adding a toString
function, (5) adding the (static) data storage methods create
, retrieve
, update
and delete
, resulting in the model shown in Figure 14.1.
We have to deal with a number of new issues:
In the model code we have to take care of:
Coding the enumeration type (BookCategoryEL
) that is used in the UI code for creating a selection list widget that allows choosing the category of book.
Code each class from the Book
class hierarchy using suitable JPA annotations for persistent storage in a corresponding database table, like books
.
In the UI code we have to take care of:
Adding a "Special type" column to the display table of the "retrieve/list all books" use case in retrieveAndListAll.xhtml
in the folder WebContent/views/books/
. A book without a special category will have an empty table cell, while for all other books their category will be shown in this cell, along with other segment-specific attribute values.
Adding a "Special type" select control, and corresponding form fields for all segment properties, in the forms of the "Create book" and "Update book" use cases in create.xhtml
and update.xhtml
in the folder WebContent/views/books/
. Segment property form fields are only displayed, and their validation is performed, when a corresponding book category has been selected. Such an approach of rendering specific form fields only on certain conditions is sometimes called dynamic forms.
The Java Entity class model can be directly coded for getting the code of the model classes of our Java EE back-end app.
Code the enumeration type (to be used in the facelets) .
Code the model classes and add the corresponding JPA annotations for class hierarchy.
These steps are discussed in more detail in the following sections.
The enumeration type BookCategoryEL
is coded as a Java enum
, providing both enumeration literals as well as (human-readable) labels:
public enum BookCategoryEL { TEXTBOOK("TextBook"), BIOGRAPHY("Biography"); private final String label; private BookCategoryEL( String label) { this.label = label; } public String getLabel() {return this.label;} }
We code the model classes Book
, TextBook
and Biography
in the form of Java Entity classes using suitable JPA annotations:
@Entity @Table( name="books") @Inheritance( strategy=InheritanceType.SINGLE_TABLE) @DiscriminatorColumn( name="category", discriminatorType=DiscriminatorType.STRING, length=16) @DiscriminatorValue( value="BOOK") @ManagedBean( name="book") @ViewScoped public class Book { @Id @Column( length=10) @NotNull( message="An ISBN is required!") @Pattern( regexp="\\b\\d{9}(\\d|X)\\b", message="The ISBN must be a ...!") private String isbn; @Column( nullable=false) @NotNull( message="A title is required!") private String title; @Column( nullable=false) @NotNull( message="A year is required!") @UpToNextYear @Min( value=1459, message="The year must not be before 1459!") private Integer year; // constructors, set, get and other methods ... }
When using the JPA Single Table Inheritance technique for a class hierarchy, the @Inheritance
annotation must be set for the superclass (e.g, Book
in our example). It provides the strategy
parameter with the value InheritanceType.SINGLE_TABLE
. The @DiscriminatorColumn
annotation specifies the name of the table column storing the values that discriminate between different types of subclasses (TextBook
and Biography
, in our example).
Unfortunately, we cannot use our BookCategoryEL
enumeration for defining the type of the discriminator column, because Java expressions like BookCategoryEL.TEXTBOOK.name()
are not constant, so a Java compiler exception is thrown. Instead, we use DiscriminatorType.STRING
. The @DiscriminatorValue
annotation specifies a unique value to be stored in the discriminator column of table rows representing an instance of the given class.
Further, we define the subtypes TextBook
and Biography
:
@Entity @Table( name="textbooks")
@DiscriminatorValue( value="TEXTBOOK")
@ManagedBean( name="textBook") @ViewScoped
public class TextBook extends Book {
@Column( nullable=false) @NotNull(
message="A subject area value is required")
String subjectArea;
// constructors, set, get and other methods
...
}
TextBook
and Biography
are subclasses of Book
, so we define them with extends Book
. For each subclass, we add a @DiscriminatorValue
annotation with a value from the BookCategoryEL
enumeration.
As a result of the annotation
@Inheritance( strategy=InheritanceType.SINGLE_TABLE)
only one database table is used for representing a class hierarchy. This table contains the columns corresponding to all the properties of all the classes from the hierarchy plus the discriminator column. In our example, the books
table contains the following columns: isbn
, title
, year
, subjectArea
, about
and category
. The simplified corresponding SQL-DDL code internally used to generate the books
table for our application is shown below:
CREATE TABLE IF NOT EXISTS `books` ( `ISBN` varchar(10) NOT NULL, `TITLE` varchar(255) NOT NULL, `YEAR` int(11) NOT NULL, `SUBJECTAREA` varchar(255) DEFAULT NULL, `ABOUT` varchar(255) DEFAULT NULL, `category` varchar(16) DEFAULT NULL, )
The user interface (UI) consists of a start page that allows navigating to the data management pages (in our example, to index.xhtml
in the folder WebContent/views/books/
). Such a data management page contains four sections: retrieve/list all books, create book, update book and delete book.
We have to take care of handling the category discriminator and the subjectArea
and about
segment properties both in the "List all books" use case as well as in the "Create book" and "Update book" use cases by
Adding a segment information column ("Special type") to the display table of the Retrieve/List All use case in retrieveAndListAll.xhtml
in the folder WebContent/views/books/
.
Adding a "Special type" select control, and corresponding form fields for all segment properties, in the forms of the "Create book" and "Update book" use cases in create.xhtml
and update.xhtml
in the folder WebContent/views/books/
. Segment property form fields are only displayed, and their validation occurs only when a corresponding book category has been selected.
We add a "Special type" column to the display table of the "List all books" use case in books.html
:
<h:dataTable value="#{bookController.books}" var="b"> ... <h:column> <f:facet name="header">Special type</f:facet> #{b.getClass().getSimpleName() != 'Book' ? b.getClass().getSimpleName() : ''} </h:column> </h:dataTable>
A conditional expression is used to check if the Java bean class name is Book
, in which case we don't show it, or if it is something else (e.g. TextBook
or Biography
), and then it is shown. This expression also shows how you can call/use various Java bean methods, not only custom methods.
In both use cases, we need to allow selecting a special category of book ('textbook' or 'biography') with the help of a select control in a panel grid element:
<h:panelGrid id="bookPanel" columns="3"> ... <h:outputText value="Special type: " /> <h:selectOneMenu id="bookType" value="#{viewScope.bookTypeVal}"> <f:selectItem itemLabel="---"/> <f:selectItems value="#{book.typeItems}" /> <f:ajax event="change" execute="@this" render="textBookPanel biographyBookPanel standardBookPanel"/> </h:selectOneMenu> <h:message for="bookType" errorClass="error" /> </h:panelGrid>
In the select control, three alternative panel groups are referenced:
A "standardBookPanel":
<h:panelGroup id="standardBookPanel">
<h:commandButton value="Create"
rendered="#{viewScope.bookTypeVal == null}"
action="#{bookController.create( book.isbn,
book.title, book.year)}" />
</h:panelGroup>
A "textBookPanel":
<h:panelGroup id="textBookPanel"> <h:panelGrid rendered="#{viewScope.bookTypeVal == 'TEXTBOOK'}" columns="3"> <h:outputText value="Subject area: " /> <h:inputText id="subjectArea" value="#{textBook.subjectArea}"/> <h:message for="subjectArea" errorClass="error" /> </h:panelGrid> <h:commandButton value="Create" rendered="#{viewScope.bookTypeVal == 'TEXTBOOK'}" action="#{textBookController.create( book.isbn, book.title, book.year, textBook.subjectArea)}" /> </h:panelGroup>
A "biographyBookPanel":
<h:panelGroup id="biographyBookPanel"> <h:panelGrid rendered="#{viewScope.bookTypeVal == 'BIOGRAPHY'}" columns="3"> <h:outputText value="About: " /> <h:inputText id="about" value="#{biographyBook.about}"/> <h:message for="about" errorClass="error" /> </h:panelGrid> <h:commandButton value="Create" rendered="#{viewScope.bookTypeVal == 'BIOGRAPHY'}" action="#{biographyBookController.create( book.isbn, book.title, book.year, biographyBook.about)}" /> </h:panelGroup>
There are a few important remarks on the above code:
the h:selectOneMenu
is used to create a single selection list which is populated with book titles by using the getTypeItems
method of the Book
class. A more detailed explanation is presented in Part 3 enumeration app.
it is possible to conditionally render facelet components by using the rendered
attribute. The JSF EL expression must return true
or false
, this making the HTML resulting elements to be part of the HTML DOM or not. Notice that the conditional expressions are evaluated in the server side. This is the method we use to hide or show the input form elements corresponding to various book types (e.g., TextBook
has a subjectArea
property while Biography has an about
property).
the render
attribute used with f:ajax
specifies which of the JSF components are to be updated . This is needed because of the live DOM changes (client side, not server side) which applies after the AJAX call.
AJAX is used to submit the form for reevaluation when the special type selection list field is changed (something is selected). As a result, it enforces the rendering of the three panels corresponding to three book cases: simple book, text book and biography. Using execute="@this"
we enforce the re-evaluation of the form at server side, so the resulting HTML DOM structure contains the changes accordiong to the conditions specified by the rendered
attributes of the various JSF elements. Notice that a JSF element that has a conditional rendered
expression must be a child of another JSF element which is always part of the DOM.
h:panelGroup
is used to define a set of elements which are either shown or hidden.
the action
attribute of the h:commandButton
can't be used with conditional expressions, therefore we have to create three command buttons, one for each case: create/update a Book
, a TextBook
or a Biography
. The create
method of the corresponding controller class (i.e., BookController
, TextBookController
or BiographyController
is called).
since we do not have a corresponding property in the Java bean class(es) for the special type (category), we can use JSF variables to store the value of the single select list, and then use the variable in rendered
conditions for various elements. Therefore, for the h:selectOneMenu
, the value="#{viewScope.bookTypeVal}"
specifies that we use a "on the fly" defined property named bookTypeVal
, which is part of the view scope internal JSF object(s). We can also define such variable outside the view scope, e.g., value="#{bookTypeVal}"
, but in this case they are request scoped, so their value is lost after submitting the form, and the rendered
conditions can't be correctly evaluated.
For a class in the class hierarchy, one corresponding controller class is defined. It contains the specific create
, update
and delete
methods. The shared methods, such as getAllObjects
are defined by the controller of the top level class (e.g., for our example, this is BookController
). See minimal app for more details on how to implement the controller class and the corresponding CRUD methods.
The Update Book test case is very similar with the Create Book test case. The Delete Book test case remains unchanged, see See minimal app for more details.