XPages to Java EE, Part 10: Data Storage
Fri Feb 15 12:48:50 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?
How you store your data in an application is a potentially-huge topic, which is one of the reasons I've pushed it off for a while.
Designer's Curse
This is particularly the case because of the habits we learned over the years as Domino developers. One of the most grievous wounds Domino inflicted on us was an encouragement to always write directly to the data-storage implementation objects - forms and views for Notes client design or the lsxbe/lotus.domino
classes for LotusScript and Java. They work, sure - fetching a document, setting fields, and storing it will get the job done - but it's an extremely-bad habit to work without a model framework and some level of indirection. Various people, including me, have made valiant efforts to add a model/DAO layer into XPages development in particular, but they've met with little uptake outside the individual developers who wrote them.
Fortunately, Java EE does not suffer from this specific brain poison, and it has a long history of abstracted data access, primarily via the Java Persistence API, traditionally backed by a JDBC driver for a SQL database. The point of an API like that is to let you write your model objects with just some annotations to explain to JPA bits about how it should be stored, and JPA will take care of the dirty work of actually mapping data types, writing queries, fetching data, and so forth.
JNoSQL
We won't be using JPA for this example, though. Instead, we'll be adding our second incubating spec: JNoSQL. JNoSQL is intended to be essentially "JPA for NoSQL", a largely-rethought API that won't crash into the hackiness of Hibernate's valiant attempt of re-using JPA directly. JNoSQL is currently slated for standardization as part of Jakarta EE and is under active development, but reached a point a while ago where it's good for use.
However, while there's technically a Domino JNoSQL driver that I put together last year, it's more of a POC than a real thing, and we'll skip it for this. For my uses, I've been using Darwino, which does have a splendid JNoSQL driver, but this series isn't the place to go through getting set up with that. For simplicity's sake, we'll be using MongoDB, which is quick to set up and is probably the furthest-developed driver in core JNoSQL.
MongoDB
So, to start out with, install MongoDB somewhere locally. This differs system-by-system - on Linux and macOS, I think it's available with package managers, or for any OS you can download an installer from their site.
Once it's installed, create a database named "exampledb" and a collection within it named "Person", as seen here with Compass, the standard admin app.
Add the Driver
In your project's "pom.xml", add the JNoSQL document DB packages and MongoDB driver to your dependencies
block:
<!-- JNoSQL -->
<dependency>
<groupId>org.jnosql.artemis</groupId>
<artifactId>artemis-core</artifactId>
<version>0.0.7</version>
</dependency>
<dependency>
<groupId>org.jnosql.artemis</groupId>
<artifactId>artemis-document</artifactId>
<version>0.0.7</version>
</dependency>
<dependency>
<groupId>org.jnosql.artemis</groupId>
<artifactId>artemis-validation</artifactId>
<version>0.0.7</version>
</dependency>
<dependency>
<groupId>org.jnosql.diana</groupId>
<artifactId>mongodb-driver</artifactId>
<version>0.0.7</version>
</dependency>
For reference, "artemis" in JNoSQL terms refers to the mapping API - the annotations we're going to use - while "diana" refers to the driver portion.
Create the Configuration Class
Create a new class in the model
package called DocumentCollectionManagerProducer
:
package model;
import java.util.Collections;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import org.jnosql.diana.api.Settings;
import org.jnosql.diana.api.document.DocumentCollectionManager;
import org.jnosql.diana.api.document.DocumentCollectionManagerFactory;
import org.jnosql.diana.api.document.DocumentConfiguration;
import org.jnosql.diana.mongodb.document.MongoDBDocumentCollectionManager;
import org.jnosql.diana.mongodb.document.MongoDBDocumentCollectionManagerFactory;
import org.jnosql.diana.mongodb.document.MongoDBDocumentConfiguration;
@ApplicationScoped
public class DocumentCollectionManagerProducer {
private DocumentConfiguration<MongoDBDocumentCollectionManagerFactory> configuration;
private DocumentCollectionManagerFactory<MongoDBDocumentCollectionManager> managerFactory;
@PostConstruct
public void init() {
configuration = new MongoDBDocumentConfiguration();
// Modify this if MongoDB is not on localhost
Map<String, Object> settings = Collections.singletonMap("mongodb-server-host-1", "localhost:27017"); //$NON-NLS-1$ //$NON-NLS-2$
managerFactory = configuration.get(Settings.of(settings));
}
@Produces
public DocumentCollectionManager getManager() {
return managerFactory.get("exampledb"); //$NON-NLS-1$
}
}
There's a lot there, but fortunately some of it builds on the CDI producer/scope functionality we encountered earlier. What we're doing here is setting up an application-wide bean that produces a configuration object for JNoSQL to use - specifically, using MongoDB. In a real situation, you'd want to externalize the settings in some way, but putting it into the code will do for now. The getManager()
method will be used behind the scenes when JNoSQL asks the environment for a document-database manager.
Create the Model
Create another new class in the model
package, this time named Person
:
package model;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.FormParam;
import org.jnosql.artemis.Column;
import org.jnosql.artemis.Entity;
import org.jnosql.artemis.Id;
@Entity
public class Person {
@Id("id")
private long id;
@Column @FormParam("name") @NotBlank
private String name;
@Column @FormParam("emailAddress") @NotBlank @Email
private String emailAddress;
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmailAddress() { return emailAddress; }
public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; }
}
This class uses JNoSQL's annotations to define an object that can be stored (@Entity
), its unique ID field (@Id
), and the fields contained in it (@Column
, a name that matches JPA's SQL-based view of the world). You can also see that it includes the JAX-RS and validation annotations from the class we set up when learning about MVC. With the artemis-validation
dependency we included, JNoSQL will, like JAX-RS, automatically enforce bean property constraints like this when saving, meaning we don't have to spent so much time dealing with validation logic ourselves.
Whether or not it's a good idea mix the JAX-RS and persistence annotations like this is something I'm not entirely sure about, but it'll work for our purposes.
Create the Repository
Create a new interface (not a class) in the model
package named PersonRepository
:
package model;
import java.util.List;
import org.jnosql.artemis.Repository;
public interface PersonRepository extends Repository<Person, Long> {
List<Person> findAll();
}
You may be thinking at this point, as I originally did, that the next step will be to create an implementation class to do the work for this. Nope! This is where some real CDI voodoo comes into play: inside JNoSQL is a bean that produces "proxy" classes on the fly for Repository
interfaces and figures out implementations of the methods based on their names, return types, and parameters. It's not magic - there are limits - but in cases like this it'll do what we'd otherwise expect to have to do ourselves.
Back to the PersonController
Return to the PersonController
class we created before and rework it to use our newly-minted JNoSQL objects:
package com.example;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.mvc.Controller;
import javax.mvc.Models;
import javax.validation.Valid;
import javax.ws.rs.BeanParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import org.jnosql.artemis.Database;
import org.jnosql.artemis.DatabaseType;
import model.Person;
import model.PersonRepository;
@Path("/people")
@Controller
@RequestScoped
public class PersonController {
@Inject
Models models;
@Inject
@Database(DatabaseType.DOCUMENT)
PersonRepository personRepository;
@GET
public String home() {
models.put("people", personRepository.findAll()); //$NON-NLS-1$
return "person-new.jsp"; //$NON-NLS-1$
}
@POST
public String createPerson(@BeanParam @Valid Person person) {
if(person.getId() == 0) {
person.setId(new Random().nextLong());
}
personRepository.save(person);
models.put("person", person); //$NON-NLS-1$
models.put("people", personRepository.findAll()); //$NON-NLS-1$
return "person-created.jsp"; //$NON-NLS-1$
}
}
Add a Person List Tag
Back in the "Deployed Resources" section of the project, create a new directory beneath "WEB-INF" called "tags", and within that create a new file named "personList.tag":
Set the file contents to:
<%@tag description="Displays List&kt;model.Person>" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" %>
<%@attribute name="value" required="true" type="java.util.List" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<h1>People</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email Address</th>
</tr>
</thead>
<tbody>
<c:forEach items="${pageScope.value}" var="person">
<tr>
<td><c:out value="${person.id}"/></td>
<td><c:out value="${person.name}"/></td>
<td><c:out value="${person.emailAddress}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
This is our JSP equivalent of an XPages custom control, though all of the configuration is done inline instead of via XPages's auto-maintained .xsp-config
side file.
Update the Person Views
Modify "person-new.jsp":
<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" trimDirectiveWhitespaces="true"%>
<%@taglib prefix="t" tagdir="/WEB-INF/tags" %>
<!DOCTYPE html>
<html lang="${translation._lang}">
<head>
<title>${translation.appTitle}</title>
</head>
<body>
<h1>Create Person</h1>
<form method="post" action="people">
<dl>
<dt>Name</dt>
<dd><input name="name" required/></dd>
</dl>
<dl>
<dt>Email Address</dt>
<dd><input type="email" name="emailAddress" required/></dd>
</dl>
<input type="submit" value="Submit"/>
</form>
<t:personList value="${people}"/>
</body>
</html>
Do similarly to "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" %>
<%@taglib prefix="t" tagdir="/WEB-INF/tags" %>
<!DOCTYPE html>
<html lang="${translation._lang}">
<head>
<title>${translation.appTitle}</title>
</head>
<body>
<h1>Created Person</h1>
<dl>
<dt>Name</dt>
<dd><c:out value="${person.name}"/></dd>
</dl>
<dl>
<dt>Email Address</dt>
<dd><c:out value="${person.emailAddress}"/></dd>
</dl>
<t:personList value="${people}"/>
</body>
</html>
Take It For a Spin
Launch the Liberty server and visit http://localhost:9091/javaeetutorial/resources/people. You should be able to add new entries, with the browser taking care of the client-side validation for you and JNoSQL and JAX-RS handling it on the server side. Best of all, the data should persist!
If you look at the database in Compass, you'll see entries there as well. JNoSQL mapped the Person
class name to the "Person" collection in the database:
Next Steps
In the next post, I plan to touch a bit on mixing MVC controller methods with JSON-based REST APIs, to bring these parts together into something that starts to approach a real application.
Update: Troubleshooting Note
One thing I encountered in my fiddling was an intermittent case where the server wouldn't load the app, instead complaining about an unsatisfied provider for the PersonRepository
class. If you run into this, make sure you have a "beans.xml" file inside your "webapp/WEB-INF" directory in "Deployed Resources", and set its contents to:
<?xml version="1.0" encoding="UTF-8"?>
<beans bean-discovery-mode="all"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"/>
This is the CDI configuration file. Though it's mostly empty, the critical part is bean-discovery-mode="all"
, which causes it to check all available providers in the classpath.
Bryan - Fri Mar 15 12:10:30 EDT 2019
Jesse,
Thank you very much for taking the time to do these tutorials. For me, you took the "scary" out of creating OSGI packages and I can't thank you enough!
I think there is an issue between the person class and the person jsp files. name vrs firstName.
Regards - Bryan
Jesse Gallagher - Fri Mar 15 12:26:17 EDT 2019
You are definitely correct - thanks for the catch!