Tuesday, June 25, 2019

Spring with Rails' jQuery UJS

I’ve always wanted to try to see if I could use Rails’ jQuery UJS in a Spring Boot project. The UJS in jquery-ujs stands for unobtrusive JavaScript. I really like how UJS wires event handlers to eligible DOM elements marked with HTML5 data-* attributes. I find myself wanting to see more of this approach being used in Spring Boot web apps. I wonder why there’s very little mentioned on the web about this. Or, may be I’ve been looking at the wrong places.

Anyway, here are some things jQuery UJS can do, and the related source code is on GitHub (albeit using a different example).

Non-GET Links (e.g. DELETE)

When I need to render a link that deletes an item, I would use a <button> wrapped in a <form> with a hidden _method field with a value of delete. The <form> is not visible to the user. But the visible button is used to submit the <form>. Some CSS is used to make the button look like a link.

<form action="/articles/42" method="post">
  <input type="hidden" name="_method" value="delete" />
  <button class="btn btn-link">Delete</button>
</form>

Thanks to Spring’s HiddenHttpMethodFilter (also automatically configured in Spring Boot), when this <form> is submitted, it will be received as a request with a method of DELETE. It maps to @DeleteMapping(path="/articles/{id}") in the related @Controller.

While the above works, there is an easier way with jQuery UJS. All that is needed to render a link to delete an item is this:

<a href="/articles/42" data-method="delete">Delete</a>

jQuery UJS will enhance links that have data-method attribute. When the above example link is clicked, the JavaScript will create a <form>. The action attribute of this <form> is set to the value of link’s href. The method is set to post. A hidden _method field is added to the <form> and set to the value of the link’s data-method. Finally, the <form> is submitted (and the link is not followed).

Confirmation Dialogs

Most often than not, when it comes to deleting anything, it would be good to confirm with the user. This could be implemented as a simple dialog via window.confirm(). If we build from the previous example, the <form> would look like this:

<form action="/articles/42" method="post"
    onsubmit="return confirm('Are you sure?')">
  <input type="hidden" name="_method" value="delete" />
  <button>Delete</button>
</form>

While the above works, jQuery UJS shows us a better way. All that is needed to confirm before deleting is this:

<a href="/articles/42?delete" data-method="delete"
    data-confirm="Are you sure?">Delete</a>

jQuery UJS will enhance links (and <form>s too) that have data-confirm attribute. When the above example link is clicked, the JavaScript will call confirm() to show a dialog containing the text that is the value of the attribute. If the user chooses to cancel, the creation/submission of the <form> (due to data-method) does not take place.

Ajax Links

After deleting an item, the page usually reloads to show that the deleted element has indeed been removed.

Let's say the items are displayed in a table. Each row has a unique id.

<table>
  <tr id="article:18">
    <!-- data cells for /articles/18 -->
    <td><a href="/articles/18?delete"
        data-method="delete" data-confirm="Are you sure?">Delete</a></td>
  </tr>
  <tr id="article:42">
    <!-- data cells for /articles/42 -->
    <td><a href="/articles/42?delete"
        data-method="delete" data-confirm="Are you sure?">Delete</a></td>
  </tr>
  <!-- other rows -->
</table>

Assuming that we can simply remove the HTML element that represented the deleted item, then we can probably send the DELETE request asynchronously and get a response that would remove the related HTML element. jQuery UJS makes this as easy as adding data-remote="true" and some minor changes to the server-side controller.

For the HTML, all we need is data-remote="true".

<a href="/articles/42?delete" data-remote="true"
    data-method="delete"
    data-confirm="Are you sure?">Delete</a>

When the link is clicked, jQuery UJS will again send the DELETE request. But this time, it will be sent asynchronously using Ajax. Doing so enables the browser not to reload the entire page. And depending on the server’s response, can update just a portion of the page. Thus, providing a slightly better user experience.

For the server-side controller, we need to send a different response when the request is expecting text/javascript. We add a handler method that will respond with text/javascript by using the produces element of @RequestMapping. The response will remove the related HTML element(s).

// inside a @Controller
@DeleteMapping(path="/articles/{id}")
String delete(... id) {
    // ... delete article with given identifier
    return "redirect:/articles";
}

@DeleteMapping(path="/articles/{id}",
    produces="text/javascript")
String delete(... id) {
    // ... delete article with given identifier
    return "articles/delete";
}

