Libraries

Fri Jan 24 10:28:09 EST 2025

Tags: libraries
Photograph of the Glenside Free Library in Pennsylvania, US
The Glenside Free Library, photo from MCLINC

Shortly after we moved to our current house, we got the usual deluge of welcome letters: ads for local business, insurance scams, that sort of thing. Among them was the newsletter for the local Friends of the Library group. "I like books," I figured, so I sent a donation their way. That led to an invitation to attend one of their board meetings, which it turns out was a (successful) scheme to get me to join their board.

What struck me immediately was just how much libraries - and Friends groups that support them - do. I went in with just the basic view that they were the place where you went to borrow books, but quickly learned about all the ancillary services: book discussions, art nights, chess and D&D clubs, computer access for those without, gardening programs, museum passes, tech education, and so on. They also generally act as one of the few places one can go to idly socialize without also being expected to pay money. They're often community centers in their own right, providing a friendly place for anyone, in particular marginalized or less-well-off patrons. Also, just being a member often gets you access to various e-book-lending and video streaming services for free.

Thus, despite being an inveterate introvert, I'm still involved with the local Friends and (thanks to the way our system works) on the system's board as well. It's quite likely that you have a similar group for your local library system, and I can heartily recommend you get involved. The people in it are almost definitely delightful and it's a great way to get involved in a light and flexible way.

Libraries and their Friends are also, of course, always in need of donations. A nice attribute of that is that they're usually small enough that it's easy to see the path of "more money" to "better programs". If you can - and especially if you're in a place where libraries are currently assailed by ruinous powers - find and donate to your local library. If you're lucky enough to be in a place where your system is well-funded, my local system and Friends group can always put donations to exceptional use.

OpenNTF's Open Mic Series

Wed Jan 22 16:10:15 EST 2025

Tags: openntf

Tomorrow, January 23rd, will kick off a new collaboration between OpenNTF and HCL: the Open Mic series. The idea of these is to replicate a bit of the "Ask the Developers" feeling from the Lotusphere days, with each session featuring developers, product managers, and other applicable HCL personnel for the topic at hand.

Like webinars, these will focus on a specific topic each month and, also like webinars, these topics will vary greatly, with some being developer-focused, others being admin-focused, and others potentially being more user-focused.

The first one is tomorrow at 11 AM US Eastern time, and you can register for it on GotoWebinar. We have a good slate of these lined up, and our plan is to run them monthly.

Data Access With XPages JEE

Thu Jan 16 12:30:21 EST 2025

Though one day I'd really like to sit down and work on expanding and categorizing the documentation for the XPages JEE project, in the mean time I can at least put together some scattered info in the form of blog posts, webinars, and example apps. Add this post to the pile! Some of it will be a rehash of previous posts, but it doesn't hurt to see it rephrased.

One of the main tentpoles of a pure XPages JEE app (I also wish I had a catchier name for it) is the data-access layer, which uses Jakarta Data and NoSQL to have an abstraction layer over the specifics of Domino data. We as Domino developers are conditioned by privation to do our data access in a fairly primitive way: though the lotus.domino classes technically sit a couple layers of abstraction over the base, they're still stuck at the level of dealing with documents, views, items, and other constructs specifically. If you want to read a the "first name" value from a document representing a person, you always have to do doc.getItemValueString("FirstName") - then the same for last name, job title, and so forth. If you're reading from a view, you have to have an alternate version of your code that loops through that - using the same loop style you've written a million times in more- and less-efficient ways - and reads from column values. In a simple case, fine, but this ends up being a lot of boilerplate code.

Anyway, without further ado, I'll get in to how it should be done.

How It Should Be Done

A basic class for representing a Person would look 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
package model;

import java.util.stream.Stream;

import org.openntf.xsp.jakarta.nosql.mapping.extension.DominoRepository;

import jakarta.nosql.Column;
import jakarta.nosql.Entity;
import jakarta.nosql.Id;

@Entity
public class Person {
	public interface Repository extends DominoRepository<Person, String> {
		Stream<Person> findAll();
		
		Stream<Person> findByLastName(String lastName);
	}
	
	@Id
	private String documentId;
	@Column
	private String firstName;
	@Column
	private String lastName;
	
	/* snip: getDocumentId(), setDocumentId(), getFirstName(), etc.) */
}

There's a bit of a nod to being Domino-specific there, which we'll cover later, but there's no business where you have to do doc.getUniversalID(), doc.getItemValueString("FirstName"), and all that. You don't even need to implement any code that does the document lookups - the framework provides that for you. Thanks to some CDI magic, the you can use the repository as-is in a bean or REST service:

 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
package rest;

import java.util.Comparator;
import java.util.List;

import jakarta.inject.Inject;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import model.Person;

@Path("person")
public class PersonResource {
	
	@Inject
	private Person.Repository personRepository;
	
	@Path("byLastName")
	@Produces(MediaType.APPLICATION_JSON)
	public List<Person> getByLastName(@QueryParam("lastName") String lastName) {
		return personRepository.findByLastName(lastName)
			.sorted(Comparator.comparing(Person::getFirstName))
			.toList();
	}
}

