Modifying Java Classes in the Generated JavaServer Faces CRUD Application
Written by Ken Ganfield and Matthew Bohm
This tutorial is the second in a three-part series that demonstrates how to modify the ConsultingAgency web application
that was originally created in the Generating a JavaServer Faces CRUD Application from
a Database tutorial.
The series is intended as a guide to show how you can use the code generated by the IDE as the foundation of your project
and then modify or add code to implement specific functionality that is required by the project.
The first tutorial in the series contains a description of the project's requirements and the changes that need to be made.
This series is organized so that each document focuses on modifying a specific part of the generated code.
The first tutorial in the series demonstrated how to modify some of the JSP pages to
optimize the pages for consultants that are logged in.
In this tutorial you will modify some of the generated Java classes to implement the methods that are invoked by the JSP pages.
In this document you will do the following:
Add a class that implements a phase listener
Add methods to the JPA controller classes and JSF controller classes
Add new navigation rules.
The third tutorial in the series is optional and demonstrates how to add some Ajax validation to some of the fields.
This tutorial uses the source code of the ConsultingAgency application.
If you have not completed the first tutorial in the series Modifying JSP Pages in the Generated JavaServer Faces CRUD Application
where you start modifying the generated code, you may want to do that now.
Alternatively, you can download the project that is used as the starting point for this series as a
.zip archive or get the project sources from Kenai.
This project uses some experimental JSF libraries to add Ajax functionality described in the third tutorial in the series.
The JSF libraries are bundled with NetBeans IDE 6.5.
If you are using NetBeans IDE 6.7, you will need to install the JSF Extensions (Ajax) plugin
from the NetBeans Beta Update Center.
Alternatively, you can remove the source code that implements the Ajax functionality and skip the third document in the series.
For details, see the section on Removing Ajax Functionality.
For more about the functionality provided by the plugin,
see the section Enabling Ajax in the Application
in the Generating a JavaServer Faces CRUD Application from a Database tutorial.
Getting the Sources
This tutorial uses the source code of the ConsultingAgency application as its base.
To complete the tutorials in this series you need to have the sources for the ConsultingAgency project on your local system.
You can get the project sources in any of the following ways.
Download and extract the
base2.zip zip archive containing the project.
Get the project sources from Kenai (requires NetBeans IDE 6.7).
Notes.
When you open the project, you might see a warning about a missing project resource if the JSF Extensions (Ajax) plugin is not installed.
You need to install the plugin to add the JSF libraries required by the project.
Alternatively, you can remove the requirement for the plugin by following the
steps in the section Removing Ajax Functionality.
For more about the functionality provided by the plugin,
see the section Enabling Ajax in the Application
in the Generating a JavaServer Faces CRUD Application from a Database tutorial.
When you open the project, you might see a warning about a missing server. To resolve the problem, right-click the project node
in the Project window, choose Resolve Missing Server Problem and then specify your local GlassFish v2.x server instance in the dialog box.
If you are using NetBeans IDE 6.7, you can get the sources from the remote repository and open the project in the IDE.
You can find the sources in the repository of the Consulting Agency Solution Project.
The repository contains the project sources for each of the tutorials in this series.
To get the sources for this tutorial from the repository on Kenai, perform the following steps.
Choose Team > Kenai > Get Sources from Kenai from the main menu.
Alternatively, you can choose Open Kenai Project to add the project to the Kenai dashboard
and then get the project sources.
In the Get Sources from Kenai dialog box, locate the Kenai Repository by clicking Browse to open the Browse Kenai Projects dialog box.
Search for the Consulting Agency Solution Project.
Select the Consulting Agency Solution Project and click OK.
Click Browse to specify the Folder to Get and select base2. Click OK.
Click Browse to specify the Local Folder for the sources (the local folder must be empty).
Click Get From Kenai.
When you click Get From Kenai, the IDE initializes the local folder as a Subversion repository
and checks out the project sources.
Click Open Project in the dialog that appears when checkout is complete.
As part of the requirements for the application, a user must be logged in as a consultant to access any of the pages to view
and modify their billable items.
To accomplish this you will create a phase listener class to authenticate the user at each phase
and pass the user to the correct page if the user is logged in.
A user that is not authenticated triggers the "login" outcome.
When you configure the JSF navigation rules you will set the "login" outcome
to the login page (login.jsp).
Choose File > Open Project to open the Open Project dialog box.
Locate the ConsultingAgency application and click Open.
Expand the Source Packages node in the Projects window.
Right-click the jsf.util package and choose New > Java Class.
Create a new class in the jsf.util package named AuthenticationPhaseListener that implements PhaseListener.
Insert the following code into the class:
public class AuthenticationPhaseListener implements PhaseListener {
/**
* <p>The outcome to trigger navigation to the login page.</p>
*/
private static final String LOGIN_OUTCOME = "login";
/**
* <p>The outcome to trigger navigation to the welcome page.</p>
*/
private static final String WELCOME_OUTCOME = "welcome";
/**
* <p>The session attribute key used to store an authenticated consultant.</p>
*/
public static final String AUTHENTICATED_CONSULTANT_KEY = "jsfcrud.timecard.consultant";
/**
* <p>Invoke <code>verifyView</code> if the current phase is apply request values or render response.</p>
*/
public void beforePhase(PhaseEvent event) {
PhaseId phase = event.getPhaseId();
if (PhaseId.APPLY_REQUEST_VALUES.equals(phase) || PhaseId.RENDER_RESPONSE.equals(phase)) {
verifyView(event);
}
}
/**
* <p>No-op.</p>
*/
public void afterPhase(PhaseEvent event) {
}
private void verifyView(PhaseEvent event) {
FacesContext context = event.getFacesContext();
String viewId = context.getViewRoot().getViewId();
String forwardToOutcome = null;
if (isForbiddenView(viewId)) {
JsfUtil.addErrorMessage("You do not have permission to view the requested page.");
//change requested view to the welcome view
//and assume we will forward to that view
viewId = "/welcomeJSF.jsp";
forwardToOutcome = WELCOME_OUTCOME;
}
if (getLoggedInConsultant() == null) {
if (isSecureView(viewId)) {
forwardToOutcome = LOGIN_OUTCOME;
}
}
if (forwardToOutcome != null) {
context.getApplication(). getNavigationHandler().handleNavigation(context, null, forwardToOutcome);
}
}
public PhaseId getPhaseId() {
return PhaseId.ANY_PHASE;
}
public static Consultant getLoggedInConsultant() {
return (Consultant)FacesContext.getCurrentInstance().getExternalContext().getSessionMap().get(AuthenticationPhaseListener.AUTHENTICATED_CONSULTANT_KEY);
}
private boolean isSecureView(String viewId) {
if (viewId == null) {
return false;
}
if (isUnrestrictedView(viewId)) {
return false;
}
return ("/welcomeJSF.jsp".equals(viewId) || viewId.startsWith("/billable/"));
}
private boolean isForbiddenView(String viewId) {
if (viewId == null) {
return true;
}
//only unrestricted and secure views are permitted
if (isUnrestrictedView(viewId) || isSecureView(viewId)) {
return false;
}
return true;
}
private boolean isUnrestrictedView(String viewId) {
return "/login.jsp".equals(viewId) || "/jsfcrud.js".equals(viewId) || "/jsfcrud.css".equals(viewId) || "/busy.gif".equals(viewId);
}
}
Fix your imports (Ctrl+Shift+I) and save your changes.
Modifying the JPA Controllers
In this section you will modify the JPA controllers generated by the IDE.
When you used the JSF Pages from Entity Classes in the original tutorial, the IDE generated
JPA controller classes for the entity classes and some exception classes used by the JPA controllers.
The logic in a JPA controller class performs the work of creating, editing, and destroying an entity and
can be invoked by other classes, JSP pages and servlets.
The IDE separated the JPA controller logic from the JSF controller logic to make modifying the code easier.
For example, if your database schema changes after you modified the JSF pages and JSF controller classes,
you can regenerate the entity classes for the database tables and then use the JPA Controllers from Entity Classes wizard
to regenerate JPA controllers for the entities.
The code for the JSF pages and JSF controllers is not changed.
In this section you will add some find methods to the JPA controller classes to retrieve the entities.
The find methods are invoked from the JSF controller classes.
Modifying the JPA Controller for Billable Entities
Open the jpa.controllers.BillableJpaController.java class in the editor and add the following find and get methods:
public List<Billable> findBillableEntities(int maxResults, int firstResult, String query, Map<String, Object> parameters) {
return findBillableEntities(false, maxResults, firstResult, query, parameters);
}
private List<Billable> findBillableEntities(boolean all, int maxResults, int firstResult, String query, Map<String, Object> parameters) {
EntityManager em = getEntityManager();
try {
Query q = em.createQuery(query);
if (parameters != null) {
for (String name : parameters.keySet()) {
q.setParameter(name, parameters.get(name));
}
}
if (!all) {
q.setMaxResults(maxResults);
q.setFirstResult(firstResult);
}
return q.getResultList();
} finally {
em.close();
}
}
public int getBillableCount(String query, Map<String, Object> parameters) {
EntityManager em = getEntityManager();
try {
Query q = em.createQuery(query);
if (parameters != null) {
for (String name : parameters.keySet()) {
q.setParameter(name, parameters.get(name));
}
}
return ((Long) q.getSingleResult()).intValue();
}
finally {
em.close();
}
}
Fix your imports (to import java.util.Map) and save your changes.
Modifying the JPA Controller for Consultant Entities
Open the jpa.controllers.ConsultantJpaController class in the editor and add the following find methods for consultants.
public List<Consultant> findConsultantEntities(int maxResults, int firstResult, String query, Map<String, Object> parameters) {
return findConsultantEntities(false, maxResults, firstResult, query, parameters);
}
private List<Consultant> findConsultantEntities(boolean all, int maxResults, int firstResult, String query, Map<String, Object> parameters) {
EntityManager em = getEntityManager();
try {
Query q = em.createQuery(query);
if (parameters != null) {
for (String name : parameters.keySet()) {
q.setParameter(name, parameters.get(name));
}
}
if (!all) {
q.setMaxResults(maxResults);
q.setFirstResult(firstResult);
}
return q.getResultList();
}
finally {
em.close();
}
}
Fix your imports (to import java.util.Map) and save your changes.
Modifying the JPA Controller for Project Entities
Open the jpa.controllers.ProjectJpaController class in the editor and add the following find methods for projects.
public List<Project> findProjectEntities(String query, Map<String, Object> parameters) {
return findProjectEntities(true, -1, -1, query, parameters);
}
private List<Project> findProjectEntities(boolean all, int maxResults, int firstResult, String query, Map<String, Object> parameters) {
EntityManager em = getEntityManager();
try {
Query q = em.createQuery(query);
if (parameters != null) {
for (String name : parameters.keySet()) {
q.setParameter(name, parameters.get(name));
}
}
if (!all) {
q.setMaxResults(maxResults);
q.setFirstResult(firstResult);
}
return q.getResultList();
}
finally {
em.close();
}
}
Fix your imports (to import java.util.Map) and save your changes.
Modifying the JSF Controller Classes
In this section you will modify the JSF controllers generated by the IDE.
When you used the JSF Pages from Entity Classes in the original tutorial, the IDE generated
JSF controller classes with code specifically for the generated JSF pages.
After modifying the JSF pages in the previous part of the tutorial, you now need
to make some corresponding changes to the JSF controllers.
For some of the changes you can modify the generated code or use the
generated code as an example.
For new functionality such as the search function you need to add new methods.
Note. You do not need to make any changes to the utility classes (JsfUtil and PagingInfo)
or the ELResolver (JsfCrudELResolver) that are used by the JSF controller classes.
Editing the JSF Controller Class for Billable
In this exercise you will modify some of the existing methods in the jsf.BillableController.java class to retrieve
billable items associated with the consultant.
You will also add some methods to implement the search function for billable items, check if the user is allowed to perform
the operation and add a validator for endDate.
Open the jsf.BillableController.java class in the editor.
Add the getNewBillable() method to the class to set values for fields the consultant cannot modify.
Notice that the method calls the getLoggedInConsultant() method in the AuthenticationPhaseListener class.
private Billable getNewBillable() {
Billable result = new Billable();
Consultant consultant = AuthenticationPhaseListener.getLoggedInConsultant();
result.setConsultantId(consultant);
result.setHourlyRate(consultant.getHourlyRate());
result.setBillableHourlyRate(consultant.getBillableHourlyRate());
return result;
}
Now you will add code to some of the methods to call an isOperationForbidden method to check if the user can perform
the operation.
Add the following code (in bold) to the create() method before the try block.
public String create() {
if (isOperationForbidden()) {
return null;
}
try {
jpaController.create(billable);
...
}
Add the following code (in bold) to the scalarSetup(String destination) method, before the return statement.
Add the following code (in bold) to the edit() method before the try block.
public String edit() {
...
JsfUtil.addErrorMessage("Could not edit billable. Try again.");
}
return outcome;
}
if (isOperationForbidden()) {
return null;
}
try {
jpaController.edit(billable);
...
}
Add the following code (in bold) to the destroy() method, before String idAsString = JsfUtil.getRequestParameter("jsfcrud.currentBillable");
public String destroy() {
if (isOperationForbidden()) {
return null;
}
String idAsString = JsfUtil.getRequestParameter("jsfcrud.currentBillable");
Long id = new Long(idAsString);
try {
...
}
Add the isOperationForbidden() method to the class to check if the logged in consultant can modify the data.
private boolean isOperationForbidden() {
boolean forbidden = false;
Consultant loggedInConsultant = AuthenticationPhaseListener.getLoggedInConsultant();
if (loggedInConsultant == null) {
forbidden = true;
} else if (billable == null) {
//destroy operation
String consultantOfCurrentBillableAsString = JsfUtil.getRequestParameter("jsfcrud.timecard.consultantOfCurrentBillable");
if (consultantOfCurrentBillableAsString != null) {
Integer consultantOfCurrentBillableAsInteger = new Integer(consultantOfCurrentBillableAsString);
if (!consultantOfCurrentBillableAsInteger.equals(loggedInConsultant.getConsultantId())) {
forbidden = true;
}
}
} else if (!loggedInConsultant.equals(billable.getConsultantId())) {
//detail or edit operation
forbidden = true;
}
if (forbidden) {
JsfUtil.addErrorMessage("You do not have permission to perform the requested operation.");
}
return forbidden;
}
Modify the getBillableItems() method to add the following code (in bold) to add search parameters and set a query when
getting the billable items.
public List<Billable> getBillableItems() {
if (billableItems == null) {
getPagingInfo();
tweakSearchParameters();
String query = getQueryForParameters("select object(o) from Billable as o");
billableItems = jpaController.findBillableEntities(pagingInfo.getBatchSize(), pagingInfo.getFirstItem(), query, searchParameters);
}
return billableItems;
}
Add the following methods to the class to implement the search function, including the tweakSearchParameters and
getQueryForParameters methods.
public String searchSetup() {
reset(false);
billable = new Billable();
billable.setHours((short)-1);
return "billable_search";
}
public String search() {
searchParameters = new HashMap<String, Object>();
Object[] values = {billable.getBillableId(), billable.getStartDate(), billable.getEndDate(), new Short(billable.getHours()), billable.getHourlyRate(), billable.getDescription(), billable.getProject(), null};
for (int i = 0; i < properties.length; i++) {
if (values[i] != null) {
if("hours".equals(properties[i])) {
if (((Short)values[i]).shortValue() == -1) {
continue;
}
} else if (values[i] instanceof String) {
String trimmedValue = ((String)values[i]).trim();
if (trimmedValue.length() == 0) {
continue;
}
trimmedValue = trimmedValue.replaceAll("~", "~~");
trimmedValue = trimmedValue.replaceAll("%", "~%");
values[i] = "%" + trimmedValue + "%";
}
searchParameters.put(properties[i], values[i]);
}
}
reset(true, false);
return "billable_list";
}
public int getItemCount() {
tweakSearchParameters();
String query = getQueryForParameters("select count(o) from Billable as o");
return jpaController.getBillableCount(query, searchParameters);
}
private String getQueryForParameters(String baseQuery) {
if (searchParameters == null) {
return baseQuery;
}
StringBuffer where = new StringBuffer();
for (int i = 0; i < properties.length; i++) {
if (searchParameters.containsKey(properties[i])) {
if (where.length() > 0) {
where.append(" and ");
}
if ("startDate".equals(properties[i])) {
where.append("(o.startDate is null or o.startDate >= :startDate) and (o.endDate is null or o.endDate >= :startDate)");
}
else if ("endDate".equals(properties[i])) {
where.append("(o.startDate is null or o.startDate <= :endDate) and (o.endDate is null or o.endDate <= :endDate)");
}
else if (searchParameters.get(properties[i]) instanceof String) {
where.append("o." + properties[i] + " like :" + properties[i] + " escape '~'");
}
else {
where.append("o." + properties[i] + " = :" + properties[i]);
}
}
}
if (where.length() > 0) {
baseQuery += " where " + where;
}
return baseQuery;
}
private void tweakSearchParameters() {
if (searchParameters == null) {
searchParameters = new HashMap<String,Object>();
}
if (searchParameters.get(properties[properties.length - 1]) == null) {
Consultant loggedInConsultant = AuthenticationPhaseListener.getLoggedInConsultant();
searchParameters.put(properties[properties.length - 1], loggedInConsultant);
}
}
Note. Notice that the searchSetup method returns "billable_search".
You will later configure the JSF navigation rules so that Search.jsp is loaded when the outcome is "billable_search".
Modify the next() and prev() methods to change reset(false) to reset(false, false)
so that searchParameters are also reset.
public String next() {
reset(false, false);
getPagingInfo().nextPage();
return "billable_list";
}
public String prev() {
reset(false, false);
getPagingInfo().previousPage();
return "billable_list";
}
Modify the reset(boolean) method to also reset searchParameters and add a new reset(boolean) method.
Add the following validator method for endDate used in Search.jsp, New.jsp and Edit.jsp.
public void validateEndDate(FacesContext facesContext, UIComponent component, Object value) {
ValueHolder startDateInputField = (ValueHolder)facesContext.getViewRoot().findComponent("form1:startDate");
Date startDate = (Date)startDateInputField.getLocalValue();
if (startDate != null) {
Date endDate = (Date)value;
if (startDate.after(endDate)) {
throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "The end date cannot precede the start date.", null));
}
}
}
Fix your imports (make sure you import java.util.Date and javax.faces.validator.ValidatorException) and save your changes.
Editing the JSF Controller Class for Consultant
In this exercise you add the login and logout methods.
The login method is invoked from login.jsp and the logout method
is invoked from welcomeJSF.jsp.
Open the jsf.ConsultantController.java class in the editor.
Add the following login() and logout() methods.
public String login() {
if (consultant == null) {
JsfUtil.addErrorMessage("An error occurred attempting to log in.");
return null;
}
String query = "select object(o) from Consultant as o where o.email = :email and o.password = :password";
Map<String, Object> parameters = new HashMap<String, Object>();
String email = consultant.getEmail();
parameters.put("email", email);
parameters.put("password", consultant.getPassword());
List<Consultant> consultants = null;
try {
consultants = jpaController.findConsultantEntities(1, 0, query, parameters);
} catch (Exception e) {
JsfUtil.ensureAddErrorMessage(e, "A persistence error occurred.");
}
if (consultants != null && consultants.size() > 0) {
Consultant c = consultants.get(0);
FacesContext.getCurrentInstance().getExternalContext().getSessionMap().put(AuthenticationPhaseListener.AUTHENTICATED_CONSULTANT_KEY, c);
JsfUtil.addSuccessMessage("Welcome, " + email + ".");
return "welcome";
} else {
JsfUtil.addErrorMessage("Invalid login.");
return null;
}
}
public String logout() {
HttpSession session = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(false);
if (session != null) {
session.invalidate();
}
JsfUtil.addSuccessMessage("You are now logged out.");
return "login";
}
Fix your imports (make sure you import java.util.Map) and save your changes.
Editing the JSF Controller Class for Project
In this exercise you add methods that determine the projects that are displayed in the project drop-down list when a consultant
creates, updates and searches billable items.
The drop down list only displays projects associated with the logged in consultant.
AssignedProjectsForLoggedInConsultantSelectOne is invoked from the New.jsp and Edit.jsp pages.
BilledProjectsForLoggedInConsultantSelectOne is invoked from the Search.jsp page.
Open the jsf.ProjectController class in the editor.
Add the following methods:
public SelectItem[] getBilledProjectsForLoggedInConsultantSelectOne() {
String query = "select distinct b.project from Billable as b where b.consultantId = :consultant";
Map<String, Object> parameters = new HashMap<String, Object>();
Consultant loggedInConsultant = AuthenticationPhaseListener.getLoggedInConsultant();
parameters.put("consultant", loggedInConsultant);
return JsfUtil.getSelectItems(jpaController.findProjectEntities(query, parameters), true);
}
public SelectItem[] getAssignedProjectsForLoggedInConsultantSelectOne() {
Consultant loggedInConsultant = AuthenticationPhaseListener.getLoggedInConsultant();
Collection<Project> assignedProjectsCollection = loggedInConsultant.getProjectCollection();
List assignedProjectsList = new ArrayList(assignedProjectsCollection);
return JsfUtil.getSelectItems(assignedProjectsList, true);
}
Fix your imports (to import java.util.Collection and java.util.Map) and save your changes.
Editing the Navigation Rules
Now that you are finished modifying and adding the required methods, you need to update the navigation rules for the JSF pages
by editing the faces-config.xml file.
You need to specify the phase listener class as part of the JSF lifecycle and create new navigation rules for the two pages that
you created in the first part of the tutorial (login.jsp and Search.jsp).
Expand the Configuration Files node in the Projects window.
Double-click faces-config.xml to open the file in the editor and click the XML tab to edit the raw XML.
Add the following <lifecycle> elements (in bold) before the <application> element.
Now that you have made all the necessary changes to the JSP files and the Java classes,
you can run the application to see if everything is working correctly.
After logging in, search for billable items and create a new billable item.
Check that your database is running.
Right-click the ConsultingAgency node in the Projects window and choose Run.
When you choose Run, the IDE deploys the application to the server and opens
the browser to the login page (http://localhost:8080/ConsultingAgency/).
Enter the Email and Password. Click Login.
Note. The login screen requires a valid email and password.
If you entered the sample data in the Adding Data to the Database section in the initial tutorial
where you created the ConsultingAgency application
(Generating a JavaServer Faces CRUD Application from a Database),
you can now type for the email address and janet.smart for the password.
If you entered different data, you can find the email and password for a consultant by right-clicking the consultant
table in the Services window of the IDE and choosing View Data.
The IDE will display the table data in the lower window of the SQL editor.
If your database is not populated, you need to follow the steps in the
Adding Data to the Database section
of the initial tutorial to run the SQL script to populate the database.
After you log in you see the welcome screen for the consultant containing
links to the screens that the consultant can access.
A logged in consultant can search for their billable items or create a new billable item.
Click Search Billable Items and search for billable items of Secret Project.
Create a new billable item.
Troubleshooting
If you are attempting to run the project for the first time and you encounter
problems displayed in the Output window, see the Troubleshooting
section of the first tutorial in the series.