Reading and validating HTML form data

If you try to submit the form, you get an error because the data submitted by your form is not sent to the browser as a JSON blob, as expected by your current Items.create action. Indeed, web browsers send the form data as application/x-www-form-urlencoded content. So, we have to update our action code to handle this content type instead of JSON.

Handling the HTML form submission

The form model you use to produce the HTML form can also be used to process the request body of a form submission. Change the Items.create action as follows:

val create = Action(parse.urlFormEncoded) { implicit request =>
  createItemFormModel.bindFromRequest().fold(
    formWithErrors => BadRequest(views.html.createForm(formWithErrors)),
    createItem => {
      shop.create(createItem.name, createItem.price) match {
        case Some(item) => Redirect(routes.Items.details(item.id))
        case None => InternalServerError
      }
    }
  )
}

The Java equivalent code is as follows:

@BodyParser.Of(BodyParser.FormUrlEncoded.class)
public static Result create() {
  Form<CreateItem> submission = Form.form(CreateItem.class).bindFromRequest();
  if (submission.hasErrors()) {
    return badRequest(views.html.createForm.render(submission));
  } else {
    CreateItem createItem = submission.get();
    Item item = shop.create(createItem.name, createItem.price);
    if (item != null) {
      return redirect(routes.Items.details(item.id));
    } else {
      return internalServerError();
    }
  }
}

The form submission handling process implemented by this action can be described as follows.

First, the urlFormEncoded body parser tries to parse the request body as application/x-www-form-urlencoded content (the @BodyParser.Of annotation achieves this in Java).

Second, the form model tries to bind the request body as a CreateItem value according to the form model definition. Basically, the form/urlencoded content is key-value pairs, so the binding process looks for each form model key in the request body to retrieve its value and then tries to coerce it to its expected type. So, form models are bi-directional mappings between a data type and an HTML form. The binding process returns a copy of the form model, either with the input data as a CreateItem value, or in the case of failure, the input data and their validation errors.

Third, the fold method tells you what to do in the case of failure and success. In the case of failure, the fold method calls its first parameter, which is a function, and supplies it the result of the form submission: the collected data and the validation errors at the origin of the failure. In our case, we return an HTTP response with a 400 status code (Bad Request) that contains the form page with the user-submitted data and their validation errors (all this information is carried by the form model). In the case of success, the fold method calls its second parameter, which is also a function, and supplies it the CreateItem value created from the user-submitted data. In our case, we ask the Shop service to effectively create the item and if this operation succeeds, redirect the user to the Items.details page.

In Java, we distinguish between form submission success and failure using the hasErrors method on a form model. In the case of failure, we pass the content of the submission (that contains the user-submitted data and their eventual validation errors) as a parameter to the HTML form page. In the case of success, we retrieve the CreateItem resulting value using the get method of the form model.

Validating the HTML form data

Your application should now be able to create items from HTML form submissions. However, as in Chapter 1, Building a Web Service, users can create items with negative prices or empty names: you should add a validation step that performs checks on the input data.

The Scala form validation API

In Scala, the form API allows you to add validation constraints on your mappings:

import play.api.data.validation.Constraints.nonEmpty
val createItemFormModel = Form(mapping(
  "name" -> text.verifying(nonEmpty),
  "price" -> of[Double].verifying("Price must be positive", _ > 0)
)(CreateItem.apply)(CreateItem.unapply))

The verifying method adds a constraint on a mapping. The method comes with overloads, allowing you to supply a Constraint object (as in the name mapping) or an error message and a predicate (as in the price mapping).

The framework comes with a set of common predefined constraints available in the Constraints object. Actually, the price mapping can leverage them as follows:

"price" -> of[Double].verifying(min(0.0, strict = true))

See the API documentation of the Constraints object for an exhaustive list of predefined constraints.

The form data binding process applies the mapping corresponding to each field as well as their validation constraints. If the validation succeeds, it returns the data. Otherwise, it returns the list of validation errors. Thus, the form model returned by the binding process either contains a CreateItem or a list of validation errors. Each error is associated with a field name, so you can either get the list of all the errors using myForm.errors or retrieve the errors associated with a particular field using myForm.errors("fieldname").

Note that it is also possible to define errors not associated with a particular field. This can be useful if the error is related to a combination of field values. For instance, here is how you can define a CreateItem mapping with a dumb validation constraint that checks whether the name of an item has more characters than its price:

mapping(
  "name" -> text.verifying(nonEmpty),
  "price" -> of[Double].verifying(min(0.0, strict = true))
)(CreateItem.apply)(CreateItem.unapply).verifying(
  "Please use a name containing more characters than the item price",
  item => item.name.size > item.price
)

