From a8b436a6c0d7083dddf226beebe3fdf8ad96f65f Mon Sep 17 00:00:00 2001 From: Sally Steuterman Date: Wed, 19 Jul 2023 15:34:41 -0500 Subject: [PATCH 1/4] Setting up chapter --- content/spring-model-validation/_index.md | 21 ++ .../exercises/_index.md | 17 + .../exercises/example-1/index.md | 15 + .../exercises/example-2/index.md | 15 + content/spring-model-validation/next-steps.md | 15 + .../spring-model-validation/reading/_index.md | 15 + .../reading/server-side-validation/index.md | 45 +++ .../reading/thymeleaf-form-tools/index.md | 295 ++++++++++++++++++ .../reading/validation-annotations/index.md | 222 +++++++++++++ .../pictures/example-picture.png | Bin 0 -> 4348 bytes .../reading/validation-controller/index.md | 142 +++++++++ 11 files changed, 802 insertions(+) create mode 100644 content/spring-model-validation/_index.md create mode 100644 content/spring-model-validation/exercises/_index.md create mode 100644 content/spring-model-validation/exercises/example-1/index.md create mode 100644 content/spring-model-validation/exercises/example-2/index.md create mode 100644 content/spring-model-validation/next-steps.md create mode 100644 content/spring-model-validation/reading/_index.md create mode 100644 content/spring-model-validation/reading/server-side-validation/index.md create mode 100644 content/spring-model-validation/reading/thymeleaf-form-tools/index.md create mode 100644 content/spring-model-validation/reading/validation-annotations/index.md create mode 100644 content/spring-model-validation/reading/validation-annotations/pictures/example-picture.png create mode 100644 content/spring-model-validation/reading/validation-controller/index.md diff --git a/content/spring-model-validation/_index.md b/content/spring-model-validation/_index.md new file mode 100644 index 0000000..dd77021 --- /dev/null +++ b/content/spring-model-validation/_index.md @@ -0,0 +1,21 @@ +--- +title: "Chapter 15: Model Validation" +date: 2023-07-17T15:23:11-05:00 +draft: false +weight: 15 +originalAuthor: Sally Steuterman # to be set by page creator +originalAuthorGitHub: gildedgardenia # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +## Learning Objectives + +## Key Terminology + +## Content Links + +{{% children %}} diff --git a/content/spring-model-validation/exercises/_index.md b/content/spring-model-validation/exercises/_index.md new file mode 100644 index 0000000..3caa6d8 --- /dev/null +++ b/content/spring-model-validation/exercises/_index.md @@ -0,0 +1,17 @@ +--- +title: "Exercises" +date: 2023-07-17T15:23:11-05:00 +draft: false +weight: 3 +originalAuthor: # to be set by page creator +originalAuthorGitHub: # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +## Exercises + +{{% children %}} \ No newline at end of file diff --git a/content/spring-model-validation/exercises/example-1/index.md b/content/spring-model-validation/exercises/example-1/index.md new file mode 100644 index 0000000..340efe6 --- /dev/null +++ b/content/spring-model-validation/exercises/example-1/index.md @@ -0,0 +1,15 @@ +--- +title: "Exercise Example 1" +date: 2023-07-17T15:23:11-05:00 +draft: false +weight: 1 +originalAuthor: # to be set by page creator +originalAuthorGitHub: # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +## Example-1 \ No newline at end of file diff --git a/content/spring-model-validation/exercises/example-2/index.md b/content/spring-model-validation/exercises/example-2/index.md new file mode 100644 index 0000000..bd63a90 --- /dev/null +++ b/content/spring-model-validation/exercises/example-2/index.md @@ -0,0 +1,15 @@ +--- +title: "Exercise Example 2" +date: 2023-07-17T15:23:11-05:00 +draft: false +weight: 2 +originalAuthor: # to be set by page creator +originalAuthorGitHub: # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +## Example-2 \ No newline at end of file diff --git a/content/spring-model-validation/next-steps.md b/content/spring-model-validation/next-steps.md new file mode 100644 index 0000000..cb74bba --- /dev/null +++ b/content/spring-model-validation/next-steps.md @@ -0,0 +1,15 @@ +--- +title: "Next Steps" +date: 2023-07-17T15:23:11-05:00 +draft: false +weight: 4 +originalAuthor: # to be set by page creator +originalAuthorGitHub: # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +## Next Steps \ No newline at end of file diff --git a/content/spring-model-validation/reading/_index.md b/content/spring-model-validation/reading/_index.md new file mode 100644 index 0000000..01d1047 --- /dev/null +++ b/content/spring-model-validation/reading/_index.md @@ -0,0 +1,15 @@ +--- +title: "Reading" +date: 2023-07-17T15:23:11-05:00 +draft: false +weight: 1 +originalAuthor: Sally Steuterman # to be set by page creator +originalAuthorGitHub: gildedgardenia # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +{{% children %}} \ No newline at end of file diff --git a/content/spring-model-validation/reading/server-side-validation/index.md b/content/spring-model-validation/reading/server-side-validation/index.md new file mode 100644 index 0000000..0bb12a8 --- /dev/null +++ b/content/spring-model-validation/reading/server-side-validation/index.md @@ -0,0 +1,45 @@ +--- +title: "Server-Side Validation" +date: 2023-07-17T15:23:11-05:00 +draft: false +weight: 1 +originalAuthor: Sally Steuterman # to be set by page creator +originalAuthorGitHub: gildedgardenia # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +Web applications work under the client-server model. We have been focusing on the server portion, using Spring Boot and Java to create server-side application code. A critical component of any well-made web application is **validation**, which is the process of checking that data conforms to certain criteria. Validation ensures that the application only stores meaningful data. + +{{% notice blue "Example" "rocket" %}} + + Consider a user registration form on a web site. Effective validation rules might require that: + + 1. The username is between 3 and 12 characters long, and + 1. The password is at least 6 characters long. + +{{% /notice %}} + +Web applications should validate *all* data submitted by users. This ensures that data remains well-structured and unexpected errors don't occur. Validation that occurs in the browser---using JavaScript or HTML attributes---is **client-side validation**. Validation that occurs on the web server is **server-side validation**. + +Even if client-side validation is done, it is still critical to validate data on the server. This is because client-side validation can often be bypassed by a savvy user. For example, such a user might modify HTML using a browser's developer tools, or disable JavaScript. + +Server-side validation involves both the model and controller. The model is responsible for *defining* validation rules, while the controller is responsible for *checking* validation rules when data is submitted to the server. + +## Check Your Understanding + +{{% notice green "Question" "rocket" %}} + + The best practice for validating data in a web app is to: + + 1. Use client-side validation + 1. Use server-side validation + 1. Use both client-side and server-side validation + 1. Don't validate incoming data + +{{% /notice %}} + + \ No newline at end of file diff --git a/content/spring-model-validation/reading/thymeleaf-form-tools/index.md b/content/spring-model-validation/reading/thymeleaf-form-tools/index.md new file mode 100644 index 0000000..1113e3f --- /dev/null +++ b/content/spring-model-validation/reading/thymeleaf-form-tools/index.md @@ -0,0 +1,295 @@ +--- +title: "Thymeleaf Form Tools" +date: 2023-07-17T15:23:11-05:00 +draft: false +weight: 4 +originalAuthor: Sally Steuterman # to be set by page creator +originalAuthorGitHub: gildedgardenia # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +Thymeleaf provides some handy attributes for working with form fields. They +make use of the fact that, when using model binding, each model field contains +a lot of information about the corresponding form input needed. In particular, +the model field and its annotations often determine: + +1. The name of the form field (that is, the value of its `name` attribute). +1. The validation rules and corresponding error messages for the field. + +Thymeleaf provides some convenient attributes to make wiring up a form much +easier and cleaner. + +## Display Validation Errors for a Field - Video + +{{< youtube yc-bSDSDuKg >}} + +{{% notice blue "Note" "rocket" %}} + + The starter code for this video is found at the `validation-errors branch `__ of the ``coding-events-demo`` repo. + The final code presented in this video is found on the `display-errors branch `__ As always, code along to the + videos on your own ``coding-events`` project. + +{{% /notice %}} + +{{% notice green "Tip" "rocket" %}} + + Some hawk-eyed students notice a small quirk after finishing their + `display-errors` constructor. Each time the page reloads, the `id` + counter for the database entries increases by 1, even if no new row is + added. This leads to a table that contains gaps between the `id` values. + + This is OK! There is no need to correct the quirk right now. + +{{% /notice %}} + +## Display Validation Errors for a Field - Text + +### Using `th:field` + +We can use the `th:field` attribute on a form input to instruct Thymeleaf to +add field-specific attributes to the form, such as `name` and `id`. + +Consider our second input, which currently looks like this: + +```html {linenos=table, linenostart=17} + +``` + +Above, we set the `name` attribute of the input element equal to +`"description"`. We do this because we want this input data to be bound to the +`description` field of the `Event` class when the form is submitted. + +A better approach uses `th:field` to bind the `description` field +*when the form is rendered*. + +```html {linenos=table, linenostart=17} + +``` + +With this syntax, Thymeleaf will look for a variable named `event` and use +its `description` property to set the values of the `name` and `id` +attributes. The generated input looks like this: + +```html + +``` + +We don't need to use the `id` attribute in this case, but it doesn't hurt +anything by being there. Now, Thymeleaf sets `name` on its own, based on the +field identifier. + +For this to work, two more steps are necessary. First, we add constructor to +`Event` that doesn't require any arguments, also called a +**no-arg constructor**. + +```java {linenos=table, linenostart=27} + public Event(String name, String description, String contactEmail) { + this(); + this.name = name; + this.description = description; + this.contactEmail = contactEmail; + } + + public Event() { + this.id = nextId; + nextId++; + } +``` + +This code includes two changes: + +1. A no-arg constructor has been created. It simply sets the `id` of the + object, leaving all other fields `null`. +1. The previously-existing constructor now calls `this()`, which calls the + no-arg constructor to set the `id` before setting the values of all other + fields. + +Finally, we have to pass in an empty `Event` created with the new no-arg +constructor when rendering the form. Back in `EventController`, we update the +handler: + +```java {linenos=table, linenostart=26} + @GetMapping("create") + public String displayCreateEventForm(Model model) { + model.addAttribute("title", "Create Event"); + model.addAttribute("event", new Event()); + return "events/create"; + } +``` + +Notice line 29, which passes in an `Event` object created by calling the +no-arg constructor. + +{{% notice blue "Note" "rocket" %}} + + It's also allowable to pass in the `Event` object without a label: + + ```java + model.addAttribute(new Event()); + ``` + + In this case, Spring will implicitly create the label `"event"`, which is + the lowercase version of the class name. + +{{% /notice %}} + +Using this technique on our other form fields completes the task of binding the +object to the form during rendering. + +```html {linenos=table, linenostart=8} +
+
+ +

