Skip to content

Commit 53406f6

Browse files
robertpatrickBalusC
authored andcommitted
Backport #5721 from 4.1 into 4.0
1 parent ff5960b commit 53406f6

4 files changed

Lines changed: 430 additions & 22 deletions

File tree

impl/src/main/java/com/sun/faces/util/Util.java

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,37 +1071,54 @@ public static HttpServletMapping getFirstWildCardMappingToFacesServlet(ExternalC
10711071
// If needed, cache this after initialization of Faces
10721072
Object context = externalContext.getContext();
10731073
if (context instanceof ServletContext) {
1074-
return getFacesServletMappings((ServletContext) context).stream()
1075-
.filter(mapping -> mapping.contains("*"))
1076-
.map(mapping -> new HttpServletMapping() {
1074+
HttpServletMapping pathMapping = null;
10771075

1078-
@Override
1079-
public String getServletName() {
1080-
return "";
1081-
}
1076+
for (String mapping : getFacesServletMappings((ServletContext) context)) {
1077+
if (!mapping.contains("*")) {
1078+
continue;
1079+
}
10821080

1083-
@Override
1084-
public String getPattern() {
1085-
return mapping;
1086-
}
1081+
HttpServletMapping wildcardMapping = toWildcardMapping(mapping);
1082+
if (wildcardMapping.getMappingMatch() == MappingMatch.EXTENSION) {
1083+
return wildcardMapping;
1084+
}
10871085

1088-
@Override
1089-
public String getMatchValue() {
1090-
return null;
1091-
}
1086+
if (pathMapping == null) {
1087+
pathMapping = wildcardMapping;
1088+
}
1089+
}
10921090

1093-
@Override
1094-
public MappingMatch getMappingMatch() {
1095-
return isPrefixMapped(mapping)? MappingMatch.PATH : MappingMatch.EXTENSION;
1096-
}
1097-
})
1098-
.findFirst()
1099-
.orElse(null);
1091+
return pathMapping;
11001092
}
11011093

11021094
return null;
11031095
}
11041096

