Overview
Early JSP code had an infamous reputation for pages packed with `<% ... %>` scriptlets full of raw Java — loops, conditionals, and business logic all mixed directly into HTML. JSTL (the JSP Standard Tag Library) and Expression Language exist specifically to eliminate that: `<c:forEach>` replaces a scriptlet `for` loop, `<c:if>`/`<c:choose>` replace scriptlet `if`/`else`, and `${...}` EL replaces `<%= ... %>` output expressions, all while keeping the page readable as HTML with a few extra tags rather than as Java wearing an HTML costume.
By the end of this tutorial you will have a `ReportServlet` that builds a `List<ReportRow>` and puts it in request scope, and a `report.jsp` view that renders it as a table using `<c:forEach>`, highlights negative values with `<c:choose>`/`<c:when>`/`<c:otherwise>`, formats currency and dates with the JSTL `fmt` taglib, and pulls in a shared header and footer through `<jsp:include>` so that markup is not duplicated across every page in the app.
- A `ReportRow` model class representing one line of a financial report.
- A `ReportServlet` that builds sample report data and forwards it to the view in request scope.
- Reusable `header.jspf` and `footer.jspf` fragments pulled in with `<jsp:include>`.
- A `report.jsp` table built with `<c:forEach>`, with conditional row styling via `<c:choose>`.
- Currency and date formatting using the JSTL `fmt` taglib's `<fmt:formatNumber>` and `<fmt:formatDate>`.
Prerequisites
- This course's Student Registration System project — Servlet-populates-model, forwards-to-JSP is reused here unchanged.
- Expression Language basics — `${...}` for reading a bean property or a collection element.
- What a JAR dependency is — this project needs the `jstl` and `taglibs-standard` (or Jakarta EE equivalent) library on the classpath for the `<c:...>`/`<fmt:...>` tags to resolve.
- Basic `List`/POJO familiarity from earlier in this course.
Project Structure
ReportApp/└── src/main/webapp/ ├── report.jsp // Main view: renders the table via JSTL, includes header/footer fragments ├── header.jspf // Reusable fragment: shared page banner ├── footer.jspf // Reusable fragment: shared page footer └── WEB-INF/ ├── lib/ │ └── jakarta.servlet.jsp.jstl-*.jar // JSTL implementation, required for <c:...>/<fmt:...> └── classes/ ├── ReportRow.java // The Model: one row of the report (label, amount, date) └── ReportServlet.java // The Controller: builds the List<ReportRow> and forwards to report.jspThe `.jspf` extension on the header/footer fragments is a common convention (not a hard requirement) for "JSP fragment" files that are never requested directly by a browser, only pulled into a parent page with `<jsp:include>` — much like `WEB-INF/` hides the compiled classes from direct requests in the previous two projects.
Step 1: Define the ReportRow Model
`ReportRow` follows the same JavaBean shape from the Student Registration System project — private fields, a no-arg constructor, and getters — because `<c:forEach>` and EL both read a collection's elements the same reflective way `<jsp:getProperty>` reads a single bean's properties. `amount` is deliberately allowed to be negative, since Step 4's conditional row styling exists specifically to highlight that case.
package model;
import java.util.Date;
// A JavaBean, same shape requirement as Student from the registration// project: EL (${row.label}, ${row.amount}) and <c:forEach> both resolve// properties through getX() methods via reflection, so this must follow the// same private-field + no-arg-constructor + getter/setter convention.public class ReportRow { private String label; // e.g. "Product Sales", "Refunds" private double amount; // Can be negative -- report.jsp highlights negative rows in Step 4 private Date date; // When this line item was recorded
public ReportRow() { // Required no-arg constructor, same reasoning as Student's in the registration project }
public ReportRow(String label, double amount, Date date) { this.label = label; this.amount = amount; this.date = date; }
public String getLabel() { return label; }
public void setLabel(String label) { this.label = label; }
public double getAmount() { return amount; }
public void setAmount(double amount) { this.amount = amount; }
public Date getDate() { return date; }
public void setDate(Date date) { this.date = date; }}Step 2: Populate Report Data in a Servlet
Just like `RegisterServlet` in the first project, `ReportServlet` is the controller: it never renders HTML, it only builds the model — here a `List<ReportRow>` — and attaches it to request scope before forwarding to the view. A real version of this servlet would build the list from a database query instead of hardcoded sample rows, but the JSP side (Steps 3-5) would not need to change at all, since it only ever depends on `report` being a `List<ReportRow>` in request scope, not on where that list came from.
package controllers;
import model.ReportRow;import java.io.IOException;import java.util.ArrayList;import java.util.Calendar;import java.util.List;import javax.servlet.RequestDispatcher;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;
@WebServlet("/report")public class ReportServlet extends HttpServlet {
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { List<ReportRow> report = new ArrayList<>();
// Sample data standing in for a real database query -- report.jsp // only cares that "report" resolves to a List<ReportRow>, never how // that list was built. report.add(new ReportRow("Product Sales", 125000.50, dayOfMonth(1))); report.add(new ReportRow("Service Revenue", 43250.00, dayOfMonth(5))); report.add(new ReportRow("Refunds", -8900.75, dayOfMonth(10))); // Negative: <c:choose> flags this row in Step 4 report.add(new ReportRow("Operating Costs", -32000.00, dayOfMonth(15))); // Also negative report.add(new ReportRow("Interest Income", 1200.25, dayOfMonth(20)));
// request scope: this data lives only for this one request/response cycle, // exactly like the Student bean in the registration project. request.setAttribute("report", report);
RequestDispatcher dispatcher = request.getRequestDispatcher("/report.jsp"); dispatcher.forward(request, response); }
// Small helper building a java.util.Date for the given day of the current month private java.util.Date dayOfMonth(int day) { Calendar cal = Calendar.getInstance(); cal.set(Calendar.DAY_OF_MONTH, day); return cal.getTime(); }}Step 3: Build Reusable Header and Footer Fragments
`<jsp:include>` pulls another page's rendered output into the current page at request time, as if it had been typed inline — it is the JSP equivalent of a reusable UI partial. Both `header.jspf` and `footer.jspf` are plain fragments with no `<html>`/`<body>` wrapper of their own, since they are only ever meant to appear inside a larger page that already provides that structure.
<%-- webapp/header.jspf --%><!-- Shared banner: included by every page in the app so the header only ever needs to be edited in one place --><header> <h1>PrograMinds Reports</h1> <nav><a href="report">Monthly Report</a></nav></header><hr><%-- webapp/footer.jspf --%><!-- Shared footer: same reuse reasoning as header.jspf above --><hr><footer> <p>Generated by ReportServlet — JSP Practice Build</p></footer>`<jsp:include>` is dynamic (evaluated on every request, so it always shows the fragment's current, live output) as opposed to the static `<%@ include file="..." %>` directive, which merges the fragment's source into the including page just once, at translation/compile time. For simple, unchanging boilerplate like this header and footer either would work, but `<jsp:include>` is used here since it is the more common convention for fragments a team may want to change independently of the pages that include them.
Step 4: Render the Table With JSTL core
`<c:forEach var="row" items="${report}">` walks the `List<ReportRow>` the servlet placed in request scope, binding each element to the loop variable `row` for the body of the tag — the direct JSTL equivalent of Java's enhanced `for (ReportRow row : report)`. Inside the loop, `<c:choose>`/`<c:when>`/`<c:otherwise>` picks a CSS class for the row based on whether `row.amount` is negative, mirroring an `if`/`else` without a single scriptlet.
<%-- Requires this taglib directive at the top of report.jsp: --%><%-- <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> --%>
<table border="1"> <thead> <tr><th>Item</th><th>Date</th><th>Amount</th></tr> </thead> <tbody> <%-- var="row" is scoped to just this tag body; items="${report}" reads the List<ReportRow> the servlet attached to request scope --%> <c:forEach var="row" items="${report}"> <c:choose> <%-- row.amount < 0 tests the ReportRow's amount property via EL --%> <c:when test="${row.amount < 0}"> <tr style="color:red;"> <%-- Negative rows: highlighted --%> <td>${row.label}</td> <td>${row.date}</td> <td>${row.amount}</td> </tr> </c:when> <c:otherwise> <tr> <%-- Non-negative rows: default styling --%> <td>${row.label}</td> <td>${row.date}</td> <td>${row.amount}</td> </tr> </c:otherwise> </c:choose> </c:forEach> </tbody></table>The raw `${row.amount}` and `${row.date}` output above is functional but unpolished — a `double` like `-8900.75` renders as a bare number with no currency symbol or thousands separator, and a `Date` renders using its default, not-very-readable `toString()`. Step 5 replaces both with proper `fmt`-tag formatting.
Step 5: Format Numbers and Dates With fmt
The JSTL formatting taglib (prefix `fmt` by convention) adds `<fmt:formatNumber>` for numeric/currency formatting and `<fmt:formatDate>` for date formatting, both of which read from a `value` attribute via EL exactly like `<c:forEach>`'s `items` did. `<c:if test="${not empty report}">` around the whole table additionally guards against rendering an empty `<table>` shell if `report` were ever an empty list.
<%-- Requires this additional taglib directive at the top of report.jsp: --%><%-- <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> --%>
<c:if test="${not empty report}"> <table border="1"> <thead> <tr><th>Item</th><th>Date</th><th>Amount</th></tr> </thead> <tbody> <c:forEach var="row" items="${report}"> <c:choose> <c:when test="${row.amount < 0}"> <tr style="color:red;"> <td>${row.label}</td> <td><fmt:formatDate value="${row.date}" pattern="MMM d, yyyy" /></td> <%-- type="currency" adds the currency symbol and thousands separators automatically --%> <td><fmt:formatNumber value="${row.amount}" type="currency" /></td> </tr> </c:when> <c:otherwise> <tr> <td>${row.label}</td> <td><fmt:formatDate value="${row.date}" pattern="MMM d, yyyy" /></td> <td><fmt:formatNumber value="${row.amount}" type="currency" /></td> </tr> </c:otherwise> </c:choose> </c:forEach> </tbody> </table></c:if><c:if test="${empty report}"> <p>No report data available.</p> <%-- Reached only if the servlet ever forwards an empty list --%></c:if>Complete Code
Here is every file assembled together, including the two taglib directives and the `<jsp:include>` calls that pull in `header.jspf` and `footer.jspf`.
// ---------- WEB-INF/classes/model/ReportRow.java ----------package model;
import java.util.Date;
public class ReportRow { private String label; private double amount; private Date date;
public ReportRow() { }
public ReportRow(String label, double amount, Date date) { this.label = label; this.amount = amount; this.date = date; }
public String getLabel() { return label; }
public void setLabel(String label) { this.label = label; }
public double getAmount() { return amount; }
public void setAmount(double amount) { this.amount = amount; }
public Date getDate() { return date; }
public void setDate(Date date) { this.date = date; }}
// ---------- WEB-INF/classes/controllers/ReportServlet.java ----------package controllers;
import model.ReportRow;import java.io.IOException;import java.util.ArrayList;import java.util.Calendar;import java.util.List;import javax.servlet.RequestDispatcher;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;
@WebServlet("/report")public class ReportServlet extends HttpServlet {
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { List<ReportRow> report = new ArrayList<>();
report.add(new ReportRow("Product Sales", 125000.50, dayOfMonth(1))); report.add(new ReportRow("Service Revenue", 43250.00, dayOfMonth(5))); report.add(new ReportRow("Refunds", -8900.75, dayOfMonth(10))); report.add(new ReportRow("Operating Costs", -32000.00, dayOfMonth(15))); report.add(new ReportRow("Interest Income", 1200.25, dayOfMonth(20)));
request.setAttribute("report", report);
RequestDispatcher dispatcher = request.getRequestDispatcher("/report.jsp"); dispatcher.forward(request, response); }
private java.util.Date dayOfMonth(int day) { Calendar cal = Calendar.getInstance(); cal.set(Calendar.DAY_OF_MONTH, day); return cal.getTime(); }}<%-- ---------- webapp/report.jsp ---------- --%><%@ page contentType="text/html;charset=UTF-8" %><%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %><!DOCTYPE html><html><head> <title>Monthly Report</title></head><body> <jsp:include page="header.jspf" />
<h2>Monthly Financial Report</h2>
<c:if test="${not empty report}"> <table border="1"> <thead> <tr><th>Item</th><th>Date</th><th>Amount</th></tr> </thead> <tbody> <c:forEach var="row" items="${report}"> <c:choose> <c:when test="${row.amount < 0}"> <tr style="color:red;"> <td>${row.label}</td> <td><fmt:formatDate value="${row.date}" pattern="MMM d, yyyy" /></td> <td><fmt:formatNumber value="${row.amount}" type="currency" /></td> </tr> </c:when> <c:otherwise> <tr> <td>${row.label}</td> <td><fmt:formatDate value="${row.date}" pattern="MMM d, yyyy" /></td> <td><fmt:formatNumber value="${row.amount}" type="currency" /></td> </tr> </c:otherwise> </c:choose> </c:forEach> </tbody> </table> </c:if> <c:if test="${empty report}"> <p>No report data available.</p> </c:if>
<jsp:include page="footer.jspf" /></body></html>
<!-- ---------- webapp/header.jspf ---------- --><header> <h1>PrograMinds Reports</h1> <nav><a href="report">Monthly Report</a></nav></header><hr>
<!-- ---------- webapp/footer.jspf ---------- --><hr><footer> <p>Generated by ReportServlet — JSP Practice Build</p></footer>Sample Run
Click Run to see what this code prints.
Extend This Project
- Add a totals row at the bottom of the table using `<c:set>` to accumulate a running sum across the `<c:forEach>` loop, formatted with `<fmt:formatNumber type="currency">`.
- Add a `<c:forEach varStatus="loopStatus">` and use `loopStatus.count` to render an alternating-shade "zebra striped" table instead of only flagging negative rows.
- Move `header.jspf`/`footer.jspf` into a real JSP tag file (`.tag`) or a `<%@ include %>` directive and compare the tradeoffs against `<jsp:include>`.
- Add a `<fmt:setLocale>` near the top of `report.jsp` and observe how `<fmt:formatNumber type="currency">` and `<fmt:formatDate>` change output for a different locale.
- Replace the hardcoded sample data in `ReportServlet` with a real JDBC query against a transactions table.
Summary
You built a fully dynamic report page without a single Java scriptlet in the JSP itself: `<c:forEach>` looped over a `List<ReportRow>` from request scope, `<c:choose>`/`<c:when>`/`<c:otherwise>` picked row styling conditionally, the `fmt` taglib formatted currency and dates properly, and `<jsp:include>` kept the header and footer defined once and reused everywhere. This JSTL-plus-EL style — tags and `${...}` expressions instead of embedded Java — is the standard, maintainable way real JSP views are written today.