Under the hood, the JNoSQL driver translates the call to personRepository.findByLastName(lastName) to a DQL query like Form = "Person" and LastName = "Fooson", reads the "FirstName", "LastName", and UNID from each document, and then returns Person objects in a stream.

Adding A Bit Of Finesse

In that example, I have the bit where the REST service sorts the users it found by first name - that's fine, but it's really the database's job. Fortunately, that's easy to add. First, we can change our repository to add a jakarta.data.Sort method:

1
2
3
4
public interface Repository extends DominoRepository<Person, String> {
	// ...	
	Stream<Person> findByLastName(String lastName, Sort<Person> sortOrder);
}

Then, we can simplify the REST service:

1
2
3
4
5
@Path("byLastName")
@Produces(MediaType.APPLICATION_JSON)
public List<Person> getByLastName(@QueryParam("lastName") String lastName) {
	return personRepository.findByLastName(lastName, Sort.asc("firstName")).toList();
}

The result will be the same (or actually probably more what you want, since this will be case-insensitive), but now the DB is doing the work. Specifically, the work it's doing is that this will cause the driver to make a temporary NSF that will house a QueryResultsProcessor view that lists those documents sorted by the FirstName item. It will also be mildly intelligent about this: if the data documents in the DB haven't changed, it'll re-use the existing index, which means that subsequent calls to the same method with the same parameters (and user) will be very fast.

Domino Optimizations

Using that default DQL+QRP behavior is sufficiently performant in a lot of cases (moreso than you might think, too), but it's still potentially prone to the usual pitfalls we have with Domino data access. If you have more documents or complex selections, you'll want to fall back to views or folders.

Say you want to juice your quarterly numbers so you can buy another house, and so you want to get rid of a bunch of employees and you're triaging them via a folder. First, let's add a column to show their salary to our list of fields in the Person class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Entity
public class Person {
	// ...
	
	@Id
	private String documentId;
	@Column
	private String firstName;
	@Column
	private String lastName;
	@Column
	private int salary;

	// snip
}

Then, we can add another method to the repository class for this:

1
2
3
4
5
public interface Repository extends DominoRepository<Person, String> {
	// ...
	@ViewEntries("Vacation-House Triage")
	Stream<Person> findDoomedEmployees(Sort<Person> sortOrder);
}

The @ViewEntries annotation there will override the default DQL-and-QRP behavior, and instead open and traverse the view or folder with a ViewNavigator. That also means altering the behavior of the Sort parameter: if you pass Sort.desc("salary"), instead of creating a sorted column in the temporary QRP view, now it will call view.resortView(...) to make use of the "Click on column header to sort" functionality in the folder's design, making the lookup as fast as possible.

Creating And Modifying Documents

So far, these examples have only been to read existing data, but other parts of CRUD similarly work with high-level objects.

If we want to create a new user, there's no more work to do in the repository - we can just add an endpoint to our REST service:

1
2
3
4
5
6
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Person createPerson(Person newPerson) {
	return personRepository.save(newPerson);
}

The save method is provided by the base repository interface, so that's all that's needed. The framework will take a value like:

1
2
3
4
{
	"firstName": "Foo",
	"lastName": "Fooson"
}

...convert the JSON to a Java object, write the values into a document with the "Person" form value, retrieve a new version of the object with the UNID filled in, and return:

1
2
3
4
5
{
	"documentId": "020322A6F79CB02C85258C140058A1FF",
	"firstName": "Foo",
	"lastName": "Fooson"
}

Similar to the Domino-specific support for views, there's another version of the save method that you can call to compute with form: personRepository.save(newPerson, true).

To update an existing document, you do basically the same thing, but you put the UNID into the object (in practice, the method above should strip the UNID just in case), which will be in a document retrieved from. For example:

1
2
3
4
5
6
7
8
@Path("{documentId}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Person updatePerson(@PathParam("documentId") String documentId, Person newPerson) {
	newPerson.setDocumentId(documentId);
	return personRepository.save(newPerson);
}

You might want to check if the document by UNID already exists for business-logic purposes, but the underlying behavior is mostly the same anyway.

What We Didn't Have To Do

The primary thing that I want to emphasize is how much work the app developer doesn't have to do. Anything you can do with the JEE framework is doable with the traditional XPages toolkit, but, by the same token, it'd also all be doable with assembly language - you definitely shouldn't do that, though.

This takes care of a tremendous amount of busywork: opening and processing documents, traversing view efficiently, efficient DQL and QRP use, converting to and from JSON, and building REST endpoints. I didn't get into other normal problems that could be solved here: model validation via annotations, sending 404s via meaningful exceptions, modifying data-reading behavior via custom reader classes (critical with weird old Notes data), calling external REST services by interface, and so forth.

Whenever I work with Domino apps not designed with this sort of thing, it's a depressing experience. It's the same mess of varyingly-performant view reading, manually mapping data to intermediate objects, manually writing or reading JSON objects, manually doing HTTP calls for REST services, and all of that over and over and over. It's annoying to write, annoying to read, easy to get wrong, and extremely difficult for a new developer (even an experienced one) to maintain. If you haven't tried the framework and you're writing web apps for Domino, I highly recommend giving it a shot.