/* * Copyright (C) 2023 Simon Bruder * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package catering.inventory; import org.salespointframework.catalog.Product; import org.salespointframework.inventory.UniqueInventory; import org.salespointframework.inventory.UniqueInventoryItem; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import catering.catalog.CateringCatalog; import catering.catalog.Consumable; import catering.catalog.Rentable; import jakarta.validation.Valid; /* * TODO TL;DR: Use HandlerMethodArgumentResolver * * This class currently has many confusing methods that do a variety of things, * but mostly work around polymorphic restrictions of Spring. * Ideally, spring would allow a form to be polymorphic * based on its attributes, if they are unique, * or based on another parameter. * From what I could find, * this is not possible without implementing a custom HandlerMethodArgumentResolver. * If there is time, the controller should be changed to use this, * which should vastly simplify it. * * Adding will always require a type parameter, * as the type is not known at first. * However, currently there are two (or even three) POST handlers, * as there are two different form types it must support * (and an infinite amount of invalid types that it should ignore). * * Editing should not require passing any additional type parameter around * as the type can be inferred from the product. * Changing the type of a product should not be possible. */ @Controller @PreAuthorize("hasRole('ADMIN')") class InventoryController { private final UniqueInventory inventory; private final CateringCatalog cateringCatalog; InventoryController(UniqueInventory inventory, CateringCatalog cateringCatalog) { Assert.notNull(inventory, "Inventory must not be null!"); Assert.notNull(inventory, "CateringCatalog must not be null!"); this.inventory = inventory; this.cateringCatalog = cateringCatalog; } /** * Lists all inventory products. * * @param model an instance of {@link Model} * @return the inventory template */ @GetMapping("/inventory") String list(Model model) { model.addAttribute("inventory", inventory.findAll()); return "inventory"; } /** * Returns the edit page for an inventory product. * * @param model an instance of {@link Model} * @param pid the {@link Product} to edit * @return the inventory-mutate template with the {@link Product} data prefilled */ @GetMapping("/inventory/edit/{pid}") String edit(Model model, @PathVariable Product pid) { UniqueInventoryItem item = inventory.findByProduct(pid).get(); final InventoryMutateForm form = InventoryMutateForm.of(pid, item); return edit(model, form); } /** * Returns the edit page for an inventory product. * * This method is not mapped, * but is used by other methods to generalise the display of the edit page. * * @param model an instance of {@link Model} * @param form an already filled or empty {@link InventoryMutateForm} * @return the inventory-mutate template with the {@link InventoryMutateForm} * data prefilled */ String edit(Model model, InventoryMutateForm form) { model.addAttribute("actionIsAdd", false); model.addAttribute("form", form); return "inventory-mutate"; } /** * Edits a consumable. * * @param form a user-filled {@link ConsumableMutateForm} * @param errors an {@link Errors} object including validation errors * @param pid the {@link Product} to edit * @param model an instance of {@link Model} * @return a redirect on success, otherwise the filled form with all errors * highlighted */ @PostMapping(path = "/inventory/edit/{pid}", params = "type=Consumable") String editConsumable(@Valid @ModelAttribute("form") ConsumableMutateForm form, Errors result, @PathVariable Consumable pid, Model model) { return edit(form, result, pid, model); } /** * Edits a rentable. * * @param form a user-filled {@link RentableMutateForm} * @param errors an {@link Errors} object including validation errors * @param pid the {@link Product} to edit * @param model an instance of {@link Model} * @return a redirect on success, otherwise the filled form with all errors * highlighted */ @PostMapping(path = "/inventory/edit/{pid}", params = "type=Rentable") String editRentable(@Valid @ModelAttribute("form") RentableMutateForm form, Errors result, @PathVariable Rentable pid, Model model) { return edit(form, result, pid, model); } /** * Edits an inventory product. * * This method is not mapped, * but is used by * {@link #editConsumable(ConsumableMutateForm, Errors, Consumable, Model)} * and {@link #editRentable(RentableMutateForm, Errors, Rentable, Model)} * to generalise the editing of products. * * @param form a user-filled {@link RentableMutateForm} * @param errors an {@link Errors} object including validation errors * @param product the {@link Product} to edit * @param model an instance of {@link Model} * @return a redirect on success, otherwise the filled form with all errors * highlighted */ String edit(InventoryMutateForm form, Errors result, Product product, Model model) { if (result.hasErrors()) { return edit(model, form); } form.modifyProduct(product); product = cateringCatalog.save(product); UniqueInventoryItem item = inventory.findByProduct(product).get(); // no setQuantity in enterprise java // (though returing a modified object is actually nice) inventory.save(item.increaseQuantity(form.getQuantity().subtract(item.getQuantity()))); return "redirect:/inventory"; } /** * Returns the add page for a new inventory product. * * @param model an instance of {@link org.springframework.ui.Model} * @param type string representation of a {@link Product} subclass supported by * {@link InventoryMutateForm} * @return the inventory-mutate template with no data prefilled */ @GetMapping(path = "/inventory/add") String add(Model model, @RequestParam String type) { switch (type) { case "Consumable": return add(model, new ConsumableMutateForm()); case "Rentable": return add(model, new RentableMutateForm()); default: // TODO better error handling return "redirect:/inventory"; } } /** * Returns the add page for a new inventory product. * * This method is not mapped, * but is used by other methods to generalise the display of the add page. * * @param model an instance of {@link Model} * @param form an already filled or empty {@link InventoryMutateForm} * @return the inventory-mutate template with the {@link InventoryMutateForm} * data prefilled */ String add(Model model, InventoryMutateForm form) { model.addAttribute("actionIsAdd", true); model.addAttribute("form", form); return "inventory-mutate"; } /** * Adds a consumable. * * @param form a user-filled {@link ConsumableMutateForm} * @param errors an {@link Errors} object including validation errors * @param model an instance of {@link Model} * @return a redirect on success, otherwise the filled form with all errors * highlighted */ @PostMapping(path = "/inventory/add", params = "type=Consumable") String addConsumable(@Valid @ModelAttribute("form") ConsumableMutateForm form, Errors result, Model model) { return add(form, result, model); } /** * Adds a rentable. * * @param form a user-filled {@link RentableMutateForm} * @param errors an {@link Errors} object including validation errors * @param model an instance of {@link Model} * @return a redirect on success, otherwise the filled form with all errors * highlighted */ @PostMapping(path = "/inventory/add", params = "type=Rentable") String addRentable(@Valid @ModelAttribute("form") RentableMutateForm form, Errors result, Model model) { return add(form, result, model); } /** * Adds an inventory product. * * @param form a user-filled {@link InventoryMutateForm} * @param errors an {@link Errors} object including validation errors * @param model an instance of {@link Model} * @return a redirect on success, otherwise the filled form with all errors * highlighted */ String add(@Valid InventoryMutateForm form, Errors result, Model model) { if (result.hasErrors()) { return add(model, form); } inventory.save(new UniqueInventoryItem(cateringCatalog.save(form.toProduct()), form.getQuantity())); return "redirect:/inventory"; } /** * Deletes an inventory product. * * @param pid the {@link Product} to delete * @return a redirect to the inventory overview */ @GetMapping("/inventory/delete/{pid}") String delete(@PathVariable Product pid) { UniqueInventoryItem item = inventory.findByProduct(pid).get(); inventory.delete(item); cateringCatalog.delete(pid); return "redirect:/inventory"; } }