The view is a JSP that contains text/javascript. This will be executed by jQuery UJS.

<%-- articles/delete.js.jsp --%>
<%@ page contentType="text/javascript" %>
$('#article:${id}').remove();

Partials and Server-generated JavaScript Responses

Now what happens if we wanted to have an edit link to get some HTML content and show it up in a modal (without a page refresh)?

Here's what we can do. We send a GET request asynchronously. The response is expected to contain JavaScript that would append the HTML in targeted places in the document, and then trigger the modal to appear.

  <a href="/articles/42?edit" data-remote="true">Edit</a>

When the response is expected to be text/javascript, articles/edit.js.jsp is rendered. Otherwise, the usual articles/edit.jsp is rendered.

// inside a @Controller
@GetMapping(path="/articles/{id}", params={"edit"})
String edit(... id, ...) {
    // ...
    return "articles/edit";
}

@GetMapping(path="/articles/{id}", params={"edit"},
    produces="text/javascript")
String editViaAjax(... id, ...) {
    // ...
    return "articles/edit";
}

The edit.jsp renders the <form> (a partial, not a complete HTML document) which has been refactored to its own JSP to avoid repetition.

<%-- articles/edit.jsp --%>
<!-- -->
  <jsp:include page="_form.jsp" />
<!-- -->

The edit.js.jsp renders the same <form> (a partial, not a complete HTML document) as a string in JS. Then includes it in the modal. It was tricky to render _form.jsp as a string. I had to use <c:import>.

<%-- articles/edit.js.jsp --%>
<%@ page contentType="text/javascript" %>
<c:import var="html" url="…_form.jsp" />
<!-- escape double quotes and remove new lines -->
(function() {
  const $modal = $('#...'); // ID of modal element
  $modal.find('.modal-body').html('${html}');
  if (!$modal.is(':visible')) {
    $modal.modal('show');
  }
})()

For this to work, another InternalResourceViewResolver (IRVR) bean with text/javascript as the contentType is configured. This bean uses the same prefix and a slightly different suffix: .js.jsp. That way, when the request is expecting text/javascript, the CNVR will favor using the IRVR bean with text/javascript and it ends up rendering articles/edit.js.jsp.

Ajax Forms

The data-remote="true" attribute can also be applied to <form>s. With it, jQuery UJS will handle the form submission as an Ajax request. And when the form is being submitted, the buttons can be disabled by adding data-disabled-with. For example,

<form ...>
  <!-- ... -->
  <button data-disable-with="Saving...">Save</button>
</form ...>

When the above form is submitted, jQuery UJS will disable the button and change its content to "Saving...".

Closing Thoughts

I’ve barely touched the surface of Rails’ jQuery UJS. There is so much more that it can offer. I look forward to using it (and similar techniques) in my web apps.

Monday, November 12, 2018

Revisions and Immutability

Here's a brief post. I'm not sure how to start it. It's one of those "why didn't I think of that" moments while reviewing some existing code. Due to NDAs, I cannot share the actual code. It has something to do with handling revisions. The closest thing I can relate to is how WordPress (WP) handles blog posts and revisions.

In WP, the wp_insert_post function inserts or updates a post. It checks the ID field to determine if it will carry out an INSERT or an UPDATE. If the post is being updated, it checks if changes were made. If so, a revision is saved. A limit for the number of revisions to keep can be set. If so, the oldest ones are deleted.

This sounds like something that can be modeled as a rich domain entity. Here's a first try.

@Entity
... class Post {
    @Id @GeneratedValue ... id;
    ... name;
    ... title;
    ... content;
    ... excerpt;
    ... status; // e.g. 'draft', 'publish', 'inherit'
    ... type; // e.g. 'post', 'revision'
    @OneToMany @JoinColumn(name="parent_post_id") ... List<Post> revisions;
    ...
    // setters and getters
}
Post post = new Post();
post.setTitle("Lorem Ipsum");
post.setContent("...");
// save post
...
post = // retrieve existing post for updates
post.setContent("..."); // how can we ensure that revision is created?

In the first try, the setter methods pose a challenge to ensuring that a revision is created when the post is updated. Let's give it another try. Here's our second try.

// Immutable class
@Embeddable
... class PostData {
    ... title;
    ... content;
    ... excerpt;
    // getters only
    ... getTitle() { return title; }
    ... getContent() { return content; }
    ... getExcerpt() { return excerpt; }
    // equals() method to compare with another post data
    // to see if there are changes
}