1097+
private static HttpServletMapping toWildcardMapping(String mapping) {
1098+
return new HttpServletMapping() {
1099+
1100+
@Override
1101+
public String getServletName() {
1102+
return "";
1103+
}
1104+
1105+
@Override
1106+
public String getPattern() {
1107+
return mapping;
1108+
}
1109+
1110+
@Override
1111+
public String getMatchValue() {
1112+
return null;
1113+
}
1114+
1115+
@Override
1116+
public MappingMatch getMappingMatch() {
1117+
return isPrefixMapped(mapping) ? MappingMatch.PATH : MappingMatch.EXTENSION;
1118+
}
1119+
};
1120+
}
1121+
11051122
/**
11061123
* <p>
11071124
* Returns true if the provided <code>url-mapping</code> is a prefix path mapping (starts with <code>/</code>).
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright (c) 2026 Contributors to Eclipse Foundation.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0, which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the
10+
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11+
* version 2 with the GNU Classpath Exception, which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
*/
16+
17+
package com.sun.faces.application.resource;
18+
19+
import static jakarta.faces.application.ProjectStage.Development;
20+
import static jakarta.faces.application.ResourceHandler.FACES_SCRIPT_LIBRARY_NAME;
21+
import static jakarta.faces.application.ResourceHandler.FACES_SCRIPT_RESOURCE_NAME;
22+
import static jakarta.faces.application.ResourceHandler.RESOURCE_IDENTIFIER;
23+
import static jakarta.servlet.http.MappingMatch.EXACT;
24+
import static jakarta.servlet.http.MappingMatch.EXTENSION;
25+
import static jakarta.servlet.http.MappingMatch.PATH;
26+
import static org.junit.jupiter.api.Assertions.assertEquals;
27+
import static org.junit.jupiter.api.Assertions.assertThrows;
28+
29+
import java.io.InputStream;
30+
import java.net.URL;
31+
32+
import org.junit.jupiter.api.Test;
33+
34+
import com.sun.faces.junit.JUnitFacesTestCaseBase;
35+
import com.sun.faces.mock.MockApplication;
36+
import com.sun.faces.mock.MockFacesMappingSupport;
37+
38+
import jakarta.faces.context.FacesContext;
39+
40+
public class ResourceImplTest extends JUnitFacesTestCaseBase {
41+
42+
private static final String CONTEXT_PATH = "/app";
43+
private static final ResourceHelper RESOURCE_HELPER = new ResourceHelper() {
44+
45+
@Override
46+
public String getBaseResourcePath() {
47+
return "/resources";
48+
}
49+
50+
@Override
51+
public String getBaseContractsPath() {
52+
return "/contracts";
53+
}
54+
55+
@Override
56+
public URL getURL(ResourceInfo resource, FacesContext ctx) {
57+
return null;
58+
}
59+
60+
@Override
61+
public LibraryInfo findLibrary(String libraryName, String localePrefix, String contract, FacesContext ctx) {
62+
return null;
63+
}
64+
65+
@Override
66+
public ResourceInfo findResource(LibraryInfo library, String resourceName, String localePrefix, boolean compressable, FacesContext ctx) {
67+
return null;
68+
}
69+
70+
@Override
71+
protected InputStream getNonCompressedInputStream(ResourceInfo resource, FacesContext ctx) {
72+
return null;
73+
}
74+
};
75+
76+
@Test
77+
public void getRequestPathReturnsExactHitForExactMappedResource() {
78+
configureRequest(MockFacesMappingSupport.mapping(EXACT, "/exact", "exact"), "/exact", RESOURCE_IDENTIFIER + "/theme.css", "*.xhtml", "/faces/*");
79+
80+
assertEquals("/app/jakarta.faces.resource/theme.css", createResource("theme.css", null, null, null, null, null).getRequestPath());
81+
}
82+
83+
@Test
84+
public void getRequestPathKeepsExtensionFallbackForExactRequests() {
85+
ResourceImpl resource = createResource("theme.css", null, null, null, null, null);
86+
87+
configureRequest(MockFacesMappingSupport.mapping(EXACT, "/exact", "exact"), "/exact", "*.xhtml");
88+
String exactRequestPath = resource.getRequestPath();
89+
90+
configureRequest(MockFacesMappingSupport.mapping(EXTENSION, "*.xhtml", "theme"), "/exact", "*.xhtml");
91+
String extensionBaselinePath = resource.getRequestPath();
92+
93+
assertEquals("/app/jakarta.faces.resource/theme.css.xhtml", exactRequestPath);
94+
assertEquals(extensionBaselinePath, exactRequestPath);
95+
}
96+
97+
@Test
98+
public void getRequestPathKeepsDirectPathBehavior() {
99+
ResourceImpl resource = createResource("theme.css", "layout", "1_0", null, "en", "blue");
100+
101+
configureRequest(MockFacesMappingSupport.mapping(PATH, "/faces/*", "theme"), "/exact", "/faces/*");
102+
String directPathRequestPath = resource.getRequestPath();
103+
104+
assertEquals("/app/faces/jakarta.faces.resource/theme.css?ln=layout&v=1_0&loc=en&con=blue", directPathRequestPath);
105+
}
106+
107+
@Test
108+
public void getRequestPathPrefersExtensionFallbackWhenBothWildcardsExist() {
109+
ResourceImpl resource = createResource("theme.css", "layout", "1_0", null, "en", "blue");
110+
111+
configureRequest(MockFacesMappingSupport.mapping(EXACT, "/exact", "exact"), "/exact", "*.jsf", "/faces/*");
112+
String exactRequestPath = resource.getRequestPath();
113+
114+
assertEquals("/app/jakarta.faces.resource/theme.css.jsf?ln=layout&v=1_0&loc=en&con=blue", exactRequestPath);
115+
assertEquals(exactRequestPath.indexOf("ln="), exactRequestPath.lastIndexOf("ln="));
116+
assertEquals(exactRequestPath.indexOf("v="), exactRequestPath.lastIndexOf("v="));
117+
assertEquals(exactRequestPath.indexOf("loc="), exactRequestPath.lastIndexOf("loc="));
118+
assertEquals(exactRequestPath.indexOf("con="), exactRequestPath.lastIndexOf("con="));
119+
}
120+
121+
@Test
122+
public void getRequestPathPrefersExtensionFallbackForFacesScriptInDevelopment() {
123+
MockApplication developmentApplication = new MockApplication() {
124+
125+
@Override
126+
public jakarta.faces.application.ProjectStage getProjectStage() {
127+
return Development;
128+
}
129+
};
130+
developmentApplication.setViewHandler(application.getViewHandler());
131+
application = developmentApplication;
132+
facesContext.setApplication(application);
133+
134+
ResourceImpl resource = createResource(FACES_SCRIPT_RESOURCE_NAME, FACES_SCRIPT_LIBRARY_NAME, null, null, null, null);
135+
136+
configureRequest(MockFacesMappingSupport.mapping(EXACT, "/exact", "exact"), "/exact", "*.jsf", "/faces/*");
137+
String exactRequestPath = resource.getRequestPath();
138+
139+
assertEquals("/app/jakarta.faces.resource/faces.js.jsf?ln=jakarta.faces&stage=Development", exactRequestPath);
140+
}
141+
142+
@Test
143+
public void getRequestPathThrowsWhenNoWildcardFallbackExists() {
144+
configureRequest(MockFacesMappingSupport.mapping(EXACT, "/exact", "exact"), "/exact");
145+
146+
assertThrows(IllegalStateException.class, () -> createResource("theme.css", null, null, null, null, null).getRequestPath());
147+
}
148+
149+
private void configureRequest(jakarta.servlet.http.HttpServletMapping mapping, String... servletMappings) {
150+
request = MockFacesMappingSupport.request(CONTEXT_PATH, mapping, session);
151+
externalContext = MockFacesMappingSupport.externalContext(servletContext, request, response);
152+
facesContext.setExternalContext(externalContext);
153+
MockFacesMappingSupport.setFacesServletMappings(servletContext, servletMappings);
154+
}
155+
156+
private ResourceImpl createResource(String resourceName, String libraryName, String libraryVersion, String resourceVersion, String localePrefix, String contract) {
157+
ContractInfo contractInfo = contract != null ? new ContractInfo(contract) : null;
158+
VersionInfo libraryVersionInfo = libraryVersion != null ? new VersionInfo(libraryVersion, null) : null;
159+
VersionInfo resourceVersionInfo = resourceVersion != null ? new VersionInfo(resourceVersion, extensionOf(resourceName)) : null;
160+
161+
ResourceInfo resourceInfo;
162+
if (libraryName != null) {
163+
LibraryInfo libraryInfo = new LibraryInfo(libraryName, libraryVersionInfo, localePrefix, contract, RESOURCE_HELPER);
164+
resourceInfo = new ResourceInfo(libraryInfo, contractInfo, resourceName, resourceVersionInfo);
165+
} else {
166+
resourceInfo = new ResourceInfo(contractInfo, resourceName, resourceVersionInfo, RESOURCE_HELPER);
167+
}
168+
169+
return new ResourceImpl(resourceInfo, "text/css", 0, 0);
170+
}
171+
172+
private String extensionOf(String resourceName) {
173+
int extensionIndex = resourceName.lastIndexOf('.');
174+
return extensionIndex >= 0 ? resourceName.substring(extensionIndex + 1) : null;
175+
}
176+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright (c) 2026 Contributors to Eclipse Foundation.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0, which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the
10+
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11+
* version 2 with the GNU Classpath Exception, which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
*/
16+
17+
package com.sun.faces.application.view;
18+
19+
import static jakarta.servlet.http.MappingMatch.EXACT;
20+
import static jakarta.servlet.http.MappingMatch.EXTENSION;
21+
import static jakarta.servlet.http.MappingMatch.PATH;
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
import static org.junit.jupiter.api.Assertions.assertThrows;
24+
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Test;
27+
28+
import com.sun.faces.junit.JUnitFacesTestCaseBase;
29+
import com.sun.faces.mock.MockFacesMappingSupport;
30+
31+
import jakarta.faces.FactoryFinder;
32+
33+
public class MultiViewHandlerTest extends JUnitFacesTestCaseBase {
34+
35+
private static final String CONTEXT_PATH = "/app";
36+
37+
private MultiViewHandler viewHandler;
38+
39+
@Override
40+
@BeforeEach
41+
public void setUp() throws Exception {
42+
super.setUp();
43+
FactoryFinder.setFactory(FactoryFinder.VIEW_DECLARATION_LANGUAGE_FACTORY, ViewDeclarationLanguageFactoryImpl.class.getName());
44+
viewHandler = new MultiViewHandler();
45+
}
46+
47+
@Test
48+
public void getActionURLReturnsExactHitForConfiguredFaceletsExtension() {
49+
configureRequest(MockFacesMappingSupport.mapping(EXACT, "/exact", "exact"), "/exact", "/target", "*.xhtml", "/faces/*");
50+
51+
assertEquals("/app/target", viewHandler.getActionURL(facesContext, "/target.xhtml"));
52+
}
53+
54+
@Test
55+
public void getActionURLKeepsExtensionFallbackForExactRequests() {
56+
configureRequest(MockFacesMappingSupport.mapping(EXACT, "/exact", "exact"), "/exact", "*.xhtml");
57+
String exactRequestUrl = viewHandler.getActionURL(facesContext, "/fallback.xhtml");
58+
59+
configureRequest(MockFacesMappingSupport.mapping(EXTENSION, "*.xhtml", "fallback"), "/exact", "*.xhtml");
60+
String extensionBaselineUrl = viewHandler.getActionURL(facesContext, "/fallback.xhtml");
61+
62+
assertEquals("/app/fallback.xhtml", exactRequestUrl);
63+
assertEquals(extensionBaselineUrl, exactRequestUrl);
64+
}
65+
66+
@Test
67+
public void getActionURLKeepsDirectPathBehavior() {
68+
configureRequest(MockFacesMappingSupport.mapping(PATH, "/faces/*", "fallback"), "/exact", "/faces/*");
69+
70+
assertEquals("/app/faces/fallback.xhtml", viewHandler.getActionURL(facesContext, "/fallback.xhtml"));
71+
}
72+
73+
@Test
74+
public void getActionURLPrefersExtensionFallbackWhenBothWildcardsExist() {
75+
configureRequest(MockFacesMappingSupport.mapping(EXACT, "/exact", "exact"), "/exact", "*.jsf", "/faces/*");
76+
77+
String exactRequestUrl = viewHandler.getActionURL(facesContext, "/fallback.xhtml");
78+
79+
assertEquals("/app/fallback.jsf", exactRequestUrl);
80+
}
81+
82+
@Test
83+
public void getActionURLThrowsWhenNoWildcardFallbackExists() {
84+
configureRequest(MockFacesMappingSupport.mapping(EXACT, "/exact", "exact"), "/exact");
85+
86+
assertThrows(IllegalStateException.class, () -> viewHandler.getActionURL(facesContext, "/fallback.xhtml"));
87+
}
88+
89+
private void configureRequest(jakarta.servlet.http.HttpServletMapping mapping, String... servletMappings) {
90+
request = MockFacesMappingSupport.request(CONTEXT_PATH, mapping, session);
91+
externalContext = MockFacesMappingSupport.externalContext(servletContext, request, response);
92+
facesContext.setExternalContext(externalContext);
93+
MockFacesMappingSupport.setFacesServletMappings(servletContext, servletMappings);
94+
}
95+
}

0 commit comments

Comments
 (0)