JSP, JSTL & the View Layer

JSTL Formatting & Functions

18 min Lesson 6 of 13

JSTL Formatting & Functions

The previous lesson covered JSTL Core tags — the control-flow backbone of any JSP view. This lesson moves into two equally practical libraries: the fmt tag library for locale-aware number and date formatting, and the fn function library for string manipulation inside EL expressions. Together they eliminate the ugly Java scriptlet code that developers historically stuffed into JSP pages to format output.

Setting Up: Adding the fmt and fn Libraries

Both libraries ship in the same jakarta.servlet.jsp.jstl JAR (JSTL 3.x for Jakarta EE 10+). You already have it from the Core lesson. Declare them at the top of each JSP that needs them:

<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %> <%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
JSTL 3 vs JSTL 1.2: Jakarta EE 10 renamed every URI from http://java.sun.com/jsp/jstl/... to jakarta.tags.*. If you are on a legacy Tomcat 9 / Java EE stack you still use the old URIs. The tag behaviour is otherwise identical.

Locale and Time Zone Scoping with fmt:setLocale and fmt:setTimeZone

All fmt formatting tags respect the current locale and time zone, which you set once per page (or per request) rather than repeating in every tag:

<!-- fix locale for the whole page --> <fmt:setLocale value="${sessionScope.userLocale}" /> <!-- fix time zone for the whole page --> <fmt:setTimeZone value="${sessionScope.userTimeZone}" />

When neither tag appears, the JSP engine falls back to the server JVM locale and UTC, which is almost never what end users want. Always set both explicitly in production views.

Formatting Numbers with fmt:formatNumber

<fmt:formatNumber> converts any numeric value into a locale-correct string. Its type attribute drives the output style:

  • type="number" — plain decimal with grouping separators (default)
  • type="currency" — currency symbol + grouping + decimal places from the locale
  • type="percent" — multiplies by 100 and appends a percent sign
<fmt:setLocale value="en_US" /> <p>Price: <fmt:formatNumber value="${product.price}" type="currency" /></p> <p>Discount: <fmt:formatNumber value="${product.discount}" type="percent" /></p> <p>Stock: <fmt:formatNumber value="${product.stock}" type="number" groupingUsed="true" /></p> <p>Weight: <fmt:formatNumber value="${product.weightKg}" type="number" minFractionDigits="2" maxFractionDigits="2" /> kg</p>

With en_US locale and price = 1299.9, the currency tag produces $1,299.90. Switching to de_DE produces 1.299,90 € — the tag handles both symbol and punctuation conventions automatically.

Use currencyCode to override the currency symbol. When your data stores an amount in one currency but the user's locale is different, add currencyCode="EUR" (or any ISO 4217 code) to display the correct symbol regardless of locale.

To capture the formatted string into a variable instead of writing it immediately, use the var and scope attributes — a pattern shared by most JSTL tags:

<fmt:formatNumber value="${order.total}" type="currency" var="formattedTotal" scope="request" /> <p>Your total: ${formattedTotal}</p>

Parsing Numbers with fmt:parseNumber

The reverse operation — turning a locale-formatted string back into a number — is handled by <fmt:parseNumber>. You rarely need this in a view layer, but it is useful in filters or tag files that receive string parameters from query strings:

<fmt:parseNumber value="1.299,90" type="number" parseLocale="de_DE" var="amount" /> <!-- ${amount} is now the Java double 1299.9 -->

Formatting Dates and Times with fmt:formatDate

<fmt:formatDate> accepts a java.util.Date (or a value that resolves to one through EL coercion) and formats it according to the locale and time zone in scope. The type attribute controls which part of the timestamp is shown:

<fmt:setLocale value="en_US" /> <fmt:setTimeZone value="America/New_York" /> <p>Date only: <fmt:formatDate value="${order.createdAt}" type="date" dateStyle="long" /></p> <p>Time only: <fmt:formatDate value="${order.createdAt}" type="time" timeStyle="short" /></p> <p>Date and time: <fmt:formatDate value="${order.createdAt}" type="both" dateStyle="medium" timeStyle="medium" /></p> <p>Custom: <fmt:formatDate value="${order.createdAt}" pattern="yyyy-MM-dd HH:mm" /></p>

The dateStyle and timeStyle attributes accept short, medium, long, and full — the same constants as java.text.DateFormat. The pattern attribute takes a SimpleDateFormat pattern and overrides style attributes when both are supplied.