@Entity
... class Post {
    @Id @GeneratedValue ... id;
    ... name; // for a revision, will contain parent ID and revision #
    @Embedded ... PostData postData; // read-only
    ... status; // e.g. 'draft', 'published', 'inherit'
    ... type; // e.g. 'post', 'revision'
    @OneToMany @JoinColumn(name="parent_post_id") ... List<Post> revisions;
    ...
    ... getTitle() { return this.postData.getTitle(); }
    ... getContent() { return this.postData.getContent(); }
    ... getExcerpt() { return this.postData.getExcerpt(); }
    ... getName() { return name; }
}

This is when I got my "why didn't I think of that" moment!

Note how we encapsulated the post data into its own type — PostData. It is immutable. This makes it possible to ensure that a revision is created when the post is updated.

PostData postData = new PostData("Lorem Ipsum", "...", "...");
Post post = new Post(postData);
// save post
...
post = // retrieve existing post for updates
// post.setContent("..."); // not possible
post.updateData(new PostData("...", "...", "...")); // ensure that revision is created

And here's how we create revisions.

@Entity
... class Post {
    ...
    @Embedded ... PostData postData; // read-only
    ...
    @OneToMany @JoinColumn(name="parent_post_id") ... List<Post> revisions;
    ...
    public Post(PostData postData) {
        this(postData, null);
    }
    /* package private */ Post(PostData postData, Post parent) {
        if (postData == null) {
            throw new IllegalArgumentException(...);
        }
        this.postData = postData;
        if (parent == null) {
            this.type = "post";
            this.status = "draft";
            this.name = null;
            this.revisions = new ArrayList<>();
        } else {
            this.type = "revision";
            this.status = "inherit";
            this.name = "" + parent.getId() + "-revision" + (parent.getRevisionsCount() + 1);
            this.revisions = null;
        }
        ...
    }
    ...
    ... void updateData(PostData newPostData) {
        if (this.postData.equals(newPostData)) {
            // no changes, no revisions added
            return;
        }
        ...
        // creates a revision
        PostData beforePostData = this.postData;
        this.revisions.add(0, new Post(beforePostData, this));
        // store latest changes
        this.postData = newPostData;
        // limit to number of revisions to keep
        if (this.revisions.size() > ...) {
            // delete the excess ones
            for (...) {
                this.revisions.remove(this.revisions.size() - 1);
            }
        }
        ...
    }
    ...
}

Like I said, this one is a brief post. Let me know in the comments below if it's something you've seen before, or, just like me, it gave you a "why didn't I think of that" moment.

Wednesday, July 18, 2018

Caching in Spring Boot with Spring Security

In this post, I’d like to share a lesson learned by one of the teams at O&B. They were using Spring Boot with Spring Security.

By default, anything that is protected by Spring Security is sent to the browser with the following HTTP header:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate

Essentially, the response will never be cached by the browser. While this may seem inefficient, there is actually a good reason for this default behavior. When one user logs out, we don’t want the next logged in user to be able to see the previous user’s resources (and this is possible if they’re cached).

It makes sense to not cache anything by default, and leave caching to be explicitly enabled. But it’s not good if nothing is cached, as it will lead to high bandwidth usage and slow page loads.

Good thing it is very easy to enable caching of static content in Spring Boot (even with Spring Security). Simply configure a cache period. And that’s it!

# Boot 2.x
spring.resources.cache.cachecontrol.max-age=14400

# Boot 1.x
spring.resources.cache-period=14400

But there are some gotchas! With some versions, it ain’t that simple! Let me explain further.

There are several ways content can be returned:

  1. Static content through Spring Boot auto-configured static resource request handler
  2. Controller method returning view name (e.g. resolves to a JSP)
  3. Controller method returning HttpEntity (or ResponseEntity)

Enable Caching of Static Content

The first (serving static content) is handled by configuring the said property (usually in application.properties as shown above).

Set via HttpServletResponse

In the second case, the controller handler method may choose to set "Cache-Control" headers through a HttpServletResponse method parameter.

@Controller
... class ... {
    @RequestMapping(...)
    public String ...(..., HttpServletResponse response) {
        response.setHeader("Cache-Control", "max-age=14400");
        return ...; // view name
    }
}

This works, as long as Spring Security does not overwrite it.

Set via HttpEntity/ResponseEntity

