- ExtensionlessURLs

Available since OmniFaces 1.0

FacesViews is a mechanism to use SEO-friendly extensionless URLs in a Faces application without the need to enlist individual Facelet source files in some configuration file.

All files found in the scanned directories are automatically mapped as Facelets files and made available using both their original extension as well as without an extension. By default, all URLs generated by ViewHandler.getActionURL(FacesContext, String), which is used by among others <h:form>, <h:link>, <h:button> and all extended tags, will also be extensionless. And, URLs with an extension will be 301-redirected to the extensionless one.

Scanning is done automatically and the feature is compatible with applications that have no web.xml or faces-config.xml. As such it can be used as an alternative to declaring the FacesServlet in web.xml for the .xhtml to .xhtml mapping, which additionally prevents exposing the source code of those Facelets that happens with the default Faces mapping.

Usage

Zero configuration

Put Facelets source files into the /WEB-INF/faces-views directory. All Facelets files in this special directory will be automatically scanned as extensionless URLs. Given the following file structure:

/WEB-INF/faces-views/index.xhtml
/WEB-INF/faces-views/users/add.xhtml
/normal.xhtml

the Facelets are available via the following URLs, given a root deployment on domain example.com:

example.com/index
example.com/users/add
example.com/index.xhtml       (301-redirects to /index by default)
example.com/users/add.xhtml   (301-redirects to /users/add by default)
example.com/normal.xhtml

Note that although the directory outside /WEB-INF/faces-views is not scanned, the FacesServlet is mapped on all extensions found in /WEB-INF/faces-views, so this also affects files outside this directory. In the above example /normal.xhtml is thus also available via the .xhtml extension. Whether the extension variant of a scanned view redirects to the extensionless one, is configurable via FACES_VIEWS_EXTENSION_ACTION_PARAM_NAME.

Minimal configuration

Below is the minimal web.xml configuration to make all Facelets source files found in the root folder and all subdirectories of the public web content (excluding /WEB-INF, /META-INF and /resources) available as extensionless URLs:

