Using Command Objects To Handle Form Data
This guide will explain how you can use command objects to validate input data in a Grails application
Authors: Matthew Moss
Grails Version: 4.0.1
1 Grails Training
Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.
2 Getting Started
In this guide you are going to use Command Objects to process form submissions with automatic type conversion, custom validation, and simple error handling.
2.1 What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 1.8 or greater installed with
JAVA_HOME
configured appropriately
2.2 How to complete the guide
To get started do the following:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/command-objects-and-forms.git
The Grails guides repositories contain two folders:
-
initial
Initial project. Often a simple Grails app with some additional code to give you a head-start. -
complete
A completed example. It is the result of working through the steps presented by the guide and applying those changes to theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-guides/command-objects-and-forms/initial
and follow the instructions in the next sections.
You can go right to the completed example if you cd into grails-guides/command-objects-and-forms/complete
|
3 What are command objects?
As described in the Grails documentation,
A command object is a class that is used in conjunction with data binding, usually to allow validation of data that may not fit into an existing domain class. A class is only considered to be a command object when it is used as a parameter of an action.
While a domain class can be used as a command object, so also can non-domain classes defined in other files (or even classes defined in the same file as the controller using the class).
4 Writing the Application
This guide will lead you through the process of creating a controller with actions that use command objects. With those command objects, you will learn to handle form inputs safely and easily, with a minimum of boilerplate.
4.1 Create the controller
Before we can create any actions using command objects, we need a controller. Create a
PlayerController
class to work with the existing domain object Player
.
$ ./grailsw create-controller demo.Player
The first time you run a grails command, application dependencies will be
downloaded form the Internet. Subsequent calls will be much faster.
|
The output of the command will look like:
| Created grails-app/controllers/demo/PlayerController.groovy | Created src/test/groovy/demo/PlayerControllerSpec.groovy
You will now have a new, mostly empty controller named PlayerController.groovy
in the grails-app/controllers/demo
directory. Update the index
action and add
the show
action to look like this:
package demo
import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
import org.springframework.http.HttpStatus
@ReadOnly
class PlayerController {
def index() {
respond Player.list(params), model: [playerCount: Player.count()]
}
def show(Player player) {
respond player
}
}
The domain class, views, and some sample data are provided for you. At this point, you should be able to run the application and navigate to http://localhost:8080/player/ to see a list of players, and click on a player’s name to view that individual player. However, none of the other actions (e.g. create, save, edit, update, delete) will work yet, not until you write those actions.
At any point, you can start the application by typing Grails application running at http://localhost:8080 in environment: development Stop the application by pressing Ctrl-C. |
4.2 Implement the create and save actions
Next, let’s add the ability to create new players. To PlayerController
, add the create
action.
@SuppressWarnings(['FactoryMethodName', 'GrailsMassAssignment'])
def create() {
respond new Player(params)
}
While this action does make use of the Player
class, the instance of Player
is not a command object
since it is not an argument to the action. This should make sense: since the intent of the
action is to create a new player object, there is no form or input handling necessary.
From the Player List (http://localhost:8080/player/index), click the New Player button
to see the form to create a new player (located at http://localhost:8080/player/create).
If you were to enter values into the form and click the Create button now, you would get
a 404 Page Not Found
error, since the save
action is not defined yet.
Add that now to PlayerController
.
def save(Player player) {
// ...
}
Here, player
is a command object, because it is an argument to the action. Behind
the scenes, Grails will recognize this and rewrite your action to fetch existing records
(if necessary), bind input data to the command object, perform dependency injection, and
validate the object. All of these tasks could be done manually, but by making use of a
command object, all of this behavior is automatic and leaves your code uncluttered of so
much boilerplate.
Arguably, the best feature here is Data Binding,
part of the automatic behavior you get from using command objects. While on the create
page, view the HTML source code (generated from the compiled create.gsp
) and look for
the <form>
tag:
<form action="/player/save" method="post" >
<fieldset class="form">
<div class='fieldcontain required'>
<label for='name'>Name<span class='required-indicator'>*</span></label>
<input type="text" name="name" value="" required="" id="name" />
</div>
<div class='fieldcontain required'>
<label for='game'>Game<span class='required-indicator'>*</span></label>
<input type="text" name="game" value="" required="" id="game" />
</div>
<div class='fieldcontain'>
<label for='region'>Region</label>
<input type="text" name="region" value="" id="region" />
</div>
<div class='fieldcontain required'>
<label for='wins'>Wins<span class='required-indicator'>*</span></label>
<input type="number" name="wins" value="0" required="" min="0" id="wins" />
</div>
<div class='fieldcontain required'>
<label for='losses'>Losses<span class='required-indicator'>*</span></label>
<input type="number" name="losses" value="0" required="" min="0" id="losses" />
</div>
</fieldset>
<fieldset class="buttons">
<input type="submit" name="create" class="save" value="Create" id="create" />
</fieldset>
</form>
The <input>
tag names match exactly the properties of the domain class Player
.
package demo
class Player {
String name
String game
String region
int wins = 0
int losses = 0
static constraints = {
name blank: false, unique: true
game blank: false
region nullable: true
wins min: 0
losses min: 0
}
}
Data binding will take the form-submitted string values for name
, game
, and region
(if set) and set those properties in your command object player
. The form-submitted
string values for wins
and losses
will also be set in player
after automatic
type-conversion to int
. The convention of using the same name for domain class properties
and input fields greatly simplifies coding and application development: view fields are
pre-filled with values from domain class objects, and command objects are filled with
submitted form data, requiring very little developer effort.
After binding, data validation will be done; as this is a domain class, any constraints
set in the class’s constraits
block will be checked. This gives us the opportunity to
add simple error-checking. Near the top of PlayerController.groovy
, add:
import org.springframework.http.HttpStatus
Then update the save
action of PlayerController
.
@Transactional
def save(Player player) {
if (player == null) {
render status: HttpStatus.NOT_FOUND
return
}
if (player.hasErrors()) {
respond player.errors, view: 'create'
return
}
}
The existing, generated gsp
files include Javascript to do as much client-side form
validation as possible. To see the Grails server-side validation and error checking, try
using curl
. On the command line (while the Grails application is running), type:
$ curl --request POST --form "name=Bob Smith" --form "wins=42" --form "losses=abc" "http://localhost:8080/player/save.json"
You should receive the following response (displayed nicely here):
{"errors": [
{"object": "demo.Player",
"field": "losses",
"rejected-value": "abc",
"message": "Property losses is type-mismatched"},
{"object": "demo.Player",
"field": "game",
"rejected-value": null,
"message": "Property [game] of class [class demo.Player] cannot be null"}
]}
We are going to verify this with a Functional Test.
Functional Tests involve making HTTP requests against the running application and verifying the resultant behaviour.
We use the Micronaut HTTP Client.
Ensure you have the dependency io.micronaut:micronaut-http-client
package demo
import grails.testing.spock.OnceBefore
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.MediaType
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
import grails.testing.mixin.integration.Integration
@SuppressWarnings(['LineLength', 'MethodName'])
@Integration
class PlayerControllerFuncSpec extends Specification {
@Shared
@AutoCleanup
private HttpClient client
@SuppressWarnings(['JUnitPublicNonTestMethod'])
@OnceBefore
void init() {
String baseUrl = "http://localhost:$serverPort" (1)
this.client = HttpClient.create(baseUrl.toURL())
}
@SuppressWarnings(['JUnitTestMethodWithoutAssert'])
void 'test save validation'() {
when:
Map<String, Object> json = [name : 'Bob Smith',
wins : 42,
losses : 'abc',]
client.toBlocking().exchange(HttpRequest.POST('/player/save', json)
.accept(MediaType.APPLICATION_JSON_TYPE), Map) (2)
then:
HttpClientResponseException e = thrown()
e.status == HttpStatus.UNPROCESSABLE_ENTITY (3)
e.response.body().errors.size() == 2
e.response.body().errors.find { it.field == 'losses' }.message == 'Property losses is type-mismatched'
e.response.body().errors.find { it.field == 'game' }.message ==
'Property [game] of class [class demo.Player] cannot be null'
}
}
1 | serverPort property is automatically injected. It contains the random port where the Grails application runs during the functional test. |
2 | If your client accepts JSON it is always a good idea to set the Accept Http Header. You will receive a JSON response instead of the HTML page. |
3 | Since command object validation failed, the server returns the Http Status - 422 Unprocessable Entity |
Now that data binding, validation, and error-checking is done, update the save
action
to actually save the object and respond to the form submission. This completes the action.
@Transactional
def save(Player player) {
if (player == null) {
render status: HttpStatus.NOT_FOUND
return
}
if (player.hasErrors()) {
respond player.errors, view: 'create'
return
}
player.save flush: true
request.withFormat {
form multipartForm { redirect player }
'*' { respond player, status: HttpStatus.CREATED }
}
}
You should now be able to successfully create and save new players to the database.
The example controller code presented here for ./grailsw generate-controller demo.Player |
4.3 Implement the edit and update actions
Next, let’s add the ability to update existing players. Add edit
and update
actions
to PlayerController
. You should notice that these are almost the same as the create
and save
actions.
def edit(Player player) {
respond player
}
def update(Player player) {
if (player == null) {
render status: HttpStatus.NOT_FOUND
return
}
if (player.hasErrors()) {
respond player.errors, view: 'edit'
return
}
player.save flush: true
request.withFormat {
form multipartForm { redirect player }
'*' { respond player, status: HttpStatus.OK }
}
}
The edit
action starts with a Player
command object; Grails fetches an existing Player
object automatically, and the properties of that object are loaded as the initial values of
the form in edit.gsp
. The update
action is the same as save
, except that errors redirect
back to edit
rather than create
, and the HTTP status for success is OK
rather than CREATED
.
On the assumption that a player’s win and loss counts will be managed elsewhere, the wins
and losses fields have already been removed from the form in edit.gsp
.
While running the application, go to the player list and click on one of the
player names. While you can show the win/loss count, clicking the Edit button will
display a form that does not let you change the win/loss counts.
But this isn’t sufficient: removing input fields from the form does not prevent those fields from being changed. A clever user can submit the form by other means that will change those values.
$ curl --request GET "http://localhost:8080/player/show/1.json"
{"id":1, "game":"Pandemic", "losses":30, "name":"Alexis Barnett", "region":"EAST", "wins":96}
$ curl --request POST --form "id=1" --form "wins=2" --form "losses=194" "http://localhost:8080/player/update.json"
After the second curl
command, player Alexis Barnett will have only two wins and 194 losses,
rather than the 96 wins and 30 losses she should have.
One way to fix this would be to avoid the use of the Player
command object (so as to avoid
automatic filling of properties) and instead manage all of the loading, binding, and verification
manually. You could customize the action’s behavior and ignore any attempts to modify the wins
and losses properties.
But command objects are not required to be a domain class: use any class! Define the following
class in PlayerController.groovy
below the PlayerController
class. Note that the property
names of PlayerInfo
match those of the form’s <input>
tags; when the form is submitted,
form values will be bound to the command object’s properties where the names match.
class PlayerInfo {
String name
String game
String region
}
Change the update
action to use PlayerInfo
as the command object. Load the appropriate
Player
instance, then update and save it using the command object’s properties.
def update(PlayerInfo info) {
Player player = Player.get(params.id)
if (player == null) {
render status: HttpStatus.NOT_FOUND
return
}
player.properties = info.properties
player.save flush: true
if (player.hasErrors()) {
respond player.errors, view: 'edit'
return
}
request.withFormat {
form multipartForm { redirect player }
'*' { respond player, status: HttpStatus.OK }
}
}
Since PlayerInfo
does not contain wins
or losses
, those values will not be
accepted from the form submission, even if values are submitted for those names.
$ curl --request GET "http://localhost:8080/player/show/4.json"
{"id":4, "name":"Catherine Newton", "game":"Scythe", "region":"WEST", "wins":66, "losses":40}
$ curl --request POST --form "id=4" --form "name=June Smith" --form "game=Chess" --form "region=NORTH" --form "wins=0" --form "losses=10" "http://localhost:8080/player/update.json"
$ curl --request GET "http://localhost:8080/player/show/4.json"
{"id":4, "name":"June Smith", "game":"Chess", "region":"NORTH", "wins":66, "losses":40}
Note that the name
, game
, and region
properties have all updated, but the wins
and
losses
properties have not changed despite our efforts to do so. By using the
PlayerInfo
command object, we’ve restricted what fields are accepted from the form
submission.
Create a Unit Test to validate this behaviour
package demo
import spock.lang.Specification
import grails.testing.gorm.DomainUnitTest
import grails.testing.web.controllers.ControllerUnitTest
@SuppressWarnings(['MethodName', 'DuplicateListLiteral', 'DuplicateNumberLiteral', 'LineLength'])
class PlayerControllerSpec extends Specification implements ControllerUnitTest<PlayerController>, DomainUnitTest<Player> {
@SuppressWarnings(['JUnitPublicNonTestMethod'])
def "test update"() {
when:
def player = new Player(name: 'Sergio', game: 'XCOM: Enemy Unkown', region: 'Spain', wins: 3, losses: 2)
player.save()
params.id = player.id
params.name = 'Sergio del Amo'
params.game = 'XCOM 2'
params.region = 'USA'
params.wins = 4
controller.update() (1)
then: 'respond model has no errors'
!model.player.hasErrors()
and: 'player properties have been updated correctly'
model.player.name == 'Sergio del Amo'
model.player.game == 'XCOM 2'
model.player.region == 'USA'
and: 'non existing properties in the command object have not been modified'
model.player.wins == 3
model.player.losses == 2
}
}
1 | Call a no argument version of the update method. Put the values in the params map. Databinding
will map those values to the Command Object. This emulates what happens at runtime. |
The current solution is lacking, though. Because PlayerInfo
is now the command object and
not Player
, validation is not done until the call to player.save
. It would be better if
validation was done earlier, as it had been done when Player
was the command object.
To resolve this, add a constraints
block to PlayerInfo
. When you defined the command
object class PlayerInfo
in the PlayerController.groovy
file, Grails recognized it as
a related command object class and made the class able to be validated. Change PlayerInfo
and add constraints.
class PlayerInfo {
String name
String game
String region
static constraints = {
name blank: false
game blank: false
region nullable: true
}
}
Create a unit test to validate the constraints defined in the command object.
package demo
import spock.lang.Specification
@SuppressWarnings(['MethodName', 'DuplicateListLiteral'])
class PlayerInfoSpec extends Specification {
@SuppressWarnings(['JUnitPublicNonTestMethod'])
def "test PlayerInfo.region can be null"() {
expect:
new PlayerInfo(region: null).validate(['region'])
}
@SuppressWarnings(['JUnitPublicNonTestMethod'])
def "test PlayerInfo.name can be blank"() {
when:
def playerInfo = new PlayerInfo(name: '')
then:
!playerInfo.validate(['name'])
playerInfo.errors['name'].code == 'blank'
when:
playerInfo = new PlayerInfo(name: null)
then:
!playerInfo.validate(['name'])
playerInfo.errors['name'].code == 'nullable'
}
@SuppressWarnings(['JUnitPublicNonTestMethod'])
def "test PlayerInfo.game can be blank"() {
when:
def playerInfo = new PlayerInfo(game: '')
then:
!playerInfo.validate(['game'])
playerInfo.errors['game'].code == 'blank'
when:
playerInfo = new PlayerInfo(game: null)
then:
!playerInfo.validate(['game'])
playerInfo.errors['game'].code == 'nullable'
}
}
Revise the update
action once more, returning error checking prior to the attempt to save.
@Transactional
def update(PlayerInfo info) {
if (info.hasErrors()) {
respond info.errors, view: 'edit'
return
}
Player player = Player.get(params.id)
if (player == null) {
render status: HttpStatus.NOT_FOUND
return
}
player.properties = info.properties
player.save flush: true
request.withFormat {
form multipartForm { redirect player }
'*' { respond player, status: HttpStatus.OK }
}
}
Command object classes can be defined elsewhere (for example, under the src/
folder hierarchy).
They need not be in the same file as the controller using them. This might be useful to maintain
a separation of concerns, or simply to make a command object class available to multiple controllers.
If your command object class is defined in the same source file as the controller using it, Grails
will automatically make it Validateable
, permitting the use of a static constraints
block. If
you define the class elsewhere and need constraints
, you need to add an implements
clause as displayed next:
class PlayerInfo implements grails.validation.Validateable {
String name
String game
String region
static constraints = {
name blank: false
game blank: false
region nullable: true
}
}
5 Running the Application
To run the application use the ./gradlew bootRun
command which will start the application on port 8080.