-
Notifications
You must be signed in to change notification settings - Fork 2
Tutorial: a Java EE Web Profile application with nemo utils, part 4
In this step we develop a simple CRUD (Create, Retrieve, Update and Delete) functionality in the AutoTrab project using nemo-utils's mini CRUD framework.
nemo-utils's mini CRUD framework is built for use in WebApps that follow a three-tier architecture as shown in the figure below.
At the Presentation Tier, the View is composed of Web pages and related resources. In the previous step, we used Facelets to apply a template to our starting page (and will continue to do so with other pages). In this package we will also use Primefaces, a JSF framework that offers a large set of components to assemble web forms and such.
The Control package contains JSF Managed Beans, which are classes responsible for establishing the communication between "the Java world" and "the Web world": JSF Web pages at the View refer to attributes and methods of JSF Beans at the Control using an Expression Language (EL), allowing users to send and obtain data from the WebApp using a Web interface.
At the Business Tier, the Application and Domain packages implement the business rules independently of the presentation and persistence technologies. In Domain we have the entity (persistent) classes that represent elements of the domain of the problem (in this case, students, classes, assignments, etc.), whereas in Application there are Session EJBs which implement the use cases of the system (create a class, create assignment for class, submit assignment, etc.).
Finally, the Data Access Tier is composed of a single package, Persistence, which is where the Data Access Objects are. The DAOs are responsible for storing, retrieving and deleting data from the domain entities in the data storage (in our case, the MySQL database) and uses JPA to do it through Object/Relational Mapping.
The dependencies between the three tiers -- Control depends on Application to execute the system's use cases upon user request and, in its turn, Application depends on Persistence to store data in the database -- are satisfied by CDI, which, along with JSF, EJBs and JPA, is part of Java EE.
In this step of the tutorial, we will implement a simple CRUD functionality going through each of the packages of the above architecture. But first, as usual, some configuration.
We begin by configuring JPA in our project: right-click on it and open its properties. Then, open Project Facets, check JPA and click OK. A new element called JPA Content
should appear under your project. Expand it, open JPA Content/persistence.xml
and switch to the Source tab of the editor. You should see the element <persistence-unit name="AutoTrab">
. Add the following configuration inside it:
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<jta-data-source>java:/jboss/datasources/AutoTrab</jta-data-source>
<class>br.ufes.inf.nemo.util.ejb3.persistence.PersistentObjectSupport</class>
<properties>
<!-- Properties for Hibernate -->
<property name="hibernate.hbm2ddl.auto" value="update" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
</properties>
This file is telling JPA the following:
- Use Hibernate as JPA implementation -- we don't need to worry about providing it, because it's provided by WildFly. When using different application servers (or different JPA implementation) you need to make sure it's provided or add it yourself;
- Store and retrieve data from the database we created in step 2 of this tutorial, which was given the name
jboss/datasources/AutoTrab
; - Consider as persistent the class
PersistentObjectSupport
and all of its subclasses. This class belongs to nemo-utils and is used as superclass of all domain classes in our application, so all of them are considered persistent; - Configure Hibernate to (a) automatically generate the database schema based on the persistent entity classes; (b) show the SQL commands it sends to the database; and (c) format these commands as they are shown for easier reading.
With JPA properly configured, we start implementing our feature with the domain class.
In the Domain package we create the classes that represent real entities from the domain of the problem. In this step we will create a simple CRUD based on a simple class: Semester
. So first, create the class: right-click Java Resources/src
and choose New > Class. Fill in:
-
Package:
br.ufes.inf.nemo.autotrab.domain
; -
Name:
Semester
; -
Superclass:
br.ufes.inf.nemo.util.ejb3.persistence.PersistentObjectSupport
(you can writePOS
and hit Ctrl+Space then choose from the list of classes presented by Eclipse).
Hit Finish and the newly created class will be opened in the editor. Add the @Entity
annotation (from javax.persistence
) to the class, plus the following attributes (Temporal
and TemporalType
also from javax.persistence
):
private Integer year;
private Integer number;
@Temporal(TemporalType.DATE)
private Date startDate;
@Temporal(TemporalType.DATE)
private Date endDate;
Now, right-click on a blank space of the class editor and choose Source > Generate getters and setters. Click the Select All button and hit OK to create accessor and mutator methods for all persistent attributes of the class.
The nemo-utils mini CRUD framework requires that classes managed by it be comparable for sorting. So add implements Comparable<Semester>
to the class definition and implement the compareTo()
overridden method as below:
@Override
public int compareTo(Semester o) {
if (year == null) return 1;
if (o.year == null) return -1;
int cmp = year.compareTo(o.year);
if (cmp != 0) return cmp;
if (number == null) return 1;
if (o.number == null) return -1;
cmp = number.compareTo(o.number);
if (cmp != 0) return cmp;
return uuid.compareTo(o.uuid);
}
In summary, we created a class to represent academic semesters, which have a start and end date and are identified by the year and a number (e.g., 2014/2, 2015/1, and so on). Our class is persistent and implements Comparable<T>
for sorting purposes, ordering semesters first by year, then by number. If both are equal, it considers the object's universal unique identifier (UUID), which will be the same if and only if it's the same object.
At this point we can test if our application's persistence configuration is done correctly. Make sure that MySQL is running, deploy and start the application. Then, open the autotrab
database in MySQL Workbench and refresh its list of tables. The Semester
table (probably along with hibernate_sequence
, a table used by Hibernate to do its business) were created automatically.
With the domain class done, we move to the Data Access Tier.
Actually, before moving on to the Persistence package, there is one more thing to do at the br.ufes.inf.nemo.autotrab.domain
package, but it is related to persistence. JPA allows us to create queries on our data store programmatically using one of its features called Criteria API. Using objects, attributes and methods to create queries has the advantage of uncovering query bugs at compile time, whereas with string-based queries most errors are discovered only at runtime.
To fully use the Criteria API, domain classes must have a static metamodel that lists their properties in a particular way, useful for building Criteria API queries. Although the Java EE specification says that these metamodels could be created in other places, my experience is that you should follow a convention to avoid trouble: create them in the same package as the classes they describe and name them with the same name as their respective persistent classes, appending an underscore (_
) as a suffix.
So, right-click Java Resources/src/br.ufes.inf.nemo.autotrab.domain
and select New > Class. Fill in the following information:
-
Name:
Semester_
; -
Superclass:
br.ufes.inf.nemo.util.ejb3.persistence.PersistentObjectSupport_
(again, you can write POS, hit ctrl+space and autocomplete).
Hit OK and once the new class is open add the @StaticMetamodel(Semester.class)
annotation (from javax.persistence.metamodel
) to the class, plus the following attributes:
public static volatile SingularAttribute<Semester, Integer> year;
public static volatile SingularAttribute<Semester, Integer> number;
public static volatile SingularAttribute<Semester, Date> startDate;
public static volatile SingularAttribute<Semester, Date> endDate;
As you can see, each attribute of the domain class has a public static volatile
version of it in the metamodel. Since Semester
only has simple attributes (no associations with other classes), all of them are represented by the SingularAttribute
class (also from javax.persistence.metamodel
). If you visit this package's documentation in the Java EE API docs you'll see other classes for attributes, such as SetAttribute
(used when association between classes are implemented through a java.util.Set
).
Lastly, since Semester
extends PersistentObjectSupport
from nemo-utils, analogously, the Semester_
metamodel class extends nemo-utils's PersistentObjectSupport_
metamodel class. Therefore, if you have inheritance in your domain model, you should mirror the inheritance in the persistence metamodel.
Now we write the DAO, which is the class responsible for using JPA to persist our objects. Following the program to an interface, not an implementation principle, we first create an interface for our DAO: right-click Java Resources/src/br.ufes.inf.nemo.autotrab.domain
and choose New > Interface. Then fill in:
-
Package:
br.ufes.inf.nemo.autotrab.persistence
(changedomain
topersistence
); -
Name:
SemesterDAO
; - In Extended interfaces, click Add... and add
br.ufes.inf.nemo.util.ejb3.persistence.BaseDAO<br.ufes.inf.nemo.autotrab.domain.Semester>
(first chooseBaseDAO
from the list, then add theSemester
class as its generic parameter. Again, ctrl+space is your friend).
Finally, mark the interface as a @Local
EJB (annotation from javax.ejb
). Since the basic DAO operations (retrieve by ID, save, delete, etc.) are provided by nemo-utils's BaseDAO
and we don't need (at first) any domain-specific query (e.g., retrieve all semesters of a given year), there's nothing else to do here.
Now to the implementation: right-click Java Resources/src/br.ufes.inf.nemo.autotrab.persistence
, choose New > Class and fill in:
-
Name =
SemesterJPADAO
; -
Superclass =
br.ufes.inf.nemo.util.ejb3.persistence.BaseJPADAO<br.ufes.inf.nemo.autotrab.domain.Semester>
; - In Extended interfaces, click Add... and add
br.ufes.inf.nemo.autotrab.persistence.SemesterDAO
.
Notice we call the class a Semester-JPA-DAO, stressing the fact that this is a JPA implementation to a otherwise generic DAO interface. This means we can provide other implementations in the future (e.g., a SemesterJDBCDAO
, a SemesterNoSQLDAO
, etc.), if we'd like. Although rarely used, this is one more advantage of splitting things in interface and implemetation.
To wrap up, follow these four simple steps to produce a basic (no queries) DAO class:
- Mark the class as
@Stateless
(also fromjavax.ejb
); - Add the annotated attribute
@PersistenceContext private EntityManager entityManager;
(both the annotation and the class are fromjavax.persistence
); - Have the
getDomainClass()
method returnSemester.class
; and - Have the
getEntityManager()
method return theentityManager
attribute.
In summary, we created a static metamodel of our domain class so it's ready to be used in Criteria API queries in the future. Moreover, we provided a DAO (divided in interface and implementation) that inherits all basic DAO features (save, retrieve, delete, etc.) from the mini CRUD framework. The DAO class asks JPA for the persistence context, receiving automagically from CDI an entity manager object capable of communicating with the database through object/relational mapping (i.e. Hibernate). Being a stateless EJB, this class can be injected by CDI wherever it's needed.
With the Persistence package covered for this CRUD, we move back to the Business Tier to implement the CRUD use case logic.
The Application package contribution for this CRUD is the service that implements the CRUD scenarios that compose the CRUD use case "Manage Semesters": create a new semester, retrieve an existing semester, update an existing semester and delete an existing semester. Although not included in the CRUD acronym, list existing semesters is also an important scenario, as one has to see the objects in order to update/delete them.
As with DAOs, service classes are also separated in interface and implementation. Following a similar procedure as before, right-click Java Resources/src/br.ufes.inf.nemo.autotrab.domain
, choose New > Interface and fill in:
-
Package:
br.ufes.inf.nemo.autotrab.application
(changedomain
toapplication
); -
Name:
ManageSemestersService
; - In Extended interfaces, click Add... and add
br.ufes.inf.nemo.util.ejb3.application.CrudService<br.ufes.inf.nemo.autotrab.domain.Semester>
.
The service interface should also be a @Local
EJB. For the implementation, right-click Java Resources/src/br.ufes.inf.nemo.autotrab.application
, select New > Class and fill in:
-
Name:
ManageSemestersServiceBean
; -
Superclass:
br.ufes.inf.nemo.util.ejb3.application.CrudServiceBean<br.ufes.inf.nemo.autotrab.domain.Semester>
; - In Extended interfaces, click Add... and add
br.ufes.inf.nemo.autotrab.application.ManageSemestersService
.
As with the DAO, there are four simple steps to finish:
- Mark the class as
@Stateless
(like the DAO); - Add the annotated attribute
@EJB private SemesterDAO semesterDAO;
(notice we refer to the DAO's interface and ask CDI to automatically inject it using thejavax.ejb.EJB
annotation); - Have the
getDAO()
method return thesemesterDAO
attribute; and - Have the
createNewEntity()
method return anew Semester()
.
In summary, this creates a basic DAO service, without any validation. You can add validation later in order to, for instance, prevent the users from creating two semesters with the same year and number. The service inherits methods from the mini CRUD framework which implement the basic CRUD use case scenarios (the CRUD acronym plus listing). The service implementation class asks CDI to inject the DAO and uses its methods to operationalize the scenarios.
We now move to the last Java part of the CRUD before switching to writing XHTML pages: the controller.
The Control package -- the Java part of the Presentation Tier -- contributes with a single class to the CRUD (no need to split between interface and implementation, as the controller is used by the Web pages via EL, not injected by CDI as the service and the DAO are). As before, nemo-utils has a base class to help. Right-click Java Resources/src/br.ufes.inf.nemo.autotrab.domain
and select New > Class. Then fill in:
-
Package:
br.ufes.inf.nemo.autotrab.controller
(changedomain
tocontroller
); -
Name =
ManageSemestersController
; -
Superclass =
br.ufes.inf.nemo.util.ejb3.controller.CrudController<br.ufes.inf.nemo.autotrab.domain.Semester>
.
Annotate the class with @Named
(from javax.inject
) and @SessionScoped
(from javax.enterprise.context
). Here, make sure to pay close attention and not use javax.faces.bean.SessionScoped
! Although we are writing a JSF bean, since we have used CDI elsewhere, we have to always use CDI annotations (i.e., javax.enterprise.context
ones) instead of the JSF ones. Yes, it's weird and CDI could have been developed in a way to consider JSF annotations, but it wasn't so trust me on this.
As before, we finish our class with four more steps:
- Add the annotated attribute
@EJB private ManageSemestersService manageSemestersService;
so the service class is injected at the controller by CDI; - Have the
createNewEntity()
method return anew Semester()
(like the service class); - Have the
getCrudService()
method return themanageSemestersService
attribute. - Add a constructor to the class, with the following code:
public ManageSemestersController() {
viewPath = "/manageSemesters/";
bundleName = "msgs";
}
The code inside the constructor specify where the mini CRUD framework should look for Web pages (the view path) and where it should look for externalized strings (the bundle name). This creates a basic CRUD controller, but we'd like to add something more this time: a simple filter. Add the code below to the initFilters()
method:
addFilter(new SimpleFilter("manageSemesters.filter.byYear", "year", getI18nMessage("msgs", "manageSemesters.text.filter.byYear")));
The getI18nMessage()
method inherited from nemo-utils retrieves text from a resource bundle registered in JSF. We created and registered the msgs
resource bundle in the previous step of the tutorial. So open Java Resources/src/br.ufes.inf.nemo.autotrab/messages.properties
and add the following:
# Messages related to the "Manage Semesters" use case:
manageSemesters.text.filter.byYear = by year
This concludes the Control package and the Java part of the CRUD. In summary, we created a CRUD controller inheriting all basic features from the mini CRUD framework, including methods to display existing objects in a paginated list, switching to a form to create new objects, or to see/update object details and to delete existing objects from the list. We also added a simple filter, which uses filter classes from the framework to add a domain-specific functionality to the CRUD. Like with the application before, CDI also plays a part here, injecting the service class so the controller can fulfill its role as the mediator between the Web pages and the application's functionality.
We can now finally provide the View.
In the last (or first?) package of our architecture, we leave the Java Resources
part of our Eclipse project and start working with the WebContent
section. For the CRUD, we have basically two pages: the listing and the form. The former shows a list with the existing objects and allows the user to delete some of them, whereas the latter contains form fields to display an entity's attributes or to create/update entities.
By now you must have noticed how CRUD functionality is repetitive and even the things that are specific for our domain (the Semester
class, for instance) could have been inserted as a parameter to a code generation tool (which doesn't exist, by the way, and you're welcome to create one and share!) which would do most of the things we have done so far.
So both the listing and the form pages have pre-written templates that allow you to quickly write them, even if they have dozens of lines of code. First, right-click WebContent
and select New > Folder to create a folder named manageSemesters
. Then right-click the folder and select New > File to create the list.xhtml
file. Repeat it to create a file named form.xhtml
.
Copy and paste the following contents to the list.xhtml
file:
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
template="/resources/templates/default/decorator.xhtml">
<ui:define name="title"><h:outputText value="#{msgs['manage${Entities}.title']}" /></ui:define>
<ui:define name="body">
<h1><h:outputText value="#{msgs['manage${Entities}.title']}" /></h1>
<div class="crud">
<!-- Filter bar: used to filter the listing (displayed when there is at least one filter available). -->
<h:form id="filterForm">
<h:panelGroup id="filterBar" rendered="#{(not empty manage${Entities}Controller.filters)}">
<div class="crudFilterBar">
<h:outputText value="#{msgs['crud.text.filter']} " />
<p:selectOneMenu id="filter" required="true" value="#{manage${Entities}Controller.filterKey}">
<f:selectItems value="#{manage${Entities}Controller.filters}" var="filter" itemLabel="#{filter.label}" itemValue="#{filter.key}" />
<f:ajax event="change" execute="@form" listener="#{manage${Entities}Controller.changeFilter}" render="@form :listingForm :buttonsForm" />
</p:selectOneMenu>
<h:outputText value=": " />
<p:inputText id="filterText" size="20" required="false" value="#{manage${Entities}Controller.filterParam}" rendered="#{manage${Entities}Controller.filter.options == null}" />
<p:selectOneMenu id="filterSelect" required="false" value="#{manage${Entities}Controller.filterParam}" rendered="#{manage${Entities}Controller.filter.options != null}">
<f:selectItems value="#{manage${Entities}Controller.filter.reversedOptionsLabels}" />
</p:selectOneMenu>
<p:commandLink action="#{manage${Entities}Controller.filter}" process="@form" update=":listingForm :buttonsForm">
<h:graphicImage style="border: 0;" value="/resources/templates/default/icons/button-find.png" title="#{msgs['crud.button.filter']}" />
</p:commandLink>
<p:hotkey bind="#{msgs['crud.hotkey.filterFocus']}" handler="if (findObjectById('filterForm:filterText') != null) findObjectById('filterForm:filterText').focus(); else findObjectById('filterForm:filterSelect').focus();" />
</div>
</h:panelGroup>
</h:form>
<!-- The listing form. -->
<h:form id="listingForm">
<!-- Filter information (displayed when the listing is being filtered. -->
<h:panelGroup id="filterInfo" rendered="#{manage${Entities}Controller.filtering}">
<div class="crudFilterInfo">
<h:outputFormat value="#{msgs['crud.text.filterInfo']} ">
<f:param value="#{manage${Entities}Controller.filter.label}" />
<f:param value="#{manage${Entities}Controller.filterLabel}" />
</h:outputFormat>
<p:commandLink action="#{manage${Entities}Controller.cancelFilter}" process="@form" update="@form :buttonsForm">
<h:graphicImage style="border: 0;" value="/resources/templates/default/icons/button-close.png" title="#{msgs['crud.button.cancelFilter']}" />
</p:commandLink>
<p:hotkey bind="#{msgs['crud.hotkey.clearFilter']}" action="#{manage${Entities}Controller.cancelFilter}" process="@form" update=":listingForm :buttonsForm" />
</div>
</h:panelGroup>
<!-- The list of entities. -->
<h:panelGroup id="listing" styleClass="listing">
<p:dataTable
widgetVar="listingTable"
value="#{manage${Entities}Controller.lazyEntities}"
var="entity"
rows="#{manage${Entities}Controller.maxDataTableRowsPerPage}"
rowsPerPageTemplate="#{manage${Entities}Controller.halfMaxDataTableRowsPerPage},#{manage${Entities}Controller.maxDataTableRowsPerPage},#{manage${Entities}Controller.doubleMaxDataTableRowsPerPage}"
lazy="true"
paginator="true"
selection="#{manage${Entities}Controller.selectedEntity}"
selectionMode="single"
paginatorTemplate="{RowsPerPageDropdown} {FirstPageLink} {PreviousPageLink} {CurrentPageReport} {NextPageLink} {LastPageLink}"
paginatorPosition="bottom"
loadingMessage="#{msgs['text.ajax.loading']}"
emptyMessage="#{manage${Entities}Controller.filtering ? msgs['manage${Entities}.text.noEntitiesFiltered'] : msgs['manage${Entities}.text.noEntities']}">
<p:column headerText="#{msgs['manage${Entities}.form.${attribute}']}">
<h:outputText value="#{entity.${attribute}}" />
</p:column>
<p:ajax event="rowSelect" update=":buttonsForm" />
</p:dataTable>
<p:hotkey bind="ctrl+left" handler="goToFirstPage(listingTable);" />
<p:hotkey bind="left" handler="goToPreviousPage(listingTable);" />
<p:hotkey bind="right" handler="goToNextPage(listingTable);" />
<p:hotkey bind="ctrl+right" handler="goToLastPage(listingTable);" />
</h:panelGroup>
</h:form>
<!-- Buttons shown below the listing. -->
<h:form id="buttonsForm">
<div class="crudButtons">
<p:commandButton action="#{manage${Entities}Controller.create}" value="#{msgs['crud.button.create']}" icon="buttonNew" />
<p:commandButton action="#{manage${Entities}Controller.retrieve}" value="#{msgs['crud.button.retrieve']}" icon="buttonView" disabled="#{manage${Entities}Controller.selectedEntity == null}" />
<p:commandButton action="#{manage${Entities}Controller.update}" value="#{msgs['crud.button.update']}" icon="buttonEdit" disabled="#{manage${Entities}Controller.selectedEntity == null}" />
<p:commandButton action="#{manage${Entities}Controller.trash}" value="#{msgs['crud.button.delete']}" icon="buttonTrash" disabled="#{manage${Entities}Controller.selectedEntity == null}" process="@this" update=":trashForm" />
<p:hotkey bind="#{msgs['crud.hotkey.create']}" action="#{manage${Entities}Controller.create}" />
<p:hotkey bind="#{msgs['crud.hotkey.retrieve']}" action="#{manage${Entities}Controller.retrieve}" rendered="#{manage${Entities}Controller.selectedEntity != null}" />
<p:hotkey bind="#{msgs['crud.hotkey.update']}" action="#{manage${Entities}Controller.update}" rendered="#{manage${Entities}Controller.selectedEntity != null}" />
<p:hotkey bind="#{msgs['crud.hotkey.delete']}" action="#{manage${Entities}Controller.trash}" rendered="#{manage${Entities}Controller.selectedEntity != null}" process="@this" update=":trashForm" />
</div>
</h:form>
<!-- The trash panel: shows entities that have been selected for deletion. -->
<h:form id="trashForm">
<p:panel id="trashPanel" styleClass="trashPanel" header="#{msgs['crud.text.trashHeader']}" toggleable="true" rendered="#{not empty manage${Entities}Controller.trashCan}">
<p:dataList value="#{manage${Entities}Controller.trashCan}" var="entity" type="unordered">
#{entity.${attribute}}
</p:dataList>
<div class="nemoUtilsFormInternalButton">
<p:commandButton action="#{manage${Entities}Controller.cancelDeletion}" value="#{msgs['crud.button.cancelDeletion']}" icon="buttonCancel" update=":trashForm :buttonsForm :listingForm" />
<p:commandButton action="#{manage${Entities}Controller.delete}" value="#{msgs['crud.button.confirmDeletion']}" icon="buttonOk" />
<p:hotkey bind="#{msgs['crud.hotkey.cancelDeletion']}" action="#{manage${Entities}Controller.cancelDeletion}" update=":trashForm :buttonsForm :listingForm" />
<p:hotkey bind="#{msgs['crud.hotkey.confirmDeletion']}" action="#{manage${Entities}Controller.delete}" />
</div>
</p:panel>
</h:form>
</div>
<p> </p>
</ui:define>
<ui:define name="help">
<h4><h:outputText value="#{msgs['text.hotkeys']}" />:</h4>
<ul>
<li><b><h:outputText value="#{msgs['crud.hotkey.filterFocus']}" /></b>: <h:outputText value="#{msgs['crud.help.hotkeys.filterFocus']}" />;</li>
<li><b><h:outputText value="#{msgs['crud.hotkey.clearFilter']}" /></b>: <h:outputText value="#{msgs['crud.help.hotkeys.clearFilter']}" />;</li>
<li><b><h:outputText value="#{msgs['crud.hotkey.create']}" /></b>: <h:outputText value="#{msgs['crud.help.hotkeys.create']}" />;</li>
<li><b><h:outputText value="#{msgs['crud.hotkey.retrieve']}" /></b>: <h:outputText value="#{msgs['crud.help.hotkeys.retrieve']}" />;</li>
<li><b><h:outputText value="#{msgs['crud.hotkey.update']}" /></b>: <h:outputText value="#{msgs['crud.help.hotkeys.update']}" />;</li>
<li><b><h:outputText value="#{msgs['crud.hotkey.delete']}" /></b>: <h:outputText value="#{msgs['crud.help.hotkeys.delete']}" />;</li>
<li><b><h:outputText value="#{msgs['crud.hotkey.cancelDeletion']}" /></b>: <h:outputText value="#{msgs['crud.help.hotkeys.cancelDeletion']}" />;</li>
<li><b><h:outputText value="#{msgs['crud.hotkey.confirmDeletion']}" /></b>: <h:outputText value="#{msgs['crud.help.hotkeys.confirmDeletion']}" />.</li>
</ul>
</ui:define>
</ui:composition>
And the following contents to the form.xhtml
file:
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
template="/resources/templates/default/decorator.xhtml">
<ui:define name="title">
<h:outputText value="#{msgs['manage${Entities}.title.create']}" rendered="#{(! manage${Entities}Controller.readOnly) and (manage${Entities}Controller.selectedEntity.id == null)}" />
<h:outputText value="#{msgs['manage${Entities}.title.update']}" rendered="#{(! manage${Entities}Controller.readOnly) and (manage${Entities}Controller.selectedEntity.id != null)}" />
<h:outputText value="#{msgs['manage${Entities}.title.retrieve']}" rendered="#{manage${Entities}Controller.readOnly}" />
</ui:define>
<ui:define name="body">
<h1>
<h:outputText value="#{msgs['manage${Entities}.title.create']}" rendered="#{(! manage${Entities}Controller.readOnly) and (manage${Entities}Controller.selectedEntity.id == null)}" />
<h:outputText value="#{msgs['manage${Entities}.title.update']}" rendered="#{(! manage${Entities}Controller.readOnly) and (manage${Entities}Controller.selectedEntity.id != null)}" />
<h:outputText value="#{msgs['manage${Entities}.title.retrieve']}" rendered="#{manage${Entities}Controller.readOnly}" />
</h1>
<div class="crud">
<ui:decorate template="/resources/templates/default/form.xhtml">
<h:form id="form">
<p:hotkey bind="#{msgs['crud.hotkey.focusFirstField']}" handler="findObjectById('form:${attribute}').focus();" />
<h:panelGroup id="${attribute}Field">
<ui:decorate template="/resources/templates/default/formfield.xhtml">
<ui:param name="fieldName" value="form:${attribute}" />
<ui:param name="tooltip" value="#{msgs['manage${Entities}.form.${attribute}.tooltip']}" />
<ui:define name="label"><h:outputText value="#{msgs['manage${Entities}.form.${attribute}']}" /></ui:define>
<p:inputText id="${attribute}" value="#{manage${Entities}Controller.selectedEntity.${attribute}}" size="50" required="true" disabled="#{manage${Entities}Controller.readOnly}">
<p:ajax event="blur" update="${attribute}Field" />
</p:inputText>
</ui:decorate>
</h:panelGroup>
<ui:decorate template="/resources/templates/default/formbutton.xhtml">
<h:panelGroup rendered="#{! manage${Entities}Controller.readOnly}">
<p:commandButton action="#{manage${Entities}Controller.list}" value="#{msgs['crud.button.cancel']}" immediate="true" icon="buttonCancel" />
<p:commandButton action="#{manage${Entities}Controller.save}" value="#{msgs['crud.button.save']}" icon="buttonOk" update=":form :globalMessages" />
<p:hotkey bind="#{msgs['crud.hotkey.backToList']}" action="#{manage${Entities}Controller.list}" immediate="true" />
<p:hotkey bind="#{msgs['crud.hotkey.save']}" action="#{manage${Entities}Controller.save}" update=":form :globalMessages" />
</h:panelGroup>
<h:panelGroup rendered="#{manage${Entities}Controller.readOnly}">
<p:commandButton action="#{manage${Entities}Controller.list}" value="#{msgs['crud.button.back']}" immediate="true" icon="buttonBack" />
<p:hotkey bind="#{msgs['crud.hotkey.backToList']}" action="#{manage${Entities}Controller.list}" immediate="true" />
</h:panelGroup>
</ui:decorate>
</h:form>
</ui:decorate>
</div>
</ui:define>
<ui:define name="help">
<h4><h:outputText value="#{msgs['text.hotkeys']}" />:</h4>
<ul>
<li><b><h:outputText value="#{msgs['crud.hotkey.focusFirstField']}" /></b>: <h:outputText value="#{msgs['crud.help.hotkeys.focusFirstField']}" />;</li>
<li><b><h:outputText value="#{msgs['crud.hotkey.backToList']}" /></b>: <h:outputText value="#{msgs['crud.help.hotkeys.backToList']}" />;</li>
<li><b><h:outputText value="#{msgs['crud.hotkey.save']}" /></b>: <h:outputText value="#{msgs['crud.help.hotkeys.save']}" />.</li>
</ul>
</ui:define>
</ui:composition>
In the above files, some variables in EL expressions have been used to represent the domain-specific aspects of the CRUD. For example:
-
#{msgs['manage${Entities}.title']}
: title of the CRUD page; -
#{msgs['manage${Entities}}
: the controller class; -
#{entity.${attribute}}
: one of the attributes of the managed entity.
The ${} notation is not part of EL, though. It's just a way to make it easier for us to replace them with their proper names. Given that our controller is called ManageSemestersController
and that one of its attributes is year
, to start customizing these generic templates to our "Manage Semesters" CRUD pages you should use Eclipse's Edit > Find/Replace... feature, check the options Case sensitive and Wrap search and perform the following replacements:
-
${Entities}
->Semesters
; -
${attribute}
->year
.
Make sure you replaced all of them. Next, open the list.xhtml
file and locate the <p:column />
tag for the year
attribute. Copy and paste it to create columns for number
and startDate
, so our list of existing semesters is a bit more informative. For the startDate
, use the following:
<h:outputFormat value="#{entity.startDate}">
<f:convertDateTime type="both" pattern="#{msgs['format.date.java']}" />
</h:outputFormat>
The form.xhtml
file, as a result of our Search & Replace, has only one form field, pointing to the year
attribute. Locate <h:panelGroup id="yearField">
and replicate it for number
, startDate
and endDate
. If you prefer, copy from the original form.xhtml
to Find/Replace ${attribute}
with the field name (don't forget ${Entities}
!). Also, adjust the size
attribute for the text fields, as it may be too big for the information they contain.
Then, for startDate
and endDate
, perform the following:
- change
p:inputText
top:inputMask
; - add attribute
mask="#{msgs['format.date.primefaces']}"
; - add
<f:convertDateTime type="date" pattern="#{msgs['format.date.java']}" />
to its contents; - and
<h:outputText value=" (#{msgs['format.date.label']})" />
right next to it.
This will show a text field with input masks from the PrimeFaces component library, preventing the users from typing things that are not dates in the fields. The format.date.*
messages have already been included in the messages.properties
file created in part 3 of the tutorial. We now need to add the rest of the messages, pertaining to the CRUD of semesters.
Open Java Resources/src/br.ufes.inf.nemo.autotrab/messages.properties
and add the following keys to it:
manage${Entities}.form.${attribute} = ${Attribute}
manage${Entities}.form.${attribute}.tooltip = Tooltip for ${attribute}
manage${Entities}.title = Manage ${entities}
manage${Entities}.title.create = Create new ${entity}
manage${Entities}.title.update = Modify ${entity}
manage${Entities}.title.retrieve = View ${entity} details
manage${Entities}.text.noEntitiesFiltered = No ${entities} match the selected filter
manage${Entities}.text.noEntities = No ${entities} created yet
Then, as before, use Eclipse's Edit > Find/Replace... feature with the options Case sensitive and Wrap search to perform the following replacements:
-
${Entities}
->Semesters
; -
${entities}
->semesters
; -
${entity}
->semester
; -
${Attribute}
->Year
; -
${attribute}
->year
.
Finally, and again, replicate the year
keys to number
, startDate
and endDate
, changing the text value accordingly. The tooltip (message shown when the user hovers the mouse cursor over the field) is currently Tooltip for year
. You might want to change it to something more appropriate. Alternatively, remove the tooltip from the form.xhtml
fields.
Deploy the application and open http://localhost:8080/AutoTrab/manageSemesters/list.faces to try it out. You can add a link to it at the WebContent/index.xhtml
home page:
<p>Try the <h:link outcome="manageSemesters/list" value="Manage Semesters" /> feature.</p>
The CRUD feature should look like the figures below:
One last thing! Depending on your locale configuration, the dates you fill in the Manage Semesters form might be registered to the day before (e.g., you type in 01/04/2014
and it stores 2014-03-31
in the database). This is due to the fact that date-only values are sent with time 00:00:00
and if you're in a -X
timezone (e.g., Brazil is -3
), JSF applied the time zone and changes the date to the day before, then sends it to the database. To prevent it from doing this, all we need is to add a context parameter to the WebContent/WEB-INF/web.xml
file:
<context-param>
<param-name>javax.faces.DATETIMECONVERTER_DEFAULT_TIMEZONE_IS_SYSTEM_TIMEZONE</param-name>
<param-value>true</param-value>
</context-param>
This is as far as the tutorial goes currently, but there's more to nemo-utils. Hopefully in the future more steps of the tutorial are written, showing advanced CRUD filters, CRUD validation, the object converter, etc. The source code of the AutoTrab project up to this step of the tutorial can be found here, in the nemo-utils repository.