In this part we build and describe an advanced JEE Web Application. This application permits to simple users to modify their passwords and to a super user to freely insert, update or delete users, profiles and roles. The application relies on Spring Security to control users access.
This application is built upon the result of
Maven/Spring/Hibernate/JSF2 Tutorial - Second Part (Skeleton JEE Web Application).
N.B: In the following by deploying the application we mean :
- In a command line console set your working directory to the path of your Test Eclipse project then successively type :
mvn clean
mvn compile
mvn war:war
- Under the sub-directory
Test/target/ you should see the
Test-1.0-SNAPSHOT folder. Copy that folder under the
webapps/ folder of Apache Tomcat.
- Start your Apache Tomcat Server (if it is not started) then type the following address in a browser:
http://localhost:8081/Test-1.0-SNAPSHOT/. (or change the port to your local setting)
Outline:
3.1 - Customizing Application:
a - Application Messages
b - JSF Validation Messages
c - Using JSF Templates
d - Richfaces Skins
3.2 - Password Update Feature
a - Presentation Layer
b - Service (BO) Layer
c - Repository (DAO) Layer
3.3 - Generic Spring Components
a - Generic Repository (DAO)
b - Generic Service (BO)
c - Generic Controller
3.4 - JSF and Richfaces useful components
a - Super User features
b - RichFaces Accordion
c - JSF Data Table
d - RichFaces Pick Lists
e - JSF Converters
f - JSF navigation
3.5 - Spring Security Access Control
3.1 - Customizing Application:
We propose here to introduce some customization to the application built in
Part 2 of this tutorial.
a - Application Messages:
It is recommended to have all the application messages (titles of pages, text inputs or command buttons) stored in property files. This permits to handle them more efficiently.
But what actually are the messages of our application ?
. In
Connection.xhtml we defined three titles in the following value attributes:
...
<h:outputText value = "User Name" />
...
<h:outputText value = "Password" />
...
<h:commandButton value = "Login" action = "#{loginController.doLogin()}" />
...
. But also in
mshj.tutorial.listener.LoginErrorPhaseListener where we used the two following messages to stress out an authentication failure or a concurrency session exception:
...
new FacesMessage(FacesMessage.SEVERITY_ERROR,
"Username or password not valid.", "Username or password not valid")
...
new FacesMessage(FacesMessage.SEVERITY_ERROR,
"Maximum number of sessions Exceeded.", "Maximum number of sessions Exceeded")
...
- Create a new file named
messages.properties under
src/main/resources/.
- Paste into this file the following entries (one per each message):
userName=User Name
password=Password
login=Login
loginFailure=Username or Password not Valid
sessionConcurrency=Maximum Number of Concurrent Sessions Exceeded
- Make JSF aware of the use of the message resource bundle. Edit
src/main/webapp/WEB-INF/faces-config.xml such that it finally looks like:
<?xml version="1.0"?>
<faces-config
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
version="2.0">
<!-- Resource Bundles (Messages and JSF Messages) -->
<application>
<resource-bundle>
<base-name>messages</base-name>
<var>messages</var>
</resource-bundle>
</application>
<!-- JSF and Spring are integrated -->
<application>
<el-resolver>
org.springframework.web.jsf.el.SpringBeanFacesELResolver
</el-resolver>
</application>
</faces-config>
In
Lines 10-15 we added the messages resource bundle.
- In
Connection.xhtml transform the previously discussed static values to ones gathered from the messages resource bundle:
...
<h:outputText value = "#{messages.userName}" />
...
<h:outputText value = "#{messages.password}" />
...
<h:commandButton value = "#{messages.login}" action = "#{loginController.doLogin()}" />
...
- In
mshj.tutorial.listener.LoginErrorPhaseListener perform these transformations:
. Replace
"Username or password not valid" by a call to the messages resource bundle with key
"loginFailure":
...
new FacesMessage(FacesMessage.SEVERITY_ERROR,
FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), "messages")
.getString("loginFailure"),
FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), "messages")
.getString("loginFailure"))
...
. Then replace
"Maximum number of sessions Exceeded" by a call to the messages resource bundle with key
"sessionConcurrency":
...
new FacesMessage(FacesMessage.SEVERITY_ERROR,
FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), "messages")
.getString("sessionConcurrency"),
FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), "messages")
.getString("sessionConcurrency"))
...
Indeed,
FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), "messages")
.getString("someKey")
returns the value mapped by
"someKey" in the application messages resource bundle.
It is also possible to define application messages depending on the user's (language) Locale settings.
You can define a default Locale, i.e. the one to be used if the user's Locale is not supported and a set of supported Locales. Here's an example of configuring french messages for users having a french Locale.
- Edit
src/main/webapp/WEB-INF/faces-config.xml such that it finally looks like:
<?xml version="1.0"?>
<faces-config
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
version="2.0">
<!-- Resource Bundles (Messages and JSF Messages) -->
<application>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>fr_FR</supported-locale>
</locale-config>
<resource-bundle>
<base-name>messages</base-name>
<var>messages</var>
</resource-bundle>
</application>
<!-- JSF and Spring are integrated -->
<application>
<el-resolver>
org.springframework.web.jsf.el.SpringBeanFacesELResolver
</el-resolver>
</application>
</faces-config>
- Now add the following
messages_fr_FR.properties file under
src/main/resources/:
userName=Nom d'Utilisateur
password=Mot de Passe
login=S'authentifier
loginFailure=Nom d'Utilisateur ou Mot de Passe Incorrect
sessionConcurrency=Nombre Maximum de Sessions Concurrentes Atteint
Change your browser Locale to french (if it is not already french) and try out the messages customization after deploying the application.
Note that to switch displayed messages depending on the user's Locale, you have only to create dedicated files and to add the Locale's name as suffix to the corresponding application messages resource bundles.
b - JSF Validation Messages:
JSF validates the inputs according to their constraints. For example, in
Connection.xhtml the two input texts (User Name and Password) are set to be required. Leaving one of these inputs empty leads to a default message, for example for password something like:
J_password : validation error. Value is required. These messages values can be customized, also depending on the user's Locale.
- Create the following
jsfMessages.properties file under
src/main/resources/:
javax.faces.converter.NumberConverter.NUMBER={2}: ''{0}'' is not a number.
javax.faces.converter.NumberConverter.NUMBER_detail={2}: ''{0}'' is not a number. Example: {1}.
javax.faces.converter.LongConverter.LONG={2}: ''{0}'' must be a number consisting of one or more digits.
javax.faces.converter.LongConverter.LONG_detail={2}: ''{0}'' must be a number between -9223372036854775808 to 9223372036854775807 Example: {1}.
javax.faces.component.UIInput.REQUIRED={0}: Validation Error: Value is required.
javax.faces.converter.BigDecimalConverter.DECIMAL={2}: ''{0}'' must be a signed decimal number.
javax.faces.converter.BigDecimalConverter.DECIMAL_detail={2}: ''{0}'' must be a signed decimal number consisting of zero or more digits, that may be followed by a decimal point and fraction. Example: {1}.
javax.faces.converter.DateTimeConverter.DATE={2}: ''{0}'' could not be understood as a date.
javax.faces.converter.DateTimeConverter.DATE_detail={2}: ''{0}'' could not be understood as a date. Example: {1}.
- Create also a french entry
jsfMessages_fr_FR.properties file under
src/main/resources/:
javax.faces.converter.NumberConverter.NUMBER=Erreur de validation pour {2}: ''{0}'' n'est pas numérique.
javax.faces.converter.NumberConverter.NUMBER_detail=Erreur de validation pour {2}: ''{0}'' n'est pas numérique. Exemple de valeur attendue: {1}.
javax.faces.converter.LongConverter.LONG=Erreur de validation pour {2}: ''{0}'' n'est pas numérique.
javax.faces.converter.LongConverter.LONG_detail=Erreur de validation pour {2}: ''{0}'' doit être un nombre compris entre -9223372036854775808 et 9223372036854775807. Exemple de valeur attendue: {1}.
javax.faces.component.UIInput.REQUIRED=Erreur de validation pour {0}: ce champ est obligatoire.
javax.faces.converter.BigDecimalConverter.DECIMAL=Erreur de validation pour {2}: ''{0}'' doit être un nombre décimal signé.
javax.faces.converter.BigDecimalConverter.DECIMAL_detail=Erreur de validation pour {2}: ''{0}'' doit être un nombre décimal signé composé de zéro ou plusieurs chiffres, qui peuvent être suivis par une virgule et une fraction. Exemple de valeur attendue: {1}.
javax.faces.converter.DateTimeConverter.DATE=Erreur de validation pour {2}:''{0}'' ne peut pas être converti en une date.
javax.faces.converter.DateTimeConverter.DATE_detail=Erreur de validation pour {2}:''{0}'' ne peut pas être converti en une date. Exemple de valeur attendue: {1}.
- Let now JSF be aware of using custom validation messages. Edit
src/main/webapp/WEB-INF/faces-config.xml such that it finally looks like:
<?xml version="1.0"?>
<faces-config
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
version="2.0">
<!-- Resource Bundles (Messages and JSF Messages) -->
<application>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>fr_FR</supported-locale>
</locale-config>
<resource-bundle>
<base-name>messages</base-name>
<var>messages</var>
</resource-bundle>
<message-bundle>jsfMessages</message-bundle>
</application>
<!-- JSF and Spring are integrated -->
<application>
<el-resolver>
org.springframework.web.jsf.el.SpringBeanFacesELResolver
</el-resolver>
</application>
</faces-config>
Note that when customizing the message you can use parameters. Example:
javax.faces.converter.NumberConverter.NUMBER_detail={2}: ''{0}'' is not a number. Example: {1}.
Here
{2} will be replaced by the input's label (or id if no label is specified),
{0} by the value actually provided by the user for the input and
{1} by some generated value that passes the validation.
c - Using JSF Templates:
Usually application pages correspond to some pattern. For example a header (with some logo and title), a footer ... etc. JSF permits to define such pages templates.
- Under
src/main/webapp/ add a new folder named
templates.
- Create the following
Default.xhtml under
src/main/webapp/templates/:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:rich="http://richfaces.org/rich"
xmlns:a4j="http://richfaces.org/a4j"
>
<h:head>
<link type="text/css" rel="stylesheet" href="../resources/css/myCSS.css" />
</h:head>
<body>
<div id = "header_div">
<div id = "banner_div" align = "center">
<ui:insert name = "title">
<h:graphicImage value="../resources/images/logo.png" />
</ui:insert>
</div>
<div id = "subtitle_div" align = "center">
<ui:insert name = "subtitle"/>
</div>
<div id = "status_div" align = "center">
<ui:insert name = "status" />
</div>
</div>
<div id = "menu_div" align = "center">
<ui:insert name = "menu"/>
</div>
<div id = "messages_div" align = "center">
<ui:insert name = "messages">
<table border = "0" align = "center">
<tr>
<td align = "center">
<h:messages globalOnly="true" styleClass="error-text" />
</td>
</tr>
</table>
</ui:insert>
</div>
<div id = "content_div" align = "center">
<ui:insert name = "content"/>
</div>
<div id = "footer_div" align = "center">
<ui:insert name = "footer">
<h:graphicImage value="../resources/images/footer.png" />
</ui:insert>
</div>
</body>
</html>
Using
<ui:insert /> tags we define insertions in the template. For example, in
Lines 20-22, we define a header containing a banner image (../resources/images/logo.png). These insertions will be displayed in any page that instantiates the template page. Let's see how to make
Connection.xhtml and
AccessDenied.xhtml instantiate the
Default.xhtml template.
- Edit the
Connection.xhtml as follows:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:rich="http://richfaces.org/rich"
xmlns:a4j="http://richfaces.org/a4j"
>
<ui:composition template="../templates/Default.xhtml">
<ui:define name = "subtitle">
<h3>#{messages.login}</h3>
</ui:define>
<ui:define name = "content">
<h:form id = "loginForm">
<table align = "center" border = "0">
<tr>
<th align = "center">
<h:outputText value = "#{messages.userName}" styleClass="title-text" />
</th>
<td align = "center">
<h:inputText
id = "j_username"
value = "#{loginController.j_username}"
label = "J_username"
size = "8"
maxlength = "8"
required = "true"
/>
</td>
<td align = "center">
<h:message for = "j_username" styleClass="error-text" />
</td>
</tr>
<tr>
<th align = "center">
<h:outputText value = "#{messages.password}" styleClass="title-text" />
</th>
<td align = "center">
<h:inputSecret
id = "j_password"
value = "#{loginController.j_password}"
label = "J_password"
size = "8"
maxlength = "8"
required = "true"
/>
</td>
<td align = "center">
<h:message for = "j_password" styleClass="error-text" />
</td>
</tr>
<tr>
<td align = "center" colspan = "3">
<h:commandButton
value = "#{messages.login}"
action = "#{loginController.doLogin()}"
>
<f:phaseListener type="mshj.tutorial.listener.LoginErrorPhaseListener" />
</h:commandButton>
</td>
</tr>
</table>
</h:form>
</ui:define>
</ui:composition>
</html>
Here the
<ui:composition /> tag states that this page instantiates a template (more precisely
Default.xhtml). Then using
<ui:insert /> tags we specify what insertions of the template will be overridden by the current page (here for example the insertion named
content is overridden).
- Lets do something analogous with
AccessDenied.xhtml by editing it as follows:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:rich="http://richfaces.org/rich"
xmlns:a4j="http://richfaces.org/a4j"
>
<ui:composition template="../templates/Default.xhtml">
<ui:define name = "subtitle" />
<ui:define name = "content">
<h3>#{messages.accessDenied}</h3>
</ui:define>
</ui:composition>
</html>
-
Default.xhtml uses two images and declares a style definition file. Download these files here (
logo.png,
footer.png and
myCSS.css) and copy them in the corresponding directories of your Eclipse Project (
src/main/webapp/resources/images for the two images and
src/main/webapp/resources/css for the css file).
-
AccessDenied.xhtml also defines an new application message identified by the key
accessDenied. Add corresponding entries to your
src/main/resources/messages*.properties files, such that:
.
src/main/resources/messages.properties becomes:
userName=User Name
password=Password
login=Login
loginFailure=Username or Password not Valid
sessionConcurrency=Maximum Number of Concurrent Sessions Exceeded
accessDenied=Access Denied !
. and
src/main/resources/messages_fr_FR.properties becomes:
userName=Nom d'Utilisateur
password=Mot de Passe
login=S'authentifier
loginFailure=Nom d'Utilisateur ou Mot de Passe Incorrect
sessionConcurrency=Nombre Maximum de Sessions Concurrentes Atteint
accessDenied=Accès Refusé !
Not all insertions defined by
Default.xhtml are instantiated for now. One can populate for example the
status insertion by a welcome message for the user logged in or the
menu insertion with options for navigating between different pages of the application (the latter will be done in further steps).
d - Richfaces Skins:
Using Richfaces one can take advantage of lovely predefined themes. Richfaces comes with a set of three themes we propose to use the
Emerald Town theme. The interesting thing here is that you can easily customize those themes.
- Under the folder
src/main/resources/ create the file
mshj.skin.properties as follows:
baseSkin=emeraldTown
headerBackgroundColor=#4E671D
Here we declare the skin we are using as base then we override some property of the skin.
- Edit
src/main/webapp/WEB-INF/web.xml and add the following context parameters to the JSF context parameters section:
<context-param>
<param-name>org.richfaces.enableControlSkinning</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>org.richfaces.enableControlSkinningClasses</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>org.richfaces.skin</param-name>
<param-value>mshj</param-value>
</context-param>
Here we ask to be allowed to control skins (and their properties). We also provide the configuration file that will contain the base skin name and the properties we want to override.
- Deploy the application and notice the graphical changes.
3.2 - Password Update Feature:
We propose in the following to create a page where an authenticated user can change his password. For that we need a Controller that will interpose between the user and a service (BO) that does the job while relying on a Repository (DAO).
In the following we precisely show how the three layers of our application are designed in a loosely-coupled way driven by the user's need (having her password updated).
a - Presentation Layer:
In the presentation layer we need two things:
(i) a page with three input texts (the old password for more security, the new password and a confirmation of the latter) and a command button to submit the password modification;
(ii) a controller that will store the user's input and relay the corresponding update request to some service.
- Under
src/main/webapp/pages/ add a new
PasswordUpdate.xhtml page as follows:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:rich="http://richfaces.org/rich"
xmlns:a4j="http://richfaces.org/a4j"
>
<ui:composition template="../templates/Default.xhtml">
<ui:define name = "subtitle">
<h3>#{messages.updatePassword}</h3>
</ui:define>
<ui:define name = "menu">
<h:form id = "menuForm">
<rich:menuItem action = "#{loginController.doLogout()}" >
<h:graphicImage value="../resources/images/logout.png" width = "64" height = "64" />
</rich:menuItem>
</h:form>
</ui:define>
<ui:define name = "content">
<h:form id = "passwordUpdateForm">
<table align = "center" border = "0">
<tr>
<th align = "center">
<h:outputText value = "#{messages.currentPassword}" styleClass="title-text" />
</th>
<td align = "center">
<h:inputSecret
id = "current_password"
value = "#{passwordUpdateController.currentPassword}"
label = "current_password"
size = "8"
maxlength = "8"
required = "true"
/>
</td>
<td align = "center">
<h:message for = "current_password" styleClass="error-text" />
</td>
</tr>
<tr>
<th align = "center">
<h:outputText value = "#{messages.newPassword}" styleClass="title-text" />
</th>
<td align = "center">
<h:inputSecret
id = "new_password"
value = "#{passwordUpdateController.newPassword}"
label = "new_password"
size = "8"
maxlength = "8"
required = "true"
/>
</td>
<td align = "center">
<h:message for = "new_password" styleClass="error-text" />
</td>
</tr>
<tr>
<th align = "center">
<h:outputText value = "#{messages.confNewPassword}" styleClass="title-text" />
</th>
<td align = "center">
<h:inputSecret
id = "confNew_password"
value = "#{passwordUpdateController.confNewPassword}"
label = "confNew_password"
size = "8"
maxlength = "8"
required = "true"
/>
</td>
<td align = "center">
<h:message for = "confNew_password" styleClass="error-text" />
</td>
</tr>
<tr>
<td align = "center" colspan = "3">
<h:commandButton
value = "#{messages.updatePassword}"
action = "#{passwordUpdateController.update()}"
/>
</td>
</tr>
</table>
</h:form>
</ui:define>
</ui:composition>
</html>
Note that we implemented the
menu insertion of the Default.xhtml template to add a
logout entry.
- This menu entry uses an image
logout.png,
download it (
here) then save it under
src/main/webapp/resources/images/.
The page uses also some application messages that should be added to the messages bundles:
-
Edit src/main/resources/messages.properties and add the following entries:
updatePassword=Update Password
currentPassword=Current Password
newPassword=New Password
confNewPassword=New Password Confirmation
-
Edit src/main/resources/messages_fr_FR.properties and add the following entries:
updatePassword=Mise à Jour du Mot de Passe
currentPassword=Mot de Passe Courant
newPassword=Nouveau Mot de Passe
confNewPassword=Confirmation du Nouveau Mot de Passe
The page also refers to a controller that we create below:
- Under the package
mshj.tutorial.controller create the new
PasswordUpdateController.java controller as follows:
package mshj.tutorial.controller;
import java.io.Serializable;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import mshj.tutorial.service.UpdatePasswordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
@Controller
@Scope("session")
public class PasswordUpdateController implements Serializable
{
private static final long serialVersionUID = 1L;
private String currentPassword;
public String getCurrentPassword() {
return currentPassword;
}
public void setCurrentPassword(String currentPassword) {
this.currentPassword = currentPassword;
}
private String newPassword;
public String getNewPassword() {
return newPassword;
}
public void setNewPassword(String newPassword) {
this.newPassword = newPassword;
}
private String confNewPassword;
public String getConfNewPassword() {
return confNewPassword;
}
public void setConfNewPassword(String confNewPassword) {
this.confNewPassword = confNewPassword;
}
@Autowired
private PasswordUpdateService passwordUpdateService;
public PasswordUpdateService getPasswordUpdateService() {
return updatePasswordService;
}
public void setPasswordUpdateService(PasswordUpdateService passwordUpdateService) {
this.passwordUpdateService = passwordUpdateService;
}
public void update()
{
FacesContext.getCurrentInstance()
.addMessage(null,
new FacesMessage(getPasswordUpdateService()
.update(getCurrentPassword(), getNewPassword(), getConfNewPassword())));
}
}
Here we defined containers (properties) for all input text values and the method update which relies on an auto-wired
UpdatePasswordService service. What is required from that service is to try to perform the update task and return a message summarizing the results (this message is then added by the controller to the application global messages). Note that for now we don't need the full implementation of the service we only need that it implements the update method (i.e. that it fulfills the contract).
For that we use the following interface:
- Create the new package
mshj.tutorial.service and create inside it the following
PasswordUpdateService interface:
package mshj.tutorial.service;
public interface PasswordUpdateService
{
String update(String currentPassword, String newPassword, String confNewPassword);
}
Note that if many profiles of developers are collaborating to fulfill the task, the presentation layer job is done and interestingly without interfering with the other layers (loose-coupling). Time now for the service layer engineer to proceed. So you can take off you "presentation layer" hat and put the "service layer" one.
b - Service (BO) Layer:
Here we start with a contract in hand. We have to provide an implementation for the
PasswordUpdateService interface having in mind that all data access stuff should be delegated to a repository bean.
But what's really to be done in the Service layer ?
(i) Checking that the password update request is coherent: that the current password is really the one of the currently connected user and that the new password and its confirmation are equal.
(ii) If the request is coherent ask the Repository bean to do the update job. If not return a message asking the user to fix the error.
- Create the new package
mshj.tutorial.service.impl and create inside it the following
PasswordUpdateServiceImpl class:
package mshj.tutorial.service.impl;
import java.io.Serializable;
import javax.faces.context.FacesContext;
import mshj.tutorial.model.User;
import mshj.tutorial.repository.UserRepository;
import mshj.tutorial.security.MyUserDetails;
import mshj.tutorial.service.PasswordUpdateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly=true)
public class PasswordUpdateServiceImpl implements Serializable, PasswordUpdateService
{
private static final long serialVersionUID = 1L;
@Autowired
private UserRepository userRepository;
public UserRepository getUserRepository() {
return userRepository;
}
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
@Transactional(readOnly=false, propagation=Propagation.REQUIRED)
public String update(String currentPassword, String newPassword,String confNewPassword)
{
if (newPassword.equals(confNewPassword))
{
User currentUser = ((MyUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUser();
if (currentUser.getPassword().equals(currentPassword))
{
currentUser.setPassword(newPassword);
getUserRepository().update(currentUser);
return(FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), "messages")
.getString("passwordUpdated"));
}
else
{
return(FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), "messages")
.getString("wrongPassword"));
}
}
else
{
return(FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), "messages")
.getString("passwordsMismatch"));
}
}
}
The bean is annotated as Service. The method update is implemented as expected: relying on the auto-wired
UserRepository repository to do the update job if the parameters of the update request passes all the checks.
Messages returned by the
update method must declared in the resource bundle:
-
Edit src/main/resources/messages.properties and add the following entries:
passwordsMismatch=New Password does not match its Confirmation
wrongPassword=Check your Current Password
passwordUpdated=Password Successfully Updated. Update will be effective after logout
-
Edit src/main/resources/messages_fr_FR.properties and add the following entries:
passwordsMismatch=Le Nouveau Mot de Passe et sa Confirmation ne correspondent pas
wrongPassword=Vérifier votre Mot de Passe Courant
passwordUpdated=Mot de Passe mis à jour avec Succès. La mise à jour sera effective après déconnexion
We stress out two things here. First the instruction
(MyUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal()
permits to retrieve the currently connected user. Second the hole service is declared to be read-only
transactional. Only the method update overrides this setting being as expected declared as read-write
transactional. Defining transaction settings at the service layer is a good habit since it permits a given method to perform many database accesses (through calling different repositories) in the same transaction.
Remark: Addressing the application message bundles in the service layer maybe looking as a violation of separation of concerns since the service layer is addressing a resource which is handled by the presentation layer. While writing this i was thinking as follows: the business layer is in charge of telling why an update request is not coherent (for example according to the business rule stating that the new password and its confirmation should match), the controller is only in charge of relaying this to the user.
Loose coupling between layers is also respected here. Indeed only the contract to be respected by the Repository is needed for now (we don't need the full implementation yet).
- Create the new package
mshj.tutorial.repository and create inside it the following
UserRepository interface:
package mshj.tutorial.repository;
import mshj.tutorial.model.User;
public interface UserRepository
{
void update(User user);
}
Now we can proceed with designing the Repository (DAO) layer of our application task.
c - Repository (DAO) Layer:
Now we have to implement the contract of the
UserRepository interface.
- Create the new package
mshj.tutorial.repository.impl and create inside it the following
UserRepositoryImpl class:
package mshj.tutorial.repository.impl;
import java.io.Serializable;
import mshj.tutorial.model.User;
import mshj.tutorial.repository.UserRepository;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class UserRepositoryImpl implements Serializable, UserRepository
{
private static final long serialVersionUID = 1L;
@Autowired
private SessionFactory sessionFactory;
public SessionFactory getSessionFactory() {
return sessionFactory;
}
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public void update(User user)
{
getSessionFactory().getCurrentSession().update(user);
}
}
The bean is declared as
Repository and relies in turn on the Hibernate Session to perform the password update. and ... we are done !
Before deploying and testing the new page, update the
mshj.tutorial.handler.MyAuthenticationSuccessHandler bean to redirect upon a successful authentication the users having no roles to
PasswordUpdate.xhtml as follows:
// ...
if (roles.isEmpty())
{
response.sendRedirect("PasswordUpdate.xhtml");
}
// ...
- Deploy and test the application. Notice that you have now a new button to log out from the application.
3.3 - Generic Spring Components:
A Repository bean will completely rely on Hibernate to perform usual database operations. This motivates thinking about a Generic Repository class (implementing some Generic Repository Interface) that groups the usual Hibernate calls (insert, update, delete, conditioned select) and which will be extended by all specific repositories needed by the application.
a - Generic Repository (DAO):
- Under the package
mshj.tutorial.repository create the parametric interface
GenericRepository as follows:
package mshj.tutorial.repository;
import java.io.Serializable;
import java.util.List;
public interface GenericRepository<T, ID extends Serializable>
{
public ID save(T entity);
public void update(T entity);
public void delete(T entity);
public List<T> findAll();
public List<T> find(String query);
}
The interface is parametric of
T the type of the Model Class (mapping some table) and
ID the type of the class'
identifier property (the property mapping the primary key of the table). It presents the usual contract (list of qualified methods) a Repository (DAO) should perform which will be implemented by the its generic implementation:
- Under the package
mshj.tutorial.repository.impl create the parametric class
GenericRepositoryImpl as follows:
package mshj.tutorial.repository.impl;
import java.io.Serializable;
import java.util.List;
import org.hibernate.SessionFactory;
import mshj.tutorial.repository.GenericRepository;
import org.springframework.beans.factory.annotation.Autowired;
public class GenericRepositoryImpl<T, ID extends Serializable> implements GenericRepository<T, ID>
{
@Autowired
private SessionFactory sessionFactory;
public SessionFactory getSessionFactory() {
return sessionFactory;
}
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
private Class<T> myClass;
public Class<T> getMyClass() {
return myClass;
}
public void setMyClass(Class<T> myClass) {
this.myClass = myClass;
}
public GenericRepositoryImpl(Class<T> myClass)
{
this.myClass = myClass;
}
@SuppressWarnings("unchecked")
public ID save(T entity)
{
return((ID)getSessionFactory().getCurrentSession().save(entity));
}
public void update(T entity)
{
getSessionFactory().getCurrentSession().update(entity);
}
public void delete(T entity)
{
getSessionFactory().getCurrentSession().delete(entity);
}
@SuppressWarnings("unchecked")
public List<T> findAll()
{
return(getSessionFactory().getCurrentSession().createQuery("from " + getMyClass().getSimpleName()).list());
}
@SuppressWarnings("unchecked")
public List<T> find(String queryString)
{
return(getSessionFactory().getCurrentSession().createQuery(queryString).list());
}
}
The class implements the usual contract (list of qualified methods) a Repository (DAO) should perform:
public ID save(T entity); That takes as parameter an instance of the class
T describing a line to be inserted in the mapped table and returns an instance of
ID which contains the value of the primary key of the inserted line. Returning the value of the primary key is very useful when it is automatically generated using a strategy supported by Hibernate, example: using some auto-incremented sequence.
public void update(T entity); That takes as parameter an instance of the class
T describing a line to be updated in the mapped table. Here
entity specifies the criteria of selection of the line to be updated (basically its primary key value) as well as the updates to be performed (all other non-primary key fields).
public void delete(T entity); That takes as parameter an instance of the class
T describing a line to be deleted in the mapped table.
public List<T> findAll(); Returns a List of instances of the class
T corresponding to all the mapped table lines.
public List<T> find(String query); Returns a List of instances of the class
T corresponding to the mapped table lines satisfying the
HQL (Hibernate Query Language) query
query.
We notice here that all methods implementations simply call corresponding methods of the available
Hibernate Session which is available using the instruction
getSessionFactory().getCurrentSession() (remark that the
SessionFactory is auto-wired).The
Hibernate Session transparently (w.r.t. to our DAO code) takes care of the database connections, statement creation and execution, result sets parsing ... etc.
Having this done one can easily derive implementations for our
UserRepository interface and
UserRepositoryImpl class as follows:
- To keep copies of the old Repository interface and class, rename (outside Eclipse in your file system explorer)
mshj.tutorial.repository.UserRepoistory.java (respectively
mshj.tutorial.repository.impl.UserRepoistoryImpl.java) to
mshj.tutorial.repository.UserRepoistory.java.old (respectively to
mshj.tutorial.repository.impl.UserRepoistoryImpl.java.old).
Then in Eclipse right-click on your project and choose
refresh.
- Under the package
mshj.tutorial.repository create the new version of
UserRepository interface as follows:
package mshj.tutorial.repository;
import mshj.tutorial.model.User;
public interface UserRepository extends GenericRepository<User, Long>
{
void update(User user);
}
- Under the package
mshj.tutorial.repository.impl create the new version of
UserRepositoryImpl class as follows:
package mshj.tutorial.repository.impl;
import java.io.Serializable;
import mshj.tutorial.model.User;
import mshj.tutorial.repository.UserRepository;
import org.springframework.stereotype.Repository;
@Repository
public class UserRepositoryImpl extends GenericRepositoryImpl<User, Long> implements Serializable, UserRepository
{
private static final long serialVersionUID = 1L;
public UserRepositoryImpl()
{
super(User.class);
}
}
Note that here we fix the type of the parameter
T of
GenericRepository(Impl) to be
User in the constructor.
Note also how concise is now the definition of the Repository layer.
- Deploy and try the new application version.
Comments: Using a Generic Repository bean can be quite useful since the code to be written for specific repositories becomes very concise. Nevertheless the methods you may bookmark in the Generic Repository may be not all needed by (or maybe not sufficient for) all the specific repositories you are brought to write for your application. An optimal solution maybe to stick to the minimum in the Generic Repository : just auto-wire the
SessionFactory, provide a
myClass property and only a getter for the
CurrentSession. Then provide the methods (database operations) you need in your specific repository: declare them as a contract in the repository interface then implement them in the repository class using the inherited
CurrentSession getter. The proposed solution release you from handling
SessionFactory in your specific repositories while keeping some level of genericity.
Remark: It would have been great if we could shift all
@Repository annotations to the
GenericRepository or
GenericRepositoryImpl ! Unfortunately this actually does not work.
b - Generic Service (BO):
Thinking about Generic Services came to my mind when i was a JEE beginner and thus writing bad JEE code ! At that time i was creating BO's that simply encapsulate DAO's calls and my controllers was wrongly calling as much BO's as i needed and using them in a "spaghetti code fashion" to obtain results requested by the user. In this flawed setting i had as many BO's as DAO's and it seemed more efficient to use Generic BO's that inject the corresponding DAO in a given BO.
Hindsight i realized this is not the perfect way to proceed. Putting a Controller back in its place (only collecting user requests and returning corresponding responses to her) i now write BO's that may use many DAO's thus breaking down the possibility of genericity for the Service layer.
Nevertheless in some cases (where a BO is really using only one DAO) one can take advantage of genericity. This is more or less our case for the Password Update task.
- In the
mshj.tutorial.service create the
GenericService interface as follows:
package mshj.tutorial.service;
import java.io.Serializable;
public interface GenericService<T,ID extends Serializable>
{
}
- In the
mshj.tutorial.service.impl create the
GenericServiceImpl interface as follows:
package mshj.tutorial.service.impl;
import java.io.Serializable;
import mshj.tutorial.service.GenericService;
import mshj.tutorial.repository.GenericRepository;
import org.springframework.beans.factory.annotation.Autowired;
public abstract class GenericServiceImpl <T, ID extends Serializable, D extends GenericRepository<T,ID>> implements GenericService<T, ID>
{
private Class<T> modelClass;
public Class<T> getModelClass() {
return modelClass;
}
public void setModelClass(Class<T> modelClass) {
this.modelClass = modelClass;
}
private Class<D> repositoryClass;
public Class<D> getRepositoryClass() {
return repositoryClass;
}
public void setRepositoryClass(Class<D> repositoryClass) {
this.repositoryClass = repositoryClass;
}
private D myRepository;
public D getMyRepository() {
return myRepository;
}
@Autowired
public void setMyRepository(D myRepository) {
this.myRepository = myRepository;
}
public GenericServiceImpl(Class<T> modelClass, Class<D> repositoryClass)
{
this.modelClass = modelClass;
this.repositoryClass = repositoryClass;
}
public ID save(T entity)
{
return(getMyRepository().save(entity));
}
public void update(T entity)
{
getMyRepository().update(entity);
}
public void delete(T entity)
{
getMyRepository().delete(entity);
}
public List findAll()
{
return(getMyRepository().findAll());
}
public List find(String request)
{
return(getMyRepository().find(request));
}
}
Note that a Repository (extending
GenericRepository) is injected in the
GenericServiceImpl class.
Very Important !!! The dependency injection is specified on the setter (vs. on the field itself). On run-time, this "gives time" to Spring to find precisely which instance of type
D (extending
GenericRepository<T,ID>) to instantiate for the field
myRepository depending on the parameters
<T, ID, D<T,ID>> of your bean service implementing
GenericServiceImpl. More precisely if you write several repositories extending
GenericRepositoryImpl (even if they have each a different set of prameters) and if you define the dependency injection on the field, Spring will through (among others) the exception:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [mshj.tutorial.repository.GenericRepository] is defined: expected single matching bean but found n: ...
where
n is here the number of classes instantiating
GenericRepositoryImpl. You can read more about Spring dependency injection
here (Especially the Section
4.4.1.2 Setter-based dependency injection).
Having this done one can easily derive implementations for our
PasswordUpdateService interface and
PasswordUpdateServiceImpl class as follows:
- To keep copies of the old Service interface and class, rename (outside Eclipse in your file system explorer)
mshj.tutorial.service.PasswordUpdateService.java (respectively
mshj.tutorial.service.impl.PasswordUpdateServiceImpl.java) to
mshj.tutorial.service.PasswordUpdateService.java.old (respectively to
mshj.tutorial.service.impl.PasswordUpdateServiceImpl.java.old)
Then in Eclipse right-click on your project and choose
refresh.
- Under the package
mshj.tutorial.service create the new version of
PasswordUpdateService interface as follows:
package mshj.tutorial.service;
import mshj.tutorial.model.User;
public interface PasswordUpdateService extends GenericService<User, Long>
{
public String update(String currentPassword, String newPassword,String confNewPassword);
}
Note that the interface declares the signature of the method
update needed by the
PasswordUpdateController.
- Under the package
mshj.tutorial.service.impl create the new version of
PasswordUpdateServiceImpl class as follows:
package mshj.tutorial.service.impl;
import java.io.Serializable;
import java.util.List;
import javax.faces.context.FacesContext;
import mshj.tutorial.model.User;
import mshj.tutorial.repository.UserRepository;
import mshj.tutorial.security.MyUserDetails;
import mshj.tutorial.service.PasswordUpdateService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly=true)
public class PasswordUpdateServiceImpl extends GenericServiceImpl<User, Long, UserRepository> implements Serializable, PasswordUpdateService
{
private static final long serialVersionUID = 1L;
public PasswordUpdateServiceImpl()
{
super(User.class, UserRepository.class);
}
@Override
@Transactional(readOnly=false, propagation=Propagation.REQUIRED)
public String update(String currentPassword, String newPassword,String confNewPassword)
{
if (newPassword.equals(confNewPassword))
{
User currentUser = ((MyUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUser();
if (currentUser.getPassword().equals(currentPassword))
{
currentUser.setPassword(newPassword);
update(currentUser);
return(FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), "messages")
.getString("passwordUpdated"));
}
else
{
return(FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), "messages")
.getString("wrongPassword"));
}
}
else
{
return(FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), "messages")
.getString("passwordsMismatch"));
}
}
}
No need to auto-wire the Repository now since this is already done by the generic super class.
Note also that the
update(currentUser); call addresses now a method provided by the generic service.
Comments: Remember that Generic Services (BOs) are useful only in specific cases (only one DAO is used by the BO). Nevertheless one can adapt Generic BOs to different needs: BOs using exactly 2 DAOs (Master/Detail setting) ... etc. Off course it is not mandatory that all your Services will be extending Generic ones, do it only if this makes writing code easier for you.
Remark: It would have been great if we could shift all our
@Transactional annotations to the
GenericService or
GenericServiceImpl ! Or at least the
@Service annotation ... Unfortunately this actually does not work. For the
@Transactional annotations this is not a big limitation since you maybe want to have a more fine-grained control on the transactional aspects of your implemented service methods.
c - Generic Controller:
As we did for other layers one can also think about grouping useful code in some Generic Controller class and making (a subset of) all other Controllers extend it. Example of features you might need for a controller are:
(i) Adding a global message to a view (targeting
<h:messages /> tag).
(ii) Getting a message from the application messages resource bundle given its key;
(iii) Getting the current connected user (for example to display a customized welcome message) ... etc.
Note that features (ii) and (iii) could also be available at the Service (BO) layer, and thus one can also think about adding them to a Generic Bo ...
Even feature (i) is used by our
LoginErrorPhaseListener ...
So what if we do simpler and implement all these features in static methods of some abstract class. Then all the layers can access the abstract class. Deal ?
- Create the package
mshj.tutorial.util and create the
ApplicationUtil abstract class as follows:
package mshj.tutorial.util;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import mshj.tutorial.model.User;
import mshj.tutorial.security.MyUserDetails;
import org.springframework.security.core.context.SecurityContextHolder;
public abstract class ApplicationUtil
{
public static User getConnectedUser()
{
return(((MyUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUser());
}
public static String getApplicationMessage(String bundle, String key)
{
return((FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), bundle)
.getString(key) != null)
?FacesContext.getCurrentInstance().getApplication()
.getResourceBundle(FacesContext.getCurrentInstance(), bundle)
.getString(key)
:"???" + key + "???");
}
public static String getApplicationMessage(String key)
{
return(getApplicationMessage("messages",key));
}
public static void addJSFMessage(String component, String message)
{
FacesContext.getCurrentInstance()
.addMessage(component,
new FacesMessage(message));
}
public static void addJSFMessageByKey(String component, String key)
{
FacesContext.getCurrentInstance()
.addMessage(component,
new FacesMessage(getApplicationMessage(key)));
}
public static void addGlobalJSFMessage(String message)
{
addJSFMessage(null, message);
}
public static void addGlobalJSFMessageByKey(String key)
{
addJSFMessageByKey(null, key);
}
}
Here we defined the different features described above. Please note that you can add a message to a specific component of a page given its "path" in the page. Let's say you have an input text with id
it_id inside a form having id
f_id the path of this component is
"f_id.it_id". The message will then be displayed at the position where a
<h:message /> tag was defined for the component.
Using this class will make our code more readable. Now we have to update classes using these features and replace the long instructions they call by simple calls to
ApplicationUtil's methods:
- Update
mshj.tutorial.listener.LoginErrorPhaseListener as follows:
package mshj.tutorial.listener;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import mshj.tutorial.util.ApplicationUtil;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
public class LoginErrorPhaseListener implements PhaseListener
{
private static final long serialVersionUID = 1L;
@Override
public void afterPhase(PhaseEvent arg0)
{
}
@Override
public void beforePhase(PhaseEvent arg0)
{
Exception e = (Exception) FacesContext.getCurrentInstance().
getExternalContext().getSessionMap().get(WebAttributes.AUTHENTICATION_EXCEPTION);
if (e instanceof BadCredentialsException)
{
FacesContext.getCurrentInstance().getExternalContext().getSessionMap().put(
WebAttributes.AUTHENTICATION_EXCEPTION, null);
ApplicationUtil.addGlobalJSFMessageByKey("loginFailure");
}
else
{
if (e instanceof SessionAuthenticationException)
{
FacesContext.getCurrentInstance().getExternalContext().getSessionMap().put(
WebAttributes.AUTHENTICATION_EXCEPTION, null);
ApplicationUtil.addGlobalJSFMessageByKey("sessionConcurrency");
}
}
}
@Override
public PhaseId getPhaseId()
{
return PhaseId.RENDER_RESPONSE;
}
}
- Update
mshj.tutorial.service.impl.PasswordUpdateServiceImpl as follows:
package mshj.tutorial.service.impl;
import java.io.Serializable;
import mshj.tutorial.model.User;
import mshj.tutorial.repository.UserRepository;
import mshj.tutorial.service.PasswordUpdateService;
import mshj.tutorial.util.ApplicationUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly=true)
public class PasswordUpdateServiceImpl extends GenericServiceImpl implements Serializable, PasswordUpdateService
{
private static final long serialVersionUID = 1L;
public PasswordUpdateServiceImpl()
{
super(User.class, UserRepository.class);
}
@Override
@Transactional(readOnly=false, propagation=Propagation.REQUIRED)
public String update(String currentPassword, String newPassword,String confNewPassword)
{
if (newPassword.equals(confNewPassword))
{
User currentUser = ApplicationUtil.getConnectedUser();
if (currentUser.getPassword().equals(currentPassword))
{
currentUser.setPassword(newPassword);
update(currentUser);
return(ApplicationUtil.getApplicationMessage("passwordUpdated"));
}
else
{
return(ApplicationUtil.getApplicationMessage("wrongPassword"));
}
}
else
{
return(ApplicationUtil.getApplicationMessage("passwordsMismatch"));
}
}
}
- Update
mshj.tutorial.controller.PasswordUpdateController as follows:
package mshj.tutorial.controller;
import java.io.Serializable;
import mshj.tutorial.service.PasswordUpdateService;
import mshj.tutorial.util.ApplicationUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
@Controller
@Scope("session")
public class PasswordUpdateController implements Serializable
{
private static final long serialVersionUID = 1L;
private String currentPassword;
public String getCurrentPassword() {
return currentPassword;
}
public void setCurrentPassword(String currentPassword) {
this.currentPassword = currentPassword;
}
private String newPassword;
public String getNewPassword() {
return newPassword;
}
public void setNewPassword(String newPassword) {
this.newPassword = newPassword;
}
private String confNewPassword;
public String getConfNewPassword() {
return confNewPassword;
}
public void setConfNewPassword(String confNewPassword) {
this.confNewPassword = confNewPassword;
}
@Autowired
private PasswordUpdateService passwordUpdateService;
public PasswordUpdateService getPasswordUpdateService() {
return passwordUpdateService;
}
public void setPasswordUpdateService(PasswordUpdateService updatePasswordService) {
this.passwordUpdateService = updatePasswordService;
}
public void update()
{
ApplicationUtil.addGlobalJSFMessage(
getPasswordUpdateService()
.update(getCurrentPassword(), getNewPassword(), getConfNewPassword())
);
}
}
We are now done with generic components. We proceed with adding more features to the application while introducing more interesting features of JSF and Richfaces.
3.4 - Super User Features: JSF and Richfaces useful components:
We are now going to add super user features to the application. We need 3 pages for these features: a page for administrating users, a page for administrating profiles and another one for administrating roles. We start by providing the pages and the different application layer they require before explaining used JSF and Richfaces components in details.
a - Super User features:
- Under the package
mshj.tutorial.controller create the following three controllers:
.
UsersAdministrationController.java:
package mshj.tutorial.controller;
import java.io.Serializable;
import java.util.ArrayList;
import javax.annotation.PostConstruct;
import mshj.tutorial.model.Profile;
import mshj.tutorial.model.User;
import mshj.tutorial.service.ManageUserService;
import mshj.tutorial.util.ApplicationUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
@Controller
@Scope("session")
public class UsersAdministrationController implements Serializable
{
private static final long serialVersionUID = 1L;
@Autowired
private ManageUserService manageUserService;
public ManageUserService getManageUserService() {
return manageUserService;
}
public void setManageUserService(ManageUserService manageUserService) {
this.manageUserService = manageUserService;
}
private User newUser;
public User getNewUser() {
return newUser;
}
public void setNewUser(User newUser) {
this.newUser = newUser;
}
private String newUserConfPassword;
public String getNewUserConfPassword() {
return newUserConfPassword;
}
public void setNewUserConfPassword(String newUserConfPassword) {
this.newUserConfPassword = newUserConfPassword;
}
@PostConstruct
public void init()
{
clearNewUserForm();
}
public void clearNewUserForm()
{
setNewUser(new User());
getNewUser().setProfiles(new ArrayList<Profile>());
setNewUserConfPassword(getNewUser().getPassword());
}
public void addUser()
{
if (getNewUserConfPassword().equals(getNewUser().getPassword()))
{
try
{
getManageUserService().addUser(getNewUser());
clearNewUserForm();
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("userAdded"));
}
catch(Exception e)
{
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("userNotAdded"));
}
}
else
{
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("userNotAddedPasswordsMismatch"));
}
}
public void updateUser(User user)
{
try
{
getManageUserService().updateUser(user);
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("userUpdated"));
}
catch (Exception e)
{
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("userNotUpdated"));
}
}
public void deleteUser(User user)
{
getManageUserService().deleteUser(user);
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("userDeleted"));
}
}
.
ProfilesAdministrationController.java:
package mshj.tutorial.controller;
import java.io.Serializable;
import java.util.ArrayList;
import javax.annotation.PostConstruct;
import mshj.tutorial.model.Profile;
import mshj.tutorial.model.Role;
import mshj.tutorial.service.ProfileService;
import mshj.tutorial.util.ApplicationUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
@Controller
@Scope("session")
public class ProfilesAdministrationController implements Serializable
{
private static final long serialVersionUID = 1L;
@Autowired
private ProfileService profileService;
public ProfileService getProfileService() {
return profileService;
}
public void setProfileService(ProfileService profileService) {
this.profileService = profileService;
}
private Profile newProfile;
public Profile getNewProfile() {
return newProfile;
}
public void setNewProfile(Profile newProfile) {
this.newProfile = newProfile;
}
@PostConstruct
public void init()
{
clearNewProfileForm();
}
public void clearNewProfileForm()
{
setNewProfile(new Profile());
getNewProfile().setRoles(new ArrayList<Role>());
}
public void addProfile()
{
try
{
getProfileService().addProfile(getNewProfile());
clearNewProfileForm();
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("profileAdded"));
}
catch (Exception e)
{
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("profileNotAdded"));
}
}
public void updateProfile(Profile profile)
{
try
{
getProfileService().updateProfile(profile);
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("profileUpdated"));
}
catch (Exception e)
{
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("profileNotUpdated"));
}
}
public void deleteProfile(Profile profile)
{
try
{
getProfileService().deleteProfile(profile);
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("profileDeleted"));
}
catch (Exception e)
{
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("profileNotDeleted"));
}
}
}
.
RolesAdministrationController.java:
package mshj.tutorial.controller;
import java.io.Serializable;
import javax.annotation.PostConstruct;
import mshj.tutorial.model.Role;
import mshj.tutorial.service.RoleService;
import mshj.tutorial.util.ApplicationUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
@Controller
@Scope("session")
public class RolesAdministrationController implements Serializable
{
private static final long serialVersionUID = 1L;
@Autowired
private RoleService roleService;
public RoleService getRoleService() {
return roleService;
}
public void setRoleService(RoleService roleService) {
this.roleService = roleService;
}
private Role newRole;
public Role getNewRole() {
return newRole;
}
public void setNewRole(Role newRole) {
this.newRole = newRole;
}
@PostConstruct
public void init()
{
clearNewRoleForm();
}
public void clearNewRoleForm()
{
setNewRole(new Role());
}
public void addRole()
{
try
{
getRoleService().addRole(getNewRole());
clearNewRoleForm();
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("roleAdded"));
}
catch (Exception e)
{
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("roleNotAdded"));
}
}
public void updateRole(Role role)
{
try
{
getRoleService().updateRole(role);
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("roleUpdated"));
}
catch (Exception e)
{
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("roleNotUpdated"));
}
}
public void deleteRole(Role role)
{
try
{
getRoleService().deleteRole(role);
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("roleDeleted"));
}
catch (Exception e)
{
ApplicationUtil.addGlobalJSFMessage(ApplicationUtil.getApplicationMessage("roleNotDeleted"));
}
}
}
Comments: Each of the the three controllers above uses a dedicated Service to perform usual database operations (add, update and delete) and provides a property field to hold user data for the creation of a new entry: example private
User newUser; for the
UsersAdministrationController.java. This field is re-initialized after each new insertion.
- Under the package
mshj.tutorial.service create the following three service interfaces:
. ManageUserService.java:
package mshj.tutorial.service;
import java.util.List;
import mshj.tutorial.model.User;
public interface ManageUserService extends GenericService<User, Long>
{
List<User> findAllButConnectedUser();
void updateUser(User user);
void deleteUser(User user);
void addUser(User newUser);
}
. ProfileService.java
package mshj.tutorial.service;
import java.util.List;
import mshj.tutorial.model.Profile;
public interface ProfileService extends GenericService<Profile, Long>
{
Profile findByTitle(String title);
List<Profile> findAllProfiles();
void updateProfile(Profile profile);
void deleteProfile(Profile profile);
void addProfile(Profile profile);
}
. RoleService.java
package mshj.tutorial.service;
import java.util.List;
import mshj.tutorial.model.Role;
public interface RoleService extends GenericService<Role, Long>
{
Role findByTitle(String title);
List<Role> findAllRoles();
void updateRole(Role role);
void deleteRole(Role role);
void addRole(Role role);
}
Comments: Each of the three service contracts expose methods used by the controllers.
- Under the package
mshj.tutorial.service.impl update the following service implementation:
. ManageUserServiceImpl.java:
package mshj.tutorial.service.impl;
import java.io.Serializable;
import java.util.List;
import mshj.tutorial.model.User;
import mshj.tutorial.repository.UserRepository;
import mshj.tutorial.service.ManageUserService;
import mshj.tutorial.util.ApplicationUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly=true)
public class ManageUserServiceImpl extends GenericServiceImpl<User, Long, UserRepository> implements Serializable, ManageUserService
{
private static final long serialVersionUID = 1L;
public ManageUserServiceImpl()
{
super(User.class, UserRepository.class);
}
@Override
public List<User> findAllButConnectedUser()
{
List<User> allUsers = findAll();
allUsers.remove(ApplicationUtil.getConnectedUser());
return(allUsers);
}
@Override
@Transactional(readOnly=false)
public void updateUser(User user)
{
update(user);
}
@Override
@Transactional(readOnly=false)
public void deleteUser(User user)
{
delete(user);
}
@Override
@Transactional(readOnly=false)
public void addUser(User newUser)
{
save(newUser);
}
}
- Under the package
mshj.tutorial.service.impl create the following two service implementations:
. ProfileServiceImpl.java
package mshj.tutorial.service.impl;
import java.io.Serializable;
import java.util.List;
import mshj.tutorial.model.Profile;
import mshj.tutorial.repository.ProfileRepository;
import mshj.tutorial.service.ProfileService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly=true)
public class ProfileServiceImpl extends GenericServiceImpl<Profile, Long, ProfileRepository> implements Serializable, ProfileService
{
private static final long serialVersionUID = 1L;
public ProfileServiceImpl()
{
super(Profile.class, ProfileRepository.class);
}
public Profile findByTitle(String title)
{
return(find("from Profile where title like '" + title + "'").get(0));
}
@Override
public List<Profile> findAllProfiles()
{
return(findAll());
}
@Override
@Transactional(readOnly=false)
public void updateProfile(Profile profile)
{
update(profile);
}
@Override
@Transactional(readOnly=false)
public void deleteProfile(Profile profile)
{
delete(profile);
}
@Override
@Transactional(readOnly=false)
public void addProfile(Profile newProfile)
{
save(newProfile);
}
}
. RoleServiceImpl.java
package mshj.tutorial.service.impl;
import java.io.Serializable;
import java.util.List;
import mshj.tutorial.model.Role;
import mshj.tutorial.repository.RoleRepository;
import mshj.tutorial.service.RoleService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly=true)
public class RoleServiceImpl extends GenericServiceImpl<Role, Long, RoleRepository> implements Serializable, RoleService
{
private static final long serialVersionUID = 1L;
public RoleServiceImpl()
{
super(Role.class, RoleRepository.class);
}
public Role findByTitle(String title)
{
return(find("from Role where title like '" + title + "'").get(0));
}
@Override
public List<Role> findAllRoles()
{
return(findAll());
}
@Override
@Transactional(readOnly=false)
public void updateRole(Role role)
{
update(role);
}
@Override
@Transactional(readOnly=false)
public void deleteRole(Role role)
{
delete(role);
}
@Override
@Transactional(readOnly=false)
public void addRole(Role newRole)
{
save(newRole);
}
}
Comments: Each of the three service implementations implement their respective contract methods (reusing methods inherited from the GenericService) and declare their transactional semantics.
- Under the package
mshj.tutorial.repository create the following two repository interfaces:
. ProfileRepository.java
package mshj.tutorial.repository;
import mshj.tutorial.model.Profile;
public interface ProfileRepository extends GenericRepository<Profile, Long>
{
}
. RoleRepository.java
package mshj.tutorial.repository;
import mshj.tutorial.model.Role;
public interface RoleRepository extends GenericRepository<Role, Long>
{
}
- Under the package
mshj.tutorial.repository.impl create the following three service implementations:
. ProfileRepositoryImpl.java
package mshj.tutorial.repository.impl;
import java.io.Serializable;
import mshj.tutorial.model.Profile;
import mshj.tutorial.repository.ProfileRepository;
import org.springframework.stereotype.Repository;
@Repository
public class ProfileRepositoryImpl extends GenericRepositoryImpl<Profile, Long> implements Serializable, ProfileRepository
{
private static final long serialVersionUID = 1L;
public ProfileRepositoryImpl()
{
super(Profile.class);
}
}
. RoleRepositoryImpl.java
package mshj.tutorial.repository.impl;
import java.io.Serializable;
import mshj.tutorial.model.Role;
import mshj.tutorial.repository.RoleRepository;
import org.springframework.stereotype.Repository;
@Repository
public class RoleRepositoryImpl extends GenericRepositoryImpl<Role, Long> implements Serializable, RoleRepository
{
private static final long serialVersionUID = 1L;
public RoleRepositoryImpl()
{
super(Role.class);
}
}
Remark: Adding, Updating or Deleting Users, Profiles or Roles entries may lead to exceptions (
org.springframework.dao.DataIntegrityViolationException). This happens whenever a request breaks a constraint at the database level: primary key, unique constraints or deleting entries which are referenced by foreign key in other tables etc. Putting the
try/catch blocks in the service level (and not in the controller level as done here) would be a cleaner way to write the code but unfortunately the exceptions will not be caught at that level in all cases. This is due to the fact that these exceptions are thrown after the current transaction commits and that we put our transactional semantics in the service layer. A way to work around this would be to define the transactional semantics at the repository level.
- Under
src/main/webapp/pages/ create the following three JSF pages:
.
UsersAdministration.xhtml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:rich="http://richfaces.org/rich"
xmlns:a4j="http://richfaces.org/a4j"
>
<ui:composition template="../templates/Default.xhtml">
<ui:define name = "subtitle">
<h3>#{messages.usersAdministration}</h3>
</ui:define>
<ui:define name = "content">
<table border = "0" align = "center">
<tr>
<td align = "center">
<rich:accordion
id = "usersAdministrationAccordion"
rendered = "true"
switchType = "client"
>
<rich:accordionItem
id = "addNewUserAccordionItem"
header = "#{messages.addNewUser}"
rendered = "true"
disabled = "false"
switchType = "client"
>
<h:form id = "addNewUserAccordionItemForm">
<table border = "0" align = "center">
<tr>
<th align = "center">
<h:outputText value = "#{messages.userId}" styleClass="title-text"/>
</th>
<th align = "center">
<h:outputText value = "#{messages.userName}" styleClass="title-text"/>
</th>
<th align = "center">
<h:outputText value = "#{messages.password}" styleClass="title-text"/>
</th>
<th align = "center">
<h:outputText value = "#{messages.confPassword}" styleClass="title-text"/>
</th>
<th align = "center">
<h:outputText value = "#{messages.userIdentity}" styleClass="title-text"/>
</th>
<th align = "center">
<h:outputText value = "#{messages.userProfiles}" styleClass="title-text"/>
</th>
<th align = "center"/>
</tr>
<tr>
<td align = "center">
<h:inputText id = "newUserId" value = "#{usersAdministrationController.newUser.id}" required = "true" size = "4" maxlength = "4" />
<h:message for = "newUserId" styleClass = "error-text" />
</td>
<td align = "center">
<h:inputText id = "newUserLogin" value = "#{usersAdministrationController.newUser.login}" required = "true" size = "8" maxlength = "8" />
<h:message for = "newUserLogin" styleClass = "error-text" />
</td>
<td align = "center">
<h:inputSecret id = "newUserPassword" value = "#{usersAdministrationController.newUser.password}" required = "true" size = "8" maxlength = "8" redisplay="true" />
<h:message for = "newUserPassword" styleClass = "error-text" />
</td>
<td align = "center">
<h:inputSecret id = "newUserConfPassword" value = "#{usersAdministrationController.newUserConfPassword}" required = "true" size = "8" maxlength = "8" redisplay="true" />
<h:message for = "newUserConfPassword" styleClass = "error-text" />
</td>
<td align = "center">
<h:inputText id = "newUserIdentity" value = "#{usersAdministrationController.newUser.identity}" required = "true" size = "25" maxlength = "256" />
<h:message for = "newUserIdentity" styleClass = "error-text" />
</td>
<td align = "center">
<rich:pickList
id = "newUserProfiles"
value = "#{usersAdministrationController.newUser.profiles}"
var = "profile"
sourceCaption = "#{messages.availableProfiles}"
targetCaption = "#{messages.ownedProfiles}"
sourceListWidth = "450px"
targetListWidth = "450px"
listHeight = "100px"
orderable = "true"
rendered = "true"
disabled = "false"
required = "false"
requiredMessage = "#{messages.profileMandatory}"
addText = "→"
addAllText = "⇒"
removeText = "←"
removeAllText = "⇐"
upTopText = "⇑"
upText = "↑"
downText = "↓"
downBottomText = "⇓"
collectionType="java.util.ArrayList"
>
<!--
collectionType="java.util.ArrayList" is necessary
see http://manuel-palacio.blogspot.com/2011/02/hibernate-jsf-2-and-manytomany.html
-->
<f:selectItems value="#{profilesSelectItems.values}" />
<f:converter binding="#{profileConverter}" />
<rich:column
rendered = "true"
>
<f:facet name = "header">
<h:outputText
value = "#{messages.id}"
rendered = "true"
styleClass = "title-text"
/>
</f:facet>
<h:outputText
value = "#{profile.id}"
rendered = "true"
styleClass = "data-text"
/>
</rich:column>
<rich:column
rendered = "true"
>
<f:facet name = "header">
<h:outputText
value = "#{messages.title}"
rendered = "true"
styleClass = "title-text"
/>
</f:facet>
<h:outputText
value = "#{profile.title}"
rendered = "true"
styleClass = "data-text"
/>
</rich:column>
</rich:pickList>
<h:message for = "newUserProfiles" styleClass = "error-text" />
</td>
<td align = "center">
<h:commandButton value = "#{messages.add}" action = "#{usersAdministrationController.addUser()}" />
</td>
</tr>
</table>
</h:form>
</rich:accordionItem>
<rich:accordionItem
id = "manageExistingUsersAccordionItem"
header = "#{messages.manageExistingUsers}"
rendered="true"
disabled = "false"
switchType = "client"
>
<h:form id = "manageExistingUsersAccordionItemForm">
<h:dataTable
value="#{usersAdministrationController.manageUserService.findAllButConnectedUser()}"
var="user"
styleClass="order-table"
headerClass="order-table-header"
rowClasses="order-table-odd-row,order-table-even-row"
>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.userId}" styleClass="title-text"/>
</f:facet>
<h:outputText value = "#{user.id}" styleClass="data-text" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.userName}" styleClass="title-text"/>
</f:facet>
<h:inputText id = "userLogin" value = "#{user.login}" required = "true" size = "8" maxlength = "8" />
<h:message for = "userLogin" styleClass = "error-text" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.password}" styleClass="title-text"/>
</f:facet>
<h:inputSecret id = "userPassword" value = "#{user.password}" required = "true" size = "8" maxlength = "8" redisplay="true" />
<h:message for = "userPassword" styleClass = "error-text" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.userIdentity}" styleClass="title-text"/>
</f:facet>
<h:inputText id = "userIdentity" value = "#{user.identity}" required = "true" size = "25" maxlength = "256" />
<h:message for = "userIdentity" styleClass = "error-text" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.userProfiles}" styleClass="title-text"/>
</f:facet>
<rich:pickList
id = "userProfiles"
value = "#{user.profiles}"
var = "profile"
sourceCaption = "#{messages.availableProfiles}"
targetCaption = "#{messages.ownedProfiles}"
sourceListWidth = "450px"
targetListWidth = "450px"
listHeight = "100px"
orderable = "true"
rendered = "true"
disabled = "false"
required = "false"
requiredMessage = "#{messages.profileMandatory}"
addText = "→"
addAllText = "⇒"
removeText = "←"
removeAllText = "⇐"
upTopText = "⇑"
upText = "↑"
downText = "↓"
downBottomText = "⇓"
collectionType="java.util.ArrayList"
>
<!--
collectionType="java.util.ArrayList" is necessary
see http://manuel-palacio.blogspot.com/2011/02/hibernate-jsf-2-and-manytomany.html
-->
<f:selectItems value="#{profilesSelectItems.values}" />
<f:converter binding="#{profileConverter}" />
<rich:column
rendered = "true"
>
<f:facet name = "header">
<h:outputText
value = "#{messages.id}"
rendered = "true"
styleClass = "title-text"
/>
</f:facet>
<h:outputText
value = "#{profile.id}"
rendered = "true"
styleClass = "data-text"
/>
</rich:column>
<rich:column
rendered = "true"
>
<f:facet name = "header">
<h:outputText
value = "#{messages.title}"
rendered = "true"
styleClass = "title-text"
/>
</f:facet>
<h:outputText
value = "#{profile.title}"
rendered = "true"
styleClass = "data-text"
/>
</rich:column>
</rich:pickList>
<h:message for = "userProfiles" styleClass = "error-text" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.administration}" styleClass="title-text"/>
</f:facet>
<h:commandButton value = "#{messages.update}" action = "#{usersAdministrationController.updateUser(user)}" />
<h:commandButton value = "#{messages.delete}" action = "#{usersAdministrationController.deleteUser(user)}" />
</h:column>
</h:dataTable>
</h:form>
</rich:accordionItem>
</rich:accordion>
</td>
</tr>
</table>
</ui:define>
</ui:composition>
</html>
.
ProfilesAdministration.xhtml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:rich="http://richfaces.org/rich"
xmlns:a4j="http://richfaces.org/a4j"
>
<ui:composition template="../templates/Default.xhtml">
<ui:define name = "subtitle">
<h3>#{messages.profilesAdministration}</h3>
</ui:define>
<ui:define name = "content">
<table border = "0" align = "center">
<tr>
<td align = "center">
<rich:accordion
id = "profilesAdministrationAccordion"
rendered = "true"
switchType = "client"
>
<rich:accordionItem
id = "addNewProfileAccordionItem"
header = "#{messages.addNewProfile}"
rendered = "true"
disabled = "false"
switchType = "client"
>
<h:form id = "addNewProfileAccordionItemForm" >
<table border = "0" align = "center">
<tr>
<th align = "center">
<h:outputText value = "#{messages.profileId}" styleClass="title-text"/>
</th>
<th align = "center">
<h:outputText value = "#{messages.profileTitle}" styleClass="title-text"/>
</th>
<th align = "center">
<h:outputText value = "#{messages.profileRoles}" styleClass="title-text"/>
</th>
<th align = "center"/>
</tr>
<tr>
<td align = "center">
<h:inputText id = "newProfileId" value = "#{profilesAdministrationController.newProfile.id}" required = "true" size = "2" maxlength = "2" />
<h:message for = "newProfileId" styleClass = "error-text" />
</td>
<td align = "center">
<h:inputText id = "newProfileTitle" value = "#{profilesAdministrationController.newProfile.title}" required = "true" size = "50" maxlength = "50" />
<h:message for = "newProfileTitle" styleClass = "error-text" />
</td>
<td align = "center">
<rich:pickList
id = "newProfileRoles"
value = "#{profilesAdministrationController.newProfile.roles}"
var = "role"
sourceCaption = "#{messages.availableRoles}"
targetCaption = "#{messages.ownedRoles}"
sourceListWidth = "450px"
targetListWidth = "450px"
listHeight = "100px"
orderable = "true"
rendered = "true"
disabled = "false"
required = "false"
requiredMessage = "#{messages.roleMandatory}"
addText = "→"
addAllText = "⇒"
removeText = "←"
removeAllText = "⇐"
upTopText = "⇑"
upText = "↑"
downText = "↓"
downBottomText = "⇓"
collectionType="java.util.ArrayList"
>
<!--
collectionType="java.util.ArrayList" is necessary
see http://manuel-palacio.blogspot.com/2011/02/hibernate-jsf-2-and-manytomany.html
-->
<f:selectItems value="#{rolesSelectItems.values}" />
<f:converter binding="#{roleConverter}" />
<rich:column
rendered = "true"
>
<f:facet name = "header">
<h:outputText
value = "#{messages.id}"
rendered = "true"
styleClass = "title-text"
/>
</f:facet>
<h:outputText
value = "#{role.id}"
rendered = "true"
styleClass = "data-text"
/>
</rich:column>
<rich:column
rendered = "true"
>
<f:facet name = "header">
<h:outputText
value = "#{messages.title}"
rendered = "true"
styleClass = "title-text"
/>
</f:facet>
<h:outputText
value = "#{role.title}"
rendered = "true"
styleClass = "data-text"
/>
</rich:column>
</rich:pickList>
<h:message for = "newProfileRoles" styleClass = "error-text" />
</td>
<td align = "center">
<h:commandButton value = "#{messages.add}" action = "#{profilesAdministrationController.addProfile()}" />
</td>
</tr>
</table>
</h:form>
</rich:accordionItem>
<rich:accordionItem
id = "manageExistingProfilesAccordionItem"
header = "#{messages.manageExistingProfiles}"
rendered="true"
disabled = "false"
switchType = "client"
>
<h:form id = "manageExistingProfilesAccordionItemForm" >
<h:dataTable
value="#{profilesAdministrationController.profileService.findAllProfiles()}"
var="profile"
styleClass="order-table"
headerClass="order-table-header"
rowClasses="order-table-odd-row,order-table-even-row"
>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.profileId}" styleClass="title-text"/>
</f:facet>
<h:outputText value = "#{profile.id}" styleClass="data-text" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.profileTitle}" styleClass="title-text"/>
</f:facet>
<h:inputText id = "profileTitle" value = "#{profile.title}" required = "true" size = "50" maxlength = "50" />
<h:message for = "profileTitle" styleClass = "error-text" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.profileRoles}" styleClass="title-text"/>
</f:facet>
<rich:pickList
id = "profileRoles"
value = "#{profile.roles}"
var = "role"
sourceCaption = "#{messages.availableRoles}"
targetCaption = "#{messages.ownedRoles}"
sourceListWidth = "450px"
targetListWidth = "450px"
listHeight = "100px"
orderable = "true"
rendered = "true"
disabled = "false"
required = "false"
requiredMessage = "#{messages.roleMandatory}"
addText = "→"
addAllText = "⇒"
removeText = "←"
removeAllText = "⇐"
upTopText = "⇑"
upText = "↑"
downText = "↓"
downBottomText = "⇓"
collectionType="java.util.ArrayList"
>
<!--
collectionType="java.util.ArrayList" is necessary
see http://manuel-palacio.blogspot.com/2011/02/hibernate-jsf-2-and-manytomany.html
-->
<f:selectItems value="#{rolesSelectItems.values}" />
<f:converter binding="#{roleConverter}" />
<rich:column
rendered = "true"
>
<f:facet name = "header">
<h:outputText
value = "#{messages.id}"
rendered = "true"
styleClass = "title-text"
/>
</f:facet>
<h:outputText
value = "#{role.id}"
rendered = "true"
styleClass = "data-text"
/>
</rich:column>
<rich:column
rendered = "true"
>
<f:facet name = "header">
<h:outputText
value = "#{messages.title}"
rendered = "true"
styleClass = "title-text"
/>
</f:facet>
<h:outputText
value = "#{role.title}"
rendered = "true"
styleClass = "data-text"
/>
</rich:column>
</rich:pickList>
<h:message for = "profileRoles" styleClass = "error-text" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.administration}" styleClass="title-text"/>
</f:facet>
<h:commandButton value = "#{messages.update}" action = "#{profilesAdministrationController.updateProfile(profile)}" />
<h:commandButton value = "#{messages.delete}" action = "#{profilesAdministrationController.deleteProfile(profile)}" />
</h:column>
</h:dataTable>
</h:form>
</rich:accordionItem>
</rich:accordion>
</td>
</tr>
</table>
</ui:define>
</ui:composition>
</html>
.
RolesAdministration.xhtml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:rich="http://richfaces.org/rich"
xmlns:a4j="http://richfaces.org/a4j"
>
<ui:composition template="../templates/Default.xhtml">
<ui:define name = "subtitle">
<h3>#{messages.rolesAdministration}</h3>
</ui:define>
<ui:define name = "content">
<table border = "0" align = "center">
<tr>
<td align = "center">
<rich:accordion
id = "rolesAdministrationAccordion"
rendered = "true"
switchType = "client"
>
<rich:accordionItem
id = "addNewRoleAccordionItem"
header = "#{messages.addNewRole}"
rendered = "true"
disabled = "false"
switchType = "client"
>
<h:form id = "addNewRoleAccordionItemForm" >
<table border = "0" align = "center">
<tr>
<th align = "center">
<h:outputText value = "#{messages.roleId}" styleClass="title-text"/>
</th>
<th align = "center">
<h:outputText value = "#{messages.roleTitle}" styleClass="title-text"/>
</th>
<th align = "center"/>
</tr>
<tr>
<td align = "center">
<h:inputText id = "newRoleId" value = "#{rolesAdministrationController.newRole.id}" required = "true" size = "2" maxlength = "2" />
<h:message for = "newRoleId" styleClass = "error-text" />
</td>
<td align = "center">
<h:inputText id = "newRoleTitle" value = "#{rolesAdministrationController.newRole.title}" required = "true" size = "50" maxlength = "50" />
<h:message for = "newRoleTitle" styleClass = "error-text" />
</td>
<td align = "center">
<h:commandButton value = "#{messages.add}" action = "#{rolesAdministrationController.addRole()}" />
</td>
</tr>
</table>
</h:form>
</rich:accordionItem>
<rich:accordionItem
id = "manageExistingRolesAccordionItem"
header = "#{messages.manageExistingRoles}"
rendered="true"
disabled = "false"
switchType = "client"
>
<h:form id = "manageExistingRolesAccordionItemForm" >
<h:dataTable
value="#{rolesAdministrationController.roleService.findAllRoles()}"
var="role"
styleClass="order-table"
headerClass="order-table-header"
rowClasses="order-table-odd-row,order-table-even-row"
>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.roleId}" styleClass="title-text"/>
</f:facet>
<h:outputText value = "#{role.id}" styleClass="data-text" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.roleTitle}" styleClass="title-text"/>
</f:facet>
<h:inputText id = "roleTitle" value = "#{role.title}" required = "true" size = "50" maxlength = "50" />
<h:message for = "roleTitle" styleClass = "error-text" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value = "#{messages.administration}" styleClass="title-text"/>
</f:facet>
<h:commandButton value = "#{messages.update}" action = "#{rolesAdministrationController.updateRole(role)}" />
<h:commandButton value = "#{messages.delete}" action = "#{rolesAdministrationController.deleteRole(role)}" />
</h:column>
</h:dataTable>
</h:form>
</rich:accordionItem>
</rich:accordion>
</td>
</tr>
</table>
</ui:define>
</ui:composition>
</html>
In the following we describe the different JSF components used in the three pages. To fix ideas we provide explanations only for the UsersAdministration.xhtml page as the other pages expose more or less the sames features.
b - RichFaces Accordion:
The
<rich:accordion /> contains a set of panels (
<rich:accordionItems />) where one panel is expanded, while the other ones are collapsed. When a collapsed panel's header is clicked, the panel is expanded and the previously expanded one is in turn collapsed (i.e. at any instant only one panel is expanded).
In the
UsersAdministration.xhtml we define such a component with two panels: one that holds the adding a new user feature, the second for managing existing users.
Read more about <rich:accordion /> ...
Online demo.
c - JSF DataTable:
The
<h:dataTable /> component is meant for rendering tables. Lines of the table are based on a underlying collection of objects and its columns can be defined using
<h:column /> tag and are possibly depending on the underlying object's fields.
In the
UsersAdministration.xhtml we use such a component (
Lines 157-267) to display the list of all users. We discuss two important attributes of the
<h:dataTable /> component:
-
value: specifies the underlying collection of objects that will be displayed. Example: in
Line 158, we define this value as the List of User objects returned by the
ManageUserService's
findAllButConnectedUser() method.
-
var: specifies an alias for an iterator over the underlying collection of objects. This alias can then be called inside
<h:column /> to get for example a specific field of an object in the underlying collection.
In
Lines 164-169 we declare the first column to be displayed by the data table. For the
<h:column /> tag a header facet (the title of the column) can be defined: in our case using a
<h:outputText /> tag in
Line 166. Then the content of the column itself is specified, again a
<h:outputText /> in
Line 168. Note how the content of the latter
<h:outputText /> is using the alias:
value = "#{user.id}", meaning here that the
<h:outputText /> will display the
id field of the current
User object.
Read more about <h:dataTable /> ...
d - RichFaces Pick Lists:
<rich: pickList /> is a component permitting to handle selecting multiple options from a list of available ones. Indeed it displays two lists: the available options and the selected ones. Using dedicated buttons (copy, copyAll, remove, removeAll) the user can move elements from one list to the other thus selecting or deselecting options.
In the UsersAdministration.xhtml we use pick lists to handle the profiles owned by a user: when adding a new one (Lines 79-141) and when managing an existing one (Lines 195-257). For the latter this means that we will display as many pick lists as users displayed by the data table.
We discuss here some of the interesting attributes and children of the pick list (on the basis of the pick list in Lines 79-141):
Attributes:
- value: tells JSF where to store the selected options of the pick list. In our case we set this attribute to #{usersAdministrationController.newUser.profiles} since we want to collect the selected profiles for the user to be created in the profiles field of the newUser field of the controller.
- var: defines an alias that will be used to refer to an item in the pick list's both available or selected options lists.
- sourceCaption: defines the title of the available options list.
- targetCaption: defines the title of the selected options list.
- requiredMessage: defines the validation message that is displayed to the user if the required
attribute is set to
true and if the user selects no item in the pick list.
-
other attributes: do what their names say they do :) Ranging from styling of the pick list to providing titles of the different buttons (copy, remove, copyAll, removeAll ...)
-
collectionType: In some settings (included ours) this attribute is vital ! It says to JSF the type of collection used for the available and selected options lists. If you do not specify this attribute with the correct concrete (no interfaces) type (in our case it is
java.list.ArrayList and certainly not
java.list.List) you'll have the following exception raised:
org.hibernate.LazyInitializationException: could not initialize proxy - no Session
It seems this is an old
RichFaces bug which has been fixed in the
RichFaces 4.3.0.CR1 release and that curiously reappeared in newer releases !
Children Elements:
-
<f:selectItems />: tells JSF how to populate the list of available options for the pick list. The
value attribute of this element should refer to a collection (in our case
java.util.ArrayList) of
javax.faces.model.SelectItem. In the first pick list of
UsersAdministration.xhtml (
Line 107) we use a dedicated controller (
ProfilesSelectItems) to provide the list of all available profiles through its
values field.
In fact two such controllers are needed for the super user admin features, the one for providing available profiles used here and another one for providing available roles used in the
ProfilesAdministration.xhtml page. We provide their implementations below:
- Under the package
mshj.tutorial.controller.si create the two following controllers:
. ProfileSelectItems.java:
package mshj.tutorial.controller.si;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.faces.model.SelectItem;
import mshj.tutorial.model.Profile;
import mshj.tutorial.service.ProfileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
@Controller
@Scope("session")
public class ProfilesSelectItems implements Serializable
{
private static final long serialVersionUID = 1L;
@Autowired
private ProfileService profileService;
public ProfileService getProfileService() {
return profileService;
}
public void setProfileService(ProfileService profileService) {
this.profileService = profileService;
}
private List<SelectItem> values;
public List<SelectItem> getValues() {
return values;
}
public void setValues(List<SelectItem> values) {
this.values = values;
}
@PostConstruct
public void init()
{
setValues(new ArrayList<SelectItem>());
for (Profile p : getProfileService().findAll())
{
getValues().add(new SelectItem(p,p.getTitle()));
}
}
}
. RolesSelectItems.java
package mshj.tutorial.controller.si;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.faces.model.SelectItem;
import mshj.tutorial.model.Role;
import mshj.tutorial.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
@Controller
@Scope("session")
public class RolesSelectItems implements Serializable
{
private static final long serialVersionUID = 1L;
@Autowired
private RoleService roleService;
public RoleService getRoleService() {
return roleService;
}
public void setRoleService(RoleService roleService) {
this.roleService = roleService;
}
private List<SelectItem> values;
public List<SelectItem> getValues() {
return values;
}
public void setValues(List<SelectItem> values) {
this.values = values;
}
@PostConstruct
public void init()
{
setValues(new ArrayList<SelectItem>());
for (Role r : getRoleService().findAll())
{
getValues().add(new SelectItem(r,r.getTitle()));
}
}
}
Comments: Note that both controllers provide a list of select items (
javax.faces.model.SelectItem instances) which will be used in the
<f:selectItems /> bound to rich pick lists in the corresponding pages. A
javax.faces.model.SelectItem is simply constructed by providing two arguments: the value and its label.
-
<f:converter />: tells JSF how to convert labels to their values. This is needed to correctly fill in the
target value of a rich pick list. The
binding attribute refers to a bean implementing
javax.faces.convert.Converter.
We need two converters one for converting profiles and another one for converting roles.
e - JSF Converters:
JSF comes with a set of built-in converters. An example is
LongConverter. It is called whenever, let's say, a
<h: inputText /> is bound to a field of type
Long (or
long) in the controller.
Why ? Simply because at the
html level only
String inputs are supported. The
LongConverter is then needed to convert the
String value of the input to a
Long (or
long) one which matches the controller's expectations. By the way if the conversion is not possible (user typed alphanumerical value) the validation phase (which starts after the conversion phase) will prompt the validation message
javax.faces.converter.LongConverter.LONG.
We discuss here Custom JSF Converters. So why they are also needed for our case ?
In fact at the
html level select options support only mapping
String labels to
String values.
So what JSF will get as list of selected options will be only the list of their labels and not the list of their values. The task of the JSF converter is to restore complete objects given their labels.
- Under the package mshj.tutorial.converter create the following two converters:
. ProfileConverter.java:
package mshj.tutorial.converter;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import mshj.tutorial.model.Profile;
import mshj.tutorial.service.ProfileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("session")
public class ProfileConverter implements Converter
{
@Autowired
private ProfileService profileService;
public ProfileService getProfileService() {
return profileService;
}
public void setProfileService(ProfileService profileService) {
this.profileService = profileService;
}
@Override
public Object getAsObject(FacesContext arg0, UIComponent arg1, String arg2)
{
return(getProfileService().findByTitle(arg2));
}
@Override
public String getAsString(FacesContext arg0, UIComponent arg1, Object arg2)
{
return (((Profile)arg2).getTitle());
}
}
. RoleConverter.java
package mshj.tutorial.converter;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import mshj.tutorial.model.Role;
import mshj.tutorial.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("session")
public class RoleConverter implements Converter
{
@Autowired
private RoleService roleService;
public RoleService getRoleService() {
return roleService;
}
public void setRoleService(RoleService roleService) {
this.roleService = roleService;
}
@Override
public Object getAsObject(FacesContext arg0, UIComponent arg1, String arg2)
{
return(getRoleService().findByTitle(arg2));
}
@Override
public String getAsString(FacesContext arg0, UIComponent arg1, Object arg2)
{
return (((Role)arg2).getTitle());
}
}
Comments: A Faces Converter implements the
javax.faces.convert.Converter interface thus it should implement two methods:
Object getAsObject(FacesContext arg0, UIComponent arg1, String arg2) and
String getAsString(FacesContext arg0, UIComponent arg1, String arg2) which respectively return the object giving its label and the label given the object.
Since we used the
title field (for both profile and role objects) as label (see
ProfileSelectItems.java and
RoleSelectItems.java above), we rely in implementing the
getAsObject methods on services that provide the object given its title.
What should be noticed here is that the services are auto-wired to the converter and that the converter is annotated as a Spring component (
@Component).
- Still some configuration is needed to have our pick lists working, JSF should be able to compare objects appearing in our pick lists. For both Role and Profile model classes we have to define
boolean equals(Object o) and
int hashCode() methods, as follows:
. Edit
mshj.tutorial.model.Profile and add the following methods:
@Override
public boolean equals(Object o)
{
if (o instanceof Profile)
{
return(((Profile)o).getId() == getId());
}
else
{
return(false);
}
}
@Override
public int hashCode()
{
return(new Long(getId()).intValue());
}
. Edit
mshj.tutorial.model.Role and add the following methods:
@Override
public boolean equals(Object o)
{
if (o instanceof Role)
{
return(((Role)o).getId() == getId());
}
else
{
return(false);
}
}
@Override
public int hashCode()
{
return(new Long(getId()).intValue());
}
f - JSF Navigation:
Now that we have four pages in our application it is time to design a menu bar to navigate from one page to another. To have the menu bar available to all pages we only have to add it once to the template
Default.xhtml.
- Edit the
src/main/webapp/templates/Default.xhtml as follows:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:rich="http://richfaces.org/rich"
xmlns:a4j="http://richfaces.org/a4j"
>
<h:head>
<link type="text/css" rel="stylesheet" href="../resources/css/myCSS.css" />
</h:head>
<body>
<div id = "header_div">
<div id = "banner_div" align = "center">
<ui:insert name = "title">
<h:graphicImage value="../resources/images/logo.png" />
</ui:insert>
</div>
<div id = "subtitle_div" align = "center">
<ui:insert name = "subtitle"/>
</div>
<div id = "status_div" align = "center">
<ui:insert name = "status" />
</div>
</div>
<div id = "menu_div" align = "center">
<ui:insert name = "menu">
<h:form id = "menuForm">
<table border = "0" align = "center">
<tr>
<td align = "center">
<rich:menuItem action = "PasswordUpdate" >
<h:outputText value = "#{messages.updatePassword}" styleClass = "big-title-text-with-bg" />
</rich:menuItem>
</td>
<td align = "center">
<rich:menuItem action = "UsersAdministration" >
<h:outputText value = "#{messages.usersAdministration}" styleClass = "big-title-text-with-bg" />
</rich:menuItem>
</td>
<td align = "center">
<rich:menuItem action = "ProfilesAdministration" >
<h:outputText value = "#{messages.profilesAdministration}" styleClass = "big-title-text-with-bg" />
</rich:menuItem>
</td>
<td align = "center">
<rich:menuItem action = "RolesAdministration" >
<h:outputText value = "#{messages.rolesAdministration}" styleClass = "big-title-text-with-bg" />
</rich:menuItem>
</td>
<td align = "center">
<rich:menuItem action = "#{loginController.doLogout()}" >
<h:graphicImage value="../resources/images/logout.png" width = "64" height = "64" />
</rich:menuItem>
</td>
</tr>
</table>
</h:form>
</ui:insert>
</div>
<div id = "messages_div" align = "center">
<ui:insert name = "messages">
<table border = "0" align = "center">
<tr>
<td align = "center">
<h:messages globalOnly="true" styleClass="error-text" />
</td>
</tr>
</table>
</ui:insert>
</div>
<div id = "content_div" align = "center">
<ui:insert name = "content"/>
</div>
<div id = "footer_div" align = "center">
<ui:insert name = "footer">
<h:graphicImage value="../resources/images/footer.png" />
</ui:insert>
</div>
</body>
</html>
We added in the
menu insertion different entries permitting to navigate to the different application pages. To define a menu entry we use the
<rich:menuItem /> tag. Each
<rich:menuItem /> has an
action attribute which indicates the target of the entry (the page relative path for example) and encapsulates a JSF component which represents the element to be displayed for that menu entry (in our case simple
<h:outputText />'s).
Since we do not want the
Connection.xhtml to display the menu bar we will have to override its menu insertion by an empty one:
- Edit
src/main/webapp/pages/Connection.xhtml as follows:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:rich="http://richfaces.org/rich"
xmlns:a4j="http://richfaces.org/a4j"
>
<ui:composition template="../templates/Default.xhtml">
<ui:define name = "subtitle">
<h3>#{messages.login}</h3>
</ui:define>
<ui:define name = "menu" />
<ui:define name = "content">
<h:form id = "loginForm">
<table align = "center" border = "0">
<tr>
<th align = "center">
<h:outputText value = "#{messages.userName}" styleClass="title-text" />
</th>
<td align = "center">
<h:inputText
id = "j_username"
value = "#{loginController.j_username}"
label = "J_username"
size = "8"
maxlength = "8"
required = "true"
/>
</td>
<td align = "center">
<h:message for = "j_username" styleClass="error-text" />
</td>
</tr>
<tr>
<th align = "center">
<h:outputText value = "#{messages.password}" styleClass="title-text" />
</th>
<td align = "center">
<h:inputSecret
id = "j_password"
value = "#{loginController.j_password}"
label = "J_password"
size = "8"
maxlength = "8"
required = "true"
/>
</td>
<td align = "center">
<h:message for = "j_password" styleClass="error-text" />
</td>
</tr>
<tr>
<td align = "center" colspan = "3">
<h:commandButton
value = "#{messages.login}"
action = "#{loginController.doLogin()}"
>
<f:phaseListener type="mshj.tutorial.listener.LoginErrorPhaseListener" />
</h:commandButton>
</td>
</tr>
</table>
</h:form>
</ui:define>
</ui:composition>
</html>
Line 18 : We overrode the
menu insertion (by an empty insertion).
Note that in the
Default.xhtml page the
<rich:menuItem /> action attributes are simple
String values. Do JSF know which page to navigate to given a String value ? Yes if we carefully choose the string returned values of action methods: if an action methods returns the name of page without the extension the user will be redirected to that page after the action method is executed. See
here for more details about the so-called implicit navigation. Or if we configure JSF well (explicit navigation), see below:
- Edit
src/main/webapp/WEB-INF/faces-config.xml as follows:
<?xml version="1.0"?>
<faces-config
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
version="2.0">
<!-- Resource Bundles (Messages and JSF Messages) -->
<application>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>fr_FR</supported-locale>
</locale-config>
<resource-bundle>
<base-name>messages</base-name>
<var>messages</var>
</resource-bundle>
<message-bundle>jsfMessages</message-bundle>
</application>
<!-- JSF and Spring are integrated -->
<application>
<el-resolver>
org.springframework.web.jsf.el.SpringBeanFacesELResolver
</el-resolver>
</application>
<navigation-rule>
<from-view-id>*</from-view-id>
<navigation-case>
<from-outcome>PasswordUpdate</from-outcome>
<to-view-id>/pages/PasswordUpdate.xhtml</to-view-id>
<redirect />
</navigation-case>
<navigation-case>
<from-outcome>UsersAdministration</from-outcome>
<to-view-id>/pages/UsersAdministration.xhtml</to-view-id>
<redirect />
</navigation-case>
<navigation-case>
<from-outcome>ProfilesAdministration</from-outcome>
<to-view-id>/pages/ProfilesAdministration.xhtml</to-view-id>
<redirect />
</navigation-case>
<navigation-case>
<from-outcome>RolesAdministration</from-outcome>
<to-view-id>/pages/RolesAdministration.xhtml</to-view-id>
<redirect />
</navigation-case>
</navigation-rule>
</faces-config>
In
Lines 29-51 we define a navigation rule (one can define many). It says that from any page (or view) (
Line 30) several navigation cases are possible. A navigation case (example
Lines 31-35) takes a
from-outcome which specifies a String value associated to a
to-view-id which specifies a page.
Given this configuration, each time JSF will encounter a String value in an
action attribute (not only associated to
<rich:menuItem /> but also to
<h:commandLink /> or
<h:commandButton />), it will check if a corresponding navigation case is concerned and if yes it will trigger a redirection to the corresponding page.
3.5 - Spring Security Access Control:
We discuss now how to control the access to the application pages by checking whether a user owns the required roles.
First let's create some additional users and grant them some privileges:
- Execute the following script:
. MySQL:
connect test/test;
USE `mshj` ;
insert into Roles values(0,'ROLE_DUMMY_0');
insert into Roles values(1,'ROLE_DUMMY_1');
insert into Roles values(2,'ROLE_DUMMY_2');
insert into Roles values(3,'ROLE_DUMMY_3');
insert into Roles values(10,'ROLE_UPDATE_OWN_PASSWORD');
insert into Roles values(21,'ROLE_ADMIN_USERS');
insert into Roles values(22,'ROLE_ADMIN_PROFILES');
insert into Roles values(23,'ROLE_ADMIN_ROLES');
insert into Profiles values(1,'DUMMY_PROFILE_1');
insert into Profiles values(2,'DUMMY_PROFILE_2');
insert into Profiles values(3,'DUMMY_PROFILE_3');
insert into Profiles values(10,'PROFILE_USER');
insert into Profiles values(20,'PROFILE_ADMIN');
insert into Users values(1,'user1','user1','User One');
insert into Users values(2,'user2','user2','User Two');
insert into Users values(20, 'admin','admin','Administrator');
insert into Profiles_Roles values(1,0);
insert into Profiles_Roles values(1,1);
insert into Profiles_Roles values(2,0);
insert into Profiles_Roles values(2,2);
insert into Profiles_Roles values(3,0);
insert into Profiles_Roles values(3,3);
insert into Profiles_Roles values(10,10);
insert into Profiles_Roles values(20,10);
insert into Profiles_Roles values(20,21);
insert into Profiles_Roles values(20,22);
insert into Profiles_Roles values(20,23);
insert into Users_Profiles values(1,1);
insert into Users_Profiles values(2,2);
insert into Users_Profiles values(0,10);
insert into Users_Profiles values(20,20);
commit;
. Oracle:
connect test/test;
insert into Roles values(0,'ROLE_DUMMY_0');
insert into Roles values(1,'ROLE_DUMMY_1');
insert into Roles values(2,'ROLE_DUMMY_2');
insert into Roles values(3,'ROLE_DUMMY_3');
insert into Roles values(10,'ROLE_UPDATE_OWN_PASSWORD');
insert into Roles values(21,'ROLE_ADMIN_USERS');
insert into Roles values(22,'ROLE_ADMIN_PROFILES');
insert into Roles values(23,'ROLE_ADMIN_ROLES');
insert into Profiles values(1,'DUMMY_PROFILE_1');
insert into Profiles values(2,'DUMMY_PROFILE_2');
insert into Profiles values(3,'DUMMY_PROFILE_3');
insert into Profiles values(10,'PROFILE_USER');
insert into Profiles values(20,'PROFILE_ADMIN');
insert into Users values(1,'user1','user1','User One');
insert into Users values(2,'user2','user2','User Two');
insert into Users values(20, 'admin','admin','Administrator');
insert into Profiles_Roles values(1,0);
insert into Profiles_Roles values(1,1);
insert into Profiles_Roles values(2,0);
insert into Profiles_Roles values(2,2);
insert into Profiles_Roles values(3,0);
insert into Profiles_Roles values(3,3);
insert into Profiles_Roles values(10,10);
insert into Profiles_Roles values(20,10);
insert into Profiles_Roles values(20,21);
insert into Profiles_Roles values(20,22);
insert into Profiles_Roles values(20,23);
insert into Users_Profiles values(1,1);
insert into Users_Profiles values(2,2);
insert into Users_Profiles values(0,10);
insert into Users_Profiles values(20,20);
commit;
- Now edit
src/main/webapp/WEB-INF/applicationContext-security.xml and between the two comment lines:
<!-- Begin Handling of Security Permission per Page -->
<!-- End Handling of Security Permission per Page -->
add the following lines:
<security:intercept-url pattern="/pages/PasswordUpdate.xhtml" access="ROLE_UPDATE_OWN_PASSWORD" />
<security:intercept-url pattern="/pages/UsersAdministration.xhtml" access="ROLE_ADMIN_USERS" />
<security:intercept-url pattern="/pages/ProfilesAdministration.xhtml" access="ROLE_ADMIN_PROFILES" />
<security:intercept-url pattern="/pages/RolesAdministration.xhtml" access="ROLE_ADMIN_ROLES" />
Every
<security:intercept-url /> specifies here that every page (or pattern of pages) defined by the
pattern attribute should only be accessed if the connected user has the roles (or the set of roles) defined in the attribute
access.
- Finally let's update
MyAuthenticationSuccessHandler to take into account the new pages and access policy:
package mshj.tutorial.handler;
import java.io.IOException;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler
{
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException
{
Set<String> roles = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
if (roles.contains("ROLE_ADMIN_USERS"))
{
response.sendRedirect("UsersAdministration.xhtml");
return;
}
if (roles.contains("ROLE_ADMIN_PROFILES"))
{
response.sendRedirect("UsersAdministration.xhtml");
return;
}
if (roles.contains("ROLE_ADMIN_ROLES"))
{
response.sendRedirect("UsersAdministration.xhtml");
return;
}
if (roles.contains("ROLE_UPDATE_OWN_PASSWORD"))
{
response.sendRedirect("PasswordUpdate.xhtml");
return;
}
if (roles.isEmpty() || (roles.contains("ROLE_DUMMY_0")) || (roles.contains("ROLE_DUMMY_1")) || (roles.contains("ROLE_DUMMY_2")))
{
response.sendRedirect("../index.jsp");
return;
}
roles = null;
}
}
Now you can compile and deploy the application and test the freshly created new users access rights.
If you are not allowed to access a page and try to display it you'll see the
AccessDenied.xhtml page.
Here we reach the end of this tutorial which, i hope, was useful for you. I can point you to some perspective work which you can take as an exercise:
The menu bar displays all the application pages even the ones to which the currently connected user has not access. Modify the code such that only the menu entries that will not lead to a denial of access are displayed. (Tip: use the set of roles owned by the connected user to condition the visibility of the
<rich:menuItem />'s).
Below the links to download the final Eclipse projects. After importing the project you'll probably have to fix some properties to match your local settings (such as the JDK and the Apache Tomcat Server).
If you need to quickly rename the project and its packages to your custom values take a look at this tutorial.