Such a validation constraint is globally associated with the mapping, so the produced error is not associated with a field name. You can retrieve global errors using myForm.globalErrors.

The Java form validation API

In Java, validation constraints are described using annotations similar to JSR-303:

import static play.data.validation.Constraints.*;
public class CreateItem {
    @Required
    public String name;
    @Required @Min(1)
    public Double price;
}

The form binding process reads the input data and looks for values for each field of the target data type (based on the name and type of fields). Then, validation constraints are applied. The resulting object contains the list of validation errors if any, or the CreateItem object if there was no validation error. The exhaustive list of provided validation annotations is available in the API documentation.

You can eventually add a global validation rule by implementing a validate method:

public class CreateItem {
  @Required
  public String name;
  @Required @Min(0)
  public Double price;
  public String validate() {
    if (name.length() < price) {
      return "Please use a name containing more characters than the item price";
    } else {
      return null;
    }
  }
}

In the preceding code, if the length of the item name is less than the item price, a global error (an error not associated with a particular form field) is added to the form.

When performing validation on an object, if the framework finds a method named validate, it calls it after having successfully checked the field's constraints. If the returned value is null, then no error is added to the object. If the returned value is of type String, it is added as a global error message. If the returned value is List<play.data.validation.ValidationError>, all these errors are added.

Note that if your form model contains nested values, you have to use the @Valid annotation on the nested fields to trigger the validation process on them:

public class Seller {
  public String name;
  @Valid
  public Position position;
}

Optional and mandatory fields

Finally, forms often distinguish between mandatory and optional fields. Remember the item's optional description suggested in Chapter 1, Building a Web Service.

In Scala, optional values are modeled with the Option data type:

val createItemFormModel = Form(mapping(
  "name" -> text.verifying(nonEmpty),
  "price" -> of[Double].verifying(min(0.0, strict = true)),
  "description" -> optional(text.verifying(nonEmpty))
)(CreateItem.apply)(CreateItem.unapply))

The optional mapping combinator turns a Mapping[A] into a Mapping[Option[A]] , which produces a value of type Some[A] if the initial mapping succeeded, or None if it failed.

Tip

Note that the text mapping treats the empty text, "", as a valid value. However, you almost never want to consider the empty text as a valid text; this is why I strongly recommend that you always use text.verifying(nonEmpty). Actually, there also is a nonEmptyText convenient shorthand for text.verifying(nonEmpty). If you want to model an optional text value, you generally will need optional(nonEmptyText).

In Java, fields are considered to be optional, unless they are annotated with @Required. A form model equivalent to the preceding code would be the following:

public class CreateItem {
    @Required
    public String name;
    @Required @Min(0)
    public Double price;

    public String description;
}

Sharing JSON validation and HTML form validation rules

The attentive reader might have noticed that our form validation logic duplicates the JSON validation logic presented in Chapter 1, Building a Web Service. How can we avoid duplicating things and reuse the same validation for both JSON processing and HTML form processing? There are essentially two ways to achieve this:

  • By supporting both HTML form content and JSON content in the form binding process
  • By making the form binding process and the JSON binding process be able to use the same validation API

Play supports the first approach. In addition to the URL-encoded data, the binding process supports multipart/form-data (used by web browsers to transfer files) and JSON data.

The other path has also been explored, though, but has not been integrated into Play at the time these lines are written. Nevertheless, you can find it at http://github.com/jto/play-validation. This means that, using the same form model, you can bind and validate data from the URL-encoded data as well as from the JSON data.

You can even share the same HTTP POST endpoints if your action is able to parse and interpret both content types. To achieve this, instead of explicitly requiring requests content to be JSON or URL-encoded data using a specific body parser, just use the default tolerant body parser. Thus, all you have to do is remove the explicit parse.urlFormEncoded body parser (or the @BodyParser.Of annotation in Java) from your action definition.

Your action will then be able to parse the JSON data as well as the URL-encoded data. The form binding process builds a key-value map from the request body. If this one is a JSON object, it uses field names as keys and field values as values.

For instance, the following two request bodies create the same item:

{ "name": "foo", "price": 42 }
name=foo&price=42

Note that an alternative would have been to define separate endpoints to handle JSON requests and HTML form submissions:

val createFromJson = Action(parse.json)(create)
val createFromHtmlForm = Action(parse.urlFormEncoded)(create)
val create: Request[_] => Result = { implicit request => … }
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.195.111