Objectos PetClinic v002: No-Reload form submission with Objectos Way v0.1.9

Marcio EndoMarcio EndoDec 1, 2024

Objectos Way is our library for writing web applications entirely in Java. It renders HTML on the server and sends it over the wire to the client. Web applications built with Objectos Way are far from static though.

Here's a video of the recently released Objectos PetClinic v002 application:

The video shows the following steps:

  • Navigate to /owners to retrieve a page listing all existing pet owners

  • Click the "Add owner" button and open a pet owner form in a modal

  • Fill in the fields for first name, last name, address, city, and telephone, then click the "Create" button

  • The form is hidden, and the listing updates to show the newly created pet owner

This functionality works without any page reloads or redirects, providing a smooth user experience.

In this blog post I will show you how the PetClinic application uses Objectos Way v0.1.9 in order to implement this functionality. We'll focus on the back-end implementation, while the front-end details will be the subject of our next post.

Let's begin.

A note on versions

This blog post refers exclusively to the following versions:

  • Objectos PetClinic: v002

  • Objectos Way: v0.1.9

Both projects are actively under development; so the information provided here is certain to become outdated.

Declaring the /owners routes

In an Objectos Way application, HTTP requests are handled by instances of the Http.Handler interface. The interface declares a single handle method:

void handle(Http.Exchange http);

The method accepts an instance of the Http.Exchange interface, which represents the HTTP request received by the server and the subsequent response sent to the client. It provides methods for accessing details about the HTTP request, such as:

  • The HTTP request target path

  • The HTTP request method

A single Http.Handler instance can theoretically handle all server requests. But dispatching requests programmatically using if-else statements is tedious and error-prone. Objectos Way simplifies this process with the Http.Module class, enabling declarative routing.

Example: PetClinic's SiteModule

In the PetClinic application there's a single Http.Module subclass, the SiteModule class. It declares all of the application's routes:

package objectos.petclinic.site;import objectos.way.Http;public class SiteModule extends Http.Module {  ...  @Override  protected final void configure() {	...  }  ...}

Each application path is defined within the configure method. For the /owners path, the configuration is as follows:

route("/owners",    interceptor(injector::transactional),    handlerFactory(Owners::new, injector));

This configuration tells the server:

  1. Intercept the request using the injector::transactional interceptor.

  2. Handle the request with a Http.Handler instance created by the handlerFactory.

The transactional interceptor is discussed here.

Understanding handlerFactory

The handlerFactory(Owners::new, injector) expression declares a handler factory that is equivalent to:

// Equivalent instantiationHttp.Handler handler = new Owners(injector);

So, for each request to the /owners path, a new Http.Handler instance is created using this factory.

The Owners class

The Owners class implements the Http.Handler interface. Here’s how its handle method processes requests:

