New: dynamic route segments
How do you serve a URL like /organizations/123/members in Faces? Until now the only answer was PHP-inspired MultiViews configured as follows in web.xml, which was already available since the very first OmniFaces version 1.0 in 2012:
<context-param>
<param-name>org.omnifaces.FACES_VIEWS_SCAN_PATHS</param-name>
<param-value>/*.xhtml/*</param-value>
</context-param>
<welcome-file-list>
<welcome-file>index</welcome-file>
</welcome-file-list>
And this in the bean associated with /organizations/index.xhtml:
@Inject
@Param(pathIndex = 0)
private Long id; // So we can preload in @PostConstruct.
@Inject
@Param(pathIndex = 1)
private String action; // Can be e.g. a path for ui:include (use enum/validation for robustness).
Whereafter the view has to branch on that action to decide whether it shows the members, the settings, or something else. E.g. a dynamic include path <ui:include src="/WEB-INF/includes/organization/#{bean.action}.xhtml" /> or even conditional rendering. Every subpage you add is another branch, and a typo in the URL possibly arrives as a perfectly valid action which nothing renders. In other words, the URL structure ends up living in your bean instead of in your file structure.
As of OmniFaces 5.5 you can put the variable part in a directory name instead, wrapped in square brackets, the way you may already know it from Next.js and friends in case you're also familiar with them. Given the following file structure:
src
└── main
└── webapp
└── organizations
├── [id]
│ ├── index.xhtml
│ └── members.xhtml
└── settings
└── members.xhtml
Yes, the directory on disk is literally named [id]. Square brackets and all, and that's exactly what marks it as a dynamic route segment. The above file structure makes the Facelets 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)
This needs no additional configuration beyond the earlier shown web.xml. The segment value is injectable by name via the new pathName attribute of @Param:
@Inject
@Param(pathName = "id")
private Long id;
A link to such a view spells out the route in the outcome and supplies each segment with the also new name attribute of <o:pathParam>:
<h:link value="Members" outcome="/organizations/[id]/members">
<o:pathParam name="id" value="#{organization.id}" />
</h:link>
which renders as <a href="/context-path/organizations/123/members">Members</a>. Without a name the tag keeps behaving as before and appends the value as the next positional path parameter of a MultiViews view.
Of course, just inlining continues to work totally fine:
<h:link value="Members" outcome="/organizations/#{organization.id}/members" />
Rules of the road
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 always wins from a dynamic one at the same level, which means a literal sibling is a value the segment can never take; in the above example an organization whose id is settings is thus unreachable. An application without any bracketed directory never reaches the dynamic route resolution at all and therefore behaves exactly as before.
The segments nest and combine with MultiViews, so a view of /[locale]/products/[sku]/reviews.xhtml answers to /nl/products/12345/reviews/2 with nl and 12345 available as @Param(pathName = "locale") and @Param(pathName = "sku"), and 2 available as @Param(pathIndex = 0) in the bean associated with reviews.xhtml. The bare /organizations/123 in the first example works because of the index.xhtml welcome file in the bracketed directory having @Param(pathIndex = 0) in the associated bean.
Only a directory name is interpreted this way. A bracketed file name such as /organizations/[oid]/members/[mid].xhtml with the intent to capture /organizations/123/members/456 is ignored and logged as a warning, especially because some containers reject those files with a 400 before the request even reaches the FacesServlet. The correct solution therefor is to continue using the @Param(pathIndex = 0) in the bean associated with /organizations/[id]/members.xhtml. The more future-proof way, though, is to make use of the welcome file facility /organizations/[oid]/members/[mid]/index.xhtml and a @Param(pathName = "mid") in the bean associated with index.xhtml. This way we can easily extend to e.g. /organizations/[oid]/members/[mid]/settings.xhtml.
All in all, your URL structure is now simply your file structure again, also when it has variable parts in it. See also the FacesViews documentation for the complete story.
Fixes
A handful small fixes next to the one big ticket feature:
<o:massAttribute> failed to apply the attribute to components conditionally created by any nested JSTL tags or dynamic includes when the view is built for the second time during a postback (#990). This gap was discovered while reviewing and improving the view build time performance of Mojarra.
@ViewScoped unload confirmation can now finally also be registered via the canonical and modern JS way window.addEventListener("beforeunload", handler) instead of only with window.onbeforeunload = handler (#986).
@ViewScoped answers the unload beacon with a 204 instead of a bodyless 200, so that an in-flight navigation (e.g. redirect from view scoped bean action) cannot anymore potentially collide with the unload when you have a window.beforeunload handler registered, which would listen on pagehide event instead (#989).
PWAResourceHandler service worker cache is now not anymore stale when resource contents change rather than resource set itself (#987).
You can find the complete list of additions, changes and fixes at What's new in OmniFaces 5.5? in the showcase.
Installation
Non-Maven users: download OmniFaces 5.5 JAR and drop it in /WEB-INF/lib the usual way, replacing the older version if any.
Maven users:
<dependency>
<groupId>org.omnifaces</groupId>
<artifactId>omnifaces</artifactId>
<version>5.5</version>
</dependency>
How about OmniFaces 4.x and 3.x?
OmniFaces 4.7.13 and 3.14.24 have been released as well. They contain the <o:massAttribute>, @ViewScoped unload 204 and PWAResourceHandler fixes, but not the dynamic route segments, nor the @ViewScoped addEventListener which are 5.x only.

No comments:
Post a Comment