What You Will Build
A fully functional Product Management web application using Jakarta Faces, Facelets templating, and the JF component library.
This tutorial walks you through building a small but complete web application step-by-step. By the end, you will have practised every skill from this week's lecture:
LO 1 Web Technologies
- Experience how JF generates HTML from components (contrast with raw HTML)
- Observe the difference between server-side rendering and static pages
LO 2 Facelets PDL
- Declare correct XML namespaces (Jakarta EE 10)
- Build a reusable master template with
ui:insert - Use
ui:compositionandui:definein child pages
LO 3 JF Components
h:form,h:inputText,h:commandButtonh:dataTable+h:columnf:validateLongRange,f:validateLengthf:convertNumber,h:message,h:messages- Conditional rendering with
rendered
Final Application Pages
- Home page — welcome message with navigation
- Product list — table of products with prices
- Add product — form with full validation
- Apache NetBeans is installed and configured with GlassFish 7 as a server
- You have completed the Week 8 lab (JF project structure is familiar)
- GlassFish 7 can be started from the Services panel in NetBeans
- You do NOT need a database for this lab — data is stored in memory (a Java List)
Creating the Project
Create a new Maven-based Jakarta EE Web Application and configure the essential files.
Step-by-Step: Create the NetBeans Project
In NetBeans, go to:
The New Project dialog will open.
In the Categories panel on the left, select Java with Maven.
In the Projects panel on the right, select Web Application.
Click Next >.
Fill in the fields as follows:
- Project Name:
Week9Lab - Group Id:
com.university - Artifact Id:
Week9Lab(filled automatically) - Package:
com.university.week9
Leave the project location as the default. Click Next >.
- Server: Select GlassFish Server from the dropdown
- Java EE Version: Select Jakarta EE 10
- Context Path: Leave as
/Week9Lab
Click Finish. NetBeans will generate the project structure.
Configure pom.xml
Open pom.xml in the project root. Replace its entire content with the following:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.university</groupId>
<artifactId>Week9Lab</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<failOnMissingWebXml>false</failOnMissingWebXml>
</properties>
<dependencies>
<!-- Jakarta EE 10 API (provided by GlassFish — do not bundle) -->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>10.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
</project>
After saving, right-click the project in the Projects panel and select Clean and Build to download dependencies.
Create web.xml and beans.xml
These two files are required for every Jakarta Faces application. Check whether they already exist under src/main/webapp/WEB-INF/. If not, create them now.
Creating web.xml
Or create a plain XML file named web.xml in the WEB-INF folder.
<?xml version="1.0" encoding="UTF-8"?>
<web-app 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-app_6_0.xsd"
version="6.0">
<!-- Register the FacesServlet to handle all .xhtml requests -->
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>
<!-- Default welcome page -->
<welcome-file-list>
<welcome-file>index.xhtml</welcome-file>
</welcome-file-list>
</web-app>
Creating beans.xml
Create src/main/webapp/WEB-INF/beans.xml with this content:
<?xml version="1.0" encoding="UTF-8"?>
<beans 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/beans_4_0.xsd"
bean-discovery-mode="all"
version="4.0">
</beans>
@Named and scope annotations on your Managed Beans will be silently ignored — your EL expressions like #{productBean.products} will return empty results with no error.
Verify the Project Structure
Before moving on, confirm your project looks like this:
src/main/
java/com/university/week9/
(empty — we create classes next)
webapp/
WEB-INF/
web.xml ✓
beans.xml ✓
(pages go here)
pom.xml ✓
Create the Master Template
Build a reusable page layout that all three pages will share. This demonstrates the Facelets PDL templating system.
Instead of copying the navigation bar and footer into every page, we define them once in a template and let each page fill in only its unique content.
layout.xhtml that defines: a header banner, a navigation bar with links to our pages, a named slot called "content" (where each page inserts its own content), and a footer. Every page in the app will use this template.
Create the Template File
In the Projects panel, expand your project → Web Pages (this represents src/main/webapp/).
Right-click Web Pages → New → Folder → name it templates.
Right-click the templates folder → New → Other → JavaServer Faces → Facelets Template.
Name it layout. NetBeans will add the .xhtml extension automatically.
If the Facelets Template option does not appear, create a plain XML file named layout.xhtml inside the templates folder, then paste the code below.
Delete everything NetBeans generated and paste this complete template:
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets">
<h:head>
<meta charset="UTF-8"/>
<title>Product Manager — Week 9 Lab</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0; padding: 0; background: #f5f5f5;
}
.header {
background: #c0392b; color: white;
padding: 16px 30px; font-size: 1.4em; font-weight: bold;
}
.nav {
background: #333; padding: 10px 30px; display: flex; gap: 20px;
}
.nav a {
color: #ccc; text-decoration: none; font-size: 0.95em;
padding: 4px 10px; border-radius: 4px;
}
.nav a:hover { background: #555; color: white; }
.main-content {
max-width: 860px; margin: 30px auto;
background: white; padding: 28px 32px;
border-radius: 6px; box-shadow: 0 1px 4px rgba(0,0,0,0.1);
}
h2 { color: #c0392b; border-bottom: 2px solid #eee; padding-bottom: 8px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 10px 14px; text-align: left; }
th { background: #f0f0f0; font-weight: bold; }
tr:nth-child(even) td { background: #fafafa; }
.btn {
background: #c0392b; color: white; border: none;
padding: 9px 20px; border-radius: 4px; cursor: pointer;
font-size: 0.95em;
}
.btn:hover { background: #a93226; }
.btn-secondary {
background: #555; color: white; border: none;
padding: 9px 20px; border-radius: 4px; cursor: pointer;
font-size: 0.95em;
}
.form-row { margin-bottom: 14px; }
.form-row label { display: block; font-weight: bold; margin-bottom: 4px; }
.form-row input, .form-row select {
padding: 8px 10px; border: 1px solid #ccc; border-radius: 4px;
width: 300px; font-size: 0.95em;
}
.error { color: #c0392b; font-size: 0.88em; margin-top: 3px; display: block; }
.success-msg {
background: #e8f5e9; border: 1px solid #4caf50; border-radius: 4px;
padding: 12px 16px; color: #2e7d32; margin-bottom: 16px;
}
.footer {
text-align: center; padding: 20px; color: #999;
font-size: 0.83em; margin-top: 30px;
}
</style>
</h:head>
<h:body>
<!-- ★ FIXED: Header — always the same across all pages -->
<div class="header">
Product Manager — Week 9 Lab
</div>
<!-- ★ FIXED: Navigation bar — always the same -->
<div class="nav">
<h:link value="Home" outcome="index"/>
<h:link value="Product List" outcome="products"/>
<h:link value="Add Product" outcome="addProduct"/>
</div>
<!-- ★ SLOT: Each page fills this in with its own content -->
<div class="main-content">
<ui:insert name="content">
<!-- Default content shown if a child page doesn't define this slot -->
<p>No content defined for this page.</p>
</ui:insert>
</div>
<!-- ★ FIXED: Footer — always the same -->
<div class="footer">
COIT20259 Enterprise Computing — Week 9 Lab & Practice
</div>
</h:body>
</html>
<ui:insert name="content"> defines a named slot. Child pages use <ui:define name="content"> to fill it. The name "content" must match exactly between the template and the child page — it is case-sensitive.
Create the Home Page Using the Template
This page demonstrates how ui:composition and ui:define work to fill in the template slot.
Create index.xhtml
Right-click Web Pages (i.e., src/main/webapp/) → New → Other → JavaServer Faces → Facelets Template Client.
Name the file index and, when prompted for the template file, browse to /templates/layout.xhtml.
Replace all generated content with:
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets">
<!--
ui:composition tells JF: "Use this template for the page layout.
Everything outside ui:composition tags is ignored."
-->
<ui:composition template="/templates/layout.xhtml">
<!--
ui:define fills the slot named "content" in the template.
Only this part is unique to the home page.
-->
<ui:define name="content">
<h2>Welcome to the Product Manager</h2>
<p>
This application demonstrates Jakarta Faces components and Facelets PDL.
Use the navigation bar above to explore the application.
</p>
<ul>
<li><h:link value="View all products" outcome="products"/>
— see the product catalogue</li>
<li><h:link value="Add a new product" outcome="addProduct"/>
— fill in a validated form</li>
</ul>
</ui:define>
</ui:composition>
</html>
- Right-click the project → Clean and Build
- Right-click the project → Run (GlassFish will start and deploy)
- Your browser should open to
http://localhost:8080/Week9Lab/ - You should see the red header, the navigation bar, and the welcome message
- Clicking the nav links will show "page not found" — that's expected, we haven't created those pages yet
Create the Product Model and Managed Bean
Before building pages that display data, we need a Java class to represent a product, and a Managed Bean to hold and manage the product list.
Create Product.java (the Model)
Right-click Source Packages → New → Java Class.
Set Class Name: Product
Set Package: com.university.week9.model
Click Finish.
package com.university.week9.model;
/**
* Plain Java class representing a Product.
* Not a database entity for this lab — data is held in memory.
* In a real application this would be a JPA @Entity.
*/
public class Product {
private int id;
private String name;
private String category;
private double price;
private int quantity;
// ── Constructor ──────────────────────────────────
public Product(int id, String name, String category,
double price, int quantity) {
this.id = id;
this.name = name;
this.category = category;
this.price = price;
this.quantity = quantity;
}
// ── No-arg constructor (required by JF) ──────────
public Product() {}
// ── Getters and Setters ───────────────────────────
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
}
Create ProductBean.java (the Managed Bean)
Right-click Source Packages → New → Java Class.
Set Class Name: ProductBean
Set Package: com.university.week9.bean
Click Finish.
package com.university.week9.bean;
import com.university.week9.model.Product;
import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Managed Bean for the Product pages.
*
* @Named → makes this bean accessible in Facelets as #{productBean}
* @SessionScoped → the product list persists for the user's browser session
*
* WHY SessionScoped?
* We want "Add Product" to actually add to the list the user sees in
* "Product List". RequestScoped would create a fresh bean each page load,
* losing the list. SessionScoped keeps it alive.
*
* WHY Serializable?
* SessionScoped beans must be serializable so GlassFish can passivate
* (save) the session to disk if needed.
*/
@Named
@SessionScoped
public class ProductBean implements Serializable {
// ── Product list (our in-memory "database") ───────────────────
private List<Product> products = new ArrayList<>();
// ── New product being entered via the form ─────────────────────
// JF will write form field values into these properties
private String newName = "";
private String newCategory = "";
private double newPrice = 0.0;
private int newQuantity = 0;
// ── Feedback message shown after adding a product ─────────────
private String successMessage = "";
// ── Constructor: populate some sample data ────────────────────
public ProductBean() {
products.add(new Product(1, "Laptop Pro 15", "Electronics", 1299.99, 15));
products.add(new Product(2, "Office Chair", "Furniture", 349.00, 8));
products.add(new Product(3, "USB-C Hub", "Electronics", 49.95, 42));
products.add(new Product(4, "Standing Desk", "Furniture", 699.00, 5));
products.add(new Product(5, "Wireless Mouse", "Electronics", 35.99, 30));
}
// ── Action method: called when the "Add Product" button is clicked ─
public String addProduct() {
int nextId = products.size() + 1;
Product p = new Product(nextId, newName, newCategory,
newPrice, newQuantity);
products.add(p);
successMessage = "Product \"" + newName + "\" added successfully!";
// Reset the form fields
newName = "";
newCategory = "";
newPrice = 0.0;
newQuantity = 0;
// Navigate to the products page (implicit navigation)
return "products?faces-redirect=true";
}
// ── Getters and Setters ────────────────────────────────────────
public List<Product> getProducts() { return products; }
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; }
}
#{productBean.newName} in a Facelet, JF calls getNewName() to read the value and setNewName() to write a submitted form value. If either method is missing, JF will throw a PropertyNotFoundException.
Build the Product List Page with h:dataTable
Create a page that displays all products from the bean in a formatted table using h:dataTable.
Create products.xhtml
Right-click Web Pages → New → Other → XHTML (or plain XML file) → name it products.xhtml.
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets">
<ui:composition template="/templates/layout.xhtml">
<ui:define name="content">
<h2>Product Catalogue</h2>
<!-- ── SUCCESS MESSAGE ────────────────────────────────────────
Conditional rendering: this div only appears in the HTML
when successMessage is NOT empty.
"not empty" is an EL operator meaning: not null AND not "".
──────────────────────────────────────────────────────────── -->
<div class="success-msg"
rendered="#{not empty productBean.successMessage}">
<h:outputText value="#{productBean.successMessage}"/>
</div>
<!-- ── PRODUCT COUNT ─────────────────────────────────────────
EL can access the .size() method of a Java List.
──────────────────────────────────────────────────────────── -->
<p>
Showing
<strong><h:outputText
value="#{productBean.products.size()}"/></strong>
products in the catalogue.
</p>
<!-- ── h:dataTable ───────────────────────────────────────────
value = the Java List to iterate over
var = the name we give each row item inside this block
So #{p.name} means: "the name property of the current product"
──────────────────────────────────────────────────────────── -->
<h:dataTable
value="#{productBean.products}"
var="p"
style="width:100%">
<!-- Column 1: ID -->
<h:column>
<f:facet name="header">ID</f:facet>
<h:outputText value="#{p.id}"/>
</h:column>
<!-- Column 2: Product Name -->
<h:column>
<f:facet name="header">Product Name</f:facet>
<h:outputText value="#{p.name}"/>
</h:column>
<!-- Column 3: Category -->
<h:column>
<f:facet name="header">Category</f:facet>
<h:outputText value="#{p.category}"/>
</h:column>
<!-- Column 4: Price (with currency formatter) -->
<h:column>
<f:facet name="header">Price</f:facet>
<!--
f:convertNumber formats the double as currency.
Without this, 1299.99 might show as 1299.9900000001
due to floating-point precision.
-->
<h:outputText value="#{p.price}">
<f:convertNumber type="currency"
currencySymbol="$"
minFractionDigits="2"/>
</h:outputText>
</h:column>
<!-- Column 5: Quantity -->
<h:column>
<f:facet name="header">Qty in Stock</f:facet>
<h:outputText value="#{p.quantity}"/>
</h:column>
</h:dataTable>
<!-- ── NAVIGATION BUTTON ──────────────────────────────────────
h:button renders a link styled as a button.
Use h:button (not h:commandButton) for navigation-only links
because it does NOT submit a form.
──────────────────────────────────────────────────────────── -->
<br/>
<h:button value="+ Add New Product"
outcome="addProduct"
styleClass="btn"/>
</ui:define>
</ui:composition>
</html>
Redeploy (Clean and Build → Run) and navigate to Product List in the nav bar. You should see a table with 5 sample products, prices formatted with a dollar sign, and no scroll — the template handles the header/footer automatically.
Build the Add Product Form with Validation
Create a form using h:form, input components, f: validators, and h:message to display per-field errors.
Create addProduct.xhtml
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets">
<ui:composition template="/templates/layout.xhtml">
<ui:define name="content">
<h2>Add New Product</h2>
<!-- ── h:messages ────────────────────────────────────────────
Displays ALL validation/conversion errors at once.
globalOnly="false" means it will show field-level errors here too.
We use this as a summary at the top and individual h:message
tags next to each field for inline errors.
──────────────────────────────────────────────────────────── -->
<h:messages style="color:#c0392b; font-weight:bold;"
layout="list"
rendered="#{facesContext.validationFailed}"/>
<!-- ── h:form ────────────────────────────────────────────────
ALL input components that submit data MUST be inside h:form.
NEVER use a plain HTML <form> tag with JF components.
──────────────────────────────────────────────────────────── -->
<h:form id="addForm">
<!-- ── PRODUCT NAME ──────────────────────────────────────
f:validateLength ensures the name is between 2 and 80 chars.
The id="name" on h:inputText must match the for="name"
on h:message to link them together.
──────────────────────────────────────────────────────────── -->
<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"/>
</h:inputText>
<!-- h:message shows the error for THIS field only -->
<h:message for="name" styleClass="error"/>
</div>
<!-- ── CATEGORY (Dropdown) ───────────────────────────────
h:selectOneMenu renders an HTML <select> dropdown.
f:selectItem defines each option individually.
required="true" prevents an empty selection.
──────────────────────────────────────────────────────────── -->
<div class="form-row">
<h:outputLabel for="category" value="Category: *"/>
<h:selectOneMenu id="category"
value="#{productBean.newCategory}"
required="true"
requiredMessage="Please select a category.">
<f:selectItem itemValue="" itemLabel="-- Select --"/>
<f:selectItem itemValue="Electronics" itemLabel="Electronics"/>
<f:selectItem itemValue="Furniture" itemLabel="Furniture"/>
<f:selectItem itemValue="Stationery" itemLabel="Stationery"/>
<f:selectItem itemValue="Software" itemLabel="Software"/>
</h:selectOneMenu>
<h:message for="category" styleClass="error"/>
</div>
<!-- ── PRICE ─────────────────────────────────────────────
f:convertNumber converts the submitted String to a double.
f:validateDoubleRange ensures it is between 0.01 and 99999.
Without the converter, JF cannot assign "29.99" to a double field.
──────────────────────────────────────────────────────────── -->
<div class="form-row">
<h:outputLabel for="price" value="Price ($): *"/>
<h:inputText id="price"
value="#{productBean.newPrice}"
required="true"
requiredMessage="Price is required.">
<f:convertNumber minFractionDigits="2"
maxFractionDigits="2"/>
<f:validateDoubleRange minimum="0.01"
maximum="99999.00"/>
</h:inputText>
<h:message for="price" styleClass="error"/>
</div>
<!-- ── QUANTITY ──────────────────────────────────────────
f:validateLongRange validates an integer range.
The minimum is 1 (can't add 0 or negative stock).
The maximum is 10000 (business rule).
──────────────────────────────────────────────────────────── -->
<div class="form-row">
<h:outputLabel for="qty" value="Quantity in Stock: *"/>
<h:inputText id="qty"
value="#{productBean.newQuantity}"
required="true"
requiredMessage="Quantity is required.">
<f:validateLongRange minimum="1"
maximum="10000"/>
</h:inputText>
<h:message for="qty" styleClass="error"/>
</div>
<!-- ── BUTTONS ───────────────────────────────────────────
h:commandButton submits the form and calls the action method.
h:button navigates WITHOUT submitting — good for Cancel.
──────────────────────────────────────────────────────────── -->
<div style="margin-top: 20px; display:flex; gap:12px;">
<h:commandButton
value="Add Product"
action="#{productBean.addProduct}"
styleClass="btn"/>
<h:button
value="Cancel"
outcome="products"
styleClass="btn-secondary"/>
</div>
</h:form>
</ui:define>
</ui:composition>
</html>
Understanding Validation Messages
| Scenario | What JF does | What the user sees |
|---|---|---|
| Name field is empty | required="true" fires; Phase 3 stops processing | Error next to the Name field; addProduct() is never called |
| Price entered as "abc" | f:convertNumber fails; conversion error generated | Error: "abc is not a valid number" next to the Price field |
| Quantity entered as "0" | f:validateLongRange fails (minimum is 1) | Error: "Validation Error: Specified attribute is not between the expected values of 1 and 10000" |
| All fields valid | All 6 lifecycle phases complete; addProduct() is called; redirect to products.xhtml | Product appears in the table; green success message shown |
Review Navigation and Deploy the Complete Application
Understand the two types of navigation JF provides, then do a final end-to-end test.
The Two Navigation Approaches Used in This Lab
Used in the nav bar and the Cancel button. These render as regular HTML anchor tags (<a>). The outcome attribute is the filename of the target page (without the .xhtml extension). No form is submitted; no bean method is called.
<!-- Goes to products.xhtml -->
<h:link value="Product List" outcome="products"/>
<h:button value="Cancel" outcome="products"/>
Used for the Add Product button. It submits the h:form, triggers all 6 lifecycle phases including validation, and calls your action method. The action method returns a String that tells JF where to go next.
<!-- Submits form → calls addProduct() → goes to products.xhtml -->
<h:commandButton
value="Add Product"
action="#{productBean.addProduct}"/>
// In Java:
// return "products?faces-redirect=true";
// ↑ filename ↑ tells browser to navigate (updates URL bar)
Final Deployment Steps
In NetBeans, press Ctrl + S (or Cmd + S on Mac) to save all open files. Any file with unsaved changes shows a * in its tab title.
Right-click the project → Clean and Build.
Wait for the Output panel to show: BUILD SUCCESS.
Right-click the project → Run. NetBeans will start GlassFish (if not already running) and deploy the WAR file. Your default browser will open.
Alternatively, if GlassFish is already running, right-click the project → Deploy.
End-to-End Testing Checklist
Work through each test below. Every item must pass before you can consider the lab complete.
Template and Navigation
- The red header "Product Manager — Week 9 Lab" appears on ALL three pages
- The navigation bar with Home / Product List / Add Product links appears on ALL pages
- The footer text appears on ALL pages — without you copying it into each page file
- Clicking each nav link takes you to the correct page
Product List Page
- The table shows exactly 5 rows of sample data on first load
- The Price column shows values formatted with a $ symbol and 2 decimal places (e.g., $1,299.99)
- The "+ Add New Product" button navigates to the Add Product page
Add Product Form — Validation Tests
- Submit the form with ALL fields empty → errors appear next to every required field; no product is added
- Enter a product name of just one character (e.g., "A") → error appears: length must be between 2 and 80
- Enter "abc" in the Price field → conversion error appears; other valid fields retain their values
- Enter "0" in the Quantity field → range validation error appears
- Enter a negative price (e.g., "-10") → range validation error appears
- Fill all fields correctly → product is added, page redirects to Product List, green success message appears
- The new product appears as a new row at the bottom of the Product List table
- Add a second product — the table now shows 7 rows total
- Clicking Cancel on the Add Product page navigates to Product List without adding anything
File Structure Check
java/com/university/week9/
bean/
ProductBean.java ✓
model/
Product.java ✓
webapp/
templates/
layout.xhtml ✓
WEB-INF/
web.xml ✓
beans.xml ✓
index.xhtml ✓
products.xhtml ✓
addProduct.xhtml ✓
pom.xml ✓
Common Errors and How to Fix Them
Consult this table whenever you encounter an error. Read the full error message in the GlassFish log (available in the NetBeans Output panel).
| Error / Symptom | Most Likely Cause | Fix |
|---|---|---|
| Page shows "404 Not Found" | The page was not deployed, or the URL is wrong | Right-click project → Run (not just Build). Check the URL includes /Week9Lab/. Ensure the .xhtml file is in src/main/webapp/ (not inside WEB-INF). |
| Products table is empty (no rows) even though sample data is set in the constructor | beans.xml is missing or has wrong content; CDI is not activated so @Named is ignored |
Ensure beans.xml exists in WEB-INF/, has bean-discovery-mode="all", and uses the jakarta namespace (not javax). |
javax.faces.application.ViewExpiredException |
The JSF state was lost — usually after redeployment while the browser still had an old page open | Press F5 to hard-refresh the page, or navigate to http://localhost:8080/Week9Lab/index.xhtml directly. |
| Navigation links in the nav bar produce a 404 for the target page | The target .xhtml file doesn't exist yet, or the outcome string doesn't match the filename |
Check that the outcome value (e.g., "products") exactly matches the filename (products.xhtml) without the extension. Case-sensitive on Linux/Mac. |
| Form submission does nothing (page just refreshes, no errors, no navigation) | h:commandButton is outside an h:form, or you used a plain HTML <form> |
Ensure all input components and commandButton are directly inside <h:form>. Never nest h:form inside a regular HTML <form>. |
PropertyNotFoundException: #{productBean.newName} |
The getter/setter for newName is missing or misspelled in ProductBean.java |
Ensure getNewName() and setNewName(String n) both exist and are public. EL uses the getter for reading and the setter for writing. |
| Template header/footer does not appear; only the "content" slot content shows | The template path in ui:composition template="..." is wrong |
The path must be absolute from the webapp root: template="/templates/layout.xhtml". The leading / is required. |
| Price shows too many decimal places (e.g., 1299.9900000001) | f:convertNumber is missing from the h:outputText in products.xhtml |
Wrap the price h:outputText with <f:convertNumber type="currency" currencySymbol="$" minFractionDigits="2"/>. |
| Validation error: "Conversion Error setting value 'xyz' for 'null Converter'" | JF cannot convert the String "xyz" to the Java type of the bound property (e.g., double or int) | Add <f:convertNumber/> inside the h:inputText for price, and ensure the quantity field is bound to an int property, not a String. |
| GlassFish fails to start in NetBeans | Port 8080 is in use, or GlassFish is already running outside NetBeans | Open Services tab → Servers → right-click GlassFish → Stop. Then try again. Or open Task Manager and kill any existing Java process using port 8080. |
The most detailed error information is in the GlassFish log. In NetBeans, go to the Output panel (bottom of the screen) → select the GlassFish Server tab. Scroll up from the bottom to find the SEVERE or WARNING lines — they contain the full Java stack trace with the exact file and line number where the error occurred.
Going Further — Consolidation & Challenge Exercises
If you finish the core lab early, attempt these extensions to deepen your understanding.
Extension 1 — Add a "Low Stock" Warning Row
In products.xhtml, modify the Quantity column in h:dataTable so that when a product's quantity is less than 10, the quantity number is displayed in red bold text.
Hint: Use the style attribute with an EL conditional expression inside h:outputText:
<h:outputText
value="#{p.quantity}"
style="#{p.quantity lt 10 ? 'color:red; font-weight:bold;' : ''}"/>
Test it by adding a product with quantity 3 — it should appear red in the table.
Extension 2 — Add a Second Template Slot for the Page Title
Modify layout.xhtml to have a second ui:insert slot named "pageTitle" that is displayed inside the browser's <title> tag and as an <h1> below the nav bar. Update all three child pages to use ui:define name="pageTitle" with appropriate titles ("Home", "Product Catalogue", "Add Product").
Goal: Each page should have a unique browser tab title and an <h1> heading that comes from the child page, not the template — while the nav bar and footer remain fixed.
Extension 3 — Custom Validator with a Meaningful Message
The default JF validation error messages are technical (e.g., "Validation Error: Specified attribute is not between the expected values of 1 and 10000"). Override the message for the Quantity field to read: "Quantity must be between 1 and 10,000 units."
Hint: Use the validatorMessage attribute on h:inputText:
<h:inputText
id="qty"
value="#{productBean.newQuantity}"
validatorMessage="Quantity must be between 1 and 10,000 units.">
<f:validateLongRange minimum="1" maximum="10000"/>
</h:inputText>
Apply custom messages to all fields so that every error is user-friendly, not a technical stack trace.
Extension 4 — Delete a Product
Add a "Delete" button to each row of the h:dataTable in products.xhtml. Clicking it should remove that product from the list.
Steps:
- Add a new column with header "Action" to the dataTable
- Inside the column, add an
h:commandButtonwith value "Delete" - Use
f:setPropertyActionListeneror pass the product ID as a parameter to identify which product to delete - Add a
deleteProduct(int id)method toProductBeanthat removes the product with that ID from the list
Hint for passing the ID:
<h:commandButton value="Delete"
action="#{productBean.deleteProduct(p.id)}"
styleClass="btn"
style="background:#555;"/>