Skip to content

Commit e0ed385

Browse files
committed
validation added
1 parent 6d887e4 commit e0ed385

File tree

5 files changed

+143
-23
lines changed

5 files changed

+143
-23
lines changed

src/main/java/net/joedoe/recipe/commands/RecipeCommand.java

+15
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
import lombok.NoArgsConstructor;
55
import lombok.Setter;
66
import net.joedoe.recipe.domains.Difficulty;
7+
import org.hibernate.validator.constraints.URL;
78

9+
import javax.validation.constraints.Max;
10+
import javax.validation.constraints.Min;
11+
import javax.validation.constraints.NotBlank;
12+
import javax.validation.constraints.Size;
813
import java.util.HashSet;
914
import java.util.Set;
1015

@@ -13,12 +18,22 @@
1318
@NoArgsConstructor
1419
public class RecipeCommand {
1520
private Long id;
21+
@NotBlank
22+
@Size(min=3, max=255)
1623
private String description;
24+
@Min(1)
25+
@Max(999)
1726
private Integer prepTime;
27+
@Min(1)
28+
@Max(999)
1829
private Integer cookTime;
30+
@Min(1)
31+
@Max(100)
1932
private Integer servings;
2033
private String source;
34+
@URL
2135
private String url;
36+
@NotBlank
2237
private String directions;
2338
private Set<IngredientCommand> ingredients = new HashSet<>();
2439
private Byte[] image;

src/main/java/net/joedoe/recipe/controllers/RecipeController.java

+17-8
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,26 @@
33
import lombok.extern.slf4j.Slf4j;
44
import net.joedoe.recipe.commands.RecipeCommand;
55
import net.joedoe.recipe.domains.Recipe;
6-
import net.joedoe.recipe.exceptions.NotFoundException;
76
import net.joedoe.recipe.services.CategoryService;
87
import net.joedoe.recipe.services.IRecipeService;
9-
import org.springframework.http.HttpStatus;
108
import org.springframework.stereotype.Controller;
119
import org.springframework.ui.Model;
12-
import org.springframework.web.bind.annotation.*;
13-
import org.springframework.web.servlet.ModelAndView;
10+
import org.springframework.validation.BindingResult;
11+
import org.springframework.web.bind.annotation.GetMapping;
12+
import org.springframework.web.bind.annotation.ModelAttribute;
13+
import org.springframework.web.bind.annotation.PathVariable;
14+
import org.springframework.web.bind.annotation.PostMapping;
15+
16+
import javax.validation.Valid;
1417

1518
@Slf4j
1619
@Controller
1720
public class RecipeController {
1821
private IRecipeService<Recipe> recipeService;
1922
private CategoryService categoryService;
2023

24+
private static final String RECIPE_FORM_URL = "recipe/recipe-form";
25+
2126
public RecipeController(IRecipeService<Recipe> recipeService, CategoryService categoryService) {
2227
this.recipeService = recipeService;
2328
this.categoryService = categoryService;
@@ -28,7 +33,7 @@ public String newRecipe(Model model) {
2833
log.debug("RecipeController: newRecipe()");
2934
model.addAttribute("recipe", new Recipe());
3035
model.addAttribute("categories", categoryService.findAll());
31-
return "recipe/recipe-form";
36+
return RECIPE_FORM_URL;
3237
}
3338

3439
@GetMapping("/recipe/{id}/show")
@@ -41,7 +46,7 @@ public String showRecipe(@PathVariable String id, Model model) {
4146
public String getUpdateViewRecipe(@PathVariable String id, Model model) {
4247
model.addAttribute("recipe", recipeService.findCommandById(Long.valueOf(id)));
4348
model.addAttribute("categories", categoryService.findAll());
44-
return "recipe/recipe-form";
49+
return RECIPE_FORM_URL;
4550
}
4651

4752
@GetMapping("/recipe/{id}/delete")
@@ -51,8 +56,12 @@ public String deleteRecipe(@PathVariable String id) {
5156
return "redirect:/";
5257
}
5358

54-
@PostMapping("/recipe")
55-
public String saveOrUpdateRecipe(@ModelAttribute RecipeCommand command) {
59+
@PostMapping("/recipe") //@ModelAttribute needs to be named "recipe" according to obj in template "recipe-form"
60+
public String saveOrUpdateRecipe(@Valid @ModelAttribute("recipe") RecipeCommand command, BindingResult bindingResult) {
61+
if (bindingResult.hasErrors()){
62+
bindingResult.getAllErrors().forEach(error -> log.debug(error.toString()));
63+
return RECIPE_FORM_URL;
64+
}
5665
log.debug("RecipeController: saveOrUpdateIngredient()");
5766
RecipeCommand savedCommand = recipeService.saveRecipeCommand(command);
5867
return "redirect:/recipe/" + savedCommand.getId() + "/show";
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Set names of properties
2+
recipe.description=Description
3+
4+
#Validation Messages
5+
#Order of precedence
6+
# 1 code.objectName.fieldName
7+
# 2 code.fieldName
8+
# 3 code.fieldType (Java data type)
9+
# 4 code
10+
NotBlank.recipe.description=Description cannot be blank
11+
Size.recipe.description={0} must be between {2} and {1} characters long.
12+
URL.recipe.url=Please provide a valid URL

src/main/resources/templates/recipe/recipe-form.html

+58-13
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
<div class="row">
1010
<div class="col-md-6 col-md-offset-3">
1111
<form th:object="${recipe}" th:action="@{/recipe}" method="post">
12+
<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">
13+
<p>Please correct errors below</p>
14+
</div>
1215
<input type="hidden" th:field="*{id}"/>
1316
<div class="pannel-group">
1417
<div class="panel panel-primary">
@@ -17,17 +20,23 @@ <h1 class="panel-title">Edit Recipe Information</h1>
1720
</div>
1821
<div class="panel-body">
1922
<div class="row">
20-
<div class="col-md-6 form-group">
23+
<div class="col-md-6 form-group" th:class="${#fields.hasErrors('description')}
24+
? 'col-md-3 form-group has-error' : 'col-md-3 form-group'">
2125
<label>Recipe Description:</label>
22-
<input type="text" class="form-control" th:field="${recipe.description}"/>
26+
<input type="text" class="form-control" th:field="${recipe.description}"
27+
th:errorclass="has-error"/>
28+
<span class="help-block" th:if="${#fields.hasErrors('description')}">
29+
<ul>
30+
<li th:each="err : ${#fields.errors('description')}" th:text="${err}"/>
31+
</ul>
32+
</span>
2333
</div>
2434
</div>
2535
<div class="row">
2636
<div class="col-md-2 form-group">
2737
<label>Categories:</label>
2838
</div>
2939
<div class="col-md-9 form-group">
30-
<!--todo-->
3140
<div class="radio" th:each="category : ${categories}">
3241
<label>
3342
<input type="checkbox" value="" th:text="${' ' + category.description}"/>
@@ -36,13 +45,27 @@ <h1 class="panel-title">Edit Recipe Information</h1>
3645
</div>
3746
</div>
3847
<div class="row">
39-
<div class="col-md-3 form-group">
48+
<div class="col-md-3 form-group" th:class="${#fields.hasErrors('prepTime')}
49+
? 'col-md-3 form-group has-error' : 'col-md-3 form-group'">
4050
<label>Prep Time:</label>
41-
<input type="text" class="form-control" th:field="${recipe.prepTime}"/>
51+
<input type="text" class="form-control" th:field="${recipe.prepTime}"
52+
th:errorclass="has-error"/>
53+
<span class="help-block" th:if="${#fields.hasErrors('prepTime')}">
54+
<ul>
55+
<li th:each="err : ${#fields.errors('prepTime')}" th:text="${err}"/>
56+
</ul>
57+
</span>
4258
</div>
43-
<div class="col-md-3 form-group">
59+
<div class="col-md-3 form-group" th:class="${#fields.hasErrors('cookTime')}
60+
? 'col-md-3 form-group has-error' : 'col-md-3 form-group'">
4461
<label>Cooktime:</label>
45-
<input type="text" class="form-control" th:field="*{cookTime}"/>
62+
<input type="text" class="form-control" th:field="*{cookTime}"
63+
th:errorclass="has-error"/>
64+
<span class="help-block" th:if="${#fields.hasErrors('cookTime')}">
65+
<ul>
66+
<li th:each="err : ${#fields.errors('cookTime')}" th:text="${err}"/>
67+
</ul>
68+
</span>
4669
</div>
4770
<div class="col-md-3 form-group">
4871
<label>Difficulty:</label>
@@ -57,17 +80,31 @@ <h1 class="panel-title">Edit Recipe Information</h1>
5780
</div>
5881
</div>
5982
<div class="row">
60-
<div class="col-md-3 form-group">
83+
<div class="col-md-3 form-group" th:class="${#fields.hasErrors('servings')}
84+
? 'col-md-3 form-group has-error' : 'col-md-3 form-group'">
6185
<label>Servings:</label>
62-
<input type="text" class="form-control" th:field="*{servings}"/>
86+
<input type="text" class="form-control" th:field="*{servings}"
87+
th:errorclass="has-error"/>
88+
<span class="help-block" th:if="${#fields.hasErrors('servings')}">
89+
<ul>
90+
<li th:each="err : ${#fields.errors('servings')}" th:text="${err}"/>
91+
</ul>
92+
</span>
6393
</div>
6494
<div class="col-md-3 form-group">
6595
<label>Source:</label>
6696
<input type="text" class="form-control" th:field="*{source}"/>
6797
</div>
68-
<div class="col-md-3 form-group">
98+
<div class="col-md-3 form-group" th:class="${#fields.hasErrors('url')}
99+
? 'col-md-3 form-group has-error' : 'col-md-3 form-group'">
69100
<label>URL:</label>
70-
<input type="text" class="form-control" th:field="*{url}"/>
101+
<input type="text" class="form-control" th:field="*{url}"
102+
th:errorclass="has-error"/>
103+
<span class="help-block" th:if="${#fields.hasErrors('url')}">
104+
<ul>
105+
<li th:each="err : ${#fields.errors('url')}" th:text="${err}"/>
106+
</ul>
107+
</span>
71108
</div>
72109
</div>
73110
</div>
@@ -103,8 +140,16 @@ <h1 class="panel-title">Directions</h1>
103140
</div>
104141
<div class="panel-body">
105142
<div class="row">
106-
<div class="col-md-12 form-group">
107-
<textarea class="form-control" rows="3" th:field="*{directions}"></textarea></div>
143+
<div class="col-md-12 form-group" th:class="${#fields.hasErrors('directions')}
144+
? 'col-md-12 form-group has-error' : 'col-md-12 form-group'">
145+
<textarea class="form-control" rows="3" th:field="*{directions}"
146+
th:errorclass="has-error"></textarea>
147+
<span class="help-block" th:if="${#fields.hasErrors('directions')}">
148+
<ul>
149+
<li th:each="err : ${#fields.errors('directions')}" th:text="${err}"/>
150+
</ul>
151+
</span>
152+
</div>
108153
</div>
109154
</div>
110155
</div>

src/test/java/net/joedoe/recipe/controllers/RecipeControllerTest.java

+41-2
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ public void testSaveOrUpdateRecipe() throws Exception {
9898

9999
//then
100100
mockMvc.perform(post("/recipe").contentType(MediaType.APPLICATION_FORM_URLENCODED)
101-
.param("id", "").param("description", "some string"))
101+
.param("id", "")
102+
.param("description", "some string")
103+
.param("directions", "some directions"))
102104
.andExpect(status().is3xxRedirection())
103105
.andExpect(view().name("redirect:/recipe/2/show"));
104106
}
@@ -122,4 +124,41 @@ public void testHandleNumberFormat() throws Exception {
122124
.andExpect(status().isBadRequest())
123125
.andExpect(view().name("400error"));
124126
}
125-
}
127+
128+
@Test
129+
public void testValidation() throws Exception {
130+
//given
131+
RecipeCommand command = new RecipeCommand();
132+
command.setId(2L);
133+
134+
//when
135+
when(recipeService.saveRecipeCommand(any())).thenReturn(command);
136+
137+
//then
138+
mockMvc.perform(post("/recipe")
139+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
140+
.param("id", ""))
141+
.andExpect(status().isOk())
142+
.andExpect(model().attributeExists("recipe"))
143+
.andExpect(view().name("recipe/recipe-form"));
144+
}
145+
146+
@Test
147+
public void testValidationFails() throws Exception {
148+
//given
149+
RecipeCommand command = new RecipeCommand();
150+
command.setId(2L);
151+
152+
//when
153+
when(recipeService.saveRecipeCommand(any())).thenReturn(command);
154+
155+
//then
156+
mockMvc.perform(post("/recipe")
157+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
158+
.param("id", "")
159+
.param("cookTime", "3000"))
160+
.andExpect(status().isOk())
161+
.andExpect(model().attributeExists("recipe"))
162+
.andExpect(view().name("recipe/recipe-form"));
163+
}
164+
}

0 commit comments

Comments
 (0)