java.util.Date is legacy. Modern Java code uses java.time.* (LocalDate, ZonedDateTime, Instant). fmt:formatDate only understands java.util.Date, so you must convert: Date.from(instant) or Date.from(localDate.atStartOfDay(ZoneId.of("UTC")).toInstant()). In Spring MVC you can register a custom converter so that EL resolves java.time objects directly, but fmt:formatDate itself still needs the old type.

Parsing Dates with fmt:parseDate

The companion tag <fmt:parseDate> converts a string back into a java.util.Date:

<fmt:parseDate value="${param.dob}" pattern="yyyy-MM-dd" var="dobDate" /> <!-- ${dobDate} is now a java.util.Date -->

Internationalising Text with fmt:message

The fmt library also owns the i18n message tag. Load a resource bundle and look up keys by name:

<fmt:setBundle basename="messages" /> <fmt:message key="welcome.title" /> <fmt:message key="order.confirmation"> <fmt:param value="${order.id}" /> </fmt:message>

The bundle file messages_en_US.properties would contain lines such as order.confirmation=Your order #{0} has been placed.. This integrates with Java's MessageFormat placeholders.

The fn Function Library

The fn library provides string and collection utility functions that can be called directly inside EL expressions. There is no separate tag — every function is invoked as ${fn:functionName(arg1, arg2)}.

The most commonly used functions:

  • fn:length(collection) — length of a String, array, or Collection
  • fn:toUpperCase(str) / fn:toLowerCase(str)
  • fn:trim(str)
  • fn:substring(str, begin, end) — zero-based, exclusive end (mirrors String.substring)
  • fn:substringBefore(str, delimiter) / fn:substringAfter(str, delimiter)
  • fn:replace(str, before, after)
  • fn:split(str, delimiter) — returns a String[]
  • fn:join(array, separator) — inverse of split
  • fn:contains(str, substring) — case-sensitive; returns boolean
  • fn:containsIgnoreCase(str, substring)
  • fn:startsWith(str, prefix) / fn:endsWith(str, suffix)
  • fn:indexOf(str, substring)
  • fn:escapeXml(str) — entity-escapes <, >, &, ", '

fn Functions in Practice

Practical snippet: display a truncated product description with an ellipsis, guard against null, and show a badge only when a tag list is non-empty:

<c:if test="${fn:length(product.description) gt 120}"> <p>${fn:substring(product.description, 0, 120)}...</p> </c:if> <c:if test="${fn:length(product.description) le 120}"> <p>${product.description}</p> </c:if> <c:if test="${fn:length(product.tags) gt 0}"> <ul class="tags"> <c:forEach items="${product.tags}" var="tag"> <li>${fn:toLowerCase(fn:trim(tag))}</li> </c:forEach> </ul> </c:if> <!-- safely render user-supplied text --> <p>${fn:escapeXml(review.body)}</p>
Always use fn:escapeXml on user-supplied content. Rendering raw user input through ${review.body} without escaping is a stored XSS vulnerability. fn:escapeXml is the last line of defence in the view layer.

Combining fmt and fn Together

Functions from fn compose with fmt tags through the var attribute. A common pattern is to build a string with fn, store it in a variable, then pass it as a parameter to a message tag:

<fmt:formatNumber value="${item.unitPrice}" type="currency" var="priceStr" /> <fmt:formatDate value="${item.shipDate}" pattern="MMM d, yyyy" var="dateStr" /> <fmt:message key="item.summary"> <fmt:param value="${fn:toUpperCase(item.sku)}" /> <fmt:param value="${priceStr}" /> <fmt:param value="${dateStr}" /> </fmt:message>

Trade-offs and When to Move Logic to the Controller

The fmt and fn libraries handle presentation-only transformations cleanly in the view. However, there are cases where you should push formatting back to the Java layer:

  • Reuse across multiple views — if six different JSPs display a price the same way, format it once in the controller and pass a pre-formatted string, or use a shared tag file.
  • Business logic in the format — choosing between tax-inclusive and tax-exclusive prices based on user geography is not presentation; it belongs in a service class.
  • java.time types — format with DateTimeFormatter in the controller and expose a plain String to the view to avoid the java.util.Date conversion workaround.
Keep JSPs dumb. The fmt and fn libraries are for how something looks, not what it means. Conditional formatting based on business rules (free shipping threshold, overdue invoice colour) is better expressed in a Java helper or a Spring ViewHelper bean where it is testable.

Summary

The fmt library gives you locale-aware, time-zone-correct formatting for numbers, currencies, percentages, and dates with zero Java in the page. The fn library brings string utilities — length, substring, trim, replace, split, join, escapeXml — directly into EL expressions. Together they cover nearly every output-formatting need in a JSP view layer without a single scriptlet. The next lesson examines the MVC pattern that governs how Servlets and JSPs collaborate.