What You Will Build
Extend the Week 9 Product Manager app with proper MVC layering, navigation rules, custom data validation, and session-based security.
This lab builds directly on the Week 9 application. By the end, the app will have:
LO 1 MVC Refactor
- Extract a
ProductServicelayer โ separating business logic from the Managed Bean - Bean becomes Controller; Service owns the Model operations
LO 2 & 3 Navigation
- Explicit navigation rules in
faces-config.xml - Live comparison of forward vs
?faces-redirect=true - Login/logout flow using outcome strings
LO 4 Converters & Validators
- Custom Converter: enforces
PRD-####product code format - Custom Validator: rejects duplicate product names
- Both wired into the existing Add Product form
LO 5 HTTP Filter
- Login page with hardcoded credentials
AuthFilterprotects/products.xhtmland/addProduct.xhtml- Unauthenticated users are redirected to
/login.xhtml
- The Week 9 lab project (
Week9Lab) is complete and deploys successfully on GlassFish 7 - You understand
@Named,@SessionScoped,h:form,h:dataTable, andf:validate* - No database is required โ data is still held in memory
Week10Lab. All work in this lab goes into the new copy, leaving Week 9 intact.
Preparing the Project
Copy the Week 9 project, verify it still runs, then review the new files you will create.
Copy and Open the Project
In the NetBeans Projects panel, right-click Week9Lab โ
In the dialog, set Project Name:Week10Lab and click Copy.
Open pom.xml in the new project. Change the <artifactId> and <name> (if present) to Week10Lab. Save.
Right-click Week10Lab โ Clean and Build, then Run. Confirm the Product Manager app loads at http://localhost:8080/Week10Lab/ before making any changes.
New Files You Will Create in This Lab
java/com/university/week9/
bean/
ProductBean.java
LoginBean.java
model/
Product.java (unchanged)
service/ProductService.java
converter/ProductCodeConverter.java
validator/UniqueNameValidator.java
filter/AuthFilter.java
webapp/
WEB-INF/
web.xml (unchanged)
beans.xml (unchanged)
faces-config.xml
templates/layout.xhtml (unchanged)
index.xhtml (unchanged)
products.xhtml
addProduct.xhtml
login.xhtml
Refactor to a Proper MVC Layer Structure
Currently, ProductBean acts as both Controller and Model โ it holds the list AND manipulates it. We will extract a ProductService (Model layer) so the Bean becomes a pure Controller.
MVC's rule: the Controller (Managed Bean) handles requests and navigation. The Model (Service + entities) owns data and business logic. If you later replace the web interface with a REST API, the Service works unchanged โ because it knows nothing about HTTP or Jakarta Faces.
Create ProductService.java
Right-click Source Packages โ New โ Java Class
Class Name: ProductService Package: com.university.week9.service
package com.university.week9.service;
import com.university.week9.model.Product;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* โโ MODEL LAYER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
* ProductService owns all product data and business operations.
*
* @ApplicationScoped: ONE instance for the entire application lifetime.
* This means ALL users share the same product list โ correct for a shared
* product catalogue. (Compare: @SessionScoped would give each user their own
* separate list โ wrong for shared data.)
*
* This class knows NOTHING about Jakarta Faces, HTTP, or web pages.
* That separation is the point of MVC.
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
*/
@Named
@ApplicationScoped
public class ProductService implements Serializable {
private final List<Product> products = new ArrayList<>();
private int nextId = 6; // starts after the 5 sample products
// โโ Sample data loaded once when the application starts โโ
public ProductService() {
products.add(new Product(1, "PRD-0001", "Laptop Pro 15", "Electronics", 1299.99, 15));
products.add(new Product(2, "PRD-0002", "Office Chair", "Furniture", 349.00, 8));
products.add(new Product(3, "PRD-0003", "USB-C Hub", "Electronics", 49.95, 42));
products.add(new Product(4, "PRD-0004", "Standing Desk", "Furniture", 699.00, 5));
products.add(new Product(5, "PRD-0005", "Wireless Mouse", "Electronics", 35.99, 30));
}
// โโ READ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
public List<Product> findAll() {
return new ArrayList<>(products); // defensive copy
}
public boolean nameExists(String name) {
return products.stream()
.anyMatch(p -> p.getName()
.equalsIgnoreCase(name.trim()));
}
// โโ WRITE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
public void addProduct(String code, String name,
String category, double price, int qty) {
products.add(new Product(nextId++, code, name, category, price, qty));
}
public boolean deleteById(int id) {
return products.removeIf(p -> p.getId() == id);
}
}
The service now stores a product code. Open Product.java and add a code field with getter and setter, then update the constructor to accept code as the second parameter (after id):
// Add this field alongside the existing ones:
private String code;
// Replace the existing constructor with this one:
public Product(int id, String code, String name,
String category, double price, int quantity) {
this.id = id;
this.code = code;
this.name = name;
this.category = category;
this.price = price;
this.quantity = quantity;
}
// Add getter and setter:
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
Rewrite ProductBean.java as a Pure Controller
The Managed Bean now uses the service instead of owning the data. Replace the entire contents of ProductBean.java:
package com.university.week9.bean;
import com.university.week9.model.Product;
import com.university.week9.service.ProductService;
import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
/**
* โโ CONTROLLER LAYER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
* ProductBean is now a pure controller. It no longer stores the product list
* or contains business logic. Its only jobs are:
* 1. Hold the form field values entered by the user
* 2. Delegate save/delete operations to ProductService
* 3. Return navigation outcomes
*
* @Inject asks CDI to supply the single @ApplicationScoped ProductService.
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
*/
@Named
@SessionScoped
public class ProductBean implements Serializable {
// CDI injects the shared ProductService automatically
@Inject
private ProductService productService;
// โโ Form fields for "Add Product" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
private String newCode = "";
private String newName = "";
private String newCategory = "";
private double newPrice = 0.0;
private int newQuantity = 0;
private String successMessage = "";
// โโ Delegate read to service โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
public List<Product> getProducts() {
return productService.findAll();
}
// โโ Action method: called by h:commandButton in addProduct.xhtml โโโโโโโโโโ
public String addProduct() {
productService.addProduct(newCode, newName, newCategory,
newPrice, newQuantity);
successMessage = "Product \"" + newName + "\" added successfully!";
resetForm();
// "success" is mapped to products.xhtml via faces-config.xml (Task 2)
return "success";
}
public String deleteProduct(int id) {
productService.deleteById(id);
successMessage = "";
return "success";
}
private void resetForm() {
newCode = ""; newName = ""; newCategory = "";
newPrice = 0.0; newQuantity = 0;
}
// โโ Expose service to Facelets for the validator (Task 4) โโโโโโโโโโโโโโโโโ
public ProductService getProductService() { return productService; }
// โโ Getters & Setters for form fields โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
public String getNewCode() { return newCode; }
public void setNewCode(String c) { this.newCode = c; }
public String getNewName() { return newName; }
public void setNewName(String n) { this.newName = n; }
public String getNewCategory() { return newCategory; }
public void setNewCategory(String c) { this.newCategory = c; }
public double getNewPrice() { return newPrice; }
public void setNewPrice(double p) { this.newPrice = p; }
public int getNewQuantity() { return newQuantity; }
public void setNewQuantity(int q) { this.newQuantity = q; }
public String getSuccessMessage() { return successMessage; }
public void setSuccessMessage(String m) { this.successMessage = m; }
}
Run Clean and Build. If there are no red errors in the Output panel, your MVC refactor is structurally correct. The app may not fully run yet โ the products.xhtml table needs its Code column added, which happens in Task 3. Continue to Task 2 now.
Explicit Navigation with faces-config.xml
Map outcome strings like "success" and "loginRequired" to specific pages using a central navigation configuration file, then observe the difference between forward and redirect.
Create faces-config.xml
Right-click WEB-INF โ New โ Other โ JavaServer Faces โ JSF Faces Configuration.
Name it faces-config. Replace all generated content with:
<?xml version="1.0" encoding="UTF-8"?>
<faces-config
xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_0.xsd"
version="4.0">
<!--
NAVIGATION RULE 1: addProduct.xhtml
When addProduct() returns "success" โ go to products.xhtml (with redirect).
When it returns "failure" โ stay on addProduct.xhtml.
-->
<navigation-rule>
<from-view-id>/addProduct.xhtml</from-view-id>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/products.xhtml</to-view-id>
<redirect/> <!-- <redirect/> = same as ?faces-redirect=true -->
</navigation-case>
<navigation-case>
<from-outcome>failure</from-outcome>
<to-view-id>/addProduct.xhtml</to-view-id>
<!-- No <redirect/> here = server-side forward, stays on same page -->
</navigation-case>
</navigation-rule>
<!--
NAVIGATION RULE 2: login.xhtml
"loginSuccess" โ home page (redirect so URL bar updates).
"loginFailed" โ stay on login.xhtml to show error.
-->
<navigation-rule>
<from-view-id>/login.xhtml</from-view-id>
<navigation-case>
<from-outcome>loginSuccess</from-outcome>
<to-view-id>/index.xhtml</to-view-id>
<redirect/>
</navigation-case>
<navigation-case>
<from-outcome>loginFailed</from-outcome>
<to-view-id>/login.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
</faces-config>
When addProduct() returns the string "success", JF looks up this file for a navigation rule whose <from-view-id> matches the current page and whose <from-outcome> matches "success". It then navigates to the <to-view-id>. The <redirect/> element instructs JF to issue an HTTP 302 redirect instead of a server-side forward โ solving the double-submission problem.
Observing Forward vs Redirect โ Live Experiment
Before adding the <redirect/> tag you will be able to observe the problem directly. Follow these steps:
In faces-config.xml, comment out the <redirect/> line under the success case. Redeploy.
Fill in the Add Product form and click Add Product. You are taken to the Product List โ but notice the URL bar still shows /addProduct.xhtml, not /products.xhtml.
The browser asks "Resend form data?" โ click Yes. A duplicate product appears in the table. This is the Post-Redirect-Get problem in action.
Uncomment the <redirect/> line. Redeploy. Repeat the test โ this time pressing Refresh reloads the Product List page cleanly with no duplicate.
Build a ProductCode Converter
Enforce a strict product code format (PRD-####) at the framework level. The converter runs automatically before any validator โ you never need to add format-checking code to your bean.
getAsObject (Phase 2โ3): the browser sends the raw string the user typed, e.g. "prd-0042". Your converter cleans and validates the format, returning a canonical "PRD-0042" โ or throwing a ConverterException if the format is wrong.
getAsString (Phase 6): when JF renders the page, it calls this to turn the Java value back into a display string for the browser.
Create ProductCodeConverter.java
Right-click Source Packages โ New โ Java Class
Class Name: ProductCodeConverter
Package: com.university.week9.converter
package com.university.week9.converter;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.ConverterException;
import jakarta.faces.convert.FacesConverter;
/**
* Custom Converter: enforces the product code format PRD-####
* where #### is exactly 4 digits (e.g. PRD-0001, PRD-0042, PRD-9999).
*
* @FacesConverter("productCodeConverter") registers this converter
* under the ID "productCodeConverter". We reference this ID in the
* Facelets page using <f:converter converterId="productCodeConverter"/>
*
* Converters run BEFORE validators (Phase 2โ3 of the JF lifecycle).
* If conversion fails the validator never runs.
*/
@FacesConverter("productCodeConverter")
public class ProductCodeConverter implements Converter<String> {
/** Valid format: PRD- followed by exactly 4 digits */
private static final String PATTERN = "^PRD-\\d{4}$";
/**
* Direction 1: String (from browser) โ Java object (for the bean).
* Called during lifecycle Phase 2 (Apply Request Values).
*
* Steps:
* 1. Trim whitespace the user may have accidentally typed
* 2. Convert to uppercase so "prd-0001" and "PRD-0001" both work
* 3. Validate against the regex โ throw ConverterException if invalid
* 4. Return the cleaned, canonical product code
*/
@Override
public String getAsObject(FacesContext context,
UIComponent component,
String value) {
// Null/empty: let required="true" handle this case
if (value == null || value.trim().isEmpty()) {
return null;
}
String cleaned = value.trim().toUpperCase();
if (!cleaned.matches(PATTERN)) {
// ConverterException tells JF to add this message and skip further phases
FacesMessage msg = new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"Invalid product code.",
"Format must be PRD-#### (e.g. PRD-0001). Got: " + value
);
throw new ConverterException(msg);
}
return cleaned;
}
/**
* Direction 2: Java object โ String (for the browser).
* Called during lifecycle Phase 6 (Render Response).
* Simply return the string value as-is for display.
*/
@Override
public String getAsString(FacesContext context,
UIComponent component,
String value) {
return (value != null) ? value : "";
}
}
Wire the Converter into addProduct.xhtml
Open addProduct.xhtml. Add a new Product Code field above the existing Product Name field. Also add a Code column to products.xhtml.
addProduct.xhtml โ add the code field (paste this before the name form-row div)
<!-- โโ PRODUCT CODE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The converter runs BEFORE any validator.
If the format is wrong (e.g. "abc") the ConverterException
fires here and validation phases are skipped.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -->
<div class="form-row">
<h:outputLabel for="code" value="Product Code: *"/>
<h:inputText id="code"
value="#{productBean.newCode}"
required="true"
requiredMessage="Product code is required."
placeholder="e.g. PRD-0006">
<!-- Reference the converter by its registered ID -->
<f:converter converterId="productCodeConverter"/>
</h:inputText>
<h:message for="code" styleClass="error"/>
</div>
products.xhtml โ add a Code column as the first column inside h:dataTable
<h:column>
<f:facet name="header">Code</f:facet>
<h:outputText value="#{p.code}"/>
</h:column>
Redeploy and open the Add Product form. Try these inputs for Product Code:
abcโ Error: "Invalid product code. Format must be PRD-####โฆ"prd-0006(lowercase) โ Accepted and stored asPRD-0006PRD-006(only 3 digits) โ Error: format mismatchPRD-0006โ Accepted; product appears in the table with code column
Build a UniqueNameValidator
Prevent duplicate product names by consulting the ProductService inside a custom Validator. This shows how validators can implement real business rules โ not just range checks.
A Converter transforms the data type. A Validator checks a rule without changing the value. They run in sequence: conversion first (Phase 2), then validation (Phase 3). If conversion fails, the validator never runs.
Create UniqueNameValidator.java
Right-click Source Packages โ New โ Java Class
Class Name: UniqueNameValidator
Package: com.university.week9.validator
package com.university.week9.validator;
import com.university.week9.service.ProductService;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.validator.FacesValidator;
import jakarta.faces.validator.Validator;
import jakarta.faces.validator.ValidatorException;
import jakarta.inject.Inject;
/**
* Custom Validator: rejects a product name that already exists in the catalogue.
*
* @FacesValidator(value="uniqueNameValidator", managed=true)
* value โ the ID used in <f:validator validatorId="uniqueNameValidator"/>
* managed โ tells CDI to manage this bean so @Inject works inside it.
* Without managed=true, @Inject would be null and
* the service call would throw NullPointerException.
*
* This validator runs in Phase 3 (Process Validations).
* If it throws ValidatorException, Phase 4 and 5 are skipped.
*/
@FacesValidator(value = "uniqueNameValidator", managed = true)
public class UniqueNameValidator implements Validator<String> {
// Inject the shared product service to check existing names
@Inject
private ProductService productService;
/**
* Called by FacesServlet during Phase 3.
*
* @param context the current FacesContext
* @param component the UI component this validator is attached to
* @param value the ALREADY-CONVERTED value (String after the converter ran)
*/
@Override
public void validate(FacesContext context,
UIComponent component,
String value) throws ValidatorException {
if (value == null || value.trim().isEmpty()) {
return; // let required="true" handle blank values
}
if (productService.nameExists(value)) {
FacesMessage msg = new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"Duplicate product name.",
"A product named \"" + value + "\" already exists. Choose a different name."
);
// Throwing ValidatorException adds the message and stops the lifecycle
throw new ValidatorException(msg);
}
// If we reach here, validation passed โ no exception thrown
}
}
Wire the Validator into addProduct.xhtml
Find the existing Product Name <h:inputText> block in addProduct.xhtml and add <f:validator> inside it:
<div class="form-row">
<h:outputLabel for="name" value="Product Name: *"/>
<h:inputText id="name"
value="#{productBean.newName}"
required="true"
requiredMessage="Product name is required.">
<f:validateLength minimum="2" maximum="80"/>
<!--
Our custom validator โ referenced by the ID from @FacesValidator.
This runs AFTER f:validateLength. If the length check fails,
this validator does NOT run (JF stops at the first failure).
-->
<f:validator validatorId="uniqueNameValidator"/>
</h:inputText>
<h:message for="name" styleClass="error"/>
</div>
- Try adding a product with name "Laptop Pro 15" (already exists) โ error: "Duplicate product name."
- Try adding a product with name "Gaming Keyboard" โ succeeds (unique name)
- The existing
f:validateLengthstill works โ enter a one-character name to confirm
Implement Session-Based Login and an HTTP Filter
Build a login page, a LoginBean that creates a session, and an AuthFilter that intercepts every request to protected pages โ redirecting unauthenticated users to the login page.
Part A โ Create LoginBean.java
Right-click Source Packages โ bean โ New โ Java Class
Class Name: LoginBean
Package: com.university.week9.bean
package com.university.week9.bean;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Named;
import jakarta.servlet.http.HttpSession;
/**
* Manages login and logout.
*
* @RequestScoped: a fresh bean for each HTTP request โ correct for a login form.
* The bean only needs to exist long enough to validate credentials.
* The actual "are you logged in?" state is stored in the HTTP SESSION,
* not in this bean. That is why the filter (AuthFilter) can check the session
* directly without needing this bean to be in scope.
*
* IMPORTANT โ hardcoded credentials are used here for simplicity.
* In a real application, passwords must be hashed (e.g. BCrypt) and
* stored in a database. Never store passwords in plain text.
*/
@Named
@RequestScoped
public class LoginBean {
/** The session attribute key used by both LoginBean and AuthFilter */
public static final String SESSION_USER_KEY = "loggedInUser";
private String username = "";
private String password = "";
// โโ Action: called by the login form's commandButton โโโโโโโโโโโโโโโโโโโโโโ
public String login() {
// Hardcoded credentials โ replace with DB lookup in a real app
if ("admin".equals(username) && "password123".equals(password)) {
// Store the username in the HTTP session โ this is the "login flag"
HttpSession session = (HttpSession) FacesContext.getCurrentInstance()
.getExternalContext().getSession(true);
session.setAttribute(SESSION_USER_KEY, username);
return "loginSuccess"; // mapped in faces-config.xml โ index.xhtml
}
// Wrong credentials: add a message and stay on login page
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR,
"Login failed.",
"Username or password is incorrect."));
return "loginFailed"; // mapped in faces-config.xml โ stay on login.xhtml
}
// โโ Action: invalidate session and return to login โโโโโโโโโโโโโโโโโโโโโโโโ
public String logout() {
FacesContext fc = FacesContext.getCurrentInstance();
HttpSession session = (HttpSession) fc.getExternalContext().getSession(false);
if (session != null) {
session.invalidate(); // destroys the entire session
}
return "/login.xhtml?faces-redirect=true";
}
// โโ Getters & Setters โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
public String getUsername() { return username; }
public void setUsername(String u) { this.username = u; }
public String getPassword() { return password; }
public void setPassword(String p) { this.password = p; }
}
Part B โ Create login.xhtml
Right-click Web Pages โ New โ XML/XHTML file โ name it login.xhtml:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core">
<h:head>
<title>Login โ Product Manager</title>
<style>
body { font-family: Arial, sans-serif; background: #f0f0f0; margin:0; }
.login-wrap { display:flex; justify-content:center; align-items:center; min-height:100vh; }
.login-box {
background: #fff; border-radius: 8px; padding: 40px 44px;
box-shadow: 0 2px 12px rgba(0,0,0,0.12); width: 360px;
}
.login-box h2 { color: #c0392b; margin-bottom: 6px; font-size:1.4rem; }
.login-box .subtitle { color: #888; font-size: .88rem; margin-bottom: 24px; }
.form-row { margin-bottom: 16px; }
.form-row label { display:block; font-weight:bold; margin-bottom:4px; font-size:.9rem; }
.form-row input { width:100%; padding:9px 10px; border:1px solid #ccc; border-radius:4px; font-size:.95rem; }
.btn { width:100%; background:#c0392b; color:white; border:none; padding:11px; border-radius:4px; cursor:pointer; font-size:1rem; margin-top:8px; }
.btn:hover { background:#a93226; }
.error-msg { color:#c0392b; font-size:.88rem; margin-top:3px; }
</style>
</h:head>
<h:body>
<div class="login-wrap">
<div class="login-box">
<h2>Product Manager</h2>
<div class="subtitle">Sign in to continue</div>
<!-- Global error message (wrong credentials) -->
<h:messages globalOnly="true" styleClass="error-msg" layout="list"/>
<h:form id="loginForm">
<div class="form-row">
<h:outputLabel for="uname" value="Username"/>
<h:inputText id="uname"
value="#{loginBean.username}"
required="true"
requiredMessage="Username is required."/>
<h:message for="uname" styleClass="error-msg"/>
</div>
<div class="form-row">
<h:outputLabel for="pw" value="Password"/>
<h:inputSecret id="pw"
value="#{loginBean.password}"
required="true"
requiredMessage="Password is required."/>
<h:message for="pw" styleClass="error-msg"/>
</div>
<h:commandButton value="Sign In"
action="#{loginBean.login}"
styleClass="btn"/>
</h:form>
<p style="font-size:.8rem; color:#aaa; margin-top:20px; text-align:center;">
Hint: username admin, password password123
</p>
</div>
</div>
</h:body>
</html>
Part C โ Create AuthFilter.java
This is the most important class in this task. Right-click Source Packages โ New โ Java Class
Class Name: AuthFilter
Package: com.university.week9.filter
package com.university.week9.filter;
import com.university.week9.bean.LoginBean;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
/**
* โโ SECURITY LAYER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
* AuthFilter intercepts EVERY request to the URL patterns listed in @WebFilter
* BEFORE FacesServlet and your Managed Beans ever see it.
*
* @WebFilter(urlPatterns = {"/products.xhtml", "/addProduct.xhtml"})
* Protects only these two pages. The home page (index.xhtml) and
* login.xhtml are intentionally NOT in this list โ they must remain
* accessible without login.
*
* Sequence of events per protected request:
* Browser โ AuthFilter.doFilter() โ [allowed?] โ FacesServlet โ Page
* โ [denied] โ redirect to login.xhtml
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
*/
@WebFilter(urlPatterns = {"/products.xhtml", "/addProduct.xhtml"})
public class AuthFilter implements Filter {
/**
* doFilter is called for every request matching the urlPatterns above.
*
* @param request the incoming HTTP request
* @param response the outgoing HTTP response (we use this to redirect)
* @param chain the filter chain โ calling chain.doFilter() passes the
* request through to FacesServlet (and ultimately your page)
*/
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
// Cast to HTTP-specific types to access session and redirect
HttpServletRequest httpReq = (HttpServletRequest) request;
HttpServletResponse httpResp = (HttpServletResponse) response;
// Get the existing session โ false means "don't create one if missing"
HttpSession session = httpReq.getSession(false);
// Check whether the session holds our login flag
boolean loggedIn = (session != null)
&& (session.getAttribute(LoginBean.SESSION_USER_KEY) != null);
if (loggedIn) {
// โ User is authenticated โ pass the request through to FacesServlet
chain.doFilter(request, response);
} else {
// โ Not logged in โ redirect to the login page
// getContextPath() returns e.g. "/Week10Lab"
// We build the full path: /Week10Lab/login.xhtml
String loginUrl = httpReq.getContextPath() + "/login.xhtml";
httpResp.sendRedirect(loginUrl);
}
}
// init and destroy are optional โ leave empty unless you need startup/cleanup
@Override public void init(FilterConfig fc) {}
@Override public void destroy() {}
}
Part D โ Add a Logout Button to the Nav Bar
Open templates/layout.xhtml. In the .nav div, add a logout button after the existing links:
<!-- Add this after the existing h:link elements -->
<h:form style="display:inline; margin:0;">
<h:commandButton
value="Logout"
action="#{loginBean.logout}"
style="background:transparent; border:1px solid #888;
color:#ccc; padding:4px 12px; border-radius:4px;
cursor:pointer; font-size:.9rem; margin-left:auto;"/>
</h:form>
h:commandButton always submits a form โ it cannot exist outside an h:form. Since the nav bar is in the template (not in any page's form), we wrap just the logout button in its own minimal h:form.
- Redeploy the application
- Navigate directly to
http://localhost:8080/Week10Lab/products.xhtmlโ you should be redirected to the login page immediately - Enter wrong credentials โ error message appears, you stay on the login page
- Enter admin / password123 โ you are redirected to the home page
- Click Product List in the nav โ the page loads normally (you are authenticated)
- Click Logout โ redirected to login page
- Try the browser's Back button โ navigating back to products.xhtml redirects you to login again
End-to-End Testing Checklist
Every item must pass. Work through them in order โ later items depend on earlier ones.
MVC Layer Separation
ProductServiceis@ApplicationScopedโ all users see the same product listProductBeanuses@Injectto get the service โ nonew ProductService()call anywhere in the beanProductServicecontains no Jakarta Faces imports and noh:/f:references- Products added by one browser tab are visible in another tab (proving shared
@ApplicationScopedstate)
Navigation
faces-config.xmlis present inWEB-INF/with both navigation rules- After adding a product, the URL bar shows
/products.xhtmlโ not/addProduct.xhtml(redirect confirmed) - Pressing Refresh on the product list page does NOT create a duplicate product
- Login failure keeps the user on
/login.xhtmlwith an error message - Successful login redirects to
/index.xhtml
Custom Converter
- Entering
abcin the Product Code field shows the converter's error message - Entering
PRD-006(3 digits) shows an error - Entering
prd-0006(lowercase) is accepted and stored asPRD-0006 - The Product List table shows a Code column with values like
PRD-0001
Custom Validator
- Entering
Laptop Pro 15(existing name) shows the duplicate name error - Entering a unique name like
Mechanical Keyboardpasses validation and adds the product - The validator error message names the duplicate: "A product named 'Laptop Pro 15' already exists"
- Entering a one-character name still triggers the
f:validateLengtherror (validator ordering confirmed)
HTTP Filter & Security
- Visiting
/products.xhtmlwithout logging in redirects to/login.xhtml - Visiting
/addProduct.xhtmlwithout logging in redirects to/login.xhtml /index.xhtmlis accessible without login (filter does not protect it)- Wrong credentials show error message; correct credentials allow access
- Logout invalidates the session โ pressing Back and then refreshing redirects to login
Final File Structure
java/com/university/week9/
bean/ProductBean.java โ
bean/LoginBean.java โ
model/Product.java โ
service/ProductService.java โ
converter/ProductCodeConverter.java โ
validator/UniqueNameValidator.java โ
filter/AuthFilter.java โ
webapp/
WEB-INF/
web.xml โ beans.xml โ faces-config.xml โ
templates/layout.xhtml โ
index.xhtml โ products.xhtml โ addProduct.xhtml โ login.xhtml โ
Common Errors and How to Fix Them
| Error / Symptom | Most Likely Cause | Fix |
|---|---|---|
| NullPointerException inside UniqueNameValidator at productService.nameExists() | managed=true is missing from the @FacesValidator annotation โ CDI cannot inject into it |
Change to @FacesValidator(value="uniqueNameValidator", managed=true). Without managed=true, @Inject is ignored and productService stays null. |
| Converter error appears for valid codes like "PRD-0001" when the form first loads (on page render, not submit) | The getAsString method is returning null or throwing an error |
Ensure getAsString returns "" (empty string) when value is null: return (value != null) ? value : ""; |
| After adding a product, the URL bar still shows /addProduct.xhtml | The <redirect/> element is missing inside the "success" navigation case in faces-config.xml |
Ensure the success navigation case in faces-config.xml contains <redirect/> as a child of <navigation-case>. |
| Visiting /products.xhtml goes to the page directly without login (filter not running) | The @WebFilter annotation URL pattern doesn't match, or beans.xml is misconfigured |
Check @WebFilter(urlPatterns={"/products.xhtml","/addProduct.xhtml"}) โ the leading slash is required. Also ensure beans.xml has bean-discovery-mode="all". |
| Logout button has no effect โ page stays the same | The h:commandButton for logout is not inside an h:form |
Wrap the logout h:commandButton in its own <h:form style="display:inline">...</h:form> directly in the template nav bar. |
| @Inject in ProductBean gives NullPointerException โ productService is null | CDI is not active โ beans.xml is missing or has wrong namespace/mode | Confirm src/main/webapp/WEB-INF/beans.xml exists with bean-discovery-mode="all" and the jakarta namespace URI (not javax). Clean and rebuild. |
| Product list is empty after login (0 rows shown) | ProductService is being instantiated multiple times โ the sample data constructor is not running once |
Confirm @ApplicationScoped is on ProductService (not @SessionScoped or @RequestScoped). Also check that getProducts() in ProductBean calls productService.findAll() and not a local field. |
| faces-config.xml navigation rules are ignored โ implicit navigation still used | The outcome string returned by the action method doesn't match the <from-outcome> in the config, or the file has the wrong namespace |
Ensure the config uses xmlns="https://jakarta.ee/xml/ns/jakartaee" and version="4.0". Verify the outcome string (e.g., "success") exactly matches โ case-sensitive. |
| Error: "The class ... is not a Converter" at deployment | ProductCodeConverter does not implement Converter<String>, or the interface is from the wrong package |
Ensure the implements clause reads implements Converter<String> and the import is jakarta.faces.convert.Converter (not javax.faces.*). |
| Ampersand & in faces-config.xml causes XML parse error at deployment | Raw & in an XML file is illegal โ must be escaped |
Replace any raw & in XML with &. This applies to all XML files: web.xml, beans.xml, faces-config.xml, and .xhtml pages. |
Going Further
Extension 1 โ Role-Based Access Control
Extend the security model so that there are two roles: admin and viewer. Add a second hardcoded user viewer/view123. Store both the username and role in the session. Modify AuthFilter so that:
/products.xhtmlโ accessible to both roles/addProduct.xhtmlโ accessible only toadmin; viewer is redirected to anaccessDenied.xhtmlpage you create
Hint: Store the role as a second session attribute: session.setAttribute("userRole", role);. In the filter, read both attributes to make the decision.
Extension 2 โ Composite Validator (Code Uniqueness)
Currently, the product code converter enforces the format but does not check for duplicates. Create a second validator called UniqueCodeValidator (following the same pattern as UniqueNameValidator) that checks whether the product code already exists in the service. Wire it into the Product Code field alongside the converter.
Verify: adding a product with code PRD-0001 (already taken by "Laptop Pro 15") should show a validator error.
Extension 3 โ Logging Filter
Create a second filter called RequestLoggingFilter that applies to all requests (urlPatterns="/*"). It should log the following to System.out for every request:
- Timestamp
- HTTP method (GET or POST)
- Request URI
- Username from the session (or "anonymous" if not logged in)
- Time taken to process the request in milliseconds (capture before and after
chain.doFilter())
View the output in NetBeans by opening the GlassFish Server tab in the Output panel while navigating the app.
Key insight: A filter can call code both before and after chain.doFilter(). Everything before the call runs on the way in; everything after runs on the way out.
Extension 4 โ Session Timeout Handling
Set the session timeout to 1 minute in web.xml:
<!-- Add inside <web-app> in web.xml -->
<session-config>
<session-timeout>1</session-timeout> <!-- minutes -->
</session-config>
Log in, wait 90 seconds, then attempt to navigate to the product list. Observe what happens. Does the filter correctly redirect you? Does clicking the Logout button after session expiry throw a ViewExpiredException? Handle the expired session gracefully by catching the exception in the filter and redirecting to the login page with a ?timeout=true query parameter. Display a "Your session has expired, please log in again" message on the login page when this parameter is present.