In the third case, the controller handler method may choose to set "Cache-Control" headers of the returned HTTP entity.

@Controller
... class ... {
    @RequestMapping(...)
    public ResponseEntity<...> ...(...) {
        return ResponseEntity.ok().cacheControl(...).body(...);
    }
}

This works, as long as Spring Security has not written its own "Cache-Control" headers yet.

Under the Hood

To understand when and why it works, here are the relevant sequences.

With Spring Security Web 4.0.x, 4.2.0 up to 4.2.4 and above, the following sequence occurs:

  1. The HeaderWriterFilter delegates to CacheControlHeadersWriter to write the "Cache-Control" headers (including "Pragma" and "Expires"), if no cache headers exist.
  2. Controller handler method (if matched) is invoked. The method can:
    • Explicitly set a header in HttpServletResponse.
    • Or, set a header in the returned HttpEntity or ResponseEntity (refer to the handleReturnValue() method of HttpEntityMethodProcessor).
      • Note that HttpEntityMethodProcessor only writes the headers (from HttpEntity) to the actual response if they do not exist yet. This becomes a problem, since back in #1, the headers have already been set.
  3. If no controller handles the request, then the Spring Boot auto-configured static resource request handler gets its chance. It tries to serve static content, and if configured to cache, it overwrites the "Cache-Control" headers (and clears the values of "Pragma" and "Expires" headers, if any). The static resource handler is a ResourceHttpRequestHandler object (refer to the applyCacheControl() method in its WebContentGenerator base class).
    • However, in Spring Web MVC 4.2.5, the WebContentGenerator only writes the "Cache-Control" headers only if it does not exist!. This becomes a problem, since back in #1, the headers have already been set.
    • In Spring Web MVC 4.2.6 and above, it adds the "Cache-Control" headers even if it already exists. So, no problem even if the headers have been set in #1.

With Spring Security Web 4.1.x, 4.2.5, and above (version 4.2.5 is used in Spring Boot 1.5.11), the sequence has changed. It goes something like this:

  1. Controller handler method (if matched) is invoked. The method can:
    • Explicitly set a header in HttpServletResponse.
    • Or, set a header in the returned HttpEntity or ResponseEntity (refer to the handleReturnValue() method of HttpEntityMethodProcessor).
      • Note that HttpEntityMethodProcessor only writes the headers (from HttpEntity) to the actual response if they do not exist yet. No problem, since no headers have been set yet.
  2. If no controller handles the request, then the Spring Boot auto-configured static resource request handler gets its chance. It tries to serve static content, and if configured to cache, it overwrites the "Cache-Control" headers (and clears the values of "Pragma" and "Expires" headers, if any).
  3. The HeaderWriterFilter delegates to CacheControlHeadersWriter to write the "Cache-Control" headers (including "Pragma" and "Expires"), if no cache headers exist.
    • No problem, since it will not overwrite if cache headers have already been set.

Working Versions

The above three cases of controlling caching all work in Spring Boot 1.5.11 and Spring Boot 2.x. But in case upgrading to those versions is not possible, please see the following classes and check if it has your desired behavior (using the above sequences):

  • HeaderWriterFilter (see doFilterInternal method)
  • CacheControlHeadersWriter (see writeHeaders() method)
  • WebContentGenerator (see applyCacheControl() method)
  • HttpEntityMethodProcessor (see handleReturnValue() method)

Also, be aware that Spring Security Web 4.2.5 and above will write the following HTTP headers (overwrite it, even if they are already set, like in a controller for example):

  • X-Content-Type-Options via XContentTypeOptionsHeaderWriter
  • Strict-Transport-Security via HstsHeaderWriter
  • X-Frame-Options via XFrameOptionsHeaderWriter
  • X-XSS-Protection via XXssProtectionHeaderWriter

This is because, unlike CacheControlHeadersWriter, the header writers for the above do not check if the headers already exist. They simply set their respective HTTP headers. Please refer to their respective header writer classes and issue #5193.

Another option is to have Spring Security ignore static resource requests. That way, the configured cache period will not be overwritten.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**");
        // If the above paths are served by the
        // Spring Boot auto-configured
        // static resource request handler,
        // and a cache period is specified,
        // then it will have a "Cache-Control"
        // HTTP header in its response.
        // And it would NOT get overwritten by Spring Security.
    }
}

That's all for now. Hope this clears things up.