<context-param>
    <param-name>org.omnifaces.FACES_VIEWS_SCAN_PATHS</param-name>
    <param-value>/*.xhtml</param-value>
</context-param>

The path pattern /*.xhtml basically means that all files with the .xhtml extension from the directory / must be scanned, including all sub directories. In case you want to scan only .xhtml files in the directory /foo, then use path pattern of /foo/*.xhtml instead. In case you want to scan all files in the directory /foo, then use path pattern of /foo. You can specify multiple values separated by a comma. Given the following file structure:

/page1.xhtml
/foo/page2.xhtml
/WEB-INF/resources/template.xhtml
/script.js

the Facelets are available via the following URLs:

example.com/page1
example.com/foo/page2
example.com/page1.xhtml       (301-redirects to /page1 by default)
example.com/foo/page2.xhtml   (301-redirects to /foo/page2 by default)

Note that /WEB-INF is not scanned and thus template.xhtml is not made publicly available. Likewise /script.js is not scanned either as it doesn't have the configured extension. Finally, although a web.xml is used, there does not need to be a mapping for the FacesServlet in it.

MultiViews configuration

A MultiViews view swallows any trailing path segments and exposes them as positional path parameters. Enabling it is a matter of suffixing the path pattern with /*. The support was added in OmniFaces 2.5. Below is the web.xml configuration which extends the above minimal configuration with MultiViews support:

<context-param>
    <param-name>org.omnifaces.FACES_VIEWS_SCAN_PATHS</param-name>
    <param-value>/*.xhtml/*</param-value>
</context-param>

On an example URL of https://example.com/context/foo/bar/baz when neither /foo/bar/baz.xhtml nor /foo/bar.xhtml exist, but /foo.xhtml does exist, then the request will forward to /foo.xhtml and make the values bar and baz available as injectable path parameters via @Param in the managed bean associated with /foo.xhtml.

@Inject
@Param(pathIndex = 0)
private String bar;

@Inject
@Param(pathIndex = 1)
private String baz;

MultiViews can be enabled on a subset of the scanned views by suffixing only the path pattern concerned. A path pattern of /*.xhtml, /foo/* thus scans every .xhtml file but enables MultiViews only on the files in /foo, so that example.com/foo/page2/bar forwards to /foo/page2.xhtml while example.com/page1/bar does not resolve at all.

Dynamic routes configuration

A directory whose name is wrapped in square brackets is a dynamic route segment. It matches exactly one path segment and exposes it under the bracketed name. The support was added in OmniFaces 5.5 and needs no configuration beyond the scan path above. Given the following file structure:

/organizations/[id]/index.xhtml
/organizations/[id]/members.xhtml
/organizations/settings/members.xhtml

the Facelets are available via the following URLs:

example.com/organizations/123                (forwards to /organizations/[id]/index.xhtml with segment "id" being "123")
example.com/organizations/123/members        (forwards to /organizations/[id]/members.xhtml with segment "id" being "123")
example.com/organizations/settings/members   (forwards to /organizations/settings/members.xhtml without any segment)

The segment value is available as an injectable path parameter via @Param in the managed bean associated with the forwarded view.

@Inject
@Param(pathName = "id")
private String id;

A dynamic route directory needs no welcome file, in which case only the views inside it answer and the bare /organizations/123 does not resolve. The first URL above thus additionally requires an extensionless welcome file.

A request path is resolved by first looking for an exact match among the scanned views, then walking the dynamic route segments, and only then falling back to MultiViews. A literal directory is always preferred over a dynamic one at the same level, which means a literal sibling is a value the dynamic segment can never take: in the above example an organization whose id is settings is unreachable. An application without any bracketed directory never reaches the dynamic route resolution at all and therefore behaves exactly as before.

Dynamic route segments nest and compose with MultiViews, so /[locale]/products/[sku]/reviews.xhtml answers to /nl/products/12345/reviews/2 with nl and 12345 available by name and 2 available as @Param(pathIndex = 0).

Note that only a directory name is interpreted this way. A bracketed file name such as [id].xhtml is scanned literally and logged as a warning, as square brackets are gen-delims per RFC 3986 which containers may refuse in a URL outright. For the same reason, a link to a dynamic route is rendered with every segment substituted, which is why <o:pathParam name> must supply them, see PathParam.

Three configurations have no correct interpretation and fail the deployment:

  • Two differently named dynamic route segments under the same parent, e.g. /organizations/[id] next to /organizations/[slug], as there is no defensible way to choose between them.
  • The same segment name twice in one path, e.g. /[id]/foo/[id]/bar, as a path parameter is addressed by name and could then not address both.
  • An unbalanced or empty square bracket pair in a directory name, e.g. [id or [].

Welcome files

If a <welcome-file> is defined in web.xml that's scanned by FacesViews and the default REDIRECT_TO_EXTENSIONLESS extension action is used, then it's necessary to define an extensionless welcome file to prevent a request to / being redirected to /[welcome file]. E.g. without this https://example.com will redirect to https://example.com/index.

<welcome-file-list>
    <welcome-file>index</welcome-file>
</welcome-file-list>

This is also what makes the bare URL of a directory answer to the welcome file inside it, which includes a dynamic route directory, as in /organizations/[id]/index.xhtml answering to /organizations/123.

If you're using MultiViews on a site-wide basis and have a welcome file configured for it, then basically any request which doesn't match any physical file will end up in that welcome file. In case this is undesirable, because you're having e.g. a REST API listening on /api/* or a websocket endpoint listening on /push/*, then you can configure them as an exclude pattern as below:

<context-param>
    <param-name>org.omnifaces.FACES_VIEWS_SCAN_PATHS</param-name>
    <param-value>/*.xhtml/*, !/api, !/push</param-value>
</context-param>

Dispatch methods

Faces normally inspects the request URI to derive a logical view id from it. It assumes the FacesServlet is either mapped on a prefix path or an extension, and will get confused when an extensionless "exactly mapped" request is encountered. To counter this, FacesViews makes use of FacesViewsForwardingFilter which intercepts each request and makes it appear to Faces that the request was a normal extension mapped one.

Two dispatch methods are used to do this: forwarding, and wrapping the request and continuing the filter chain. For the latter the FacesServlet is programmatically mapped to every individual scanned view. A dynamic route is not expressible as a servlet URL pattern, as the specification has only exact, prefix, extension and default mappings, and is therefore always resolved by forwarding.

These internal details are important to be aware of, since they greatly influence how extensionless requests interact with other filter based functionality such as security filters, compression filters, file upload filters, etcetera. With the forwarding method, such filters typically have to be set to dispatch type FORWARD as well. The FacesViews filter is by default the first in the chain, so other filters which are set to dispatch type REQUEST are then not invoked at all, as the chain is ended. Set FACES_VIEWS_FILTER_AFTER_DECLARED_FILTERS_PARAM_NAME to place it after the filters declared in web.xml, in which case those are invoked, but they should not modify the response, as a forward clears the response buffer so far when it is not yet committed.

Configuration

The following context parameters are available.

All available context parameters
"org.omnifaces.FACES_VIEWS_ENABLED" Used to completely switch scanning off.
Allowed values: {true,false}
Default value: true
(note that if no /WEB-INF/faces-views directory is present and no explicit paths have been configured, no scanning will be done either)
"org.omnifaces.FACES_VIEWS_SCAN_PATHS" A comma separated list of paths that are to be scanned in addition to /WEB-INF/faces-views.
Allowed values: any path relative to the web root, including the root path (/) and /WEB-INF. A wildcard can be added to the path, which will cause only files with the given extension te be scanned.
Examples:
- Scan all files in both folder1 and folder2: /folder1, /folder2
- Scan only .xhtml files in the root: /*.xhtml
Note that when the root path is given, all its sub paths are also scanned EXCEPT WEB-INF, META-INF and resources. If those have to be scanned as well, they can be added to the list of paths explicitly.
Default value: /WEB-INF/faces-views (note when this value is set, those paths will be in addition to the default /WEB-INF/faces-views)
"org.omnifaces.FACES_VIEWS_SCANNED_VIEWS_ALWAYS_EXTENSIONLESS" Used to set how scanned views should be rendered in Faces controlled links. With this setting 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 they are rendered with an extension. When set to true links are always rendered without an extension.
Default value: true
"org.omnifaces.FACES_VIEWS_EXTENSION_ACTION" Determines the action that is performed whenever a resource is requested WITH extension that's also available without an extension.
Allowed values are enumerated in ExtensionAction, which have the following meaning:
- REDIRECT_TO_EXTENSIONLESS: Send a 301 (permanent) redirect to the same URL, but with the extension removed. E.g. /foo.xhtml redirects to /foo.
- SEND_404: Send a 404 (not found), makes it look like e.g. /foo.xhtml never existed and there's only /foo.
- PROCEED: No special action is taken. Both /foo.xhtml and /foo are processed as-if they were separate views (with same content).
Default value: REDIRECT_TO_EXTENSIONLESS
"org.omnifaces.FACES_VIEWS_PATH_ACTION" 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 (e.g. the paths set by "org.omnifaces.FACES_VIEWS_SCAN_PATHS", but excluding the root path /).
Allowed values are enumerated in PathAction, which have the following meaning:
- SEND_404: Send a 404 (not found), makes it look like e.g. /path/foo.xhtml never existed and there's only /foo and optionally /foo.xhtml.
- REDIRECT_TO_SCANNED_EXTENSIONLESS: Send a 301 (permanent) redirect to the resource corresponding with the one that was scanned. E.g. /path/foo.xml redirects to /foo.
- PROCEED: No special action is taken. /path/foo.xml and /foo (and optionally /foo.xhtml) will be accessible.
Default value: SEND_404
"org.omnifaces.FACES_VIEWS_FILTER_AFTER_DECLARED_FILTERS" Used to set whether the FacesViewsForwardingFilter should match before declared filters (false) or after declared filters (true).
Default value: false (the FacesViews forwarding filter is the first in the filter chain)
"org.omnifaces.FACES_VIEWS_LOWERCASED_REQUEST_URI" Used to set whether the request URI should only match the lowercased form of the file name. By default, a scanned view of for example /TitleCasedFileName.xhtml will listen to a request URI of /TitleCasedFileName, but when this setting is set to true, then it will instead listen to a lowercased request URI of /titlecasedfilename.
Default value: false (the request URI must exactly match the letter case of the file name)

PrettyFaces

Note that there is some overlap between this feature and PrettyFaces. 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 .xhtml and to no-extension without needing to explicitly declare the FacesServlet in web.xml.

Specifically, FacesViews will thus not 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 PrettyFaces.

Demo

The showcase application runs on FacesViews and is thus an implicit demo. An extra demo is given below:

Navigate to FacesViews pages without an extension: viewsdemo

Demo source code
<p>
    The showcase application runs on FacesViews and is thus an implicit demo. An extra demo
    is given below:
</p>
<p>
    Navigate to FacesViews pages without an extension: 
    <a href="#{request.contextPath}/viewsdemo">viewsdemo</a>
</p>