COIT20259 — Week 9 Lab Tutorial

Jakarta Faces: Facelets PDL & Components

🛠 Apache NetBeans ☕ GlassFish 7 📦 Jakarta EE 10 ⏱ Estimated time: 90–120 min
Introduction

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:composition and ui:define in child pages

LO 3 JF Components

  • h:form, h:inputText, h:commandButton
  • h:dataTable + h:column
  • f:validateLongRange, f:validateLength
  • f: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
Prerequisites
  • 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)
Setup

Creating the Project

Create a new Maven-based Jakarta EE Web Application and configure the essential files.

Step-by-Step: Create the NetBeans Project

1
Open the New Project wizard

In NetBeans, go to:

The New Project dialog will open.

2
Select the project category

In the Categories panel on the left, select Java with Maven.
In the Projects panel on the right, select Web Application.
Click Next >.

3
Name and location

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 >.

4
Select server and Jakarta EE version
  • 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.

If GlassFish does not appear in the dropdown
Go to ToolsServersAdd Server, select GlassFish Server, and point it to your GlassFish 7 installation folder. Then return to this step.

Configure pom.xml

Open pom.xml in the project root. Replace its entire content with the following:

pom.xml XML
<?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

1
Right-click WEB-INF → New → Other → Web → Standard Deployment Descriptor (web.xml)

Or create a plain XML file named web.xml in the WEB-INF folder.

src/main/webapp/WEB-INF/web.xml XML
<?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:

src/main/webapp/WEB-INF/beans.xml XML
<?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>
Why beans.xml?
This file activates CDI (Contexts and Dependency Injection) on GlassFish. Without it, @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:

Week9Lab/
  src/main/
    java/com/university/week9/
      (empty — we create classes next)
    webapp/
      WEB-INF/
        web.xml ✓
        beans.xml ✓
      (pages go here)
  pom.xml ✓
Task 1 — Facelets PDL

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.

What we are building
A file called 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

1
Create a templates folder

In the Projects panel, expand your project → Web Pages (this represents src/main/webapp/).

Right-click Web PagesNewFolder → name it templates.

2
Create layout.xhtml inside the templates folder

Right-click the templates folder → NewOtherJavaServer FacesFacelets 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.

3
Replace all content with the template below

Delete everything NetBeans generated and paste this complete template:

src/main/webapp/templates/layout.xhtml Facelets / 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">

<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 &amp; Practice
    </div>

</h:body>
</html>
Key concept: the ui:insert slot
The line <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.
Task 2 — Facelets PDL

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/) → NewOtherJavaServer FacesFacelets Template Client.
Name the file index and, when prompted for the template file, browse to /templates/layout.xhtml.

Replace all generated content with:

src/main/webapp/index.xhtml Facelets / XHTML
<!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>
Checkpoint — Deploy and Test
  1. Right-click the project → Clean and Build
  2. Right-click the project → Run (GlassFish will start and deploy)
  3. Your browser should open to http://localhost:8080/Week9Lab/
  4. You should see the red header, the navigation bar, and the welcome message
  5. Clicking the nav links will show "page not found" — that's expected, we haven't created those pages yet
Task 3 — Java Backend

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)

1
Create a new Java class

Right-click Source PackagesNewJava Class.
Set Class Name: Product
Set Package: com.university.week9.model
Click Finish.

src/main/java/com/university/week9/model/Product.java Java
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)

1
Create a new Java class

Right-click Source PackagesNewJava Class.
Set Class Name: ProductBean
Set Package: com.university.week9.bean
Click Finish.

src/main/java/com/university/week9/bean/ProductBean.java Java
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; }
}
Why do getters and setters matter for JF?
Jakarta Faces uses Java Bean conventions — when you write #{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.
Task 4 — JF Components

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 PagesNewOtherXHTML (or plain XML file) → name it products.xhtml.

src/main/webapp/products.xhtml Facelets / 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>
Checkpoint — Test the Product List

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.

Task 5 — JF Components

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

src/main/webapp/addProduct.xhtml Facelets / 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
Task 6 — Navigation

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

h:link / h:button — Navigation without form submission

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"/>
h:commandButton — Submits the form, then navigates

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

1
Confirm all files are saved

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.

2
Clean and Build

Right-click the project → Clean and Build.
Wait for the Output panel to show: BUILD SUCCESS.

If you see BUILD FAILURE
Read the red error message in the Output panel. Common causes: a missing semicolon in Java, an unclosed XML tag in XHTML, or a wrong import statement. Fix the error and try Clean and Build again.
3
Deploy to GlassFish

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.

Verification

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

Week9Lab/src/main/
  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 ✓
Troubleshooting

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.
Reading the GlassFish Log

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.

Extension Tasks

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:

  1. Add a new column with header "Action" to the dataTable
  2. Inside the column, add an h:commandButton with value "Delete"
  3. Use f:setPropertyActionListener or pass the product ID as a parameter to identify which product to delete
  4. Add a deleteProduct(int id) method to ProductBean that 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;"/>
COIT20259 Enterprise Computing — Week 9 Lab Tutorial  ·  Jakarta EE 10 / GlassFish 7 / Apache NetBeans