+
+
+ +

+
+
+ +

+
+
+ +
+
+``` + +One additional result of using `th:field` is that if the `Event` object has +a value in any bound field, the input will be created with that value in its +`value` attribute. For example, if the `event` object has a +`contactEmail` of `me@me.com`, then the resulting form input would be: + +```html + +``` + +The value is then visible in the form field when the page loads. This may not +seem immediately useful, but it actually is. Recall our form submission +handler: + +```java {linenos=table, linenostart=33} + @PostMapping("create") + public String processCreateEventForm(@ModelAttribute @Valid Event newEvent, + Errors errors, Model model) { + if(errors.hasErrors()) { + model.addAttribute("title", "Create Event"); + return "events/create"; + } + + EventData.add(newEvent); + return "redirect:"; + } +``` + +This method checks for validation errors and returns the user to the form if it +finds any. It uses model binding to create a new event object, but this event +object is *also passed into the view when re-rendering the form*. This means +that if there are validation errors, the form will be rendered with the values +that the user previously entered, preventing the user from having to re-enter +all of their data. + +Using ``th:errors`` +^^^^^^^^^^^^^^^^^^^ + +The Thymeleaf attribute ``th:errors`` is used similarly to ``th:field`` to +display field-specific error messages. Recall that when we added our validation +annotations to each model field, we also +:ref:`added a message argument `. Setting ``th:errors`` to +a field will display any validation errors for that field. + +For example, let's add a new element to the first form group: + +.. sourcecode:: html + :lineno-start: 9 + +
+ +

+
+ +Setting ``th:errors="${event.name}"`` tells Thymeleaf to insert any error +messages related to the ``name`` field of ``event`` into the paragraph element. +We add ``class="error"`` to allow us to style this element, for example with +red text. A simple rule in our ``styles.css`` file will do the trick: + +.. sourcecode:: css + + .error { + color: red; + } + +.. admonition:: Note + + Make sure that ``styles.css`` is included in the ``head`` fragment of ``fragments.html``, or the stylesheet will not load. + +Using this attribute on all of the fields gives us our final form template +code: + +.. sourcecode:: html + :lineno-start: 8 + +
+
+ +

+
+
+ +

+
+
+ +

+
+
+ +
+
+ +Now, when the form is submitted with invalid data, our custom validation error +messages will display just below the given inputs. + +.. figure:: figures/display-validation-errors.png + :alt: Our Create Event form after submission with all fields blank. Red error messages are visible next to the fields that failed validation. + :width: 700px + + The result of submitting an empty form + +Check Your Understanding +------------------------ + +.. admonition:: Question + + Which HTML attributes will a ``th:field`` attribute NOT influence? + + #. ``id`` + #. ``name`` + #. ``value`` + #. ``field`` + +.. ans: D, which is not even an attribute. All other attributes are set by + th:field \ No newline at end of file diff --git a/content/spring-model-validation/reading/validation-annotations/index.md b/content/spring-model-validation/reading/validation-annotations/index.md new file mode 100644 index 0000000..73edfca --- /dev/null +++ b/content/spring-model-validation/reading/validation-annotations/index.md @@ -0,0 +1,222 @@ +--- +title: "Validation Annotations" +date: 2023-07-17T15:23:11-05:00 +draft: false +weight: 2 +originalAuthor: Sally Steuterman # to be set by page creator +originalAuthorGitHub: gildedgardenia # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +Within the model of a Java web application, we can define validation rules using annotations from the `javax.validation.constraints` package. This package provides a variety of annotations that are useful in common circumstances, and which can be applied to model fields. + +## Common Annotations + +We'll use only a few of these annotations, but you can find a full list in the [package documentation](https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/package-summary.html). + +| Annotation | Description | Syntax | +|------------|-------------|--------| +| `@Size` | Specifies minimum and/or maximum length for a string. | `@Size(min = 3, max = 12)` | +| `@Min` | Specifies the minimum value of a numeric field. | `@Min(0)` | +| `@Max` | Specifies the maximum value of a numeric field. | `@Max(365)` | +| `@Email` | Specifies that a string field should conform to email formatting standards. | `@Email` | +| `@NotNull` | Specifies that a field may not be `null`. | `@NotNull` | +| `@NotBlank` | Specifies that a string field contains at least one non-whitespace character. | `@NotBlank` | + +{{% notice blue "Example" "rocket" %}} + + To apply the validation rules of the example on the previous page to the fields of a `User` model class, we can use `@Size` and `@NotBlank`. + + ```java + @NotBlank + @Size(min = 3, max = 12) + private String username; + + @NotBlank + @Size(min = 6) + private String password; + ``` + +{{% /notice %}} + +## Defining Validation Messages + +Each of these annotations takes an optional `message` parameter that allows you to define a user-friendly description to be used when validation fails. + +{{% notice blue "Example" "rocket" %}} + + ```java + @NotBlank(message = "Username is required") + @Size(min = 3, max = 12, message = "Username must be between 3 and 12 characters long") + private String username; + + @NotBlank(message = "Password is required") + @Size(min = 6, message = "Sorry, but the given password is too short. Passwords must be at least 6 characters long.") + private String password; + ``` + +{{% /notice %}} + +We will see how to ensure these messages are properly displayed in the next section on validating models. + +## Applying Validation Annotations - Video + +{{< youtube 1aZxU0-dhgw >}} + +{{% notice blue "Note" "rocket" %}} + + The starter code for this video is found at the `model-binding branch `__. of the ``coding-events-demo`` repo. + The final code presented in this video is found on the `add-validation-annotations branch `__. As always, code along to the + videos on your own ``coding-events`` project. + +{{% /notice %}} + +## Applying Validation Annotations - Text + +To configure validation on the model-side, we begin by adding validation annotations to each field to which we want to apply constraints. + +For our `Event` class, we add `@Size` and `@NotBlank` to the `name` field, and just `@Size` to the `description` field. + +```java {linenos=table, linenostart=16} + @NotBlank(message = "Name is required.") + @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters") + private String name; + + @Size(max = 500, message = "Description too long!") + private String description; +``` + +The `min` and `max` parameters for `@Size` specify the minimum and maximum number of allowed characters, respectively. Omitting either of these means that no min or max will be applied for the field. For our `description` field, leaving off `min` effectively makes this field optional. + +Each of our annotations also receives a `message` parameter, which provides a user-friendly message to display to the user if the particular validation rule fails. We will see how to display these in a view a bit later. + +Next, we add a new field to store a contact email for each event. This is a `String` named `contactEmail`. Validating email addresses by directly applying each of the rules that an email must satisfy is *extremely* difficult. Thankfully, there is an `@Email` validation annotation that we can apply to our new field. + +After adding this new field to our constructor, and generating a getter and setter, our class is done for the moment. + + ```java {linenos=table, linenostart=11} + public class Event { + + private int id; + private static int nextId = 1; + + @NotBlank + @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters") + private String name; + + @Size(max = 500, message = "Description too long!") + private String description; + + @Email(message = "Invalid email. Try again.") + private String contactEmail; + + public Event(String name, String description, String contactEmail) { + this.name = name; + this.description = description; + this.contactEmail = contactEmail; + this.id = nextId; + nextId++; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getContactEmail() { + return contactEmail; + } + + public void setContactEmail(String contactEmail) { + this.contactEmail = contactEmail; + } + + public int getId() { + return id; + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Event event = (Event) o; + return id == event.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + } +``` + +{{% notice green "Tip" "rocket" %}} + + The full list of Java validation annotations is in the [documentation](https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/package-summary.html) for `javax.validation.constraints`. + +{{% /notice %}} + +Before we can start up our application, we need to add a new column to the `events/index` template to make `contactEmail` visible. + +```html {linenos=table, linenostart=8} + + + + + + + + + + + + + + + +
IDNameDescriptionContact Email
+``` + +Now we can start up our application and test. Submitting an empty form at `/events/create` still results in an event being created, which may not be what you were expecting. + +Rather than a bug, this is expected behavior. Recall that validation involves *both* the model and controller, but we have not modified the controller in any way. Validation annotations simply define the validation rules that *should* be used to check data. The responsibility of checking the data before saving a new event lies with the controller. + +In the next section, we'll modify the controller to properly check for valid data. + +## Check Your Understanding + +{{% notice green "Question" "rocket" %}} + + **True or False:** When using `@Size` you must provide both `min` and `max` arguments. + +{{% /notice %}} + + + +{{% notice green "Question" "rocket" %}} + + **True or False:** Adding validation annotations to a model ensures that bad data is not saved. + +{{% /notice %}} + + \ No newline at end of file diff --git a/content/spring-model-validation/reading/validation-annotations/pictures/example-picture.png b/content/spring-model-validation/reading/validation-annotations/pictures/example-picture.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f6ebd49c0f2a9e0d17423f4b93933d3461b6a3 GIT binary patch literal 4348 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 NuBL%Tu^J-y{{S=_Z(RTY literal 0 HcmV?d00001 diff --git a/content/spring-model-validation/reading/validation-controller/index.md b/content/spring-model-validation/reading/validation-controller/index.md new file mode 100644 index 0000000..e3713a5 --- /dev/null +++ b/content/spring-model-validation/reading/validation-controller/index.md @@ -0,0 +1,142 @@ +--- +title: "Validating Models in a Controller" +date: 2023-07-17T15:23:11-05:00 +draft: false +weight: 3 +originalAuthor: Sally Steuterman # to be set by page creator +originalAuthorGitHub: gildedgardenia # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +Validation involves both model and controller components of an MVC application. After we have defined validation rules using annotations on the model, we must also update the controller to ensure that the rules are checked and appropriate action is taken when validation fails. + +## Validation Flow + +Before diving into the details of the code, let's consider the logical flow of control for validating data in a request. Recall our `POST` handler from the previous chapter, which used model binding to create new `Event` objects from form submissions. + +```java + @PostMapping("create") + public String processCreateEventForm(@ModelAttribute Event newEvent) { + EventData.add(newEvent); + return "redirect:"; + } +``` + +The flow of this request can be described as follows: + +1. Server receives `POST` request +1. Server creates `newEvent` object using request parameters +1. `processCreateEventForm` is called with `newEvent` +1. `newEvent` is saved +1. A 303 redirect response is returned, redirecting the user to `/events` + +The request creates an `Event` object using data from the incoming request. Regardless of what the data looks like, the new object is saved to the data layer. The user could submit an empty form, with no name or description filled in, and our code would be happy to create an `Event` and save it. Similarly, a user could submit the full text of the massive novel "War and Peace" as the description. This isn't great. + +{{% notice blue "Note" "rocket" %}} + + Technically, submitting a request containing "War and Peace" would fail with most applications. This is because web servers typically set a limit on the maximum size of a `POST` request. However, our application code is willing to take requests of any size, at this point. + +{{% /notice %}} + + +Some modest validation rules for a new `Event` object might be: + +1. The `name` field must contain between 3 and 20 characters, and +1. The `description` field may be empty, but may contain no more than 500 characters. + +With these rules in place, conceptually, the flow of our controller code should look more like the following: + +1. Server receives `POST` request +1. Server creates `newEvent` object using request parameters +1. `processCreateEventForm` is called with `newEvent` +1. Controller checks for validation errors in the model object. If errors are found, return the user to the form. Otherwise, proceed. +1. `newEvent` is saved +1. A 303 redirect response is returned, redirecting the user to `/events` + +Let's look at how we can practically do this within Spring Boot. + +## Handling Validation Errors - Video + +{{< youtube omSQO6721cU >}} + +{{% notice blue "Note" "rocket" %}} + + The starter code for this video is found at the `add-validation-annotations branch `__ of the ``coding-events-demo`` repo. + The final code presented in this video is found on the `validation-errors branch `__. As always, code along to the + videos on your own ``coding-events`` project. + +{{% /notice %}} + +## Handling Validation Errors - Text + +When using model binding, we can use Spring Boot annotations and tools to validate new model objects before they are saved to a data layer or database. + +### Using `@Valid` + +Within `EventController`, the `processCreateEventForm` handler method uses model binding to receive an `Event` object that is created using form data. This object is NOT validated automatically, even if validation annotations are present on its fields. + +Recall that *both* the model and controller play a role in validation. The model's responsibility is simply to define validation rules. The controller must check that those rules are satisfied. + +The first step to enable validation in a controller is to add `@Valid` to a method parameter that is created using model binding. + +```java + @PostMapping("create") + public String processCreateEventForm(@ModelAttribute @Valid Event newEvent) { + EventData.add(newEvent); + return "redirect:"; + } +``` + +In lieu of explicit validation error handling (which we will cover below), Spring Boot throws an exception if validation fails for the new object. This means that an object that fails validation will NOT be saved. + +However, the user experience for this flow is not great. If a user submits bad data, rather than showing them a complicated stack trace, we would be better off to provide a helpful message and allow them to try again. + +{{% notice blue "Note" "rocket" %}} + + Remember, exceptions should be messages to programmers and programs, not end users. Even if an exception occurs, we should avoid displaying it to the user. + +{{% /notice %}} + +### Using the `Errors` Object + +We can prevent a validation exception from being thrown by explicitly handling validation errors. Spring Boot makes an object of type `Errors` available when a method uses `@Valid`. As with `Model` objects, we can access this object by placing it in a method's parameter list. + +```java {linenos=table, linenostart=33} + @PostMapping("create") + public String processCreateEventForm(@ModelAttribute @Valid Event newEvent, + Errors errors, Model model) { + if(errors.hasErrors()) { + model.addAttribute("title", "Create Event"); + model.addAttribute("errorMsg", "Bad data!"); + return "events/create"; + } + + EventData.add(newEvent); + return "redirect:"; + } +``` + +Here, we have added `Errors errors` to our handler. This object has a boolean method, `.hasErrors()` that we can use to check for the existence of validation errors. If there are validation errors, we return the form again, along with a simple message for the user. This message can be displayed in the `events/create` template by adding some code above the form: + +```html +

+``` + +Now, when a user submits the form with bad data they will be notified and no exception will be thrown. However, the message "Bad data!" is far from ideal. The next section introduces a technique to display more useful error messages. + +## Check Your Understanding + +{{% notice green "Question" "rocket" %}} + Which of the following statements are true? + + 1. A method parameter may have only one annotation. + 1. `@Valid` can only be used in conjunction with model binding. + 1. Using `@Valid` means that a method will never be called with invalid data. + 1. Spring Boot can infer validation requirements based on the name of a field. +{{% /notice %}} + + \ No newline at end of file From 1965b516dce14a80bc643930391aaeb3427fdfef Mon Sep 17 00:00:00 2001 From: Sally Steuterman Date: Fri, 28 Jul 2023 19:46:04 -0500 Subject: [PATCH 2/4] Full draft of chapter --- content/spring-model-validation/_index.md | 17 ++- .../exercises/_index.md | 53 ++++++- .../exercises/example-1/index.md | 15 -- .../exercises/example-2/index.md | 15 -- content/spring-model-validation/next-steps.md | 9 +- .../reading/thymeleaf-form-tools/index.md | 66 ++++----- .../reading/validation-annotations/index.md | 12 +- .../pictures/example-picture.png | Bin 4348 -> 0 bytes .../reading/validation-controller/index.md | 6 +- .../spring-model-validation/studio/_index.md | 136 ++++++++++++++++++ 10 files changed, 243 insertions(+), 86 deletions(-) delete mode 100644 content/spring-model-validation/exercises/example-1/index.md delete mode 100644 content/spring-model-validation/exercises/example-2/index.md delete mode 100644 content/spring-model-validation/reading/validation-annotations/pictures/example-picture.png create mode 100644 content/spring-model-validation/studio/_index.md diff --git a/content/spring-model-validation/_index.md b/content/spring-model-validation/_index.md index dd77021..4c56f47 100644 --- a/content/spring-model-validation/_index.md +++ b/content/spring-model-validation/_index.md @@ -14,8 +14,23 @@ lastMod: # UPDATE ANY TIME CHANGES ARE MADE ## Learning Objectives +Upon completing all the content in this chapter, you should be able to do the following: + +1. Validate form submission data using annotations and model binding + ## Key Terminology -## Content Links +Here is a list of key terms for this chapter listed by the page the terms first appear on. + +### Server-Side Validation + +1. client-side validation +1. server-side validation + +### Thymeleaf Form Tools + +1. no-arg constructor + +## Chapter Content {{% children %}} diff --git a/content/spring-model-validation/exercises/_index.md b/content/spring-model-validation/exercises/_index.md index 3caa6d8..91ebe19 100644 --- a/content/spring-model-validation/exercises/_index.md +++ b/content/spring-model-validation/exercises/_index.md @@ -1,10 +1,10 @@ --- -title: "Exercises" +title: "Exercises: Model Validation" date: 2023-07-17T15:23:11-05:00 draft: false -weight: 3 -originalAuthor: # to be set by page creator -originalAuthorGitHub: # to be set by page creator +weight: 2 +originalAuthor: Sally Steuterman # to be set by page creator +originalAuthorGitHub: gildedgardenia # to be set by page creator reviewer: # to be set by the page reviewer reviewerGitHub: # to be set by the page reviewer lastEditor: # update any time edits are made after review @@ -12,6 +12,47 @@ lastEditorGitHub: # update any time edits are made after review lastMod: # UPDATE ANY TIME CHANGES ARE MADE --- -## Exercises +Let’s practice adding more fields onto our event objects and +validating them. Create a new branch from your own `display-errors` branch. Here's the `display-errors` branch on +[CodingEventsJava](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/display-errors) if you need to get up to speed. -{{% children %}} \ No newline at end of file +Below, we describe some new fields for you to add to the `Event` class. +For each field, consider the following factors: + +1. What will you call your field? +1. Will you need accessors for this field? +1. What type of input should be added to capture the field's information from the user? +1. Refer to the documentation page to find an appropriate annotation to fit the constraints. +1. What should the error message convey to the user? +1. What, if anything, will you need to update on the controller to account for the new field? + +Event information to add: + +1. Add a field to collect information about where the event will take place. This field should not be + null or blank. + + {{% expand "Check your solution" %}} + + ```java + @NotBlank(message="Location cannot be left blank.") + private String location; + ``` + + {{% /expand %}} + +1. Add a field to collect information about whether an attendee must register for the event or not. For + the purposes of validation practice, make this field only able to be marked as true. + +1. Add a field to collect information about the number of attendees for the event. Valid values for this + field should be any number over zero. + + {{% expand "Check your solution" %}} + + ```java + @Positive(message="Number of attendees must be one or more.") + private int numberOfAttendees; + ``` + + {{% /expand %}} + +1. Browse the validation annotations to find one to use on another new field of your choosing. diff --git a/content/spring-model-validation/exercises/example-1/index.md b/content/spring-model-validation/exercises/example-1/index.md deleted file mode 100644 index 340efe6..0000000 --- a/content/spring-model-validation/exercises/example-1/index.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Exercise Example 1" -date: 2023-07-17T15:23:11-05:00 -draft: false -weight: 1 -originalAuthor: # to be set by page creator -originalAuthorGitHub: # to be set by page creator -reviewer: # to be set by the page reviewer -reviewerGitHub: # to be set by the page reviewer -lastEditor: # update any time edits are made after review -lastEditorGitHub: # update any time edits are made after review -lastMod: # UPDATE ANY TIME CHANGES ARE MADE ---- - -## Example-1 \ No newline at end of file diff --git a/content/spring-model-validation/exercises/example-2/index.md b/content/spring-model-validation/exercises/example-2/index.md deleted file mode 100644 index bd63a90..0000000 --- a/content/spring-model-validation/exercises/example-2/index.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Exercise Example 2" -date: 2023-07-17T15:23:11-05:00 -draft: false -weight: 2 -originalAuthor: # to be set by page creator -originalAuthorGitHub: # to be set by page creator -reviewer: # to be set by the page reviewer -reviewerGitHub: # to be set by the page reviewer -lastEditor: # update any time edits are made after review -lastEditorGitHub: # update any time edits are made after review -lastMod: # UPDATE ANY TIME CHANGES ARE MADE ---- - -## Example-2 \ No newline at end of file diff --git a/content/spring-model-validation/next-steps.md b/content/spring-model-validation/next-steps.md index cb74bba..3786211 100644 --- a/content/spring-model-validation/next-steps.md +++ b/content/spring-model-validation/next-steps.md @@ -3,8 +3,8 @@ title: "Next Steps" date: 2023-07-17T15:23:11-05:00 draft: false weight: 4 -originalAuthor: # to be set by page creator -originalAuthorGitHub: # to be set by page creator +originalAuthor: Sally Steuterman # to be set by page creator +originalAuthorGitHub: gildedgardenia # to be set by page creator reviewer: # to be set by the page reviewer reviewerGitHub: # to be set by the page reviewer lastEditor: # update any time edits are made after review @@ -12,4 +12,7 @@ lastEditorGitHub: # update any time edits are made after review lastMod: # UPDATE ANY TIME CHANGES ARE MADE --- -## Next Steps \ No newline at end of file +Before diving into the next chapter, here are some of our favorite resources on model validation in Spring Boot in case you want to review them. + +1. [Validation in Spring Boot](https://www.baeldung.com/spring-boot-bean-validation) +1. [Validating Form Input](https://spring.io/guides/gs/validating-form-input/) \ No newline at end of file diff --git a/content/spring-model-validation/reading/thymeleaf-form-tools/index.md b/content/spring-model-validation/reading/thymeleaf-form-tools/index.md index 1113e3f..1214448 100644 --- a/content/spring-model-validation/reading/thymeleaf-form-tools/index.md +++ b/content/spring-model-validation/reading/thymeleaf-form-tools/index.md @@ -29,9 +29,9 @@ easier and cleaner. {{% notice blue "Note" "rocket" %}} - The starter code for this video is found at the `validation-errors branch `__ of the ``coding-events-demo`` repo. - The final code presented in this video is found on the `display-errors branch `__ As always, code along to the - videos on your own ``coding-events`` project. + The starter code for this video is found at the [validation-errors branch](https://github.com/LaunchCodeEducation/coding-events/tree/validation-errors) of the `CodingEventsJava` repo. + The final code presented in this video is found on the [display-errors branch](https://github.com/LaunchCodeEducation/coding-events/tree/display-errors) As always, code along to the + videos on your own `codingevents` project. {{% /notice %}} @@ -204,48 +204,44 @@ that if there are validation errors, the form will be rendered with the values that the user previously entered, preventing the user from having to re-enter all of their data. -Using ``th:errors`` -^^^^^^^^^^^^^^^^^^^ +### Using `th:errors` -The Thymeleaf attribute ``th:errors`` is used similarly to ``th:field`` to +The Thymeleaf attribute `th:errors` is used similarly to `th:field` to display field-specific error messages. Recall that when we added our validation annotations to each model field, we also -:ref:`added a message argument `. Setting ``th:errors`` to +added a message argument. Setting `th:errors` to a field will display any validation errors for that field. For example, let's add a new element to the first form group: -.. sourcecode:: html - :lineno-start: 9 - +```html {linenos=table, linenostart=9}

+``` -Setting ``th:errors="${event.name}"`` tells Thymeleaf to insert any error -messages related to the ``name`` field of ``event`` into the paragraph element. -We add ``class="error"`` to allow us to style this element, for example with -red text. A simple rule in our ``styles.css`` file will do the trick: - -.. sourcecode:: css +Setting `th:errors="${event.name}"` tells Thymeleaf to insert any error +messages related to the `name` field of `event` into the paragraph element. +We add `class="error"` to allow us to style this element, for example with +red text. A simple rule in our `styles.css` file will do the trick: +```css .error { color: red; } +``` -.. admonition:: Note - - Make sure that ``styles.css`` is included in the ``head`` fragment of ``fragments.html``, or the stylesheet will not load. +{{% notice blue "Note" "rocket" %}} + Make sure that `styles.css` is included in the `head` fragment of `fragments.html`, or the stylesheet will not load. +{{% /notice %}} Using this attribute on all of the fields gives us our final form template code: -.. sourcecode:: html - :lineno-start: 8 - +```html {linenos=table, linenostart=8}
+``` Now, when the form is submitted with invalid data, our custom validation error messages will display just below the given inputs. -.. figure:: figures/display-validation-errors.png - :alt: Our Create Event form after submission with all fields blank. Red error messages are visible next to the fields that failed validation. - :width: 700px - - The result of submitting an empty form +## Check Your Understanding -Check Your Understanding ------------------------- +{{% notice green "Question" "rocket" %}} -.. admonition:: Question + Which HTML attributes will a `th:field` attribute NOT influence? - Which HTML attributes will a ``th:field`` attribute NOT influence? + 1. `id` + 1. `name` + 1. `value` + 1. `field` - #. ``id`` - #. ``name`` - #. ``value`` - #. ``field`` +{{% /notice %}} -.. ans: D, which is not even an attribute. All other attributes are set by - th:field \ No newline at end of file + \ No newline at end of file diff --git a/content/spring-model-validation/reading/validation-annotations/index.md b/content/spring-model-validation/reading/validation-annotations/index.md index 73edfca..c0d3735 100644 --- a/content/spring-model-validation/reading/validation-annotations/index.md +++ b/content/spring-model-validation/reading/validation-annotations/index.md @@ -12,11 +12,11 @@ lastEditorGitHub: # update any time edits are made after review lastMod: # UPDATE ANY TIME CHANGES ARE MADE --- -Within the model of a Java web application, we can define validation rules using annotations from the `javax.validation.constraints` package. This package provides a variety of annotations that are useful in common circumstances, and which can be applied to model fields. +Within the model of a Java web application, we can define validation rules using annotations from the `jakarta.validation.constraints` package. This package provides a variety of annotations that are useful in common circumstances, and which can be applied to model fields. ## Common Annotations -We'll use only a few of these annotations, but you can find a full list in the [package documentation](https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/package-summary.html). +We'll use only a few of these annotations, but you can find a full list in the [package documentation](https://jakarta.ee/specifications/bean-validation/3.0/apidocs/jakarta/validation/constraints/package-summary.html). | Annotation | Description | Syntax | |------------|-------------|--------| @@ -69,9 +69,9 @@ We will see how to ensure these messages are properly displayed in the next sect {{% notice blue "Note" "rocket" %}} - The starter code for this video is found at the `model-binding branch `__. of the ``coding-events-demo`` repo. - The final code presented in this video is found on the `add-validation-annotations branch `__. As always, code along to the - videos on your own ``coding-events`` project. + The starter code for this video is found at the [model-binding branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/model-binding) of the `CodingEventsJava` repo. + The final code presented in this video is found on the [add-validation-annotations branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/add-validation-annotations). As always, code along to the + videos on your own `codingevents` project. {{% /notice %}} @@ -172,7 +172,7 @@ After adding this new field to our constructor, and generating a getter and sett {{% notice green "Tip" "rocket" %}} - The full list of Java validation annotations is in the [documentation](https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/package-summary.html) for `javax.validation.constraints`. + The full list of Java validation annotations is in the [documentation](https://jakarta.ee/specifications/bean-validation/3.0/apidocs/jakarta/validation/constraints/package-summary.html) for `jakarta.validation.constraints`. {{% /notice %}} diff --git a/content/spring-model-validation/reading/validation-annotations/pictures/example-picture.png b/content/spring-model-validation/reading/validation-annotations/pictures/example-picture.png deleted file mode 100644 index a2f6ebd49c0f2a9e0d17423f4b93933d3461b6a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4348 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 NuBL%Tu^J-y{{S=_Z(RTY diff --git a/content/spring-model-validation/reading/validation-controller/index.md b/content/spring-model-validation/reading/validation-controller/index.md index e3713a5..b21114e 100644 --- a/content/spring-model-validation/reading/validation-controller/index.md +++ b/content/spring-model-validation/reading/validation-controller/index.md @@ -65,9 +65,9 @@ Let's look at how we can practically do this within Spring Boot. {{% notice blue "Note" "rocket" %}} - The starter code for this video is found at the `add-validation-annotations branch `__ of the ``coding-events-demo`` repo. - The final code presented in this video is found on the `validation-errors branch `__. As always, code along to the - videos on your own ``coding-events`` project. + The starter code for this video is found at the [add-validation-annotations branch](https://github.com/LaunchCodeEducation/CodingEventsJava/tree/add-validation-annotations) of the `CodingEventsJava` repo. + The final code presented in this video is found on the [validation-errors branch](https://github.com/LaunchCodeEducation/coding-events/tree/validation-errors). As always, code along to the + videos on your own `codingevents` project. {{% /notice %}} diff --git a/content/spring-model-validation/studio/_index.md b/content/spring-model-validation/studio/_index.md new file mode 100644 index 0000000..dab9689 --- /dev/null +++ b/content/spring-model-validation/studio/_index.md @@ -0,0 +1,136 @@ +--- +title: "Studio: Spa User Validation" +date: 2023-07-17T15:23:11-05:00 +draft: false +weight: 3 +originalAuthor: Sally Steuterman # to be set by page creator +originalAuthorGitHub: gildedgardenia # to be set by page creator +reviewer: # to be set by the page reviewer +reviewerGitHub: # to be set by the page reviewer +lastEditor: # update any time edits are made after review +lastEditorGitHub: # update any time edits are made after review +lastMod: # UPDATE ANY TIME CHANGES ARE MADE +--- + +We’ll build on the User Signup studio from last +class, adding in model validation. + +## Getting Started + +Open up your `spaday` application and checkout the [user-signup-pt2](https://github.com/LaunchCodeEducation/Java17-Spa-Day/tree/user-signup-pt2) branch. + +## Add Validation Annotations + +Navigate to the `User` model class. Add validation +annotations +to ensure these conditions are satisfied: + +1. Username, password, and verify are required (they can’t be empty) +1. Username is between 5 and 15 characters +1. Email is optional +1. If provided, the email has the format of a valid email address. +1. The password is at least 6 characters long + +## Using the Model to Render the Form + +In the `UserController`, modify the `add` method that displays the +form so that it passes in an empty `User` object with: + +```java + model.addAttribute(new User()); +``` + +This object will be accessible in the template, by name, as `user`. + +{{% notice green "Tip" "rocket" %}} + Now that you're passing in an empty `User` object, you may notice some redundant code + in your `processAddUserForm` controller. Remove the model attribute additions + and update the `user/add` template to make use of the model fields (eg. `user.username`). +{{% /notice %}} + +While you’re in the `add.html` template remove the `type="email"` designation from the email +input. The last studio had you add this type to provide some client-side validation on the email +field, but we shouldn’t consider that sufficient. Now that we know how to use model validation to +validate an email field, we'll favor this technique over client-side validation. Even with client-side +validation (that is, in the browser), you should always validate data on +the server as well. You might want to provide constraints in addition to +or beyond what the browser does, and it’s also possible for a clever +(or, more often, malicious) user to bypass the browser’s validation. For this studio, we'll remove the +input type to make it easier to test the server-side validation. + + +## Validating Form Submission Data + +Now that you have your form set up, go back to `UserController` and +add validation on form submission by adding the `@Valid` annotation to +the `User` parameter that is bound, along with an additional +parameter: `Errors errors`. + +{{% notice orange "Warning" "rocket" %}} + + Remember, you must put this parameter directly + after the `User` parameter in the method definition for it to work + properly. + +{{% /notice %}} + +Within the `processAddUserForm` handler, check for errors configured by the +validation annotation using `errors.hasErrors()`. If this returns +`true`, return the user to the form. + +### Validating That Passwords Match + +As we mentioned above, we are not able to use Spring’s validation +machinery to validate that the two password fields match given the setup +we have here. Checking `errors.hasErrors()` will only tell us if there +are errors in other form data fields. + +Last studio, we added some validation checks to make sure the password fields match. +Now we have two validation sections: one for the annotation-configured +validation (which checks `errors.hasErrors()`), and one that checks +that the password fields match. Make sure they work in-sync with each +other to properly return to the form if any of the validation conditions +fail. + +{{% notice green "Tip" "rocket" %}} + + You can, in fact, validate that passwords match using annotations by + taking a slightly more difficult approach than we’ve done here. We + outline how to do so in the Bonus Mission section. + +{{% /notice %}} + +## Test, Test, Test! + +You made a lot of changes! Be sure to thoroughly test them to make sure +everything works as expected. + +## Bonus Mission + +Let’s set up our `User` class so we can validate that the password +fields match using annotations. + +1. Add a `private String verifyPassword` field to `User`, along with + getters and setters. +2. Add a new method, `private void checkPassword`, that compares + `password` and `verifyPassword`. If neither is `null` and they + don’t match, then set `verifyPassword = null`. +3. In both `setPassword` and `setVerifyPassword`, call + `checkPassword` *after* setting the given field. +4. Add `@NotNull` to the `verifyPassword` field with the error + message: “Passwords do not match”. +5. Refactor the controller and `add.html` template to use the + built-in, annotation-based validation instead of the manual password + validation that we carried out previously. Be sure to update the + verify field and label in the form to use the field on the `User` + class, and to remove `String verify` from the `add` method + signature. + +The result of these changes is that when the `User` object is bound to +the request, both `password` and `verifyPassword` are set. Spring +does this by calling the setters on these fields. The setters call +`checkPassword`, so when the second one is set (whichever that may +be), we’ll know that both `password` and `verifyPassword` are not +`null` and we’ll compare them. If they don’t match, we manually +violate the `@NotNull` validation on `verifyPassword` by setting +that field to `null`. \ No newline at end of file From 28845640d103bcbb068dbb6ccab6862ddd07524a Mon Sep 17 00:00:00 2001 From: John Woolbright Date: Mon, 21 Aug 2023 12:54:23 -0500 Subject: [PATCH 3/4] minor copy/edit updates. Mostly code spacing and link update for studio --- .../spring-model-validation/reading/_index.md | 2 + .../reading/thymeleaf-form-tools/index.md | 182 ++++++++-------- .../reading/validation-annotations/index.md | 200 +++++++++--------- .../reading/validation-controller/index.md | 44 ++-- .../spring-model-validation/studio/_index.md | 26 +-- 5 files changed, 226 insertions(+), 228 deletions(-) diff --git a/content/spring-model-validation/reading/_index.md b/content/spring-model-validation/reading/_index.md index 01d1047..df99f64 100644 --- a/content/spring-model-validation/reading/_index.md +++ b/content/spring-model-validation/reading/_index.md @@ -12,4 +12,6 @@ lastEditorGitHub: # update any time edits are made after review lastMod: # UPDATE ANY TIME CHANGES ARE MADE --- +## Reading Content Links + {{% children %}} \ No newline at end of file diff --git a/content/spring-model-validation/reading/thymeleaf-form-tools/index.md b/content/spring-model-validation/reading/thymeleaf-form-tools/index.md index 1214448..92c1abd 100644 --- a/content/spring-model-validation/reading/thymeleaf-form-tools/index.md +++ b/content/spring-model-validation/reading/thymeleaf-form-tools/index.md @@ -56,9 +56,9 @@ add field-specific attributes to the form, such as `name` and `id`. Consider our second input, which currently looks like this: ```html {linenos=table, linenostart=17} - + ``` Above, we set the `name` attribute of the input element equal to @@ -69,9 +69,9 @@ A better approach uses `th:field` to bind the `description` field *when the form is rendered*. ```html {linenos=table, linenostart=17} - + ``` With this syntax, Thymeleaf will look for a variable named `event` and use @@ -79,7 +79,7 @@ its `description` property to set the values of the `name` and `id` attributes. The generated input looks like this: ```html - + ``` We don't need to use the `id` attribute in this case, but it doesn't hurt @@ -91,17 +91,17 @@ For this to work, two more steps are necessary. First, we add constructor to **no-arg constructor**. ```java {linenos=table, linenostart=27} - public Event(String name, String description, String contactEmail) { - this(); - this.name = name; - this.description = description; - this.contactEmail = contactEmail; - } - - public Event() { - this.id = nextId; - nextId++; - } +public Event(String name, String description, String contactEmail) { + this(); + this.name = name; + this.description = description; + this.contactEmail = contactEmail; +} + +public Event() { + this.id = nextId; + nextId++; +} ``` This code includes two changes: @@ -117,12 +117,12 @@ constructor when rendering the form. Back in `EventController`, we update the handler: ```java {linenos=table, linenostart=26} - @GetMapping("create") - public String displayCreateEventForm(Model model) { - model.addAttribute("title", "Create Event"); - model.addAttribute("event", new Event()); - return "events/create"; - } +@GetMapping("create") +public String displayCreateEventForm(Model model) { + model.addAttribute("title", "Create Event"); + model.addAttribute("event", new Event()); + return "events/create"; +} ``` Notice line 29, which passes in an `Event` object created by calling the @@ -133,7 +133,7 @@ no-arg constructor. It's also allowable to pass in the `Event` object without a label: ```java - model.addAttribute(new Event()); + model.addAttribute(new Event()); ``` In this case, Spring will implicitly create the label `"event"`, which is @@ -145,29 +145,29 @@ Using this technique on our other form fields completes the task of binding the object to the form during rendering. ```html {linenos=table, linenostart=8} -
-
- -

-
-
- -

-
-
- -

-
-
- -
-
+
+
+ +

+
+
+ +

+
+
+ +

+
+
+ +
+
``` One additional result of using `th:field` is that if the `Event` object has @@ -176,7 +176,7 @@ a value in any bound field, the input will be created with that value in its `contactEmail` of `me@me.com`, then the resulting form input would be: ```html - + ``` The value is then visible in the form field when the page loads. This may not @@ -184,17 +184,17 @@ seem immediately useful, but it actually is. Recall our form submission handler: ```java {linenos=table, linenostart=33} - @PostMapping("create") - public String processCreateEventForm(@ModelAttribute @Valid Event newEvent, - Errors errors, Model model) { - if(errors.hasErrors()) { - model.addAttribute("title", "Create Event"); - return "events/create"; - } - - EventData.add(newEvent); - return "redirect:"; +@PostMapping("create") +public String processCreateEventForm(@ModelAttribute @Valid Event newEvent, + Errors errors, Model model) { + if(errors.hasErrors()) { + model.addAttribute("title", "Create Event"); + return "events/create"; } + + EventData.add(newEvent); + return "redirect:"; +} ``` This method checks for validation errors and returns the user to the form if it @@ -215,12 +215,12 @@ a field will display any validation errors for that field. For example, let's add a new element to the first form group: ```html {linenos=table, linenostart=9} -
- -

-
+
+ +

+
``` Setting `th:errors="${event.name}"` tells Thymeleaf to insert any error @@ -229,9 +229,9 @@ We add `class="error"` to allow us to style this element, for example with red text. A simple rule in our `styles.css` file will do the trick: ```css - .error { - color: red; - } +.error { + color: red; +} ``` {{% notice blue "Note" "rocket" %}} @@ -242,29 +242,29 @@ Using this attribute on all of the fields gives us our final form template code: ```html {linenos=table, linenostart=8} -
-
- -

-
-
- -

-
-
- -

-
-
- -
-
+
+
+ +

+
+
+ +

+
+
+ +

+
+
+ +
+
``` Now, when the form is submitted with invalid data, our custom validation error diff --git a/content/spring-model-validation/reading/validation-annotations/index.md b/content/spring-model-validation/reading/validation-annotations/index.md index c0d3735..707aa88 100644 --- a/content/spring-model-validation/reading/validation-annotations/index.md +++ b/content/spring-model-validation/reading/validation-annotations/index.md @@ -32,13 +32,13 @@ We'll use only a few of these annotations, but you can find a full list in the [ To apply the validation rules of the example on the previous page to the fields of a `User` model class, we can use `@Size` and `@NotBlank`. ```java - @NotBlank - @Size(min = 3, max = 12) - private String username; + @NotBlank + @Size(min = 3, max = 12) + private String username; - @NotBlank - @Size(min = 6) - private String password; + @NotBlank + @Size(min = 6) + private String password; ``` {{% /notice %}} @@ -50,13 +50,13 @@ Each of these annotations takes an optional `message` parameter that allows you {{% notice blue "Example" "rocket" %}} ```java - @NotBlank(message = "Username is required") - @Size(min = 3, max = 12, message = "Username must be between 3 and 12 characters long") - private String username; + @NotBlank(message = "Username is required") + @Size(min = 3, max = 12, message = "Username must be between 3 and 12 characters long") + private String username; - @NotBlank(message = "Password is required") - @Size(min = 6, message = "Sorry, but the given password is too short. Passwords must be at least 6 characters long.") - private String password; + @NotBlank(message = "Password is required") + @Size(min = 6, message = "Sorry, but the given password is too short. Passwords must be at least 6 characters long.") + private String password; ``` {{% /notice %}} @@ -82,12 +82,12 @@ To configure validation on the model-side, we begin by adding validation annotat For our `Event` class, we add `@Size` and `@NotBlank` to the `name` field, and just `@Size` to the `description` field. ```java {linenos=table, linenostart=16} - @NotBlank(message = "Name is required.") - @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters") - private String name; +@NotBlank(message = "Name is required.") +@Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters") +private String name; - @Size(max = 500, message = "Description too long!") - private String description; +@Size(max = 500, message = "Description too long!") +private String description; ``` The `min` and `max` parameters for `@Size` specify the minimum and maximum number of allowed characters, respectively. Omitting either of these means that no min or max will be applied for the field. For our `description` field, leaving off `min` effectively makes this field optional. @@ -99,75 +99,75 @@ Next, we add a new field to store a contact email for each event. This is a `Str After adding this new field to our constructor, and generating a getter and setter, our class is done for the moment. ```java {linenos=table, linenostart=11} - public class Event { - - private int id; - private static int nextId = 1; - - @NotBlank - @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters") - private String name; - - @Size(max = 500, message = "Description too long!") - private String description; - - @Email(message = "Invalid email. Try again.") - private String contactEmail; - - public Event(String name, String description, String contactEmail) { - this.name = name; - this.description = description; - this.contactEmail = contactEmail; - this.id = nextId; - nextId++; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public String getContactEmail() { - return contactEmail; - } - - public void setContactEmail(String contactEmail) { - this.contactEmail = contactEmail; - } - - public int getId() { - return id; - } - - @Override - public String toString() { - return name; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Event event = (Event) o; - return id == event.id; - } - - @Override - public int hashCode() { - return Objects.hash(id); - } + public class Event { + + private int id; + private static int nextId = 1; + + @NotBlank + @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters") + private String name; + + @Size(max = 500, message = "Description too long!") + private String description; + + @Email(message = "Invalid email. Try again.") + private String contactEmail; + + public Event(String name, String description, String contactEmail) { + this.name = name; + this.description = description; + this.contactEmail = contactEmail; + this.id = nextId; + nextId++; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getContactEmail() { + return contactEmail; + } + + public void setContactEmail(String contactEmail) { + this.contactEmail = contactEmail; + } + + public int getId() { + return id; + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Event event = (Event) o; + return id == event.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); } +} ``` {{% notice green "Tip" "rocket" %}} @@ -179,22 +179,22 @@ After adding this new field to our constructor, and generating a getter and sett Before we can start up our application, we need to add a new column to the `events/index` template to make `contactEmail` visible. ```html {linenos=table, linenostart=8} - - - - - - - - - - - - - - +
IDNameDescriptionContact Email
+ + + + + + -
IDNameDescriptionContact Email
+ + + + + + + + ``` Now we can start up our application and test. Submitting an empty form at `/events/create` still results in an event being created, which may not be what you were expecting. diff --git a/content/spring-model-validation/reading/validation-controller/index.md b/content/spring-model-validation/reading/validation-controller/index.md index b21114e..db42ae7 100644 --- a/content/spring-model-validation/reading/validation-controller/index.md +++ b/content/spring-model-validation/reading/validation-controller/index.md @@ -19,11 +19,11 @@ Validation involves both model and controller components of an MVC application. Before diving into the details of the code, let's consider the logical flow of control for validating data in a request. Recall our `POST` handler from the previous chapter, which used model binding to create new `Event` objects from form submissions. ```java - @PostMapping("create") - public String processCreateEventForm(@ModelAttribute Event newEvent) { - EventData.add(newEvent); - return "redirect:"; - } +@PostMapping("create") +public String processCreateEventForm(@ModelAttribute Event newEvent) { + EventData.add(newEvent); + return "redirect:"; +} ``` The flow of this request can be described as follows: @@ -84,11 +84,11 @@ Recall that *both* the model and controller play a role in validation. The model The first step to enable validation in a controller is to add `@Valid` to a method parameter that is created using model binding. ```java - @PostMapping("create") - public String processCreateEventForm(@ModelAttribute @Valid Event newEvent) { - EventData.add(newEvent); - return "redirect:"; - } +@PostMapping("create") +public String processCreateEventForm(@ModelAttribute @Valid Event newEvent) { + EventData.add(newEvent); + return "redirect:"; +} ``` In lieu of explicit validation error handling (which we will cover below), Spring Boot throws an exception if validation fails for the new object. This means that an object that fails validation will NOT be saved. @@ -106,24 +106,24 @@ However, the user experience for this flow is not great. If a user submits bad d We can prevent a validation exception from being thrown by explicitly handling validation errors. Spring Boot makes an object of type `Errors` available when a method uses `@Valid`. As with `Model` objects, we can access this object by placing it in a method's parameter list. ```java {linenos=table, linenostart=33} - @PostMapping("create") - public String processCreateEventForm(@ModelAttribute @Valid Event newEvent, - Errors errors, Model model) { - if(errors.hasErrors()) { - model.addAttribute("title", "Create Event"); - model.addAttribute("errorMsg", "Bad data!"); - return "events/create"; - } - - EventData.add(newEvent); - return "redirect:"; +@PostMapping("create") +public String processCreateEventForm(@ModelAttribute @Valid Event newEvent, + Errors errors, Model model) { + if(errors.hasErrors()) { + model.addAttribute("title", "Create Event"); + model.addAttribute("errorMsg", "Bad data!"); + return "events/create"; } + + EventData.add(newEvent); + return "redirect:"; +} ``` Here, we have added `Errors errors` to our handler. This object has a boolean method, `.hasErrors()` that we can use to check for the existence of validation errors. If there are validation errors, we return the form again, along with a simple message for the user. This message can be displayed in the `events/create` template by adding some code above the form: ```html -

+

``` Now, when a user submits the form with bad data they will be notified and no exception will be thrown. However, the message "Bad data!" is far from ideal. The next section introduces a technique to display more useful error messages. diff --git a/content/spring-model-validation/studio/_index.md b/content/spring-model-validation/studio/_index.md index dab9689..f1e7d07 100644 --- a/content/spring-model-validation/studio/_index.md +++ b/content/spring-model-validation/studio/_index.md @@ -17,7 +17,7 @@ class, adding in model validation. ## Getting Started -Open up your `spaday` application and checkout the [user-signup-pt2](https://github.com/LaunchCodeEducation/Java17-Spa-Day/tree/user-signup-pt2) branch. +Open up your `spaday` application and checkout the [user-signup-pt2](https://github.com/LaunchCodeEducation/Java17-SpaDay/tree/user-signup-pt2) branch. ## Add Validation Annotations @@ -37,15 +37,15 @@ In the `UserController`, modify the `add` method that displays the form so that it passes in an empty `User` object with: ```java - model.addAttribute(new User()); +model.addAttribute(new User()); ``` This object will be accessible in the template, by name, as `user`. {{% notice green "Tip" "rocket" %}} - Now that you're passing in an empty `User` object, you may notice some redundant code - in your `processAddUserForm` controller. Remove the model attribute additions - and update the `user/add` template to make use of the model fields (eg. `user.username`). +Now that you're passing in an empty `User` object, you may notice some redundant code +in your `processAddUserForm` controller. Remove the model attribute additions +and update the `user/add` template to make use of the model fields (eg. `user.username`). {{% /notice %}} While you’re in the `add.html` template remove the `type="email"` designation from the email @@ -67,11 +67,9 @@ the `User` parameter that is bound, along with an additional parameter: `Errors errors`. {{% notice orange "Warning" "rocket" %}} - - Remember, you must put this parameter directly - after the `User` parameter in the method definition for it to work - properly. - +Remember, you must put this parameter directly +after the `User` parameter in the method definition for it to work +properly. {{% /notice %}} Within the `processAddUserForm` handler, check for errors configured by the @@ -93,11 +91,9 @@ other to properly return to the form if any of the validation conditions fail. {{% notice green "Tip" "rocket" %}} - - You can, in fact, validate that passwords match using annotations by - taking a slightly more difficult approach than we’ve done here. We - outline how to do so in the Bonus Mission section. - +You can, in fact, validate that passwords match using annotations by +taking a slightly more difficult approach than we’ve done here. We +outline how to do so in the Bonus Mission section. {{% /notice %}} ## Test, Test, Test! From 83ef9d36ddfa168254b7652a714c45e5ec6bc41b Mon Sep 17 00:00:00 2001 From: Sally Steuterman Date: Thu, 24 Aug 2023 12:04:44 -0500 Subject: [PATCH 4/4] Fixed chapter order --- content/enums/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/enums/_index.md b/content/enums/_index.md index 8741bae..2b80b70 100644 --- a/content/enums/_index.md +++ b/content/enums/_index.md @@ -2,7 +2,7 @@ title: "Chapter 16: Enums" date: 2021-10-01T09:28:27-05:00 draft: false -weight: 14 +weight: 16 originalAuthor: John Woolbright # to be set by page creator originalAuthorGitHub: jwoolbright23 # to be set by page creator reviewer: Sally Steuterman # to be set by the page reviewer