Rewriting The OpenNTF Site With Jakarta EE: UI
Jun 27, 2022, 3:06 PM
- Rewriting The OpenNTF Site With Jakarta EE, Part 1
- Rewriting The OpenNTF Site With Jakarta EE: UI
In what may be the last in this series for a bit, I'll talk about the current approach I'm taking for the UI for the new OpenNTF web site. This post will also tread ground I've covered before, when talking about the Jakarta MVC framework and JSP, but it never hurts to reinforce the pertinent aspects.
MVC
The entrypoint for the UI is Jakarta MVC, which is a framework that sits on top of JAX-RS. Unlike JSF or XPages, it leaves most app-structure duties to other components. This is due both to its young age (JSF predates and often gave rise to several things we've discussed so far) and its intent. It's "action-based", where you define an endpoint that takes an incoming HTTP request and produces a response, and generally won't have any server-side UI state. This is as opposed to JSF/XPages, where the core concept is the page you're working with and the page state generally exists across multiple requests.
Your starting point with MVC is a JAX-RS REST service marked with @Controller
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | package webapp.controller; import java.text.MessageFormat; import bean.EncoderBean; import jakarta.inject.Inject; import jakarta.mvc.Controller; import jakarta.mvc.Models; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import model.home.Page; @Path("/pages") public class PagesController { @Inject Models models; @Inject Page.Repository pageRepository; @Inject EncoderBean encoderBean; @Path("{pageId}") @GET @Produces(MediaType.TEXT_HTML) @Controller public String get(@PathParam("pageId") String pageId) { String key = encoderBean.cleanPageId(pageId); Page page = pageRepository.findBySubject(key) .orElseThrow(() -> new NotFoundException(MessageFormat.format("Unable to find page for ID: {0}", key))); models.put("page", page); //$NON-NLS-1$ return "page.jsp"; //$NON-NLS-1$ } } |
In the NSF, this will respond to requests like /foo.nsf/xsp/app/pages/Some_Page_Name
. Most of what is going on here is the same sort of thing we saw with normal REST services: the @Path
, @GET
, @Produces
, and @PathParam
are all normal JAX-RS, while @Inject
uses the same CDI scaffolding I talked about in the last post.
MVC adds two things here: @Inject Models models
and @Controller
.
The Models
object is conceptually a Map
that houses variables that you can populate to be accessible via EL on the rendered page. You can think of this like viewScope
or requestScope
in XPages and is populated in something like the beforePageLoad
phase. Here, I use the Models
object to store the Page
object I look up with JNoSQL.
The @Controller
annotation marks a method or a class as participating in the MVC lifecycle. When placed on a class, it applies to all methods on the class, while placing it on a method specifically allows you to mix MVC and "normal" REST resources in the same class. Doing that would be useful if you want to, for example, provide HTML responses to browsers and JSON responses to API clients at the same resource URL.
When a resource method is marked for MVC use, it can return a string that represents either a page to render or a redirection in the form "redirect:some/resource"
. Here, it's hard-coded to use "page.jsp"
, but in another situation it could programmatically switch between different pages based on the content of the request or state of the app.
While this looks fairly clean on its own, it's important to bear in mind both the strengths and weaknesses of this approach. I think it will work here, as it does for my blog, because the OpenNTF site isn't heavy on interactive forms. When dealing with forms in MVC, you'll have to have another endpoint to listen for @POST
(or other verbs with a shim), process that request from scratch, and return a new page. For example, from the XPages JEE example app:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @Path("create") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Controller public String createPerson( @FormParam("firstName") @NotEmpty String firstName, @FormParam("lastName") String lastName, @FormParam("birthday") String birthday, @FormParam("favoriteTime") String favoriteTime, @FormParam("added") String added, @FormParam("customProperty") String customProperty ) { Person person = new Person(); composePerson(person, firstName, lastName, birthday, favoriteTime, added, customProperty); personRepository.save(person); return "redirect:nosql/list"; } |
That's already fiddlier than the XPages version, where you'd bind fields right to bean/document properties, and it gets potentially more complicated from there. In general, the more form-based your app is, the better a fit XPages/JSF is.
JSP
While MVC isn't intrinsically tied to JSP (it ships with several view engine hooks and you can write your own), JSP has the advantage of being built in to all Java webapp servers and is very well fit to purpose. When writing JSPs for MVC, the default location is to put them in WEB-INF/views
, which is beneath WebContent
in an NSF project:
The "tags" there are the general equivalent of XPages Custom Controls, and their presence in WEB-INF/tags
is convention. An example page (the one used above) will tend to look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <%@page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" session="false" %> <%@taglib prefix="t" tagdir="/WEB-INF/tags" %> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <t:layout> <turbo-frame id="page-content-${page.linkId}"> <div> ${page.html} </div> <c:if test="${not empty page.childPageIds}"> <div class="tab-container"> <c:forEach items="${page.cleanChildPageIds}" var="pageId" varStatus="pageLoop"> <input type="radio" id="tab${pageLoop.index}" name="tab-group" ${pageLoop.index == 0 ? 'checked="checked"' : ''} /> <label for="tab${pageLoop.index}">${fn:escapeXml(encoder.cleanPageId(pageId))}</label> </c:forEach> <div class="tabs"> <c:forEach items="${page.cleanChildPageIds}" var="pageId"> <turbo-frame id="page-content-${pageId}" src="xsp/app/pages/${encoder.urlEncode(pageId)}" class="tab" loading="lazy"> </turbo-frame> </c:forEach> </div> </div> </c:if> </turbo-frame> </t:layout> |
There are, by shared lineage and concept, a lot of similarities with an XPage here. The first four lines of preamble boilerplate are pretty similar to the kind of stuff you'd see in an <xp:view/>
element to set up your namespaces and page options. The tag prefixing is the same idea, where <t:layout/>
refers to the "layout" custom tag in the NSF and <c:forEach/>
refers to a core control tag that ships with the standard tag library, JSTL. The <turbo-frame/>
business isn't JSP - I'll deal with that later.
The bits of EL here - all wrapped in ${...}
- are from Expression Language 4.0, which is the current version of XPages's aging EL. On this page, the expressions are able to resolve variables that we explicitly put in the Models
object, such as page
, as well as CDI beans with the @Named
annotation, such as encoderBean
. There are also a number of implicit objects like request
, but they're not used here.
In general, this is safely thought of as an XPage where you make everything load-time-bound and set viewState="nostate"
. The same sorts of concepts are all there, but there's no concept of a persistent component that you interact with. Any links, buttons, and scripts will all go to the server as a fresh request, not modifying an existing page. You can work with application and session scopes, but there's no "view" scope.
Hotwired Turbo
Though this app doesn't have much need for a lot of XPages's capabilities, I do like a few components even for a mostly "read-only" app. In particular, the <xe:djContentPane/>
and <xe:djTabContainer/>
controls have the delightful capability of deferring evaluation of their contents to later requests. This is a powerful way to speed up initial page load and, in the case of the tab container, skip needing to render parts of the page the user never uses.
For this and a couple other uses, I'm a fan of Hotwired Turbo, which is a library that grew out of 37 Signals's Rails-based development. The goal of Turbo and the other Hotwired components is to keep the benefits of server-based HTML rendering while mixing in a lot of the niceties of JS-run apps. There are two things that Turbo is doing so far in this app.
The first capability is dubbed "Turbo Drive", and it's sort of a freebie: you enable it for your app, tell it what is considered the app's base URL, and then it will turn any in-app links into "partial refresh" links: it downloads the page in the background and replaces just the changed part on the page. Though this is technically doing more work than a normal browser navigation, it ends up being faster for the user interface. And, since it also updates the URL to match the destination page and doesn't require manual modification of links, it's a drop-in upgrade that will also degrade gracefully if JavaScript isn't enabled.
The second capability is <turbo-frame/>
up there, and it takes a bit more buy-in to the JS framework in your app design. The way I'm using Turbo Frames here is to support the page structure of OpenNTF, which is geared around a "primary" page as well as zero or more referenced pages that show up in tabs. Here, I'm buying in to Turbo Frames by surrounding the whole page in a <turbo-frame/>
element with an id
using the page's key, and then I reference each "sub-page" in a tab with that same ID. When loading the frame, Turbo makes a call to the src
page, finds the element with the matching id
value, and drops it in place inside the main document. The loading="lazy"
parameter means that it defers loading until the frame is visible in the browser, which is handy when using the HTML/CSS-based tabs I have here.
I've been using this library for a while now, and I've been quite pleased. Though it was created for use with Rails, the design is independent of the server implementation, and the idioms fit perfectly with this sort of Java app too.
Conclusion
I think that wraps it up for now. As things progress, I may have more to add to this series, but my hope is that the app doesn't have to get much more complicated than the sort of stuff seen in this series. There are certainly big parts to tackle (like creating and managing projects), but I plan to do that by composing these elements. I remain delighted with this mode of NSF-based app development, and look forward to writing more clean, semi-declarative code in this vein.