/*
* Copyright 2017 OmniFaces
*
* Licensed 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.omnifaces.facesviews;
import static java.lang.Boolean.parseBoolean;
import static java.lang.String.format;
import static java.util.Collections.unmodifiableMap;
import static java.util.Collections.unmodifiableSet;
import static java.util.Locale.US;
import static java.util.regex.Pattern.quote;
import static javax.faces.view.facelets.ResourceResolver.FACELETS_RESOURCE_RESOLVER_PARAM_NAME;
import static javax.servlet.DispatcherType.FORWARD;
import static javax.servlet.DispatcherType.REQUEST;
import static org.omnifaces.facesviews.ExtensionAction.REDIRECT_TO_EXTENSIONLESS;
import static org.omnifaces.facesviews.FacesServletDispatchMethod.DO_FILTER;
import static org.omnifaces.facesviews.PathAction.SEND_404;
import static org.omnifaces.facesviews.ViewHandlerMode.STRIP_EXTENSION_FROM_PARENT;
import static org.omnifaces.util.Faces.getApplicationFromFactory;
import static org.omnifaces.util.Faces.getServletContext;
import static org.omnifaces.util.Platform.getFacesServletMappings;
import static org.omnifaces.util.Platform.getFacesServletRegistration;
import static org.omnifaces.util.ResourcePaths.filterExtension;
import static org.omnifaces.util.ResourcePaths.getExtension;
import static org.omnifaces.util.ResourcePaths.isDirectory;
import static org.omnifaces.util.ResourcePaths.isExtensionless;
import static org.omnifaces.util.ResourcePaths.stripExtension;
import static org.omnifaces.util.ResourcePaths.stripPrefixPath;
import static org.omnifaces.util.ResourcePaths.stripTrailingSlash;
import static org.omnifaces.util.Servlets.getApplicationAttribute;
import static org.omnifaces.util.Servlets.getRequestBaseURL;
import static org.omnifaces.util.Utils.csvToList;
import static org.omnifaces.util.Utils.isEmpty;
import static org.omnifaces.util.Utils.reverse;
import static org.omnifaces.util.Utils.startsWithOneOf;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.faces.application.Application;
import javax.faces.application.ViewHandler;
import javax.faces.context.FacesContext;
import javax.faces.webapp.FacesServlet;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import javax.servlet.http.HttpServletRequest;
import org.omnifaces.ApplicationInitializer;
import org.omnifaces.ApplicationListener;
import org.omnifaces.cdi.Param;
import org.omnifaces.config.WebXml;
/**
* <p>
* FacesViews is a mechanism to use SEO-friendly extensionless URLs in a JSF application without the need to enlist
* individual Facelet source files in some configuration file.
* <p>
* By default, all URLs generated by {@link ViewHandler#getActionURL(FacesContext, String)}, which is used by among
* others <code><h:form></code>, <code><h:link></code>, <code><h:button></code> and all extended tags,
* will also be extensionless. And, URLs with an extension will be 301-redirected to the extensionless one.
*
* <h3>Usage</h3>
*
* <h4>Zero configuration</h4>
* <p>
* Put Facelets source files into <code>/WEB-INF/faces-views</code> directory. All Facelets files in this special
* directory will be automatically scanned as extensionless URLs.
*
* <h4>Minimal configuration</h4>
* <p>
* Below is the minimal <code>web.xml</code> configuration to make all Facelets source files found in the root folder
* and all subdirectories of the public web content (excluding <code>/WEB-INF</code>, <code>/META-INF</code> and
* <code>/resources</code>) available as extensionless URLs:
* <pre>
* <context-param>
* <param-name>org.omnifaces.FACES_VIEWS_SCAN_PATHS</param-name>
* <param-value>/*.xhtml</param-value>
* </context-param>
* </pre>
* <p>
* The path pattern <code>/*.xhtml</code> basically means that all files with the <code>.xhtml</code> extension from the
* directory <code>/</code> must be scanned, including all sub directories. In case you want to scan only
* <code>.xhtml</code> files in the directory <code>/foo</code>, then use path pattern of <code>/foo/*.xhtml</code>
* instead. In case you want to scan <em>all</em> files in the directory <code>/foo</code>, then use path pattern of
* <code>/foo</code>. You can specify multiple values separated by a comma.
*
* <h4>MultiViews configuration</h4>
* <p>
* Enabling MultiViews is a matter of suffixing the path pattern with <code>/*</code>. The support was added in
* OmniFaces 2.5. Below is the <code>web.xml</code> configuration which extends the above minimal configuration with
* MultiViews support:
* <pre>
* <context-param>
* <param-name>org.omnifaces.FACES_VIEWS_SCAN_PATHS</param-name>
* <param-value>/*.xhtml/*</param-value>
* </context-param>
* </pre>
* <p>
* On an example URL of <code>http://example.com/context/foo/bar/baz</code> when neither <code>/foo/bar/baz.xhtml</code>
* nor <code>/foo/bar.xhtml</code> exist, but <code>/foo.xhtml</code> does exist, then the request will forward to
* <code>/foo.xhtml</code> and make the values <code>bar</code> and <code>baz</code> available as injectable path
* parameters via <code>@</code>{@link Param} in the managed bean associated with <code>/foo.xhtml</code>.
* <pre>
* @Inject @Param(pathIndex=0)
* private String bar;
*
* @Inject @Param(pathIndex=1)
* private String baz;
* </pre>
*
* <h4>Advanced configuration</h4>
* <p>
* See <a href="package-summary.html">package documentation</a> for configuration settings as to mapping, filtering
* and forwarding behavior.
*
* <h3>PrettyFaces</h3>
* <p>
* Note that there is some overlap between this feature and <a href="http://ocpsoft.org/prettyfaces">PrettyFaces</a>.
* The difference is that FacesViews has a focus on zero- or very minimal config, where PrettyFaces has a focus on very
* powerful mapping mechanisms, which of course need some level of configuration. As such FacesViews will only focus on
* auto discovering views and mapping them to both <code>.xhtml</code> and to no-extension without needing to explicitly
* declare the <code>FacesServlet</code> in <code>web.xml</code>.
* <p>
* Specifically, FacesViews will thus <em>not</em> become a general URL rewriting tool (e.g. one that maps path segments
* to parameters, or that totally changes the name of the URL). For this the user is advised to look at the
* aforementioned <a href="http://ocpsoft.org/prettyfaces">PrettyFaces</a>.
*
* @author Arjan Tijms
* @see FacesViewsResolver
* @see FacesViewsForwardingFilter
* @see ExtensionAction
* @see PathAction
* @see UriExtensionRequestWrapper
* @see FacesViewsViewHandlerInstaller
* @see FacesViewsViewHandler
* @see ViewHandlerMode
*/
public final class FacesViews {
// Defaults -------------------------------------------------------------------------------------------------------
/**
* A special dedicated "well-known" directory where facelets implementing views can be placed.
* This directory is scanned by convention so that no explicit configuration is needed.
*/
public static final String WEB_INF_VIEWS = "/WEB-INF/faces-views/";
// Context parameter names ----------------------------------------------------------------------------------------
/**
* The name of the boolean context parameter to switch auto-scanning completely off for Servlet 3.0 containers.
*/
public static final String FACES_VIEWS_ENABLED_PARAM_NAME = "org.omnifaces.FACES_VIEWS_ENABLED";
/**
* The name of the commaseparated context parameter where the value holds a comma separated list of paths that are
* to be scanned by faces views.
*/
public static final String FACES_VIEWS_SCAN_PATHS_PARAM_NAME = "org.omnifaces.FACES_VIEWS_SCAN_PATHS";
/**
* The name of the boolean context parameter via which the user can set scanned views to be always rendered
* extensionless. Without this setting (or it being set to false), it depends on whether the request URI uses an
* extension or not. If it doesn't, links are also rendered without one, otherwise are rendered with an extension.
*/
public static final String FACES_VIEWS_SCANNED_VIEWS_EXTENSIONLESS_PARAM_NAME = "org.omnifaces.FACES_VIEWS_SCANNED_VIEWS_ALWAYS_EXTENSIONLESS";
/**
* The name of the enum context parameter that determines the action that is performed whenever a resource
* is requested WITH extension that's also available without an extension. See {@link ExtensionAction}
* @see ExtensionAction
*/
public static final String FACES_VIEWS_EXTENSION_ACTION_PARAM_NAME = "org.omnifaces.FACES_VIEWS_EXTENSION_ACTION";
/**
* The name of the enum context parameter that determines the action that is performed whenever a resource
* is requested in a public path that has been used for scanning views by faces views. See {@link PathAction}
* @see PathAction
*/
public static final String FACES_VIEWS_PATH_ACTION_PARAM_NAME = "org.omnifaces.FACES_VIEWS_PATH_ACTION";
/**
* The name of the enum context parameter that determines the method used by FacesViews to invoke the FacesServlet.
* See {@link FacesServletDispatchMethod}.
* @see FacesServletDispatchMethod
* @deprecated Since 2.6 As this is superfluous since Servlet 3.0.
* It will default to DO_FILTER and automatically use FORWARD when resource is not mapped.
*/
@Deprecated // TODO: remove in OmniFaces 3.0.
public static final String FACES_VIEWS_DISPATCH_METHOD_PARAM_NAME = "org.omnifaces.FACES_VIEWS_DISPATCH_METHOD";
/**
* The name of the boolean context parameter via which the user can set whether the
* {@link FacesViewsForwardingFilter} should match before declared filters (false) or after declared filters (true).
*/
public static final String FACES_VIEWS_FILTER_AFTER_DECLARED_FILTERS_PARAM_NAME = "org.omnifaces.FACES_VIEWS_FILTER_AFTER_DECLARED_FILTERS";
/**
* The name of the enum context parameter via which the user can set whether the {@link FacesViewsViewHandler}
* should strip the extension from the parent view handler's outcome or construct the URL itself and only take the
* query parameters (if any) from the parent.
* @see ViewHandlerMode
* @deprecated Since 2.6 As this is superfluous since Servlet 3.0.
*/
@Deprecated // TODO: remove in OmniFaces 3.0.
public static final String FACES_VIEWS_VIEW_HANDLER_MODE_PARAM_NAME = "org.omnifaces.FACES_VIEWS_VIEW_HANDLER_MODE";
// Request attributes ---------------------------------------------------------------------------------------------
/**
* The name of the request attribute under which the original request servlet path is stored.
*/
public static final String FACES_VIEWS_ORIGINAL_SERVLET_PATH = "org.omnifaces.facesviews.original.servlet_path";
/**
* The name of the request attribute under which the original request path info is stored.
*/
public static final String FACES_VIEWS_ORIGINAL_PATH_INFO = "org.omnifaces.facesviews.original.path_info";
// Constants ------------------------------------------------------------------------------------------------------
private static final String[] RESTRICTED_DIRECTORIES = { "/WEB-INF/", "/META-INF/", "/resources/" };
// TODO: those should be properties of an @ApplicationScoped bean.
private static final String SCAN_PATHS = "org.omnifaces.facesviews.scan_paths";
private static final String PUBLIC_SCAN_PATHS = "org.omnifaces.facesviews.public_scan_paths";
private static final String MULTIVIEWS_PATHS = "org.omnifaces.facesviews.multiviews_paths";
private static final String FACES_SERVLET_EXTENSIONS = "org.omnifaces.facesviews.faces_servlet_extensions";
private static final String MAPPED_RESOURCES = "org.omnifaces.facesviews.mapped_resources";
private static final String REVERSE_MAPPED_RESOURCES = "org.omnifaces.facesviews.reverse_mapped_resources";
private static final String ENCOUNTERED_EXTENSIONS = "org.omnifaces.facesviews.encountered_extensions";
private static final String MAPPED_WELCOME_FILES = "org.omnifaces.facesviews.mapped_welcome_files";
private static final String MULTIVIEWS_WELCOME_FILE = "org.omnifaces.facesviews.multiviews_welcome_file";
private static volatile Boolean facesViewsEnabled;
private static volatile Boolean multiViewsEnabled;
private FacesViews() {
//
}
// Initialization -------------------------------------------------------------------------------------------------
/**
* This will register the {@link FacesViewsForwardingFilter}.
* This is invoked by {@link ApplicationInitializer}.
* @param servletContext The involved servlet context.
*/
public static void registerForwardingFilter(ServletContext servletContext) {
if (!isFacesViewsEnabled(servletContext)) {
return;
}
// Scan our dedicated directory for Faces resources that need to be mapped.
Map<String, String> collectedViews = scanAndStoreViews(servletContext, true);
if (collectedViews.isEmpty()) {
return;
}
// Register a Filter that forwards extensionless requests to an extension mapped request, e.g. /index to /index.xhtml
// The FacesServlet doesn't work well with the exact mapping that we use for extensionless URLs.
FilterRegistration filterRegistration = servletContext.addFilter(FacesViewsForwardingFilter.class.getName(), FacesViewsForwardingFilter.class);
// Register a Facelets resource resolver that resolves requests like /index.xhtml to /WEB-INF/faces-views/index.xhtml
// TODO: Migrate ResourceResolver to ResourceHandler.
servletContext.setInitParameter(FACELETS_RESOURCE_RESOLVER_PARAM_NAME, FacesViewsResolver.class.getName());
addForwardingFilterMappings(servletContext, collectedViews, filterRegistration);
// We now need to map the Faces Servlet to the extensions we found,
// but at this point in time this Faces Servlet might not be created yet,
// so we do this part in the FacesViews#addFacesServletMappings() method below,
// which is called from ApplicationListener#contextInitialized() later.
}
private static void addForwardingFilterMappings(ServletContext servletContext, Map<String, String> collectedViews, FilterRegistration filterRegistration) {
boolean filterAfterDeclaredFilters = parseBoolean(servletContext.getInitParameter(FACES_VIEWS_FILTER_AFTER_DECLARED_FILTERS_PARAM_NAME));
if (hasMultiViewsWelcomeFile(servletContext)) {
// When MultiViews is enabled and there are mapped welcome files, we need to filter on /* otherwise path params won't work on root.
filterRegistration.addMappingForUrlPatterns(EnumSet.of(REQUEST, FORWARD), filterAfterDeclaredFilters, "/*");
}
else {
// Map the forwarding filter to all the resources we found.
for (String mapping : collectedViews.keySet()) {
filterRegistration.addMappingForUrlPatterns(EnumSet.of(REQUEST, FORWARD), filterAfterDeclaredFilters, mapping);
}
// Additionally map the filter to all paths that were scanned and which are also directly accessible.
// This is to give the filter an opportunity to block these.
for (String path : getPublicRootPaths(servletContext)) {
filterRegistration.addMappingForUrlPatterns(null, false, path + "*");
}
}
}
/**
* This will map the {@link FacesServlet} to extensions found during scanning in {@link ApplicationInitializer}.
* This is invoked by {@link ApplicationListener}, because the {@link FacesServlet} has to be available.
* @param servletContext The involved servlet context.
*/
public static void addFacesServletMappings(ServletContext servletContext) {
if (!isFacesViewsEnabled(servletContext)) {
return;
}
Set<String> encounteredExtensions = getEncounteredExtensions(servletContext);
if (isEmpty(encounteredExtensions)) {
return;
}
Set<String> mappings = new HashSet<>(encounteredExtensions);
mappings.addAll(getMappedWelcomeFiles(servletContext));
if (getFacesServletDispatchMethod(servletContext) == DO_FILTER) {
// In order for the DO_FILTER method to work the FacesServlet, in addition the forward filter,
// has to be mapped on all extensionless resources.
mappings.addAll(filterExtension(getMappedResources(servletContext).keySet()));
}
ServletRegistration facesServletRegistration = getFacesServletRegistration(servletContext);
if (facesServletRegistration != null) {
Collection<String> existingMappings = facesServletRegistration.getMappings();
for (String mapping : mappings) {
if (!existingMappings.contains(mapping)) {
facesServletRegistration.addMapping(mapping);
}
}
}
}
/**
* Register a view handler that transforms a view id with extension back to an extensionless one.
* This is invoked by {@link FacesViewsViewHandlerInstaller}, because the {@link Application} has to be available.
* @param servletContext The involved servlet context.
*/
public static void registerViewHander(ServletContext servletContext) {
if (isFacesViewsEnabled(servletContext) && !isEmpty(getEncounteredExtensions(servletContext))) {
Application application = getApplicationFromFactory();
application.setViewHandler(new FacesViewsViewHandler(application.getViewHandler()));
}
}
// Scanning -------------------------------------------------------------------------------------------------------
/**
* Scans for faces-views resources recursively.
*
* @param servletContext The involved servlet context.
* @return The views found during scanning, or an empty map if no views encountered.
*/
static Map<String, String> scanAndStoreViews(ServletContext servletContext, boolean collectExtensions) {
scanAndStoreWelcomeFiles(servletContext);
Map<String, String> collectedViews = new HashMap<>();
Set<String> collectedExtensions = new HashSet<>();
for (String[] rootPathAndExtension : getRootPathsAndExtensions(servletContext)) {
String rootPath = rootPathAndExtension[0];
String extension = rootPathAndExtension[1];
scanViews(servletContext, rootPath, servletContext.getResourcePaths(rootPath), collectedViews, extension, collectedExtensions);
}
if (!collectedViews.isEmpty()) {
servletContext.setAttribute(MAPPED_RESOURCES, unmodifiableMap(collectedViews));
servletContext.setAttribute(REVERSE_MAPPED_RESOURCES, unmodifiableMap(reverse(collectedViews)));
if (collectExtensions) {
storeExtensions(servletContext, collectedViews, collectedExtensions);
}
}
return collectedViews;
}
private static void scanAndStoreWelcomeFiles(ServletContext servletContext) {
Set<String> mappedWelcomeFiles = new HashSet<>();
for (String welcomeFile : WebXml.INSTANCE.init(servletContext).getWelcomeFiles()) {
if (isExtensionless(welcomeFile)) {
if (!welcomeFile.startsWith("/")) {
welcomeFile = "/" + welcomeFile;
}
mappedWelcomeFiles.add(stripTrailingSlash(welcomeFile));
}
}
servletContext.setAttribute(MAPPED_WELCOME_FILES, unmodifiableSet(mappedWelcomeFiles));
}
@SuppressWarnings("unchecked")
private static Set<String[]> getRootPathsAndExtensions(ServletContext servletContext) {
Set<String[]> rootPaths = (Set<String[]>) servletContext.getAttribute(SCAN_PATHS);
if (rootPaths == null) {
rootPaths = new HashSet<>();
rootPaths.add(new String[] { WEB_INF_VIEWS, null });
Set<String> multiViewsPaths = new HashSet<>();
for (String rootPath : csvToList(servletContext.getInitParameter(FACES_VIEWS_SCAN_PATHS_PARAM_NAME))) {
boolean multiViews = rootPath.endsWith("/*");
if (multiViews) {
rootPath = rootPath.substring(0, rootPath.lastIndexOf("/*"));
}
String[] rootPathAndExtension = rootPath.contains("*") ? rootPath.split(quote("*")) : new String[] { rootPath, null };
rootPathAndExtension[0] = normalizeRootPath(rootPathAndExtension[0]);
rootPaths.add(rootPathAndExtension);
if (multiViews) {
multiViewsPaths.add(rootPathAndExtension[0]);
}
}
servletContext.setAttribute(SCAN_PATHS, unmodifiableSet(rootPaths));
servletContext.setAttribute(MULTIVIEWS_PATHS, unmodifiableSet(multiViewsPaths));
}
return rootPaths;
}
private static void storeExtensions(ServletContext servletContext, Map<String, String> collectedViews, Set<String> collectedExtensions) {
servletContext.setAttribute(ENCOUNTERED_EXTENSIONS, unmodifiableSet(collectedExtensions));
if (!collectedExtensions.isEmpty()) {
for (String welcomeFile : getMappedWelcomeFiles(servletContext)) {
if (isMultiViewsEnabled(servletContext) && collectedViews.containsKey(welcomeFile + "/*")) {
servletContext.setAttribute(MULTIVIEWS_WELCOME_FILE, welcomeFile);
}
}
}
}
/**
* A public path is a path that is also directly accessible, e.g. is world readable.
* This excludes the special path /, which is by definition world readable but not included in this set.
*/
@SuppressWarnings("unchecked")
private static Set<String> getPublicRootPaths(ServletContext servletContext) {
Set<String> publicRootPaths = (Set<String>) servletContext.getAttribute(PUBLIC_SCAN_PATHS);
if (publicRootPaths == null) {
publicRootPaths = new HashSet<>();
for (String[] rootPathAndExtension : getRootPathsAndExtensions(servletContext)) {
String rootPath = rootPathAndExtension[0];
if (!"/".equals(rootPath) && !startsWithOneOf(rootPath, RESTRICTED_DIRECTORIES)) {
publicRootPaths.add(rootPath);
}
}
servletContext.setAttribute(PUBLIC_SCAN_PATHS, unmodifiableSet(publicRootPaths));
}
return publicRootPaths;
}
/**
* Scans resources (views) recursively starting with the given resource paths for a specific root path, and collects
* those and all unique extensions encountered in a flat map respectively set.
*
* @param servletContext The involved servlet context.
* @param rootPath One of the paths from which views are scanned. By default this is typically /WEB-INF/faces-view/
* @param resourcePaths Collection of paths to be considered for scanning, can be either files or directories.
* @param collectedViews A mapping of all views encountered during scanning. Mapping will be from the simplified
* form to the actual location relatively to the web root. E.g key "foo", value "/WEB-INF/faces-view/foo.xhtml"
* @param extensionToScan A specific extension to scan for. Should start with a ., e.g. ".xhtml". If this is given,
* only resources with that extension will be scanned. If null, all resources will be scanned.
* @param collectedExtensions Set in which all unique extensions will be collected. May be null, in which case no
* extensions will be collected.
*/
private static void scanViews(ServletContext servletContext, String rootPath, Set<String> resourcePaths,
Map<String, String> collectedViews, String extensionToScan, Set<String> collectedExtensions)
{
if (isEmpty(resourcePaths)) {
return;
}
boolean hasMultiViewsWelcomeFile = hasMultiViewsWelcomeFile(servletContext);
for (String resourcePath : resourcePaths) {
if (isDirectory(resourcePath)) {
if (canScanDirectory(rootPath, resourcePath)) {
scanViews(servletContext, rootPath, servletContext.getResourcePaths(resourcePath), collectedViews, extensionToScan, collectedExtensions);
}
}
else if (canScanResource(resourcePath, extensionToScan)) {
scanView(servletContext, rootPath, resourcePath, collectedViews, collectedExtensions, hasMultiViewsWelcomeFile);
}
}
}
private static void scanView(ServletContext servletContext, String rootPath, String resourcePath,
Map<String, String> collectedViews, Set<String> collectedExtensions, boolean hasMultiViewsWelcomeFile)
{
// Strip the root path from the current path.
// E.g. /WEB-INF/faces-views/foo.xhtml will become foo.xhtml if the root path = /WEB-INF/faces-view/
String resource = stripPrefixPath(rootPath, resourcePath);
// Store the resource with and without an extension, e.g. store both foo.xhtml and foo
collectedViews.put(resource, resourcePath);
String extensionlessResource = stripExtension(resource);
if (isMultiViewsEnabled(servletContext, extensionlessResource)) {
collectedViews.put(extensionlessResource + "/*", resourcePath);
}
else {
if (hasMultiViewsWelcomeFile) { // This will install forwarding filter on /* and therefore we need to cover / ourselves.
collectedViews.put(extensionlessResource + "/", resourcePath);
}
collectedViews.put(extensionlessResource, resourcePath);
}
// Optionally, collect all unique extensions that we have encountered.
if (collectedExtensions != null) {
collectedExtensions.add("*" + getExtension(resourcePath));
}
}
private static String normalizeRootPath(String rootPath) {
String normalizedPath = rootPath;
if (!normalizedPath.startsWith("/")) {
normalizedPath = "/" + normalizedPath;
}
if (!normalizedPath.endsWith("/")) {
normalizedPath = normalizedPath + "/";
}
return normalizedPath;
}
private static boolean canScanDirectory(String rootPath, String directory) {
if (!"/".equals(rootPath)) {
// If a user has explicitly asked for scanning anything other than /, every sub directory of it can be scanned.
return true;
}
// For the special root path /, don't scan /WEB-INF, /META-INF and /resources directories.
return !startsWithOneOf(directory, RESTRICTED_DIRECTORIES);
}
private static boolean canScanResource(String resource, String extensionToScan) {
// If no extension has been explicitly defined, we scan all extensions encountered.
return (extensionToScan == null) || resource.endsWith(extensionToScan);
}
// Helpers for FacesViewsForwardingFilter -------------------------------------------------------------------------
static ExtensionAction getExtensionAction(ServletContext servletContext) {
return getEnumInitParameter(servletContext, FACES_VIEWS_EXTENSION_ACTION_PARAM_NAME, ExtensionAction.class, REDIRECT_TO_EXTENSIONLESS);
}
static PathAction getPathAction(ServletContext servletContext) {
return getEnumInitParameter(servletContext, FACES_VIEWS_PATH_ACTION_PARAM_NAME, PathAction.class, SEND_404);
}
static FacesServletDispatchMethod getFacesServletDispatchMethod(ServletContext servletContext) {
return getEnumInitParameter(servletContext, FACES_VIEWS_DISPATCH_METHOD_PARAM_NAME, FacesServletDispatchMethod.class, DO_FILTER);
}
static boolean isResourceInPublicPath(ServletContext servletContext, String resource) {
for (String path : getPublicRootPaths(servletContext)) {
if (resource.startsWith(path)) {
return true;
}
}
return false;
}
/**
* Obtains the full request URL from the given request and the given resource complete with the query string,
* but with the extension (if any) cut out.
* E.g. <code>http://localhost/foo/bar.xhtml?kaz=1</code> becomes <code>http://localhost/foo/bar?kaz=1</code>
*/
static String getExtensionlessURLWithQuery(HttpServletRequest request, String resource) {
String queryString = (request.getQueryString() == null) ? "" : ("?" + request.getQueryString());
String baseURL = getRequestBaseURL(request);
return baseURL.substring(0, baseURL.length() - 1) + stripExtension(resource) + queryString;
}
// Helpers for FacesViewsViewHandler ------------------------------------------------------------------------------
static boolean isScannedViewsAlwaysExtensionless(ServletContext servletContext) {
String alwaysExtensionless = servletContext.getInitParameter(FACES_VIEWS_SCANNED_VIEWS_EXTENSIONLESS_PARAM_NAME);
return isEmpty(alwaysExtensionless) || parseBoolean(alwaysExtensionless);
}
static ViewHandlerMode getViewHandlerMode(ServletContext servletContext) {
return getEnumInitParameter(servletContext, FACES_VIEWS_VIEW_HANDLER_MODE_PARAM_NAME, ViewHandlerMode.class, STRIP_EXTENSION_FROM_PARENT);
}
@SuppressWarnings("unchecked")
static Set<String> getFacesServletExtensions(ServletContext servletContext) {
Set<String> extensions = (Set<String>) servletContext.getAttribute(FACES_SERVLET_EXTENSIONS);
if (extensions == null) {
extensions = new HashSet<>();
for (String mapping : getFacesServletMappings(servletContext)) {
if (mapping.startsWith("*")) {
extensions.add(mapping.substring(1));
}
}
servletContext.setAttribute(FACES_SERVLET_EXTENSIONS, unmodifiableSet(extensions));
}
return extensions;
}
// Helpers for FacesViewsResolver ---------------------------------------------------------------------------------
static String getMappedPath(String path) {
Map<String, String> mappedResources = getMappedResources(getServletContext());
return (mappedResources != null && mappedResources.containsKey(path)) ? mappedResources.get(path) : path;
}
// Internal helpers -----------------------------------------------------------------------------------------------
private static <E extends Enum<E>> E getEnumInitParameter(ServletContext servletContext, String name, Class<E> type, E defaultValue) {
String value = servletContext.getInitParameter(name);
if (isEmpty(value)) {
return defaultValue;
}
try {
return Enum.valueOf(type, value.toUpperCase(US));
}
catch (Exception e) {
throw new IllegalArgumentException(format("Value '%s' is not valid for context parameter '%s'", value, name), e);
}
}
static Map<String, String> getMappedResources(ServletContext servletContext) {
return getApplicationAttribute(servletContext, MAPPED_RESOURCES);
}
static Map<String, String> getReverseMappedResources(ServletContext servletContext) {
return getApplicationAttribute(servletContext, REVERSE_MAPPED_RESOURCES);
}
static Set<String> getEncounteredExtensions(ServletContext servletContext) {
return getApplicationAttribute(servletContext, ENCOUNTERED_EXTENSIONS);
}
static Set<String> getMappedWelcomeFiles(ServletContext servletContext) {
return getApplicationAttribute(servletContext, MAPPED_WELCOME_FILES);
}
static String getMultiViewsWelcomeFile(ServletContext servletContext) {
return getApplicationAttribute(servletContext, MULTIVIEWS_WELCOME_FILE);
}
static boolean isMultiViewsEnabled(ServletContext servletContext, String resource) {
Set<String> multiviewsPaths = getApplicationAttribute(servletContext, MULTIVIEWS_PATHS);
if (multiviewsPaths != null) {
String path = resource + "/";
for (String multiviewsPath : multiviewsPaths) {
if (path.startsWith(multiviewsPath)) {
return true;
}
}
}
return false;
}
static boolean hasMultiViewsWelcomeFile(ServletContext servletContext) {
return isMultiViewsEnabled(servletContext) && !getMappedWelcomeFiles(servletContext).isEmpty();
}
// Utility --------------------------------------------------------------------------------------------------------
/**
* Returns whether FacesViews feature is enabled. That is, when the <code>org.omnifaces.FACES_VIEWS_ENABLED</code>
* context parameter value does not equal <code>false</code>.
* @param servletContext The involved servlet context.
* @return Whether FacesViews feature is enabled.
* @since 2.5
*/
public static boolean isFacesViewsEnabled(ServletContext servletContext) {
if (facesViewsEnabled == null) {
facesViewsEnabled = !"false".equals(servletContext.getInitParameter(FACES_VIEWS_ENABLED_PARAM_NAME));
}
return facesViewsEnabled;
}
/**
* Returns whether MultiViews feature is enabled. This is implicitly enabled when
* <code>org.omnifaces.FACES_VIEWS_SCAN_PATHS</code> context parameter value is suffixed with <code>/*</code>.
* @param servletContext The involved servlet context.
* @return Whether MultiViews feature is enabled.
* @since 2.5
*/
public static boolean isMultiViewsEnabled(ServletContext servletContext) {
if (multiViewsEnabled == null) {
multiViewsEnabled = !isEmpty((Set<?>) servletContext.getAttribute(MULTIVIEWS_PATHS));
}
return multiViewsEnabled;
}
/**
* Returns whether MultiViews feature is enabled on given request.
* @param request The involved HTTP servlet request.
* @return Whether MultiViews feature is enabled on given request.
* @since 2.6
*/
public static boolean isMultiViewsEnabled(HttpServletRequest request) {
return isMultiViewsEnabled(request.getServletContext(), request.getServletPath());
}
/**
* Strips any mapped welcome file prefix path from the given resource.
* @param servletContext The involved servlet context.
* @param resource The resource.
* @return The resource without the welcome file prefix path, or as-is if it didn't start with this prefix.
* @since 2.5
*/
public static String stripWelcomeFilePrefix(ServletContext servletContext, String resource) {
for (String mappedWelcomeFile : getMappedWelcomeFiles(servletContext)) {
if (resource.endsWith(mappedWelcomeFile)) {
return resource.substring(0, resource.length() - mappedWelcomeFile.length()) + "/";
}
}
return resource;
}
/**
* Strips any special '/WEB-INF/faces-views' prefix path from the given resource.
* @param resource The resource.
* @return The resource without the special prefix path, or as-is if it didn't start with this prefix.
*/
public static String stripFacesViewsPrefix(String resource) {
return stripPrefixPath(WEB_INF_VIEWS, resource);
}
}