@Overridepublic final void handle(Http.Exchange http) {  switch (http.method()) {    case GET, HEAD -> get(http);    case POST -> post(http);    default -> http.methodNotAllowed();  }}private void get(Http.Exchange http) {  // Handles GET and HEAD requests}private void post(Http.Exchange http) {  // Handles POST request}

It switches over the HTTP request method:

  • GET or HEAD: dispatches to the private get method.

  • POST: dispatches to the private post method.

  • Other Methods: The server responds with a 405 Method Not Allowed message.

Declarative Java without annotations

And that's how we declare the /owners routes in the PetClinic application.

It exemplifies one of Objectos Way’s design goals: allows for declarative Java without the use of annotations whenever possible.

Handling the /owners form submission

Now that we've declared the /owners route, let's examine how the server processes form submissions to add a new pet owner.

The /owners page contains an HTML form that allows users to input details for a new pet owner. The form includes fields for the following attributes: first name, last name, address, city, and telephone. Additionally, the form declares these attributes:

form(    action("/owners"),    method("post"),        // fields    ...    // buttons)

When submitted, the form sends an HTTP request message to the server with the following characteristics:

  • Method: POST

  • Path: /owners

So the request is processed by the private post method we outlined in the previous section:

private void post(Http.Exchange http) {  // Handles POST request}

Let's examine how this method is implemented.

Parsing Form Fields

The first step is to parse the request body containing the submitted form fields. Since the form does not declare an explicit enctype attribute, the request body uses the x-www-form-urlencoded media type. Objectos Way provides the Web.FormData interface to handle this:

Web.FormData formData;formData = Web.FormData.parse(http);

The formData object now contains the parsed form data, allowing access to each submitted field.

Retrieving the current database transaction

Next, we retrieve the active SQL transaction associated with the HTTP request:

Sql.Transaction trx;trx = http.get(Sql.Transaction.class);

Because the /owners route is intercepted by the injector::transactional interceptor, the transaction is already initialized and ready to use.

Creating the new pet owner record

We use the parsed form data and the active transaction to insert a new record into the database:

trx.sql("""insert into owners (first_name, last_name, address, city, telephone)values (?, ?, ?, ?, ?)""");trx.add(formData.getOrDefault("firstName", ""));trx.add(formData.getOrDefault("lastName", ""));trx.add(formData.getOrDefault("address", ""));trx.add(formData.getOrDefault("city", ""));trx.add(formData.getOrDefault("telephone", ""));trx.update();

The getOrDefault method retrieves the value of a specified form field, or returns a default value (an empty string in this case) if the field is missing.

Currently, this implementation does not include data validation, which is planned for a future release to ensure only valid data is stored in the database.

Re-rendering the pet owners listing

If no errors occur, the database transaction now includes the newly created pet owner. To update the client, we render an updated version of the /owners page:

OwnersView view;view = renderView(http);

The OwnersView class is responsible for generating the HTML for the pet owners page.

Finally, we send the updated content back to the client. Before doing so, we need to check if the request was initiated by the Objectos Way JS library.

The Objectos Way JS Library

Objectos Way applications include a JavaScript library to help build dynamic web user interfaces. This library:

  • Intercepts HTML form submissions

  • Sends the form data using AJAX

  • Adds a custom Way-Request: true header to the request

  • Processes the server's response to update the page without a full reload

Conditional response handling

Depending on whether the request originated from the Objectos Way JS library, the response format varies:

String wayRequest;wayRequest = http.header(Http.HeaderName.WAY_REQUEST);if ("true".equals(wayRequest)) {  Script.Action action;  action = view.createAction();  http.ok(action);} else {  http.ok(view);}

If the Way-Request header is present and set to true, the server sends a Script.Action object with the client-side updates:

private final Html.Id overlay = Html.Id.of("overlay");private final Html.Id tearsheet = Html.Id.of("tearsheet");final Script.Action createAction() {  return Script.actions(      Script.toggleClass(overlay, "opacity-0", "opacity-100"),      Script.toggleClass(tearsheet, "translate-y-3/4", "translate-y-0"),      Script.delay(          350,          Script.toggleClass(overlay, "invisible", "visible"),          Script.html(this)      )  );}

This action instructs the client-side Objectos Way JS library to:

  1. Change the overlay opacity by toggling CSS style classes

  2. Animate the form overlay downard by changing its Y-axis CSS translation

  3. Wait for 350 milliseconds

  4. Make the overlay invisible by toggling CSS style classes

  5. Update the pet owners listing with the newly rendered HTML

If the request did not originate from the Objectos Way JS library, the server sends the updated HTML directly.

No-reload form submission

And that's how the PetClinic application handles the /owners form submission.

It shows how Objectos Way permits for dynamic user interfaces without page reloads or redirections.

Testing the /owners form submission

Another design goal of Objectos Way is to support testable code. The PetClinic application includes a test to verify the /owners form submission handling logic. Here's how it works:

Simulating a form submission with Http.TestingExchange

The test begins by creating an instance of Http.TestingExchange to simulate a form submission:

Http.TestingExchange http;http = Http.TestingExchange.create(config -> {  config.method(Http.Method.POST);  config.path("/owners");  config.queryParam("page", "2");  config.formParam("firstName", "NEW");  config.formParam("lastName", "YYY");  config.formParam("address", "New Address");  config.formParam("city", "New City");  config.formParam("telephone", "1122334455");  config.set(Sql.Transaction.class, trx);});

This instance represents an HTTP request with the following characteristics:

  • Method: POST

  • Path: /owners

  • Query Parameters: page=2

  • Form Data:

    • firstName: NEW

    • lastName: YYY

    • address: New Address

    • city: New City

    • telephone: 1122334455

By using Http.TestingExchange we can simulate HTTP interactions without the need of a running server.

Handling the request

Next, the test creates an instance of the Owners class and invokes its handle method with the Http.TestingExchange instance:

Owners owners;owners = new Owners(siteInjector);owners.handle(http);assertEquals(http.responseStatus(), Http.Status.OK);

This simulates processing a form submission through the /owners route and verifies that the response status code is 200 OK.

Verifying database changes

After the handle method runs, the test verifies that a new pet owner record was added to the database. It confirms the record by asserting that the updated owners listing includes the new data:

assertEquals(    writeResponseBody(http, "testCase03"),        """    owner.name: NEW YYY    owner.address: New Address    owner.city: New City    owner.telephone: 1122334455    owner.pets:    owner.name: OW15 ZZZ    owner.address: Add 15    owner.city: City 15    owner.telephone: 15    owner.pets:    """);

This assertion uses the Html.Template testable facility which was discussed here.

Conclusion

In this post, we walked through how the Objectos PetClinic application handles form submissions using Objectos Way.

We discussed the declaration of the /owners route, the parsing and processing of form data on the server, and the mechanisms for instructing the client to update the user interface. We also demonstrated how the PetClinic project tests this functionality, showcasing Objectos Way's emphasis on testable code.

In the next post, we will focus on the front-end aspects of the PetClinic application. You will see how Objectos Way enables the creation of dynamic user interfaces written entirely in Java.

Stay tuned!