Saturday, March 22, 2008

RESTful Form Beans with Struts 1

What Struts is Good At

Struts works great for view-only pages, particularly when you need to build those pages from the database. But there are a number of subtle issues with update pages which can probably best be solved with a RESTful solution.

REST and state

The database holds persistent state. My understanding is that one of the concepts of REST is that there is rarely a reason to cache that state on the server and/or the browser/client when it's already stored semi-permanently in the database. It seems that the session on the server only needs to hold state that doesn't belong in the database. That would be data specific to what the user is currently viewing or doing. Some examples are:
  • Logging in - that is usually (and probably best) accomplished by establishing a session on the server and storing a cookie on the client containing a token that the client's browser sends with every request instead of resending their user-id and password. The token tells the server which session is associated with that user. That way you aren't repeatedly sending the user ID and password over the wire. If someone eavesdrops on the session and steals the token, it expires after a certain period so that an attacker will (theoretically) not be able to log back in.

  • Other session-specific state. This is for global-controls analogous to the caps-lock key on the keyboard. All the other controls (keys) are still available to you, but their behavior is slightly different because the state (caps-lock) has changed.

Session Timeout

Sessions need to time out eventually (usually in 30 minutes or so for security reasons). End-users find this incredibly annoying. A little JavaScript can soften the blow:

setTimeout(alert('Your session will expire (silently) in 2 minutes...'), 1680000);

This is still annoying, and not very secure, since the point of having sessions expire is to prevent an unauthorized person from accessing a still-valid session. To a hacker, this is essentially a pop-up, advertising: "You can hijack someone's session if you act now!".

If you're application is mostly RESTful, storing only the login information on the server, you can pop up a login window to allow them to log-back in without loosing whatever they are working on. This is secure because the user ID and password are required. Also, the user does not have to respond within a certain period because they are effectively logging in and creating a new session.

if (confirm('Your session has expired.  Log back in to continue your work?')) {

loginWin = window.open("pre_login.do");
}


Since you do not rely on server side state or cookies for anything but the login, the user can get a form and fill it out in one session, then submit it in a completely different session, so the login pop-up works. If you have a little bit of state on the server and you are clever, you can enhance this solution to send a setting or two from your server-side state from the old session to the login page so that the new session will match the old.

Struts Problem 1: Clearing session-scoped Form Beans

Form beans are session scoped by default. A user GETting the form in preparation for POSTing a new record will see whatever data was left over in the form bean from the last record they edited. You need to work around this by figuring out when the user is going to add a new record and clearing the fields in the form.

Struts Problem 2: Session expiration with session-scoped form beans

Sooner or later the session will time out and the form bean will be completely lost. Writing an Action and pairing it with a log-back-in JavaScript function becomes prohibitively ugly from a maintenance and security standpoint if even a few different fields are required for a few different screens. As far as I can see, there is no reasonable work-around for this scenario, which is why REST is so useful.

Struts Problem 3: Request-scoped Form Beans and the Struts validator

When there is a failed validation on a form bean, the model isn't invoked, so only the fields that the HTML form posted are filled in from the request. Any extra data (breadcrumbs, related records from the database, or other variable text that provides context for the update taking place) is lost unless that data was also built into fields in the form <input type="hidden"... />. But REST is supposed to make things simpler and more lightweight. Building breadcrumbs into URLs requires escaping any special characters, increases both the request and response sizes, and is generally a very ugly solution. Storing these fields in client side cookies is possible too, but very complicated.

A RESTful solution

REST precludes any unnecessary server state and as mentioned above, the validator is not RESTful. So you'll need to declare actions:
<action ... validate="false" scope="request" />
All validation must take place on the action, not the form bean (see Problem 3, above).

Query the database for all the data to build the screen with each request. Any extra load on the database is small because this amounts to making extra queries only for failed validations. The catch is that you need to keep track of whether any data on the form bean is dirty (user has changed it) or clean. Remember, a user can clear a field, so just checking for null or 0 doesn't mean that the data is unchanged.

Fortunately, we already know whether the form data is dirty or not by the method the user used on the request.

GET without a database record number
Clean: Show the user a blank form so that they can add (POST) a new record.

GET with a database record number
Clean: Show the user the current values of the record from the database so that they can update (PUT) the existing record.

PUT
Dirty. If input passes validation, update existing record with changes. If it doesn't pass, return the dirty form plus any "extra" data from the DB for building the screen.

POST
Dirty. If input passes validation, check that the record doesn't exist already as the user can resubmit a post by pressing the back button. This resubmitted post should either be ignored or treated as a put (see above) since the record already exists. If it is a valid POST add a new record to the database. If the input is invalid, return the dirty form plus any "extra" data from the DB for building the screen.


Note: HTML only supports GET (all links) and POST (only in <form method="post"...>). Not surprisingly, most browsers don't support the other methods mentioned in the HTTP spec either. I'm sure people are signing petitions calling for an overhaul of HTML and all browsers, but it's really not an issue to send an extra parameter to designate the "method" of each request. Your application may require some methods that HTTP does not provide, in which case you'd have to send a "method" parameter anyway.

In order for the extra data from the DB to be refreshed on a request-scoped form, the action still needs to be invoked. Something like the following would probably be a good template for Actions:

  1. Check that the user is logged in. This might better be done in a servlet, but it needs to be done.

  2. Check for required parameters (like the unique identifier of the record you are editing). Send failures to a Not Authorized page.

  3. Check that the user is authorized to do what they are trying to do. Send failures to a Not Authorized page.

  4. Validate any other parameters that would make the request just invalid - essentially assertions that your UI is working properly go here. Send failures to a Not Authorized page.

  5. Create your ActionMessages object to hold user-friendly validation errors

  6. User input validation. Accumulate friendly error messages in your errors object.

  7. If there are no errors and the method is POST or PUT, perform update.

  8. If the form is not dirty - fill form fields with data from database. Build or rebuild other data for the screen to show the user and put it on the form bean. Or you might create a new form bean and forward to a different screen after a successful update.


That list is a starting point, but I always code Actions to minimize the number and extent of database queries, so that sometimes shuffles the later stages in that list.

Advantages:

  • You don't have to worry about clearing old data from the form bean (you get a new form bean with each request).

  • You don't need to encode any "extra" data (breadcrumbs, etc.) into the form to make it get included in the request (it's reread from the database).

  • You can log back in elegantly, without loosing what you were working on (all necessary data is reread from the database or carried as form data in the request).

  • You can use any objects you want for building the screen - you are not limited to Strings and other primitive types.

  • This requires less code than any of the other ways I tried solving these issues.


Disadvantages

You can't use the Struts validator. All your validation must be done in the action.

Conclusion

So far, this is the neatest solution I can come up with to all of the above issues. And it can be as RESTful as the design of your application allows (uses a minimum of server-side state). It fails to leverage the struts validator, but I was never eager to perform that much XML configuration anyway. Struts 1 has been around for a long time. People ask much more of a web application now than they used to. Maybe Struts 2 has some easier shortcuts for these same issues?

No comments: