Skip to content

TOMEE-4651 - register the jakarta.tags.* TLD URIs - #2844

Open
jungm wants to merge 3 commits into
apache:mainfrom
jungm:issue/TOMEE-4651
Open

TOMEE-4651 - register the jakarta.tags.* TLD URIs#2844
jungm wants to merge 3 commits into
apache:mainfrom
jungm:issue/TOMEE-4651

Conversation

@jungm

@jungm jungm commented Jul 21, 2026

Copy link
Copy Markdown
Member

What

Registers the five Jakarta Tags 3.0 taglib URIs (jakarta.tags.core, .fmt, .sql, .xml, .functions) so JSP pages can actually use them.

Those are the five the spec renamed. permittedTaglibs and scriptfree are Apache-Standard-Taglibs TLVs that kept their http://jakarta.apache.org/taglibs/standard/* URIs, so there is no jakarta.tags.* spelling to register for them.

Why

Jakarta Tags 3.0 renamed the JSTL taglib URIs from the legacy http://java.sun.com/jsp/jstl/* form to the short jakarta.tags.* form.

The taglibs-standard implementation we shade in deps/taglibs-shade predates that rename. The shade only rewrites bytecode (javax.servletjakarta.servlet); it does not touch the TLD descriptors, so the shipped TLDs still declare the legacy URIs only. Confirmed by inspecting the jar — none of its 12 TLDs contains a jakarta.tags.* URI.

The result is that any page containing

<%@ taglib uri="jakarta.tags.core" prefix="c" %>

fails at JSP compile time. This breaks the entire Jakarta Tags 3.0 TCK, and the EJB Lite JSP vehicles as a side effect since their shared harness imports the same URI.

How

TomEETldScanner already pre-populates the TLD cache with the container's own taglibs so they don't have to be parsed on every deployment. This adds the Jakarta Tags URIs as additional keys for those same resources, plus widens the permittedTaglibs validator's URI list so a page guarded by that TLV may use either spelling.

Note this claims jakarta.tags.* for the container. TldScanner.scan() runs scanPlatform() first, parseTld() only registers a URI it does not already know, and scanJspConfig() warns and skips a taglib whose URI is taken — so an application bundling its own Jakarta Tags implementation, or overriding via <jsp-config><taglib>, no longer wins. That matches how the legacy http://java.sun.com/jsp/jstl/* URIs have always behaved here, and is pinned by a test driving the real parseTld().

Two details worth a reviewer's attention:

  • The aliases reuse the very same TldResourcePath instance rather than constructing new ones. scanPlatform() tells myfaces and jstl entries apart by reference identity on the resource path's URL, and TLD_RESOURCE_TAG_LIB is keyed on it. Both are load-bearing and commented in the code.
  • The aliasing lives outside the dumped data block, keeping hand-written logic separate from the pasted TLD metadata. It is a no-op when JSTL is absent (JSTL_URL == null), degrading to normal scanning rather than failing.

I also corrected the class-level comment. It claimed the file is "generated so we don't really care", which is misleading: there is no generator in the build. The data was dumped once by the sysout() method kept commented out at the bottom of the same file and pasted in, and the file has been hand-edited several times since. That sysout() is itself stale — it still recognises JSTL entries by the taglibs-standard-jstlel jar name whereas the shipped jar is taglibs-shade-*.jar — so it can no longer reproduce the file. The new comment says so.

Considered and rejected

Switching to a Jakarta-native implementation (org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1) would be the more principled fix — it ships both namespaces, keeps the same org.apache.taglibs.standard.* packages, and self-shades Xalan. Apache Taglibs itself never released a Jakarta version (1.2.5 is still latest), which is why the shade exists at all. But the swap touches ~40 sites across BOMs, assembly descriptors, jarsToSkip patterns and jar-name filters, so it does not belong in a bug fix. Probably worth its own issue.

Testing

Unit tests in TomEETldScannerJakartaTagsTest cover the alias mapping, legacy URIs still resolving, the validator widening, idempotency, and the no-JSTL no-op. Note these drive the aliasing against a synthetic resource map via reflection, because the shaded taglibs jar is not on that module's test classpath.

The real proof is the TCK. Built the plume distro, confirmed the fix is in the shipped tomee-catalina jar, and ran the Jakarta EE 11 Web Profile TCK tags partition from apache/tomee-tck with the exclusion lifted:

