I am following the Spring in Action (5th Edition) by Craig Walls, and I am stuck trying to figure out why the form
element my Thymeleaf view isn't passing on the correct data type to my pojo object.
Short of it is: When visiting localhost:8080/design
, I should be able to select whatever checkboxes from a Thymeleaf html view which should then add those ingredients to a Taco
object's List<Ingredient> ingredients
parameter.
However, when I select the ingredients in the form and hit the submit button, I get the following error:
Fri Oct 28 14:07:37 EDT 2022
There was an unexpected error (type=Bad Request, status=400).
Validation failed for object='taco'. Error count: 1
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'taco' on field 'ingredients': rejected value [FLTO,GRBF,CHED,TMTO,SLSA]; codes [typeMismatch.taco.ingredients,typeMismatch.ingredients,typeMismatch.java.util.List,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [taco.ingredients,ingredients]; arguments []; default message [ingredients]]; default message [Failed to convert property value of type 'java.lang.String[]' to required type 'java.util.List' for property 'ingredients'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Long] for value 'FLTO'; nested exception is java.lang.NumberFormatException: For input string: "FLTO"]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:175)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1071)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:681)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1789)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:833)
Is someone able to offer some guidance on why this issue is happening?
Below is the my code that makes up the above workflow:
Dependencies
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.4</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>sia</groupId>
<artifactId>taco-cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>taco-cloud</name>
<description>Taco Cloud Example Project from Spring In Action</description>
<properties>
<java.version>15</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>htmlunit-driver</artifactId>
<scope>test</scope>
</dependency>
<!-- Installed as a dependency as well as installed
on Eclipse IDE: https://projectlombok.org/setup/eclipse -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- For JavaBean Validation Annotations -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.16.Final</version>
</dependency>
<!-- For JDBCTemplate and embedded H2 database -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- For Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Database
I am using the H2 embedded database so the schema and accomponying data is loaded at runtime
src/main/resources/schema.sql
create table if not exists Ingredient (
id varchar(10) not null,
name varchar(25) not null,
INGREDIENT_TYPE ENUM('WRAP', 'PROTEIN', 'VEGGIES', 'CHEESE', 'SAUCE') NOT NULL
);
create table if not exists Taco (
id identity,
name varchar(50) not null,
createdAt timestamp not null
);
create table if not exists Taco_Ingredients (
taco bigint not null,
ingredient varchar(4) not null
);
alter table Ingredient
add constraint Ingredient_PK primary key (id);
alter table Taco_Ingredients
add foreign key (taco) references Taco(id);
alter table Taco_Ingredients
add foreign key (ingredient) references Ingredient(id);
create table if not exists Taco_Order (
id identity,
deliveryName varchar(50) not null,
deliveryStreet varchar(50) not null,
deliveryCity varchar(50) not null,
deliveryState varchar(2) not null,
deliveryZip varchar(10) not null,
ccNumber varchar(16) not null,
ccExpiration varchar(5) not null,
ccCVV varchar(3) not null,
placedAt timestamp not null
);
create table if not exists Taco_Order_Tacos (
tacoOrder bigint not null,
taco bigint not null
);
alter table Taco_Order_Tacos
add foreign key (tacoOrder) references Taco_Order(id);
alter table Taco_Order_Tacos
add foreign key (taco) references Taco(id);
src/main/resources/data.sql
delete from Taco_Order_Tacos;
delete from Taco_Ingredients;
delete from Taco;
delete from Taco_Order;
delete from Ingredient;
insert into Ingredient(id, name, INGREDIENT_TYPE )
values ('FLTO', 'Flour Tortilla', 'WRAP' );
insert into Ingredient(id, name, INGREDIENT_TYPE )
values('COTO','Corn Tortilla', 'WRAP');
insert into Ingredient(id, name, INGREDIENT_TYPE )
values('GRBF', 'Ground Beef', 'PROTEIN');
insert into Ingredient(id, name, INGREDIENT_TYPE )
values('CARN', 'Carnitas', 'PROTEIN');
insert into Ingredient(id, name, INGREDIENT_TYPE )
values('TMTO', 'Diced Tomatoes', 'VEGGIES');
insert into Ingredient(id, name, INGREDIENT_TYPE )
values('LETC', 'Lettuce', 'VEGGIES');
insert into Ingredient(id, name, INGREDIENT_TYPE )
values('CHED', 'Cheddar', 'CHEESE');
insert into Ingredient(id, name, INGREDIENT_TYPE )
values('JACK', 'Monterrey Jack', 'CHEESE');
insert into Ingredient(id, name, INGREDIENT_TYPE )
values('SLSA', 'Salsa', 'SAUCE');
insert into Ingredient(id, name, INGREDIENT_TYPE )
values('SRCR', 'Sour Cream', 'SAUCE');
POJOs
src/main/java/tacos/domain/Taco
@Data
@Entity
public class Taco {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private String id;
private Date createdAt;
@NotNull
@Size(min=4, message="Name must be at least 4 characters long")
private String name;
@ManyToMany(targetEntity=Ingredient.class)
@Size(min=1, message="You must choose at least 1 ingredient")
private List<Ingredient> ingredients;
@PrePersist
void createdAt() {
this.createdAt = new Date();
}
}
src/main/java/tacos/domain/Ingredient
@Data
@RequiredArgsConstructor
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@Entity
public class Ingredient {
@Id
private final String id;
private final String name;
@Enumerated(EnumType.STRING)
private final Type ingredientType;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
src/main/java/tacos/domain/Order
@Data
@Entity
@Table(name="Taco_Order")
public class Order implements Serializable{
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private Date placedAt;
@NotBlank(message="Name is required")
private String name;
@NotBlank(message="Street is required")
private String street;
@NotBlank(message="City is required")
private String city;
@NotBlank(message="State is required")
private String state;
@NotBlank(message="Zip Code is required")
private String zip;
@CreditCardNumber(message="Not a valid credit card number")
private String ccNumber;
@Pattern(regexp="^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$", message="Must be formatted MM/YY")
private String ccExpiration;
@Digits(integer=3, fraction=0, message="Invalid CVV")
private String ccCVV;
@ManyToMany(targetEntity=Taco.class)
private List<Taco> tacos = new ArrayList<>();
public void addTaco(Taco taco) {this.tacos.add(taco);}
@PrePersist
void placedAt() {this.placedAt = new Date();}
}
Controllers
src/main/java/tacos/controllers/DesignTacoController
@Slf4j
@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {
private final IngredientRepository ingredientRepo;
private TacoRepository tacoRepo;
@Autowired
public DesignTacoController(IngredientRepository ingredientRepo, TacoRepository tacoRepo) {
this.ingredientRepo = ingredientRepo;
this.tacoRepo = tacoRepo;
};
@ModelAttribute(name = "order")
public Order order() {
return new Order();
}
@ModelAttribute(name = "taco")
public Taco taco() {
return new Taco();
}
@GetMapping
public String showDesignForm(Model model) {
List<Ingredient> ingredients = new ArrayList<>();
ingredientRepo.findAll().forEach(i -> ingredients.add(i));
log.info(ingredients.toString());
Type[] types = Ingredient.Type.values();
for (Type type : types) {
model.addAttribute(type.toString().toLowerCase(),
filterByType(ingredients, type));
}
model.addAttribute("taco", new Taco());
return "design";
}
@PostMapping
public String processDesign(
@Valid Taco design,
@ModelAttribute Order order,
Errors errors) {
log.info("design: " + design);
log.info("order: " + order);
log.warn("errors: " + errors);
if(errors.hasErrors()) return "design";
Taco savedTaco = tacoRepo.save(design);
order.addTaco(savedTaco);
return "redirect:orders/current";
}
private List<Ingredient> filterByType(
List<Ingredient> ingredients,
Type type) {
return ingredients
.stream()
.filter(x -> x.getIngredientType().equals(type))
.collect(Collectors.toList());
}
}
src/main/java/tacos/controllers/OrderController
@Slf4j
@Controller
@RequestMapping("/orders")
@SessionAttributes("order")
public class OrderController {
private OrderRepository orderRepo;
public OrderController(OrderRepository orderRepo) {this.orderRepo = orderRepo;}
@GetMapping("/current")
public String orderForm(Model model) {
log.info(model.toString());
return "orderForm";
}
@PostMapping
public String processOrder(@Valid Order order, SessionStatus sessionStatus, Errors error) {
if(error.hasErrors()) return "orderForm";
orderRepo.save(order);
sessionStatus.setComplete();
return "redirect:/";
}
}
Views
src/main/resources/templates/design.html
<body>
<h1>Design Your Taco</h1>
<img th:src="@{/images/TacoCloud.jpeg}" />
<form method="POST" th:object="${taco}">
<div class="grid">
<div class="ingredient-group" id="wraps">
<h3>Design Your Wrap:</h3>
<div th:each="ingredient : ${wrap}">
<!-- <input name="ingredients" type="checkbox" th:value="${ingredient.id}" /> -->
<!-- <input name="ingredients" type="checkbox" th:value="${ingredient.getId()}" /> -->
<input name="ingredients" type="checkbox" th:value="${ingredient.getId()}" th:field="*{ingredients}" />
<span th:text="${ingredient.getName()}">INGREDIENT</span>
<br>
</div>
</div>
<div class="ingredient-group" id="proteins">
<h3>Pick Your Protein:</h3>
<div th:each="ingredient : ${protein}">
<!-- <input name="ingredients" type="checkbox" th:value="${ingredient.id}" /> -->
<!-- <input name="ingredients" type="checkbox" th:value="${ingredient.getId()}" /> -->
<input name="ingredients" type="checkbox" th:value="${ingredient.getId()}" th:field="*{ingredients}" />
<span th:text="${ingredient.getName()}">INGREDIENT</span>
<br>
</div>
</div>
<div class="ingredient-group" id="cheeses">
<h3>Pick Your Cheese:</h3>
<div th:each="ingredient : ${cheese}">
<!-- <input name="ingredients" type="checkbox" th:value="${ingredient.id}" /> -->
<!-- <input name="ingredients" type="checkbox" th:value="${ingredient.getId()}" /> -->
<input name="ingredients" type="checkbox" th:value="${ingredient.getId()}" th:field="*{ingredients}" />
<span th:text="${ingredient.getName()}">INGREDIENT</span>
<br>
</div>
</div>
<div class="ingredient-group" id="veggies">
<h3>Pick Your Veggies:</h3>
<div th:each="ingredient : ${veggies}">
<!-- <input name="ingredients" type="checkbox" th:value="${ingredient.id}" /> -->
<!-- <input name="ingredients" type="checkbox" th:value="${ingredient.getId()}" /> -->
<input name="ingredients" type="checkbox" th:value="${ingredient.getId()}" th:field="*{ingredients}" />
<span th:text="${ingredient.getName()}">INGREDIENT</span>
<br>
</div>
</div>
<div class="ingredient-group" id="sauces">
<h3>Pick Your Sauces:</h3>
<div th:each="ingredient : ${sauce}">
<!-- <input name="ingredients" type="checkbox" th:value="${ingredient.id}" /> -->
<!-- <input name="ingredients" type="checkbox" th:value="${ingredient.getId()}" /> -->
<input name="ingredients" type="checkbox" th:value="${ingredient.getId()}" th:field="*{ingredients}" />
<span th:text="${ingredient.getName()}">INGREDIENT</span>
<br>
</div>
</div>
</div>
<div>
<h3>Name Your Taco Creation:</h3>
<input type="text" th:field="*{name}" /> <br>
<button>Submit Your Taco</button>
</div>
</form>
</body>
src/main/resources/templates/orderForm.html
<body>
<form method="POST" th:action="@{/orders}" th:object="${order}">
<h1>Order Your Taco Creations!</h1>
<img th:src="@{/images/TacoCloud.jpeg}" /> <a th:href="@{/design}"
id="another"> Design Another Taco</a> <br>
<div th:if="${#fields.hasErrors()}">
<span class="validationError">Please correct the problems below and resubmit</span>
</div>
<h3>Deliver my taco masterpieces to ...</h3>
<label for="name">Name: </label> <input type="text" th:field="*{name}" />
<span class="validationError" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"> Name Can't be Empty </span>
<br>
<label for="street"> Street Address: </label>
<input type="text" th:field="*{street}" />
<span class="validationError" th:if="${#fields.hasErrors('street')}" th:errors="*{street}"> Street Can't be Empty </span>
<br>
<label for="city"> City: </label>
<input type="text" th:field="*{city}" />
<span class="validationError" th:if="${#fields.hasErrors('city')}" th:errors="*{city}"> City Can't be Empty </span>
<br>
<label for="state"> State:</label>
<input type="text" th:field="*{state}" />
<span class="validationError" th:if="${#fields.hasErrors('state')}" th:errors="*{state}"> State Can't be Empty </span>
<br>
<label for="zip"> Zip Code: </label>
<input type="text" th:field="*{zip}" />
<span class="validationError" th:if="${#fields.hasErrors('zip')}" th:errors="*{zip}"> Zip Code Can't be Empty </span>
<br>
<h3>Here's How I'll Pay ...</h3>
<label for="ccNumber"> Credit Card #: </label>
<input type="text" th:field="*{ccNumber}" />
<span class="validationError" th:if="${#fields.hasErrors('ccNumber')}" th:errors="*{ccNumber}"> CC Number Error </span>
<br>
<label for="ccExpiration"> Expiration: </label>
<input type="text" th:field="*{ccExpiration}" />
<span class="validationError" th:if="${#fields.hasErrors('ccExpiration')}" th:errors="*{ccExpiration}"> CC Expiration Date Invalid </span>
<br>
<label for="ccCVV">CVV: </label>
<input type="text" th:field="*{ccCVV}" />
<span class="validationError" th:if="${#fields.hasErrors('ccCVV')}" th:errors="*{ccCVV}"> CVV Invalid </span>
<br>
<input type="submit" value="Submit Order" />
</form>
</body>
I have tried changing the schema for my sql file but this didn't work.