Using Spring Framework 5.3.20, Hibernate 6.1.0, Thymeleaf 3.1.0M2 I want to make a controller method for class Dish, that would display map of checkboxes containing names of ingredients that are checked, if ingredients are present and edit those ingredients using it.
So far I managed only to display ingredients that were previously saved in the database, but I cannot change/update ingredients for those dishes using my Update Dish Template.
Currently I'm getting
java.lang.NullPointerException: Cannot invoke "java.util.Map.keySet()" because "dishIngredients" is null at project.service.DishController.getIngredientSetFromMap(DishController.java:104) ~[classes/:na]
So it seems like I can create the map in my view, but cannot get it back.
Class Dish:
@Entity
@Table(name="Dish")
public class Dish implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@ManyToMany(cascade = CascadeType.MERGE)
@JoinTable(
name = "dish_ingredient",
joinColumns = { @JoinColumn(name = "fk_dish_id")},
inverseJoinColumns = { @JoinColumn(name = "fk_ingredient_id")} )
private Set<Ingredient> ingredients = new HashSet<>();
public Set<Ingredient> getIngredients() { return ingredients; }
public String getPictureLink() {
return pictureLink;
}
public void setIngredients(Set<Ingredient> ingredients) {
this.ingredients = ingredients; } }
Class Ingredient:
@Entity
@Table(name="ingredient")
public class Ingredient implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long ingredientId;
@Column(name = "name")
private String name;
@ManyToMany(cascade = CascadeType.MERGE)
@JoinTable(
name = "dish_ingredient",
joinColumns = { @JoinColumn(name = "fk_ingredient_id")},
inverseJoinColumns = { @JoinColumn(name = "fk_dish_id")} )
private Set<Dish> dishes;
public Long getIngredientId() {
return ingredientId;
}
public void setIngredientId(Long ingredientId) {
this.ingredientId = ingredientId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Dish> getDishes() {
return dishes;
}
public void setDishes(Set<Dish> dishes) {
this.dishes = dishes;
}
}
class DishIngredient:
@Entity
@Table(name = "dish_ingredient")
public class DishIngredient {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@ManyToOne
private Dish dish;
@ManyToOne
private Ingredient ingredient;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Dish getDish() {
return dish;
}
public void setDish(Dish dish) {
this.dish = dish;
}
public Ingredient getIngredient() {
return ingredient;
}
public void setIngredient(Ingredient ingredient) {
this.ingredient = ingredient;
}
}
Class DishController:
@GetMapping("/dish/update/{id}")
public String updateDishForm(@PathVariable("id") long id, Model model) {
Dish dish = dishService.getDish(id);
model.addAttribute("dish", dish);
List<Ingredient> dishIngredients = new ArrayList<>(dishService.getIngredients(id));
List<Ingredient> allIngredients = new ArrayList<>(ingredientService.getAllIngredients());
getIngredientSetFromMap(getCheckedIngredientsMap(dishIngredients, allIngredients));
model.addAttribute("map", getCheckedIngredientsMap(dishIngredients, allIngredients));
return "updatedish";
}
@PostMapping("dish/save")
public String saveDish(@ModelAttribute("dish") Dish dish, Model model){
Map<String, Boolean> dishIngredientsMap = (Map<String, Boolean>) model.getAttribute("map");
Set<Ingredient> ingredients = getIngredientSetFromMap(dishIngredientsMap);
model.addAttribute("map", dishIngredientsMap);
dish.setIngredients(ingredients);
dishService.saveDish(dish);
return "redirect:/";
}
public Map<String, Boolean> getCheckedIngredientsMap(List<Ingredient> dishIngredients,
List<Ingredient> allIngredients) {
Map<String,Boolean> ingredientsPresentInDish = new TreeMap<>();
Set<String> dishIngredientsNames = dishIngredients.stream().map(Ingredient::getName).collect(Collectors.toSet());
Set<String> allIngredientsNames = allIngredients.stream().map(Ingredient::getName).collect(Collectors.toSet());
for (String ingredientName : allIngredientsNames) ingredientsPresentInDish.put(ingredientName, false);
for (String dishIngredientsName : dishIngredientsNames) {
for (String allIngredientsName : allIngredientsNames) {
if (allIngredientsName.equals(dishIngredientsName)) {
ingredientsPresentInDish.put(allIngredientsName, true);
}
}
}
return ingredientsPresentInDish;
}
public Set<Ingredient> getIngredientSetFromMap(Map<String, Boolean> dishIngredients) {
Set<Ingredient> ingredientsSet = new HashSet<>();
for (String key : dishIngredients.keySet()) {
if (dishIngredients.get(key)) ingredientsSet.add(ingredientService.getIngredientByName(key));
}
return ingredientsSet;
}
Template updatedish:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<div th:if="${dish} != null">
<div class="container">
<h1>Edit dish</h1>
<hr>
<form action="#" th:action="@{/dish/save}" th:object="${dish}" method="POST">
Dish name: <input type="text" th:field="*{name}" placeholder="Dish name"><br>
<div id ="map">
<span>MAP:</span>
<input type="checkbox" class="ingredientsMap" name="ingredientsMap"
th:each="key: ${map.keySet()}"
th:text="${key}"
th:value="${key}"
th:checked="${map.get(key)}">
</div>
<button type="submit">Save Dish</button>
</form>
</div>
</div>
<div th:unless="${dish} != null">
Dish not found!
</div>
<a th:href="@{/}">Back to Ingredients List</a>
</body>
</html>