Before After
Tags suite all 50 classes fail JSP compilation 541 tests, 482 passing, 0 taglib resolution errors
JSTL 1.0 compatibility class 37 of 38 methods fail 33 of 38 pass
Tags partition, non-TLD gaps excluded 381 tests, 0 failures, 0 errors
EJB Lite JSP vehicles HTTP 500 before EJB assertions run 9 of 9 ClientEjblitejspTest green

The shipped jar's c.tld still declares only <uri>http://java.sun.com/jsp/jstl/core</uri>, so the fact that jakarta.tags.core pages now compile can only come from this change.

The 8 classes still failing are unrelated to TLD registration: the five sql/* classes and the compatibility class hit SQLSyntaxErrorException: Table/View 'JSTL_TAB1' does not exist (the harness never creates those Derby tables), and xmlcore.parse / xtransform.transform fail resolving an external systemId. A matching tomee-tck change narrows exclusions/tags.txt to just those.

🤖 Generated with Claude Code

Jakarta Tags 3.0 renamed the JSTL taglib URIs from the legacy
http://java.sun.com/jsp/jstl/* form to the short jakarta.tags.* form.

The taglibs-standard implementation shaded in deps/taglibs-shade predates
that rename. The shade only rewrites bytecode (javax.servlet to
jakarta.servlet), it does not touch the TLD descriptors, so the shipped
TLDs still declare the legacy URIs only. Any page with

    <%@ taglib uri="jakarta.tags.core" %>

therefore failed at JSP compile time.

Register the seven Jakarta Tags URIs as additional keys for the TLD
resources TomEETldScanner already pre-populates for their legacy
spellings, and widen the permittedTaglibs validator so a page guarded by
that TLV may use either spelling.

The aliases reuse the very same TldResourcePath instance: scanPlatform()
tells myfaces and jstl entries apart by reference identity on the
resource path's URL, and TLD_RESOURCE_TAG_LIB is keyed on it. The
aliasing is kept out of the dumped data block so hand-written logic stays
separate from the pasted TLD metadata, and is a no-op when JSTL is absent.

Verified against the Jakarta EE 11 Web Profile TCK, tags partition. With
the exclusion lifted the suite goes from all 50 classes failing JSP
compilation to 482 of 541 tests passing with no taglib resolution errors
left; the 8 classes still failing need Derby tables the harness does not
create, or fail resolving an external systemId. The EJB Lite JSP vehicles
that broke as a side effect pass again.
@jungm
jungm requested a review from rzo1 July 21, 2026 06:10
- restrict the alias map to the five URIs Jakarta Tags 3.0 actually renamed;
  permittedTaglibs/scriptfree are Apache-Standard-Taglibs TLVs that kept their
  legacy URIs, so aliasing them advertised keys the spec never defines
- log a warning when a legacy URI has no TLD registered, so a taglibs upgrade
  that renames one is observable instead of silently dropping the alias
- drop the unreachable null-checks in permitJakartaTagsUris(); ValidatorXml
  always has init params and the permittedTaglibs TLD always has a validator
- scope the PMD suppression to the pasted data block so the hand-written
  methods are analysed again
- cover scanPlatform() forwarding the aliases into the per-deployment map
@rzo1

rzo1 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Confirmed the premise against the shipped jar: taglibs-shade-11.0.0-SNAPSHOT.jar
declares only the legacy URIs in all 16 TLDs, so <%@ taglib uri="jakarta.tags.core" %>
fails at compile time. Aliasing onto the same TldResourcePath instances is the right
call and is actually load-bearing — scanPlatform() discriminates myfaces vs jstl by
reference identity on path.getUrl().

The ordering claim in the description is backwards, though, and that's the main thing
I want addressed:

  • I decompiled org.apache.jasper.servlet.TldScanner from tomcat-jasper 11.0.21.
    scan() runs scanPlatform() first, and parseTld() only registers a URI
    if (!uriTldResourcePathMap.containsKey(uri)) — first writer wins. scanJspConfig()
    is stricter still: it checks containsKey(taglibURI) and, if present, logs a warning
    and skips the mapping entirely.

    So a WAR bundling a Jakarta-native JSTL does not still win. After this change,
    jakarta.tags.* is claimed by the container's shaded taglibs-standard-1.2 TLD for
    every deployment. Concretely, an app that works around TOMEE-4651 today by bundling
    org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1 in WEB-INF/lib gets its TLDs
    silently ignored (that jar isn't in default.exclusions, so it's scanned, parsed and
    discarded), and an explicit <jsp-config><taglib> override in web.xml is ignored with
    a warning. That removes the only workaround users currently have.

    It's arguably consistent with how the legacy http://java.sun.com/jsp/jstl/* URIs
    already behave, so I'm not asking you to change the precedence — but it needs a note
    in the commit message and ideally a test pinning the intended precedence.

Smaller ones:

  • The no-op guard tests map emptiness rather than JSTL presence, so it logs spurious
    WARNINGs on a distribution where MyFaces is present but JSTL is not.
  • permittedTaglibs is split on "\n", but the value is whitespace-separated per the
    TLV contract — split on \s+.
  • Nothing asserts aliasJakartaTagsUris() is actually wired into the static
    initialiser; unwiring it would leave the test green.
  • Description says seven URIs, the code registers five.

…V split

Follow-up on review feedback.

Registering jakarta.tags.* in scanPlatform() claims those URIs for the
container: TldScanner.scan() runs scanPlatform() first, parseTld() only
registers a URI it does not already know, and scanJspConfig() logs a
warning and skips a taglib whose URI is taken. An application bundling a
Jakarta-native JSTL in WEB-INF/lib therefore no longer overrides these,
and neither does an explicit <jsp-config><taglib> in web.xml.

That is kept as-is because it matches how the legacy
http://java.sun.com/jsp/jstl/* URIs have always behaved here, but it is
now stated in the javadoc and pinned by a test driving the real
parseTld() so the assertion tracks Tomcat rather than restating it.

Also:

- guard the aliasing on JSTL_URL rather than on URI_TLD_RESOURCE being
  empty. myfaces is pre-populated independently, so on a distribution
  carrying myfaces but no JSTL the emptiness check fell through and
  logged a warning for every alias. Takes the URL as a parameter so the
  guard is reachable from a test: JSTL_URL is static final and modern
  JDKs reject reflective writes to it.
- split permittedTaglibs on any whitespace. PermittedTaglibsTLV tokenizes
  it with a plain StringTokenizer, so a space-separated list came back as
  one token and the widening silently did nothing.
- assert aliasJakartaTagsUris() is called from the static initialiser.
  Every other test drives it by hand and stayed green when it was
  unwired.

Each of these was confirmed by mutating the fix and watching the matching
test fail.
@jungm

jungm commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Thanks — I verified all of it independently and you're right on every count. Pushed in 164b44b.

Precedence. Confirmed against tomcat-jasper 11.0.23 bytecode: scan() calls scanPlatform() first, parseTld() guards its put with containsKey + ifne (first writer wins), and scanJspConfig() does containsKeyLog.warn → skip. So the description was backwards and a bundled Jakarta-native JSTL does not win.

Taking your steer and keeping the precedence as-is, since it matches how the legacy http://java.sun.com/jsp/jstl/* URIs already behave. It is now stated in the aliasJakartaTagsUris() javadoc and in the commit message, and pinned by containerAliasWinsOverAnApplicationBundledTld, which drives the real parseTld() with an app TLD declaring jakarta.tags.core — so the assertion tracks Tomcat rather than restating my reading of it.

The isEmpty() guard. Real bug, exactly as you described: populateMyfacesAndJstlData() guards myfaces (L125) and JSTL (L4142) independently, so myfaces-without-JSTL left the map non-empty and warned five times. Now guards on JSTL_URL. To make that reachable from a test I added a package-private aliasJakartaTagsUris(URL) overload — JSTL_URL is static final and modern JDKs reject reflective writes, so stubbing it wasn't an option. The test asserts zero warnings, which is what actually distinguishes the two guards.

permittedTaglibs split. Confirmed — PermittedTaglibsTLV uses a single-arg StringTokenizer, i.e. any whitespace. Split is now \s+; a space-separated list previously came back as one token so the widening silently no-oped. The test helper read it back with the same wrong delimiter, which is why nothing caught it.

Static-initialiser wiring. Added staticInitialiserRegistersTheAliases. Since the shaded jar isn't on the module's test classpath, it asserts over <clinit> bytecode that aliasJakartaTagsUris is called and that populateMyfacesAndJstlData precedes it.

Seven vs five. Stale — bfc0e16 already cut the map to the five URIs the spec actually renamed (permittedTaglibs/scriptfree are Apache-Standard-Taglibs TLVs that kept their URIs). The PR description still said seven; I've corrected it.

I mutation-tested each fix — reverting the guard, the split, the wiring, and the precedence each makes exactly the corresponding test fail. Module suite is 19/19.

🤖 Addressed by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants