XPages to Java EE, Part 7: MVC
Mon Feb 11 15:04:22 EST 2019
- XPages to Java EE, Part 1: Overview
- XPages to Java EE, Part 2: Terminology
- XPages to Java EE, Part 3: Hello, World
- XPages to Java EE, Part 4: Application Servers
- XPages to Java EE, Part 5: Web Pages
- XPages to Java EE, Part 6: Dependencies
- XPages to Java EE, Part 7: MVC
- XPages to Java EE, Part 8: IDE Server Integration
- XPages to Java EE, Part 9: IDE Features Grab Bag
- XPages to Java EE, Part 10: Data Storage
- XPages to Java EE, Part 11: Mixing MVC and an API
- XPages to Java EE, Part 12: Container Authentication
- XPages to Java EE, Part 13: Why Do This, Anyway?
I mentioned in the last post that the rest of this tutorial is going to involve making some technology choices that are less universal than the ones up until this point. While pretty much every new EE app is going to involve CDI and JAX-RS, things will start to diverge once you decide on how you're going to handle authentication, your UI, and your data storage. We're going to dip into the the second one of those today.
Specifically, we'll be choosing the characteristically-dryly-named MVC Spec as the foundation for our interface.
MVC
If you're not familiar with the term "MVC" in a general sense, it stands for Model-View-Controller, and it represents one of the common ways to structure your application to keep the code clean and growable without it turning into a nightmare. There are other ways, and there are some flaws in the design that require band-aid solutions, but in general it remains a very-useful way to write your app. The general idea is that you have three components: the "model" (your data), the "view" (what your user sees), and the "controller" (what connects the two). On the web, this often takes the shape of having controllers pointed to by the "routing" within your application (e.g. /posts/new
), which then do the work of fetching the data and binding it to the view.
XPages is ostensibly MVC-based, but that doesn't really play out in reality. The stack and IDE encourage direct mingling of the view and data (the <xp:dominoDocument/>
data source and SSJS are the primary culprits here), and the lack of a model framework and the stultifying strictures of the built-in NHTTP routing make it very difficult to do it right even if you try (and lord knows we tried).
The MVC Spec
I've talked a bit about the spec before, and the idea of it is to build a simple-to-write-and-read MVC standard for Java EE, using existing EE technologies for the "view" part. I believe that the spec is heavily based on Spring MVC, though I haven't written any Spring apps.
The main reason I'm such a big fan of this spec is that it builds cleanly on top of JAX-RS, which already provides an excellent skeleton for cleanly-organized apps. JAX-RS already encourages clean, RESTful design for accessing data objects, and MVC builds on that to provide a natural way to display a web UI for non-API clients (which is to say, humans).
One thing to bear in mind with MVC 1.0 is that it's not quite a true Java EE component. It was slated for inclusion in Java EE 8, but Oracle cut it from the list before release. However, the spec has support within the Jakarta EE community and remains a likely candidate for future inclusion. Moreover, because the spec is so small and strongly encourages clean design, I feel that it's worth building upon - even if it disappeared tomorrow, almost all of your code would still work.
Adding to the Project
Since the spec isn't included in the default Java EE 8 spec or with EE servers, we'll need to add explicit dependencies to the pom.xml for the project, for both the spec itself and the reference implementation (currently named "Ozark", but it's going through a rename to "Krazo" for trademark purposes):
<!-- MVC 1.0 -->
<dependency>
<groupId>javax.mvc</groupId>
<artifactId>javax.mvc-api</artifactId>
<version>1.0-pfd</version>
</dependency>
<dependency>
<groupId>org.mvc-spec.ozark</groupId>
<artifactId>ozark-core</artifactId>
<version>1.0.0-m04</version>
</dependency>
Creating the Controller
MVC Controllers are implemented as normal JAX-RS resources with the extra @Controller
annotation, and whose methods return an indicator of what view to use. Create a class in the com.example
package called HomeController
:
package com.example;
import java.time.LocalDateTime;
import javax.inject.Inject;
import javax.mvc.Controller;
import javax.mvc.Models;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@Path("/")
@Controller
public class HomeController {
@Inject
Models models;
@GET
public String hello() {
models.put("hello", "Hello, MVC!"); //$NON-NLS-1$
models.put("now", LocalDateTime.now()); //$NON-NLS-1$
return "hello.jsp"; //$NON-NLS-1$
}
}
The "Models" object is a MVC-provided object that acts sort of like a viewScope
: you toss whatever objects you'd like into it and they're available as variables on your page. Despite having an important-sounding name, it's really just a simplified Map
.
Creating the View
MVC supports several "templating" technologies, among them JSP and JSF Facelets. However, though it uses JSF technology, it's not meant to be used for a full JSF app with server-side state. From what I can tell, the JSF support is more for those who already have JSF apps to use. Despite XPages's heritage, that doesn't really apply to us. Create a new folder named "views" within the "webapp/WEB-INF" directory in "Deployed Resources" (which is "src/main/webapp" in the filesystem). Then, create a new file and call it "hello.jsp":
Set its contents to:
<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" trimDirectiveWhitespaces="true"%>
<!DOCTYPE html>
<html lang="${translation._lang}">
<head>
<title>${translation.appTitle}</title>
</head>
<body>
<h1>${hello}</h1>
<p>I was loaded at ${now}.</p>
</body>
</html>
Since this still uses CDI, we're still able to use the translation
bean we set up earlier, but now we have access to the extra variables the controller set up.
If you do another Maven build with goals "clean install tomee:run" and visit http://localhost:8081/javaeetutorial/resources now, you should be greeted with the expected basic page:
Working With a Model
So that's two out of three down; time to simulate some data access. Using a real backing database will be its own large topic, so for now we'll create a simple in-memory object.
Create a new class in the "com.example" package named "PersonController":
package com.example;
import javax.inject.Inject;
import javax.mvc.Controller;
import javax.mvc.Models;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.ws.rs.BeanParam;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
@Path("/people")
@Controller
public class PersonController {
public static class FormPerson {
@FormParam("firstName") @NotEmpty
private String firstName;
@FormParam("lastName") @NotEmpty
private String lastName;
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
}
@Inject
Models models;
@GET
public String home() {
return "person-new.jsp"; //$NON-NLS-1$
}
@POST
public String createPerson(@BeanParam @Valid FormPerson person) {
models.put("person", person); //$NON-NLS-1$
return "person-created.jsp"; //$NON-NLS-1$
}
}
Next, create a new file in the "WEB-INF/views" directory named "person-new.jsp":
<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" trimDirectiveWhitespaces="true"%>
<!DOCTYPE html>
<html lang="${translation._lang}">
<head>
<title>${translation.appTitle}</title>
</head>
<body>
<h1>Create Person</h1>
<form method="post" action="people">
<dl>
<dt>First Name</dt>
<dd><input name="firstName" required/></dd>
</dl>
<dl>
<dt>Last Name</dt>
<dd><input name="lastName"/></dd>
</dl>
<input type="submit" value="Submit"/>
</form>
</body>
</html>
Finally, create a second JSP file in the same directory named "person-created.jsp":
<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" trimDirectiveWhitespaces="true"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="${translation._lang}">
<head>
<title>${translation.appTitle}</title>
</head>
<body>
<h1>Created Person</h1>
<dl>
<dt>First Name</dt>
<dd><c:out value="${person.firstName}"/></dd>
</dl>
<dl>
<dt>Last Name</dt>
<dd><c:out value="${person.lastName}"/></dd>
</dl>
</body>
</html>
Now, do another Maven build and visit http://localhost:9091/javaeetutorial/resources/people:
In this screenshot, you can see that the "First Name" field got a red outline because it's marked as required
and I entered and then deleted a first name value. Neat!
Anyway, fill in something for "First Name" but not "Last Name", and hit "Submit". You'll be greeted with... nothing! Well, visibly nothing, anyway. You'll have a 400
response in your browser with no content, and you'll see a line like this in your server console:
11-Feb-2019 14:52:55.942 WARNING [http-nio-9091-exec-2] org.apache.cxf.jaxrs.validation.ValidationExceptionMapper.toResponse Value '' of PersonController.createPerson.arg0.lastName: {javax.validation.constraints.NotEmpty.message}
Hey, the server-side validation worked! Down the line, we'll probably add an exception handler to display this type of thing to the user, but, for now, the important part is that invalid data was blocked before it even got to our code.
Go back and enter something for each of the name fields, and then hit "Submit" again. While doing so, bask in the pleasant fact that, because there's no server-side state, you don't need to worry about mangled JSF view state or anything. Once you submit, you should be greeted with the "Created Person" page with your data:
As a nice bonus, because this output page uses the JSTL <c:out/>
tag, the values are nicely HTML-escaped, making it a bit more secure than our original Hello World page.
Next Steps
In the next couple posts, we'll cover the ominous topics of authentication and data storage.