0

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>
Seweryn
  • 1
  • 1
  • 2

1 Answers1

0

The way you are handling name is the problem. with a static ingredientsMap name you wil end up with a List<Boolean> ingredientsMap once you post to your controller. to correctly parse your values back into a map you will have to include the key in your fieldname like name="ingredientsMap[flour] in thymeleaf this would result into ${mapName[__${keyName}__]}

Also i don't believe input can hold text so you would have to include a span or label for your checkboxes

<th:block th:each="ingredientEntry : ${map}">
    <label th:text="${ingredientEntry.key}"
        th:for="${#ids.next('ingredientsMap[__${ingredientEntry.key}__]')}"></label>
    <input type="checkbox" class="ingredientsMap"
        th:field="${ingredientsMap[__${ingredientEntry.key}__]}" />
</th:block>

See Thymeleaf Map Form Binding for a related question

Ralan
  • 651
  • 6
  • 17