-
Notifications
You must be signed in to change notification settings - Fork 1
Tutorial04
The new version of JButler is now hosted at Labes/UFES and there is a new tutorial as well. This repository contains an older version, if you're interested.
In this step we develop a simple CRUD (Create, Retrieve, Update and Delete) functionality in the Oldenburg project using JButler's CRUD mini-framework. The feature to be developed is the Manage Workshops use case, which allows the administrator to create, retrieve, update and delete workshops from the system.
Before we implement the CRUD, it's important to understand the architecture on which JButler is based.
JButler's mini CRUD framework is built for use in WISs that follow a three-tier architecture on top of Java EE, as shown in the figure below.
At the Presentation Tier, the View is composed of Web pages and related resources (scripts, stylesheets, images, etc.). Facelets applies a template to our pages, whereas Primefaces, a JSF component library that offers a large set of components, is used to assemble web forms and such. In the Oldenburg project, the View is mostly contained in Deployed Resources/webapp
, but we also have view
packages under Java Resources
to place specific resource bundles (for i18n).
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 WIS using a Web-based user interface. JSF Managed Beans will be placed in a controller
package under Java Resources
.
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, workshops, authors, reviewers, submissions, etc.), whereas in Application there are Session EJBs which implement the use cases of the system (manage workshops, submit a paper, submit a review, etc.). This tier is divided in application
and domain
packages under Java Resources
.
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. DAOs are placed in the persistence
package under Java Resources
.
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.
Next, we implement a CRUD feature using JButler's mini CRUD framework on top of the above architecture.
To implement the CRUD, we need JButler itself. To declare it as a dependency in our project, perform the following changes in pom.xml
:
-
In the
<project></project>
tag, add NEMO's Maven2 repository:<repositories> <repository> <releases> <enabled>true</enabled> <updatePolicy>always</updatePolicy> <checksumPolicy>fail</checksumPolicy> </releases> <id>br.ufes.inf.nemo</id> <name>Nemo Maven Repository</name> <url>http://dev.nemo.inf.ufes.br/maven2</url> <layout>default</layout> </repository> </repositories>
-
In the
<dependencies></dependencies>
tag, add JButler for Web Profile:<dependency> <groupId>br.ufes.inf.nemo</groupId> <artifactId>jbutler-wp</artifactId> <version>1.2.7</version> </dependency>
-
Save
pom.xml
and check that the dependencies have been included inJava Resources/Libraries/Maven Dependencies
.
To communicate with the database, we also need to set up JPA:
-
Right-click the oldenburg project and select Properties;
-
In the Project Facets page, check JPA;
-
Click Further configuration available, in JPA implementation select Disable Library Configuration and in Persistent class management (at the bottom) make sure Discover annotated classes automatically is selected;
-
Click Apply and Close, then open JPA Content/persistence.xml. Open the Source tab and include the following inside the
<persistence-unit></persistence-unit>
tag:<provider>org.hibernate.ejb.HibernatePersistence</provider> <jta-data-source>java:jboss/datasources/Oldenburg</jta-data-source> <class>br.ufes.inf.nemo.jbutler.ejb.persistence.PersistentObjectSupport</class> <properties> <property name="hibernate.hbm2ddl.auto" value="update" /> </properties>
The above configuration tells our application to use Hibernate (which is provided by WildFly) as the JPA implementation, to connect to the Oldenburg datasource we created in the preamble of this tutorial and to ask Hibernate to automatically generate the database schema for us.
Moreover, the PersistentObjectSupport
class, from JButler is explicitly included in the configuration due to a bug that makes Eclipse think that the entity classes from our project do not have an @Id
attribute (a primary key), when they actually get it from this class. Explicitly including it in the persistence.xml
file like this circumvents this problem. Again, I didn't check if this bug has been fixed in the latest versions of the tools used in this tutorial, but it doesn't hurt to have this configuration.
It's important to explain how JButler is dependant on the previously described architecture. If you want to have less trouble, you should follow these guidelines:
-
Your packages should follow this naming convention:
(organization).(system).(subsystem).(module)
. For instance,br.ufes.informatica.oldenburg.core.controller
refers to the controller's module of the core subsystem of oldenburg, from the organization informatica.ufes.br; -
Your controller modules should be called
controller
(e.g.br.ufes.informatica.oldenburg.core.controller
,br.ufes.informatica.oldenburg.review.controller
, etc.); -
Your controller classes' names should end with
Controller
(e.g.,ManageWorkshopsController
,SubmitReviewController
, etc.); -
Web pages for CRUD features should be called exactly index.xhtml and form.xhtml and be placed in a standard directory structure following this convention: (module)/(controller-base-name), where
(controller-base-name)
is the name of the controller class, without theController
suffix and with first letter lower case. For instance, the web pages (index.xhtml and form.xhtml) forbr.ufes.informatica.oldenburg.core.controller.ManageWorkshopsController
should be in Deployed Resources/webapp/core/manageWorkshops/, whereas those ofbr.ufes.informatica.oldenburg.review.controller.SubmitReviewController
should be in Deployed Resources/webapp/review/submitReview; -
Your i18n resource bundle should be registered in Deployed Resources/webapp/WEB-INF/faces-config.xml under a name following this convention:
msgs(Subsystem)
, where(Subsystem)
is the name of the subsystem with the first letter capitalized. For instance,msgsCore
is the bundle name for the core subsystem andmsgsReview
is the bundle name for the review subsystem; -
Keys in your i18n resource bundle should be prefixed with
(controller-base-name)
(as above with the folder for the Web pages). For instance, i18n messages forManageWorkshopsController
should be prefixed withmanageWorkshops.
, whereas those ofSubmitReviewController
should be prefixed withsubmitReview.
.
If you don't want to observe so many rules, you will have to override some methods in your controller classes, as they expect things to follow the above rules. This is what you need to override (you can do it for each controller or create a new superclass with your own conventions):
-
JSFController::getBundleName()
: determines the name of the i18n resource bundle, based on rules #1, #2 and #5; -
JSFController::getBundlePrefix()
: determines the prefix for the keys in the i18n resource bundle, based on rules #3 and #6; -
ListingController::getViewPath()
: determines the folder in which the list.xhtml and form.xhtml Web pages can be found, based on rules #1, #2 and #4; -
ListingController::getListingPageName()
: determines the name of the listing (index) Web page, as per rule #4. -
CrudController::getFormPageName()
: determines the name of the form Web page, as per rule #4.
Finally, to the simple CRUD feature! Here are the steps (again, explanations follow at the end):
-
Right-click
Java Resources/src/main/java
and select New > Package in order to create the following packages:-
br.ufes.informatica.oldenburg.core.application
; -
br.ufes.informatica.oldenburg.core.controller
; -
br.ufes.informatica.oldenburg.core.domain
; -
br.ufes.informatica.oldenburg.core.persistence
; -
br.ufes.informatica.oldenburg.core.view
.
-
-
Right-click the
br.ufes.informatica.oldenburg.core.domain
package and select New > Class to create theWorkshop
class, as follows:package br.ufes.informatica.oldenburg.core.domain; import java.util.Date; import javax.persistence.Entity; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import br.ufes.inf.nemo.jbutler.ejb.persistence.PersistentObjectSupport; @Entity public class Workshop extends PersistentObjectSupport implements Comparable<Workshop> { private static final long serialVersionUID = 1L; @Size(max = 100) private String name; @Size(max = 10) private String acronym; @NotNull private int year; @NotNull @Temporal(TemporalType.DATE) private Date submissionDeadline; @NotNull @Temporal(TemporalType.DATE) private Date reviewDeadline; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAcronym() { return acronym; } public void setAcronym(String acronym) { this.acronym = acronym; } public int getYear() { return year; } public void setYear(int year) { this.year = year; } public Date getSubmissionDeadline() { return submissionDeadline; } public void setSubmissionDeadline(Date submissionDeadline) { this.submissionDeadline = submissionDeadline; } public Date getReviewDeadline() { return reviewDeadline; } public void setReviewDeadline(Date reviewDeadline) { this.reviewDeadline = reviewDeadline; } @Override public int compareTo(Workshop o) { return year - o.year; } }
-
Right-click the
br.ufes.informatica.oldenburg.core.persistence
package and select New > Interface to create theWorkshopDAO
interface, as follows:package br.ufes.informatica.oldenburg.core.persistence; import javax.ejb.Local; import br.ufes.inf.nemo.jbutler.ejb.persistence.BaseDAO; import br.ufes.informatica.oldenburg.core.domain.Workshop; @Local public interface WorkshopDAO extends BaseDAO<Workshop> { }
-
Right-click the
br.ufes.informatica.oldenburg.core.persistence
package and select New > Class to create theWorkshopJPADAO
class, as follows:package br.ufes.informatica.oldenburg.core.persistence; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import br.ufes.inf.nemo.jbutler.ejb.persistence.BaseJPADAO; import br.ufes.informatica.oldenburg.core.domain.Workshop; @Stateless public class WorkshopJPADAO extends BaseJPADAO<Workshop> implements WorkshopDAO { private static final long serialVersionUID = 1L; @PersistenceContext private EntityManager entityManager; @Override protected EntityManager getEntityManager() { return entityManager; } }
-
Right-click the
br.ufes.informatica.oldenburg.core.application
package and select New > Interface to create theManageWorkshopsService
interface, as follows:package br.ufes.informatica.oldenburg.core.application; import javax.ejb.Local; import br.ufes.inf.nemo.jbutler.ejb.application.CrudService; import br.ufes.informatica.oldenburg.core.domain.Workshop; @Local public interface ManageWorkshopsService extends CrudService<Workshop> { }
-
Right-click the
br.ufes.informatica.oldenburg.core.application
package and select New > Class to create theManageWorkshopsServiceBean
class, as follows:package br.ufes.informatica.oldenburg.core.application; import javax.annotation.security.PermitAll; import javax.ejb.EJB; import javax.ejb.Stateless; import br.ufes.inf.nemo.jbutler.ejb.application.CrudServiceBean; import br.ufes.inf.nemo.jbutler.ejb.persistence.BaseDAO; import br.ufes.informatica.oldenburg.core.domain.Workshop; import br.ufes.informatica.oldenburg.core.persistence.WorkshopDAO; @Stateless @PermitAll public class ManageWorkshopsServiceBean extends CrudServiceBean<Workshop> implements ManageWorkshopsService { private static final long serialVersionUID = 1L; @EJB private WorkshopDAO workshopDAO; @Override public BaseDAO<Workshop> getDAO() { return workshopDAO; } }
-
Right-click the
br.ufes.informatica.oldenburg.core.controller
package and select New > Class to create theManageWorkshopsController
class, as follows:package br.ufes.informatica.oldenburg.core.controller; import javax.ejb.EJB; import javax.enterprise.context.SessionScoped; import javax.inject.Named; import br.ufes.inf.nemo.jbutler.ejb.application.CrudService; import br.ufes.inf.nemo.jbutler.ejb.controller.CrudController; import br.ufes.informatica.oldenburg.core.application.ManageWorkshopsService; import br.ufes.informatica.oldenburg.core.domain.Workshop; @Named @SessionScoped public class ManageWorkshopsController extends CrudController<Workshop> { private static final long serialVersionUID = 1L; @EJB private ManageWorkshopsService manageWorkshopsService; @Override protected CrudService<Workshop> getCrudService() { return manageWorkshopsService; } @Override protected void initFilters() { } }
-
Right-click
Deployed Resources/webapp
and use New > Folder in order to create the folder structurecore/manageWorkshops
; -
Copy
Deployed Resources/webapp/index.xhtml
intocore/manageWorkshops
and change its contents to:<ui:define name="title"> <h:outputText value="#{msgsCore['manageWorkshops.title']}" /> </ui:define> <ui:define name="description"> <h:outputText value="#{msgsCore['manageWorkshops.title.description']}" /> </ui:define> <ui:define name="body"> <adm:breadcrumb link="/core/manageWorkshops/index" title="#{msgsCore['manageWorkshops.title']}" /> <h:form id="listingForm"> <p:dataTable id="entitiesDataTable" var="entity" value="#{manageWorkshopsController.lazyEntities}" selection="#{manageWorkshopsController.selectedEntity}" selectionMode="single" paginator="true" rows="#{manageWorkshopsController.maxDataTableRowsPerPage}" paginatorTemplate="{RowsPerPageDropdown} {FirstPageLink} {PreviousPageLink} {CurrentPageReport} {NextPageLink} {LastPageLink}" rowsPerPageTemplate="#{manageWorkshopsController.halfMaxDataTableRowsPerPage},#{manageWorkshopsController.maxDataTableRowsPerPage},#{manageWorkshopsController.doubleMaxDataTableRowsPerPage}" lazy="true" paginatorPosition="bottom" emptyMessage="#{msgsCore['manageWorkshops.text.noEntities']}"> <p:ajax event="rowSelect" update="buttonsGroup" /> <p:ajax event="rowUnselect" update="buttonsGroup" /> <f:facet name="header"> <h:outputText value="#{msgsCore['manageWorkshops.text.entities']}" /> </f:facet> <p:column headerText="#{msgsCore['manageWorkshops.field.year']}"> <h:outputText value="#{entity.year}" /> </p:column> <p:column headerText="#{msgsCore['manageWorkshops.field.name']}"> <h:outputText value="#{entity.name}" /> </p:column> <p:column headerText="#{msgsCore['manageWorkshops.field.submissionDeadline']}"> <h:outputText value="#{entity.submissionDeadline}"> <f:convertDateTime type="date" pattern="#{msgs['jbutler.format.date.java']}" /> </h:outputText> </p:column> <f:facet name="footer"> <h:panelGroup id="buttonsGroup"> <p:commandButton action="#{manageWorkshopsController.create}" icon="fa fa-plus" value="#{msgs['jbutler.crud.button.create']}" /> <p:commandButton action="#{manageWorkshopsController.retrieve}" icon="fa fa-search" value="#{msgs['jbutler.crud.button.retrieve']}" disabled="#{manageWorkshopsController.selectedEntity == null}" /> <p:commandButton action="#{manageWorkshopsController.update}" icon="fa fa-edit" value="#{msgs['jbutler.crud.button.update']}" disabled="#{manageWorkshopsController.selectedEntity == null}" /> <p:commandButton action="#{manageWorkshopsController.trash}" icon="fa fa-trash" value="#{msgs['jbutler.crud.button.delete']}" disabled="#{manageWorkshopsController.selectedEntity == null}" update=":listingForm:trashGroup" /> </h:panelGroup> </f:facet> </p:dataTable> <h:panelGroup id="trashGroup"> <hr /> <p:panel id="trashPanel" header="#{msgs['jbutler.crud.text.trashHeader']}" toggleable="true" toggleSpeed="500" rendered="#{not empty manageWorkshopsController.trashCan}"> <p:dataTable id="trashDataTable" var="entity" value="#{manageWorkshopsController.trashCan}"> <p:column headerText="#{msgsCore['manageWorkshops.field.year']}"> <h:outputText value="#{entity.year}" /> </p:column> <p:column headerText="#{msgsCore['manageWorkshops.field.name']}"> <h:outputText value="#{entity.name}" /> </p:column> <p:column headerText="#{msgsCore['manageWorkshops.field.submissionDeadline']}"> <h:outputText value="#{entity.submissionDeadline}"> <f:convertDateTime type="date" pattern="#{msgs['jbutler.format.date.java']}" /> </h:outputText> </p:column> <f:facet name="footer"> <p:commandButton action="#{manageWorkshopsController.cancelDeletion}" value="#{msgs['jbutler.crud.button.cancelDeletion']}" icon="fa fa-fw fa-close" process="@this" update=":listingForm" /> <p:commandButton action="#{manageWorkshopsController.delete}" value="#{msgs['jbutler.crud.button.confirmDeletion']}" icon="fa fa-fw fa-trash-o" process="@this" update=":listingForm" /> </f:facet> </p:dataTable> </p:panel> </h:panelGroup> </h:form> </ui:define>
-
Inside
core/manageWorkshops
, copyindex.xhtml
toform.xhtml
and change its contents to:<ui:define name="title"> <h:outputText value="#{msgsCore['manageWorkshops.title.create']}" rendered="#{(! manageWorkshopsController.readOnly) and (manageWorkshopsController.selectedEntity.id == null)}" /> <h:outputText value="#{msgsCore['manageWorkshops.title.update']}" rendered="#{(! manageWorkshopsController.readOnly) and (manageWorkshopsController.selectedEntity.id != null)}" /> <h:outputText value="#{msgsCore['manageWorkshops.title.retrieve']}" rendered="#{manageWorkshopsController.readOnly}" /> </ui:define> <ui:define name="description"> <h:outputText value="#{msgsCore['manageWorkshops.title.create.description']}" rendered="#{(! manageWorkshopsController.readOnly) and (manageWorkshopsController.selectedEntity.id == null)}" /> <h:outputText value="#{msgsCore['manageWorkshops.title.update.description']}" rendered="#{(! manageWorkshopsController.readOnly) and (manageWorkshopsController.selectedEntity.id != null)}" /> <h:outputText value="#{msgsCore['manageWorkshops.title.retrieve.description']}" rendered="#{manageWorkshopsController.readOnly}" /> </ui:define> <ui:define name="body"> <h:form id="entitiesForm"> <p:panelGrid columns="2" columnClasses="ui-grid-col-4,ui-grid-col-8" layout="grid" styleClass="ui-panelgrid-blank"> <p:outputLabel for="yearField" value="#{msgsCore['manageWorkshops.field.year']}" /> <p:inputText id="yearField" value="#{manageWorkshopsController.selectedEntity.year}" style="width: 100%;" /> <p:outputLabel for="nameField" value="#{msgsCore['manageWorkshops.field.name']} " /> <p:inputText id="nameField" value="#{manageWorkshopsController.selectedEntity.name}" style="width: 100%;" /> <p:outputLabel for="acronymField" value="#{msgsCore['manageWorkshops.field.acronym']}" /> <p:inputText id="acronymField" value="#{manageWorkshopsController.selectedEntity.acronym}" style="width: 100%;" /> <p:outputLabel for="submissionDeadlineField" value="#{msgsCore['manageWorkshops.field.submissionDeadline']}" /> <p:calendar id="submissionDeadlineField" value="#{manageWorkshopsController.selectedEntity.submissionDeadline}" showOn="button" pattern="dd/MM/yyyy" mask="true" style="width: 100%;" /> <p:outputLabel for="reviewDeadlineField" value="#{msgsCore['manageWorkshops.field.reviewDeadline']}" /> <p:calendar id="reviewDeadlineField" value="#{manageWorkshopsController.selectedEntity.reviewDeadline}" showOn="button" pattern="dd/MM/yyyy" mask="true" style="width: 100%;" /> </p:panelGrid> <p:commandButton id="cancelButton" value="#{msgs['jbutler.crud.button.cancel']}" icon="fa fa-close" action="#{manageWorkshopsController.list}" immediate="true" rendered="#{! manageWorkshopsController.readOnly}" /> <p:commandButton id="saveButton" value="#{msgs['jbutler.crud.button.save']}" icon="fa fa-save" action="#{manageWorkshopsController.save}" rendered="#{! manageWorkshopsController.readOnly}" /> <p:defaultCommand target="saveButton" /> <p:commandButton id="backButton" value="#{msgs['jbutler.crud.button.back']}" icon="fa fa-arrow-circle-left" action="#{manageWorkshopsController.list}" immediate="true" rendered="#{manageWorkshopsController.readOnly}" /> </h:form> </ui:define>
-
Right-click the
br.ufes.informatica.oldenburg.core.view
package and select New > Other.... Choose General > File from the list and click Next. Create a file calledmessages.properties
with the following contents:## ## Resource bundle for package: core ## Language: American English ## # Menu labels for all functionalities of the package: core.menu.admin = Administration core.menu.admin.manageWorkshops = Manage Workshops # Text for use case "Manage Workshops": manageWorkshops.field.acronym = Acronym manageWorkshops.field.name = Name manageWorkshops.field.reviewDeadline = Review deadline manageWorkshops.field.submissionDeadline = Submission deadline manageWorkshops.field.year = Year manageWorkshops.text.deleteSucceeded = Successfully deleted {0,choice,0#no workshops|1#one workshop|1<{0,number,integer} workshops} manageWorkshops.text.entities = Workshops manageWorkshops.text.noEntities = No Workshop registered yet manageWorkshops.text.noEntitiesFiltered = No workshop found for this filter manageWorkshops.title = Manage Workshops manageWorkshops.title.description = Create, retrieve, update and delete data about workshops manageWorkshops.title.create = Register new Workshop manageWorkshops.title.create.description = Insert new workshop information manageWorkshops.title.update = Update Workshop manageWorkshops.title.update.description = Modify an existing workshop's information manageWorkshops.title.retrieve = Workshop's data manageWorkshops.title.retrieve.description = View an existing workshop's information
-
Open
Deployed Resources/webapp/WEB-INF/faces-config.xml
, go to the Source tab and add the resource bundle for the core package inside<application></application>
:<resource-bundle> <base-name>br.ufes.informatica.oldenburg.core.view.messages</base-name> <var>msgsCore</var> </resource-bundle>
-
Finally, add a menu entry for the CRUD feature in the template file
Deployed Resources/webapp/WEB-INF/templates/template.xhtml
inside<ul class="sidebar-menu"></ul>
:<li class="header"><h:outputText value="#{msgsCore['core.menu.admin']}" /></li> <li><p:link outcome="/core/manageWorkshops/index" onclick="clearBreadCrumbs()"> <i class="fa fa-calendar"></i> <span><h:outputText value="#{msgsCore['core.menu.admin.manageWorkshops']}" /></span> </p:link></li>
Finally, redeploy the application, click on the new menu item for the CRUD feature and try creating, retrieving, updating and deleting some workshops.
Work in progress...