One-Time Data plugin

  • Authors : Marc Palmer
0 vote
Dependency :
compile ":one-time-data:1.0"

Documentation Source Issues

Summary

A safe replacement for "flash" scope where you stash data in the session which can only be read once, but at any point in the future of the session provided you have the "id" required.

Description

This plugin provides a multi-window safe alternative to flash scope that makes it possible to defer accessing the data until any future request (so long as the session is preserved).

These docs are work in progress

What's wrong with flash scope?

Flash scope stores a value and lets you retrieve it in the next request. The problem is that the next request may not be what you think it is. It might be another browser tab/window from the same user. It might be an AJAX request. It might be a JS script or CSS file served up by your application (via a controller).

In addition, extra redirects/forwards can cause confusion with flash and/or data to be "used up" in the wrong request.

In short - it is inherently a dodgy concept.

How does this plugin fix it?

Its pretty simple. The fundamental problem here is there is no connection between the request that stores a value, and the one that retrieves it. There is no "conversation id" for those familiar with Grails "Web Flow".

Anyway, using a similar mechanism to flash scope, your data is put into the user's session (so yes, you still need some session affinity in clustering) but you have a key associated with your data. When you need to access the data, you retrieve it using the key you originally used, and then it is automatically removed from the one time data store.

You mean I need to supply a unique request key for this data?

No. This plugin will generate user-unique ids for you if you like, or you can specify a key.

This key is user-session scoped, so you may for example use the id of some new entity the user has created as the key, or their email address. It just needs to be something that is available to you when you want to retrieve the data for that user.

Is this just a replacement for flash or does it do more?

It does go a little further. Often in an application you may need to present information to the user that is not persisted anywhere, and you don't want to include it in the URL - i.e. it should be a pretty URL and not permalink-able.

In this case you just stuff the data you do have into the one time data store and use it up on the page. If the user reloads that page the info is no longer available no matter what they do, and you can detect this and tell them they can't reload that page.

This is useful for the final page of a simple form submission for example, where you don't want to submit to the complexity of web flows and stale conversation scopes.

Using it

The oneTimeData method is added to all controller actions. This delegates to a OneTimeDataService that you can use in non-controller contexts also.

You call this method, and set properties in the closure you supply. These are then associated with the generated or specified user-specific id, ready for retrieval later.

Storing and retrieving some data

Take the following code which might be in your controller action:

// set up the one time data for a future request, using a manual "conversation" key
def otID = book.isbn
oneTimeData(otID) {
    customerName = customer.name
    statusMessage = "Update to book saved!"
}

redirect( controller:'books', action:'updated', id:otID)

So here it stores the data against the isbn of the book, and then redirects the user to another page, including the id set to the same key used for the onetime data (the isbn).

To retrieve it, you use the getOneTimeData method.

In a controller action:

def userInfo = getOneTimeData()

render(view:'updated', model:[name:userInfo.customerName, message:userInfo.statusMessage])

Here the dynamic getOneTimeData() method is being used with no arguments - this defaults to using params.id as your one-time data key. If you try to get that data again, it will return null. It's now gone forever.

If you have your id somewhere that isn't params.id, you can just pass it as an argument to getOneTimeData.

You can have the plugin generate a user-unique id for you if your usage pattern is just to replace "flash" scope, just by omitting the "id" argument when storing the data, and keeping the result:

// set up the one time data for a future request, using a manual "conversation" key
def otID = oneTimeData {
    customerName = customer.name
    statusMessage = "Update to book saved!"
}

redirect( controller:'books', action:'updated', id:otID)

This has the same effect, and gives you a truly "unique" conversation/flow with the user. For example this could be used in place of flash scope for displaying status messages after updating some database rows.

There are some GSP tags supplied that let you customize the page output based on the presence or not of the one time data you needed (even if you have already retrieved it from the store).

<onetime:exists>
Hey welcome back ${name}
</onetime:exists>

<onetime:missing> Sorry, I can't show you this information a 2nd time! </onetime:missing>

<onetime:notExpected> Hello, this is just our generic message to you because we made no attempt to get user-specific information about you </onetime:notExpected>

This example assumes the one time data id is in params.id - you can just set the "id" attribute on the tags to give them a different id if your controller does not follow this default convention.

The nuances of these tags can be a little tricky to understand at first - see the full tag docs below.

Commercial Support

Commercial support is available for this and other Grailsrocks plugins.

Reference

Below are the details of the API you have to manipulate the one time data.

Controller methods

oneTimeData(Closure)

Store some one-time data with a generated unique id. You will need to keep the return value and do something with it to make it available to the user in a subsequent request to retrieve the data.

// set up the one time data for a future request, using a manual "conversation" key
def otID = oneTimeData {
    customerName = customer.name
    statusMessage = "Update to book saved!"
}

redirect( controller:'books', action:'updated', id:otID)

In the closure you can set any property names you like. Beware - the same rules apply as usual here, that it is generally unsafe to put domain objects into here.

oneTimeData(id, Closure)

This is the same as the closure-only method, but you pass in the id you want to use, as the first argument.

// set up the one time data for a future request, using a manual "conversation" key
def otID = book.isbn
oneTimeData(otID) {
    customerName = customer.name
    statusMessage = "Update to book saved!"
}

redirect( controller:'books', action:'updated', id:otID)

getOneTimeData()

Call this to retrieve and remove the one-time data specified by the value of params.id. The result is a map of the original values you put in.

def userInfo = getOneTimeData()

render(view:'updated', model:[name:userInfo.customerName, message:userInfo.statusMessage])

getOneTimeData(id)

Call this to retrieve and remove the one-time data specified by the key specified. Use this where a manual key makes sense i.e. it is a functionality group / you have a reliable unique id already.

def userInfo = getOneTimeData('book-update-outcome')

render(view:'updated', model:[name:userInfo.customerName, message:userInfo.statusMessage])

Tags

There are some tags provided so that you can render content dependent on the outcome of attempt to access one time data.

onetime:exists

This tag will render the body of it only if the one time data with a matching id was successfully retrieved in this current request. It will not render its body if either of the following is true:

  • a retrieval attempt was made but failed (data never added or already retrieved)
  • a retrieval attempt for that key was not made
By default it will assume the one time data key is in params.id but you can override this by specifying the id attribute:

<onetime:exists id="signup-completion">
Thanks for signing up, ${name}
</onetime:exists>

onetime:missing

This tag will render the body of it only if the one time data with a matching id was not successfully retrieved in this current request. It will not render its body if either of the following is true:

  • a retrieval attempt was made successfully
  • a retrieval attempt for that key was not made
By default it will assume the one time data key is in params.id but you can override this by specifying the id attribute:

<onetime:missing id="signup-completion">
Hey sorry but you can't reload this page, please click the link below to create a new account… 
</onetime:missing>

onetime:notExpected

This tag will render the body of it only if the one time data with a matching id was not event subjected to a retrieval attempt in this current request. It will not render its body if either of the following is true:

  • a retrieval attempt was made successfully
  • a retrieval attempt was made unsuccessfully (the data was never added or had already been retrieved
By default it will assume the one time data key is in params.id but you can override this by specifying the id attribute:

<onetime:notExpected id="signup-completion">
Hey it turns out you already have an account with us - no need to sign up. Here is your profile below…
</onetime:notExpected >

Service

The OneTimeDataService lets you manipulate the data outside of a controller, but still requires an active request so that it can access the user's session.

See the javadocs link soon (+).