This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch tomee-10.x in repository https://gitbox.apache.org/repos/asf/tomee.git
commit 32e370b1cfafecaaac0cd1d9700768c2be01f390 Author: Markus Jung <[email protected]> AuthorDate: Fri Jul 31 16:17:48 2026 +0200 TOMEE-4651 - register the jakarta.tags.* TLD URIs (#2844) (cherry picked from commit 580a5feed5ece87e1ab34b68301e91aac7c4fefd) --- tomee/tomee-catalina/pom.xml | 6 + .../org/apache/tomee/jasper/TomEETldScanner.java | 124 +++++- .../jasper/TomEETldScannerJakartaTagsTest.java | 460 +++++++++++++++++++++ 3 files changed, 589 insertions(+), 1 deletion(-) diff --git a/tomee/tomee-catalina/pom.xml b/tomee/tomee-catalina/pom.xml index 67a8eedcb2..8fa8a0393e 100644 --- a/tomee/tomee-catalina/pom.xml +++ b/tomee/tomee-catalina/pom.xml @@ -135,5 +135,11 @@ <groupId>org.apache.geronimo.components</groupId> <artifactId>geronimo-transaction</artifactId> </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>openejb-mockito</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> </dependencies> </project> diff --git a/tomee/tomee-catalina/src/main/java/org/apache/tomee/jasper/TomEETldScanner.java b/tomee/tomee-catalina/src/main/java/org/apache/tomee/jasper/TomEETldScanner.java index 1d2657a4c9..8aee2e17a6 100644 --- a/tomee/tomee-catalina/src/main/java/org/apache/tomee/jasper/TomEETldScanner.java +++ b/tomee/tomee-catalina/src/main/java/org/apache/tomee/jasper/TomEETldScanner.java @@ -27,24 +27,65 @@ import org.apache.tomee.installer.Paths; import java.io.File; import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.logging.Logger; import jakarta.servlet.ServletContext; import jakarta.servlet.jsp.tagext.FunctionInfo; import jakarta.servlet.jsp.tagext.TagAttributeInfo; import static org.apache.openejb.loader.JarLocation.jarLocation; -@SuppressWarnings("PMD") // this is generated so we don't really care +/** + * Pre-populates the TLD cache with the container's own taglibs (myfaces, jstl) so they don't have to + * be parsed on every deployment; see {@code TomEEJarScanner.TomEEFilter} which skips those jars. + * <p> + * The bulk of {@link #populateMyfacesAndJstlData()} was dumped once from a running server by the + * {@code sysout()} method kept commented out at the bottom of this file, then pasted in. It is not + * generated by the build and is maintained by hand -- note that {@code sysout()} itself is stale: it + * still recognises JSTL entries by the {@code taglibs-standard-jstlel} jar name, whereas the shipped + * jar is {@code taglibs-shade-*.jar}. + */ public class TomEETldScanner extends TldScanner { + private static final Logger LOGGER = Logger.getLogger(TomEETldScanner.class.getName()); private static final Paths PATHS = new Paths(null); private static final URL MYFACES_URL = findJar("myfaces-impl", "org.apache.myfaces.webapp.AbstractFacesInitializer"); private static final URL JSTL_URL = findJar("taglibs-standard-impl", "jakarta.servlet.jsp.jstl.core.ConditionalTagSupport"); private static final Map<String, TldResourcePath> URI_TLD_RESOURCE = new HashMap<>(); private static final Map<TldResourcePath, TaglibXml> TLD_RESOURCE_TAG_LIB = new HashMap<>(); + /** + * TOMEE-4651: Jakarta Tags 3.0 renamed the JSTL taglib URIs from the legacy + * {@code http://java.sun.com/jsp/jstl/*} form to the short {@code jakarta.tags.*} form. + * <p> + * The taglibs-standard implementation we shade in {@code deps/taglibs-shade} predates that rename: + * the shade only rewrites bytecode ({@code javax.servlet} to {@code jakarta.servlet}), it does not + * touch the TLD descriptors, so its TLDs still declare the legacy URIs only. Without this mapping + * every {@code <%@ taglib uri="jakarta.tags.core" %>} fails at JSP compile time. + * <p> + * Keys are the Jakarta Tags 3.0 URIs, values the legacy URI whose TLD resource they resolve to. + * The pairing mirrors what a Jakarta-native JSTL implementation ships, where both spellings are + * served by the same TLD. + * <p> + * Only the five functional taglibs are listed: the spec renamed those alone. The + * {@code permittedTaglibs} and {@code scriptfree} TLVs are Apache-Standard-Taglibs extensions that + * kept their {@code http://jakarta.apache.org/taglibs/standard/*} URIs, so there is no + * {@code jakarta.tags.*} spelling for them to alias. + */ + private static final Map<String, String> JAKARTA_TAGS_URI_ALIASES = new HashMap<>(); + static { + JAKARTA_TAGS_URI_ALIASES.put("jakarta.tags.core", "http://java.sun.com/jsp/jstl/core"); + JAKARTA_TAGS_URI_ALIASES.put("jakarta.tags.fmt", "http://java.sun.com/jsp/jstl/fmt"); + JAKARTA_TAGS_URI_ALIASES.put("jakarta.tags.sql", "http://java.sun.com/jsp/jstl/sql"); + JAKARTA_TAGS_URI_ALIASES.put("jakarta.tags.xml", "http://java.sun.com/jsp/jstl/xml"); + JAKARTA_TAGS_URI_ALIASES.put("jakarta.tags.functions", "http://java.sun.com/jsp/jstl/functions"); + populateMyfacesAndJstlData(); + aliasJakartaTagsUris(); } private final Map<String, TldResourcePath> uriTldResourcePathMapParent; @@ -78,6 +119,7 @@ public class TomEETldScanner extends TldScanner { } //CHECKSTYLE:OFF + @SuppressWarnings("PMD") // huge pasted data block, style rules aren't useful here private static void populateMyfacesAndJstlData() { // pre-populate with shared libraries (myfaces, jstl) if (MYFACES_URL != null) { @@ -7195,6 +7237,86 @@ public class TomEETldScanner extends TldScanner { } //CHECKSTYLE:ON + /** + * Registers the Jakarta Tags 3.0 URIs as additional keys for the TLD resources that + * {@link #populateMyfacesAndJstlData()} already pre-populated for their legacy spellings. + * <p> + * Deliberately kept out of the dumped block above, to keep hand-written logic separate from the + * pasted data. It is a no-op when JSTL was not pre-populated (no shaded taglibs in {@code lib/}), in + * which case TLD resolution falls back to plain scanning. Once JSTL is there, a legacy URI that + * cannot be found is a broken mapping rather than an absent JSTL, and is logged. + * <p> + * Note this claims the {@code jakarta.tags.*} URIs for the container: {@code TldScanner.scan()} + * runs {@code scanPlatform()} first and later registrations are first-writer-wins, so an + * application bundling its own Jakarta Tags implementation no longer overrides these. That matches + * how the legacy {@code http://java.sun.com/jsp/jstl/*} URIs have always behaved here. + * + * @see #JAKARTA_TAGS_URI_ALIASES + */ + private static void aliasJakartaTagsUris() { + aliasJakartaTagsUris(JSTL_URL); + } + + /** + * @param jstlUrl the resolved JSTL jar, or {@code null} when JSTL is not available + */ + private static void aliasJakartaTagsUris(final URL jstlUrl) { + // JSTL was not pre-populated (no shaded taglibs in lib/), so there is nothing to alias and a + // missing legacy URI below would be expected rather than a broken mapping. Guard on JSTL + // rather than on the map being empty: myfaces is populated independently and would mask this. + if (jstlUrl == null) { + return; + } + + for (final Map.Entry<String, String> alias : JAKARTA_TAGS_URI_ALIASES.entrySet()) { + final String legacyUri = alias.getValue(); + final TldResourcePath path = URI_TLD_RESOURCE.get(legacyUri); + if (path == null) { + // the data block above no longer carries this legacy URI -- most likely a taglibs + // upgrade renamed it. Aliasing silently degrades, so make it observable. + LOGGER.warning("Cannot register '" + alias.getKey() + "': no TLD registered for '" + legacyUri + + "'. JSP pages using that URI will fall back to scanning."); + continue; + } + + // reuse the very same 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 + URI_TLD_RESOURCE.put(alias.getKey(), path); + } + + permitJakartaTagsUris(); + } + + /** + * The {@code permittedTaglibs} TLV restricts pages to a fixed list of taglib URIs, and the shaded + * TLD only lists the legacy spellings. Adds the Jakarta Tags equivalents so a page guarded by that + * validator may use either spelling. + */ + private static void permitJakartaTagsUris() { + final TldResourcePath path = URI_TLD_RESOURCE.get("http://jakarta.apache.org/taglibs/standard/permittedTaglibs"); + if (path == null) { + return; + } + + // the permittedTaglibs TLD always declares a validator, and ValidatorXml always has init params + final ValidatorXml validator = TLD_RESOURCE_TAG_LIB.get(path).getValidator(); + final String permitted = validator.getInitParams().get("permittedTaglibs"); + if (permitted == null) { + return; + } + + // PermittedTaglibsTLV tokenizes this with a plain StringTokenizer, i.e. on any whitespace -- + // splitting on "\n" alone would leave a space-separated list as one unmatchable token + final List<String> permittedUris = new ArrayList<>(Arrays.asList(permitted.trim().split("\\s+"))); + for (final Map.Entry<String, String> alias : JAKARTA_TAGS_URI_ALIASES.entrySet()) { + // only widen the list where the legacy spelling is already permitted, and stay idempotent + if (permittedUris.contains(alias.getValue()) && !permittedUris.contains(alias.getKey())) { + permittedUris.add(alias.getKey()); + } + } + validator.addInitParam("permittedTaglibs", String.join("\n", permittedUris)); + } + private static URL findJar(final String s, final String api) { try { final File tomEELibJar = PATHS.findTomEELibJar(s); diff --git a/tomee/tomee-catalina/src/test/java/org/apache/tomee/jasper/TomEETldScannerJakartaTagsTest.java b/tomee/tomee-catalina/src/test/java/org/apache/tomee/jasper/TomEETldScannerJakartaTagsTest.java new file mode 100644 index 0000000000..1e5f721f06 --- /dev/null +++ b/tomee/tomee-catalina/src/test/java/org/apache/tomee/jasper/TomEETldScannerJakartaTagsTest.java @@ -0,0 +1,460 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomee.jasper; + +import org.apache.jasper.servlet.TldScanner; +import org.apache.tomcat.util.descriptor.tld.TaglibXml; +import org.apache.xbean.asm9.ClassReader; +import org.apache.xbean.asm9.ClassVisitor; +import org.apache.xbean.asm9.MethodVisitor; +import org.apache.xbean.asm9.Opcodes; +import org.apache.tomcat.util.descriptor.tld.TldResourcePath; +import org.apache.tomcat.util.descriptor.tld.ValidatorXml; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import jakarta.servlet.ServletContext; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * TOMEE-4651: Jakarta Tags 3.0 renamed the JSTL taglib URIs to the {@code jakarta.tags.*} form. + * The shaded taglibs-standard TLDs only declare the legacy {@code http://java.sun.com/jsp/jstl/*} + * spellings, so {@link TomEETldScanner} registers the new URIs as aliases of the same resources. + * <p> + * The shaded taglibs jar is not on this module's test classpath, so the scanner's own static data is + * empty here. These tests therefore drive the aliasing against a synthetic resource map instead. + */ +public class TomEETldScannerJakartaTagsTest { + + private static final String PERMITTED_TAGLIBS_URI = "http://jakarta.apache.org/taglibs/standard/permittedTaglibs"; + private static final Logger LOGGER = Logger.getLogger(TomEETldScanner.class.getName()); + + /** + * Mirrors what a Jakarta-native JSTL implementation ships, where both spellings resolve to the + * same TLD. Kept independent of the production map on purpose so a typo there fails the test. + */ + private static final String[][] ALIASES = { + {"jakarta.tags.core", "http://java.sun.com/jsp/jstl/core"}, + {"jakarta.tags.fmt", "http://java.sun.com/jsp/jstl/fmt"}, + {"jakarta.tags.sql", "http://java.sun.com/jsp/jstl/sql"}, + {"jakarta.tags.xml", "http://java.sun.com/jsp/jstl/xml"}, + {"jakarta.tags.functions", "http://java.sun.com/jsp/jstl/functions"}, + }; + + /** + * The TLV taglibs the spec did not rename -- they must stay reachable under their legacy URIs and + * must not gain a {@code jakarta.tags.*} spelling. + */ + private static final String[] NOT_RENAMED = { + PERMITTED_TAGLIBS_URI, + "http://jakarta.apache.org/taglibs/standard/scriptfree", + }; + + private Map<String, TldResourcePath> uris; + private Map<TldResourcePath, TaglibXml> taglibs; + private Map<String, TldResourcePath> uriBackup; + private Map<TldResourcePath, TaglibXml> taglibBackup; + /** The JSTL jar {@link #givenJstlIsPopulated()} pretended to find, i.e. a non-null JSTL_URL. */ + private URL populatedJstlUrl; + private Handler logHandler; + + @Before + public void backupStaticState() throws Exception { + uris = staticMap("URI_TLD_RESOURCE"); + taglibs = staticMap("TLD_RESOURCE_TAG_LIB"); + uriBackup = new HashMap<>(uris); + taglibBackup = new HashMap<>(taglibs); + uris.clear(); + taglibs.clear(); + } + + @After + public void restoreStaticState() { + if (logHandler != null) { + LOGGER.removeHandler(logHandler); + logHandler = null; + } + uris.clear(); + uris.putAll(uriBackup); + taglibs.clear(); + taglibs.putAll(taglibBackup); + } + + @Test + public void jakartaTagsUrisAliasTheLegacyJstlResources() throws Exception { + givenJstlIsPopulated(); + + aliasJakartaTagsUris(populatedJstlUrl); + + for (final String[] alias : ALIASES) { + assertTrue(alias[0] + " was not registered", uris.containsKey(alias[0])); + // must be the very same 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 + assertSame(alias[0] + " must reuse the legacy resource path", uris.get(alias[1]), uris.get(alias[0])); + } + } + + @Test + public void legacyUrisKeepWorking() throws Exception { + givenJstlIsPopulated(); + + aliasJakartaTagsUris(populatedJstlUrl); + + for (final String[] alias : ALIASES) { + assertTrue("legacy URI " + alias[1] + " was dropped", uris.containsKey(alias[1])); + } + for (final String uri : NOT_RENAMED) { + assertTrue("legacy URI " + uri + " was dropped", uris.containsKey(uri)); + } + } + + /** + * Jakarta Tags 3.0 renamed only the five functional taglibs. permittedTaglibs/scriptfree are + * Apache-Standard-Taglibs TLVs that kept their URIs, so we must not advertise keys the spec never + * defines. + */ + @Test + public void tlvTaglibsGetNoJakartaTagsSpelling() throws Exception { + givenJstlIsPopulated(); + + aliasJakartaTagsUris(populatedJstlUrl); + + assertFalse(uris.containsKey("jakarta.tags.permittedTaglibs")); + assertFalse(uris.containsKey("jakarta.tags.scriptfree")); + } + + /** + * The guard has to key off JSTL specifically, not off the map being empty: myfaces is + * pre-populated independently, so on a distribution carrying myfaces but no JSTL an emptiness + * check would fall through and log a warning for every alias. + */ + @Test + public void aliasingIsANoopWithoutJstl() throws Exception { + givenMyfacesIsPopulatedWithoutJstl(); + + final List<String> warnings = captureWarnings(); + aliasJakartaTagsUris(null); + + for (final String[] alias : ALIASES) { + assertFalse(alias[0] + " must not be registered without JSTL", uris.containsKey(alias[0])); + } + // an emptiness check instead of a JSTL check would fall through to the loop and warn per alias + assertEquals("absent JSTL is normal and must stay quiet", 0, warnings.size()); + } + + @Test + public void permittedTaglibsAcceptsBothSpellings() throws Exception { + givenJstlIsPopulated(); + + aliasJakartaTagsUris(populatedJstlUrl); + + final List<String> permitted = permittedTaglibs(); + // the four URIs the shaded TLD restricts pages to, plus their Jakarta Tags equivalents + assertEquals(8, permitted.size()); + for (final String uri : new String[]{"core", "fmt", "sql", "xml"}) { + assertTrue("legacy " + uri + " missing", permitted.contains("http://java.sun.com/jsp/jstl/" + uri)); + assertTrue("jakarta.tags." + uri + " missing", permitted.contains("jakarta.tags." + uri)); + } + } + + /** + * The static initialiser runs once, but guard against the widening being applied twice: doing so + * would keep appending the same URIs to the validator's init param. + */ + @Test + public void aliasingTwiceDoesNotDuplicatePermittedTaglibs() throws Exception { + givenJstlIsPopulated(); + + aliasJakartaTagsUris(populatedJstlUrl); + final List<String> once = permittedTaglibs(); + aliasJakartaTagsUris(populatedJstlUrl); + + assertEquals(once, permittedTaglibs()); + } + + @Test + public void permittedTaglibsIsLeftAloneWithoutJstl() throws Exception { + givenMyfacesIsPopulatedWithoutJstl(); + + aliasJakartaTagsUris(null); + + assertEquals(0, taglibs.size()); + } + + /** + * The TLV tokenizes {@code permittedTaglibs} with a plain {@code StringTokenizer}, i.e. on any + * whitespace. A TLD writing the list space-separated must still be widened. + */ + @Test + public void permittedTaglibsHandlesWhitespaceSeparatedLists() throws Exception { + givenJstlIsPopulated(); + validator().addInitParam("permittedTaglibs", + "http://java.sun.com/jsp/jstl/core http://java.sun.com/jsp/jstl/fmt"); + + aliasJakartaTagsUris(populatedJstlUrl); + + final List<String> permitted = permittedTaglibs(); + assertTrue("jakarta.tags.core missing", permitted.contains("jakarta.tags.core")); + assertTrue("jakarta.tags.fmt missing", permitted.contains("jakarta.tags.fmt")); + assertFalse("sql was never permitted", permitted.contains("jakarta.tags.sql")); + } + + /** + * Pins that the aliasing is wired into the static initialiser -- every other test drives + * {@code aliasJakartaTagsUris()} by hand and would stay green if that call were dropped. + * <p> + * The shaded taglibs jar is not on this module's test classpath, so the initialiser cannot be + * observed through its effect on the maps. Assert the call is present in the initialiser's + * bytecode instead, which holds regardless of what is resolvable at test time. + */ + @Test + public void staticInitialiserRegistersTheAliases() throws Exception { + final ClassReader reader = new ClassReader(TomEETldScanner.class.getName()); + final List<String> calls = new ArrayList<>(); + reader.accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod(final int access, final String name, final String descriptor, + final String signature, final String[] exceptions) { + if (!"<clinit>".equals(name)) { + return null; + } + return new MethodVisitor(Opcodes.ASM9) { + @Override + public void visitMethodInsn(final int opcode, final String owner, final String method, + final String desc, final boolean isInterface) { + calls.add(method); + } + }; + } + }, ClassReader.SKIP_FRAMES); + + assertTrue("aliasJakartaTagsUris() is not called from the static initialiser", + calls.contains("aliasJakartaTagsUris")); + assertTrue("populateMyfacesAndJstlData() must run before the aliasing", + calls.indexOf("populateMyfacesAndJstlData") < calls.indexOf("aliasJakartaTagsUris")); + } + + /** + * Pins the precedence this change establishes: because {@code TldScanner.scan()} runs + * {@code scanPlatform()} first and {@code parseTld()} only registers a URI it does not already + * know, the container's alias wins over one an application bundles in {@code WEB-INF/lib}. + * <p> + * This is a deliberate trade-off rather than an accident -- it matches how the legacy + * {@code http://java.sun.com/jsp/jstl/*} URIs have always behaved -- but it does mean bundling a + * Jakarta-native JSTL is no longer a way to override the container. Driven against the real + * {@code parseTld()} so the assertion tracks Tomcat rather than a restatement of it. + */ + @Test + public void containerAliasWinsOverAnApplicationBundledTld() throws Exception { + givenJstlIsPopulated(); + aliasJakartaTagsUris(populatedJstlUrl); + + final TomEETldScanner scanner = new TomEETldScanner(mock(ServletContext.class), true, false, true); + final Map<String, TldResourcePath> deployment = scanner.getUriTldResourcePathMap(); + deployment.putAll(uris); + final TldResourcePath containerPath = deployment.get("jakarta.tags.core"); + + // an application jar declaring the same URI, parsed the way scanJars() would + final File appTld = new File("target/test-classes/jakarta-tags-core.tld"); + appTld.getParentFile().mkdirs(); + Files.write(appTld.toPath(), ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + + "<taglib xmlns=\"https://jakarta.ee/xml/ns/jakartaee\" version=\"3.0\">" + + "<tlib-version>3.0</tlib-version><short-name>c</short-name>" + + "<uri>jakarta.tags.core</uri></taglib>").getBytes(StandardCharsets.UTF_8)); + parseTld(scanner, new TldResourcePath(appTld.toURI().toURL(), null)); + + assertSame("an application TLD must not displace the container alias", + containerPath, deployment.get("jakarta.tags.core")); + } + + /** + * The aliases are only useful if {@code scanPlatform()} copies them into the per-deployment map + * Jasper actually resolves {@code <%@ taglib uri="..." %>} against. Asserts each alias arrives + * there pointing at the same TLD resource as its legacy spelling. + * <p> + * Note this only covers the {@code shouldSkipJsf()} branch: that method returns true for every + * {@code jakarta.faces.*} name unconditionally, so the myfaces-exclude branch below it is + * currently unreachable and cannot be driven from a test. + */ + @Test + public void scanPlatformForwardsAliasesToTheDeploymentMap() throws Exception { + givenJstlIsPopulated(); + + aliasJakartaTagsUris(populatedJstlUrl); + + final Map<String, TldResourcePath> deployment = scanPlatform(); + + for (final String[] alias : ALIASES) { + assertTrue(alias[0] + " never reached the deployment map", deployment.containsKey(alias[0])); + assertSame(alias[0] + " must resolve to the legacy TLD resource", + uris.get(alias[1]), deployment.get(alias[0])); + } + } + + /** + * Invokes the protected {@code scanPlatform()} and returns the per-deployment URI map it + * populated -- that map is the one Jasper resolves {@code <%@ taglib uri="..." %>} against. + */ + private Map<String, TldResourcePath> scanPlatform() throws Exception { + final TomEETldScanner scanner = new TomEETldScanner(mock(ServletContext.class), false, false, false); + + final Method scanPlatform = TomEETldScanner.class.getDeclaredMethod("scanPlatform"); + scanPlatform.setAccessible(true); + scanPlatform.invoke(scanner); + + return scanner.getUriTldResourcePathMap(); + } + + /** + * Simulates {@code populateMyfacesAndJstlData()} having run against the shaded taglibs jar: + * every legacy URI mapped to a resource path, and the permittedTaglibs validator configured as + * that jar's TLD configures it. + */ + private void givenJstlIsPopulated() throws Exception { + final URL jar = jstlUrl(); + populatedJstlUrl = jar; + for (final String[] alias : ALIASES) { + uris.put(alias[1], new TldResourcePath(jar, null, "META-INF/fake.tld")); + } + for (final String uri : NOT_RENAMED) { + uris.put(uri, new TldResourcePath(jar, null, "META-INF/fake.tld")); + } + + final ValidatorXml validator = new ValidatorXml(); + validator.setValidatorClass("jakarta.servlet.jsp.jstl.tlv.PermittedTaglibsTLV"); + validator.addInitParam("permittedTaglibs", "http://java.sun.com/jsp/jstl/core\n" + + "http://java.sun.com/jsp/jstl/fmt\n" + + "http://java.sun.com/jsp/jstl/sql\n" + + "http://java.sun.com/jsp/jstl/xml"); + final TaglibXml taglibXml = new TaglibXml(); + taglibXml.setValidator(validator); + taglibs.put(uris.get(PERMITTED_TAGLIBS_URI), taglibXml); + } + + /** + * A distribution carrying myfaces but no JSTL: the pre-populated map is non-empty, but none of the + * legacy JSTL URIs are in it. + */ + private void givenMyfacesIsPopulatedWithoutJstl() throws Exception { + uris.put("http://java.sun.com/jsf/html", + new TldResourcePath(new URL("file:/fake/myfaces-impl.jar"), null, "META-INF/myfaces_html.tld")); + } + + /** + * Collects warnings the scanner emits for the rest of the test; the handler is detached in + * {@link #restoreStaticState()}. + */ + private List<String> captureWarnings() { + final List<String> warnings = new ArrayList<>(); + logHandler = new Handler() { + @Override + public void publish(final LogRecord record) { + if (record.getLevel().intValue() >= Level.WARNING.intValue()) { + warnings.add(record.getMessage()); + } + } + + @Override + public void flush() { + // no-op + } + + @Override + public void close() { + // no-op + } + }; + LOGGER.addHandler(logHandler); + return warnings; + } + + private ValidatorXml validator() { + return taglibs.get(uris.get(PERMITTED_TAGLIBS_URI)).getValidator(); + } + + private List<String> permittedTaglibs() { + // read back the way PermittedTaglibsTLV does, i.e. on any whitespace + return Arrays.asList(validator().getInitParams().get("permittedTaglibs").trim().split("\\s+")); + } + + /** + * The URL the pre-populated JSTL entries carry. Uses the real {@code JSTL_URL} when the shaded + * taglibs jar happens to be resolvable, so the identity comparison in {@code scanPlatform()} is + * exercised against the production value rather than a stand-in. + */ + private URL jstlUrl() throws Exception { + final URL real = staticUrl("JSTL_URL"); + return real != null ? real : new URL("file:/fake/taglibs-shade.jar"); + } + + private static URL staticUrl(final String name) throws Exception { + final Field field = TomEETldScanner.class.getDeclaredField(name); + field.setAccessible(true); + return (URL) field.get(null); + } + + @SuppressWarnings("unchecked") + private static <K, V> Map<K, V> staticMap(final String name) throws Exception { + final Field field = TomEETldScanner.class.getDeclaredField(name); + field.setAccessible(true); + return (Map<K, V>) field.get(null); + } + + private static void parseTld(final TomEETldScanner scanner, final TldResourcePath path) throws Exception { + final Method method = TldScanner.class.getDeclaredMethod("parseTld", TldResourcePath.class); + method.setAccessible(true); + method.invoke(scanner, path); + } + + private static void aliasJakartaTagsUris() throws Exception { + final Method method = TomEETldScanner.class.getDeclaredMethod("aliasJakartaTagsUris"); + method.setAccessible(true); + method.invoke(null); + } + + /** + * Drives the JSTL-presence guard, which cannot be exercised through the no-arg overload: + * {@code JSTL_URL} is {@code static final} and modern JDKs reject reflective writes to it. + */ + private static void aliasJakartaTagsUris(final URL jstlUrl) throws Exception { + final Method method = TomEETldScanner.class.getDeclaredMethod("aliasJakartaTagsUris", URL.class); + method.setAccessible(true); + method.invoke(null, jstlUrl); + } +}
