Tuesday, April 11, 2023

How To Use Thymeleaf To Create Recipe Organizer In Spring Boot

java,springframework,springboot,controller,thymeleaf,programming,software development,technology
We will create a Recipe Organizer with Thymeleaf and Spring Boot in this tutorial. This is a straightforward Spring MVC Web application. It will assist us in organizing our favorite recipes.  

java,springboot,thymeleaf,programming,software development


POM.XML

First and foremost, we require a Spring Boot application. Here is the Spring Boot application's pom.xml file:
<?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.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.raven</groupId>
<artifactId>springboot-thymeleaf-recipe-organizer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-thymeleaf-recipe-organizer</name>
<description>Recipe Organizer with Thymeleaf and Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>springboot-thymeleaf-recipe-organizer</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
view raw pm.xml hosted with ❤ by GitHub
Spring Boot version 2.7.9 was used in this example. The Spring Framework version for this version of Spring Boot is 5.3.25. This application contains the spring-boot-starter-data-jpa, spring-boot-starter-web, spring-boot-starter-thymeleaf, and mysql-connector-j dependencies.

application.properties

We have configured the data source and file upload settings here.
server.port=9090
## DATA SOURCE
spring.datasource.url=jdbc:mysql://172.17.0.2:3306/recipe_organizer?useSSL=false
spring.datasource.username=root
spring.datasource.password=admin@123
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

Entities

For this application, two entities are required: RecipeImage for recipe images and Recipe for recipe details.

The RecipeImage entity's code snippets are as follows:
package com.raven.springbootthymeleafrecipeorganizer.entity;
// imports are omitted
@Entity
@Table(name = "RECIPE_IMAGE")
public class RecipeImage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int Id;
@Column(name = "original_filename", columnDefinition = "varchar(40)")
private String originalFileName = "";
@Column(name = "path", columnDefinition = "varchar(120)")
private String path = "";
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created_on", nullable = false, updatable = false)
@CreatedDate
private Date createdOn = new Date();
public RecipeImage() {
}
public RecipeImage(String originalFileName, String path) {
this.originalFileName = originalFileName;
this.path = path;
}
public int getId() {
return Id;
}
public String getOriginalFileName() {
return originalFileName;
}
public String getPath() {
return path;
}
public Date getCreatedOn() {
return createdOn;
}
public void setOriginalFileName(String originalFileName) {
this.originalFileName = originalFileName;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return "RecipeImage [Id=" + Id + ", originalFileName=" + originalFileName
+ ", path=" + path + ", createdOn=" + createdOn + "]";
}
}
Using this entity, we will create a table in the database called RECIPE_IMAGE to store the image name and path.

The Recipe entity's code snippets are now as follows:
package com.raven.springbootthymeleafrecipeorganizer.entity;
// imports are omitted
@Entity
@Table(name = "RECIPE_DETAILS")
public class Recipe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int Id;
@Column(name = "recipe_name", columnDefinition = "varchar(100)", nullable = false)
private String recipeName = "";
@Column(name = "description", columnDefinition = "text")
private String description = "";
@Column(name = "dish_type", columnDefinition = "varchar(30)")
private String dishType = "";
@Column(name = "preparation_time", columnDefinition = "varchar(20)")
private String preparationTime = "";
@Column(name = "cooking_time", columnDefinition = "varchar(20)")
private String cookingTime = "";
@Column(name = "ingredients", columnDefinition = "text")
private String ingredients = "";
@Column(name = "directions", columnDefinition = "longtext")
private String directions = "";
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "image_id")
private RecipeImage recipeImage;
@Transient
private String imageName = "";
@Transient
private String imageURL = "";
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created_on", nullable = false, updatable = false)
@CreatedDate
private Date createdOn = new Date();
public Recipe() {
}
public Recipe(String recipeName, String description, String dishType,
String preparationTime, String cookingTime, String ingredients, String directions,
RecipeImage recipeImage) {
this.recipeName = recipeName;
this.description = description;
this.dishType = dishType;
this.preparationTime = preparationTime;
this.cookingTime = cookingTime;
this.ingredients = ingredients;
this.directions = directions;
this.recipeImage = recipeImage;
}
public int getId() {
return Id;
}
public String getRecipeName() {
return recipeName;
}
public String getDescription() {
return description;
}
public String getDishType() {
return dishType;
}
public String getPreparationTime() {
return preparationTime;
}
public String getCookingTime() {
return cookingTime;
}
public String getIngredients() {
return ingredients;
}
public String getDirections() {
return directions;
}
public RecipeImage getRecipeImage() {
return recipeImage;
}
public Date getCreatedOn() {
return createdOn;
}
public void setRecipeName(String recipeName) {
this.recipeName = recipeName;
}
public void setDescription(String description) {
this.description = description;
}
public void setDishType(String dishType) {
this.dishType = dishType;
}
public void setPreparationTime(String preparationTime) {
this.preparationTime = preparationTime;
}
public void setCookingTime(String cookingTime) {
this.cookingTime = cookingTime;
}
public void setIngredients(String ingredients) {
this.ingredients = ingredients;
}
public void setDirections(String directions) {
this.directions = directions;
}
public void setRecipeImage(RecipeImage recipeImage) {
this.recipeImage = recipeImage;
}
public String getImageName() {
return imageName;
}
public void setImageName(String imageName) {
this.imageName = imageName;
}
public String getImageURL() {
return imageURL;
}
public void setImageURL(String imageURL) {
this.imageURL = imageURL;
}
@Override
public String toString() {
return "Recipe [Id=" + Id + ", recipeName=" + recipeName + ", description=" + description + ", dishType="
+ dishType + ", preparationTime=" + preparationTime + ", cookingTime=" + cookingTime + ", ingredients="
+ ingredients + ", directions=" + directions + ", recipeImage=" + recipeImage + ", imageName="
+ imageName + ", imageURL=" + imageURL + ", createdOn=" + createdOn + "]";
}
}
view raw Recipe.java hosted with ❤ by GitHub
This will create a table in the database called RECIPE_DETAILS. We store recipe information in the database table.

Repositories

Create a repository package and include the IRecipeRepository interface in it. Include this code snippet:
package com.raven.springbootthymeleafrecipeorganizer.repository;
// imports are omitted
@Repository
public interface IRecipeRepository extends JpaRepository<Recipe, Integer> {
}

Service to List Recipes

Make a package called service. Create the RecipeService class in this package and include the following code snippet:
package com.raven.springbootthymeleafrecipeorganizer.service;
// imports are omitted
@Service
public class RecipeService {
private final IRecipeRepository recipeRepository;
@Autowired
public RecipeService(IRecipeRepository recipeRepository) {
this.recipeRepository = recipeRepository;
}
public List<Recipe> findAll() {
return this.recipeRepository.findAll();
}
}
We made this class a bean by annotating it with @Service and autowiring IRecipeRepository with constructor injection. The findAll() function will return all recipes in the database.

Controller to List Recipes

Create a package called controller, and then create the RecipeController class:
package com.raven.springbootthymeleafrecipeorganizer.controller;
// imports are omitted
@Controller
@RequestMapping("/recipe")
public class RecipeController {
private final RecipeService recipeService;
@Autowired
public RecipeController(RecipeService recipeService) {
this.recipeService = recipeService;
}
@GetMapping("/list")
public ModelAndView getRecipeList() {
List<Recipe> recipes = new ArrayList<>();
List<Recipe> recipeList = this.recipeService.findAll();
recipeList.forEach(t -> {
t.setImageName(t.getRecipeImage().getOriginalFileName().toString());
t.setImageURL(MvcUriComponentsBuilder
.fromMethodName(RecipeController.class, "getImage",
t.getRecipeImage().getOriginalFileName().toString())
.build().toString());
recipes.add(t);
});
ModelAndView modelAndView = new ModelAndView("recipe/recipelist");
modelAndView.addObject("recipes", recipes);
return modelAndView;
}
@GetMapping("/images/{fileName}")
public ResponseEntity<Resource> getImage(@PathVariable(name = "fileName") String fileName) {
Resource file = this.recipeService.loadImage(fileName);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"")
.body(file);
}
}
This class has been annotated with the @Controller annotation to create a Spring bean and autowired RecipeService using constructor injection. We create a recipe list in getRecipeList() to obtain the image name and URL. This image URL will be placed on the view page so it enables you to preview the recipe image. Then, using the addObject() method, we insert our recipe list object into the model with the attribute name recipes.

Because we specified recipe/recipelist as the view name in the ModelAndView object, we must create recipelist.html in the recipe directory.

Recipe List View Page

To list the recipes, we must now create a view page. To do so, create a recipelist.html HTML page in the /resources/templates/recipe directory:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="" />
<title>Recipe Organizer</title>
<link rel="stylesheet" th:href="@{/css/style.css}" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous" />
<meta name="theme-color" content="#712cf9" />
</head>
<body>
<header>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a
th:href="@{/recipe/list}"
class="navbar-brand d-flex align-items-center"
><strong>Recipe Organizer</strong>
</a>
<nav class="d-inline-flex mt-2 mt-md-0 ms-md-auto">
<a class="me-3 py-2 text-dark text-decoration-none" href="#"
>Features</a
>
<a class="me-3 py-2 text-dark text-decoration-none" href="#"
>Enterprise</a
>
</nav>
</div>
</div>
</header>
<main>
<section
class="py-6 text-center container"
style="background-color: antiquewhite"
>
<div class="row py-lg-5">
<div class="col-lg-6 col-md-8 mx-auto">
<h1 class="fw-light">Recipe</h1>
<p class="text-muted">
Recipe Organizer will help you keep better track of all your
favorite recipes. Once you’ve added all the recipes you love or
have been meaning to try, you can view them in the Recipe
Organizer as it currently is.
</p>
<p>
<a th:href="@{/recipe/new}" class="btn btn-primary my-2"
>New Recipe</a
>
</p>
</div>
</div>
</section>
<div class="album py-5 bg-light">
<div class="container">
<div
class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"
data-masonry='{"percentPosition": true }'
>
<div class="col" th:each="recipe : ${recipes}">
<div class="card shadow-sm">
<img
th:src="@{${recipe.imageURL}}"
alt="${recipe.imageName}"
height="250"
width="100%"
/>
<div class="card-body">
<a
th:href="@{'/recipe/view/' + ${recipe.Id}}"
style="text-decoration: none"
><h5 class="card-title" th:text="${recipe.recipeName}"></h5
></a>
<p class="card-text" th:text="${recipe.description}"></p>
<div
class="d-flex justify-content-between align-items-center"
>
<div class="btn-group">
<a
th:href="@{'/recipe/view/' + ${recipe.Id}}"
class="btn btn-sm btn-outline-secondary"
>View</a
>
<a
th:href="@{/##}"
class="btn btn-sm btn-outline-secondary"
>Edit</a
>
</div>
<small
class="text-muted"
th:text="${recipe.cookingTime}"
></small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
crossorigin="anonymous"
></script>
<script
async
src="https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js"
integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D"
crossorigin="anonymous"
></script>
</body>
</html>
view raw recipelist.html hosted with ❤ by GitHub
This page was created with Bootstrap. In this case, we used the model attribute (recipes) to pair the data to the HTML. 

We add th:href="@/recipe/new" in an anchor tag on this page to create a new recipe. As a result, we must implement a get method for /new in the controller. The @ symbol is used here to refer to our application's context path.

Controller to Create New Recipe

In the RecipeController class, create newRecipe() and map it to /new with the @GetMapping annotation:
package com.raven.springbootthymeleafrecipeorganizer.controller;
// imports are omitted
@Controller
@RequestMapping("/recipe")
public class RecipeController {
private final RecipeService recipeService;
@Autowired
public RecipeController(RecipeService recipeService) {
this.recipeService = recipeService;
}
@GetMapping("/new")
public ModelAndView newRecipe() {
ModelAndView modelAndView = new ModelAndView("recipe/newrecipe");
modelAndView.addObject("recipe", new Recipe());
return modelAndView;
}
@GetMapping("/list")
public ModelAndView getRecipeList() {
// existing code
}
}
We created a ModelAndView object with the view name recipe/newrecipe in the newRecipe() method. We passed a Recipe object as an attribute to the ModelAndView object so that we could bind it to the HTML form. For the view page, we must now create newrecipe.html in the recipe directory.  

New Recipe View Page

Create a newrecipe.html HTML page in the /resources/templates/recipe directory:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="" />
<title>Recipe Organizer</title>
<link rel="stylesheet" th:href="@{/css/style.css}" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous" />
<meta name="theme-color" content="#712cf9" />
</head>
<body>
<header>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a th:href="@{/recipe/list}" class="navbar-brand d-flex align-items-center"><strong>Recipe Organizer</strong></a>
</div>
</div>
</header>
<div class="container">
<hr /><h3>Recipe details</h3><hr />
<form id="recipeForm" action="#" th:action="@{/recipe/save}" th:object="${recipe}" method="POST" enctype="multipart/form-data">
<div class="row mb-3">
<label class="col-sm-2 col-form-label">Recipe Name</label>
<div class="col-sm-6">
<input type="text" class="form-control" th:field="*{recipeName}" required/>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-8">
<textarea class="form-control" rows="2" th:field="*{description}" required></textarea>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">Dish Type</label>
<div class="col-sm-3">
<select class="form-select" th:field="*{dishType}" required>
<option th:value="select">Select...</option>
<option th:value="MainDish">MainDish</option>
<option th:value="Dessert">Dessert</option>
<option th:value="Salad">Salad</option>
<option th:value="Soup">Soup</option>
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">Preparation Time</label>
<div class="col-sm-3">
<input type="text" class="form-control" th:field="*{preparationTime}" required />
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">Cooking Time</label>
<div class="col-sm-3">
<input type="text" class="form-control" th:field="*{cookingTime}" required />
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">Ingredients</label>
<div class="col-sm-8">
<textarea class="form-control" rows="3" th:field="*{ingredients}" required></textarea>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">Directions</label>
<div class="col-sm-8">
<textarea class="form-control" rows="3" th:field="*{directions}" required></textarea>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">Image</label>
<div class="col-sm-8">
<input class="form-control" id="inputFile" type="file" name="inputFile" accept="image/png, image/jpeg"/>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-6">
<img src="" id="imagePreview" height="350" width="100%" style="display: none"/>
</div>
</div>
<div class="col-10 text-end">
<button type="submit" class="btn btn-info">Save</button>
</div>
</form>
<hr />
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
crossorigin="anonymous"
></script>
<script>
// TO PREVIEW UPLOADED IMAGE
var reader = new FileReader();
reader.onload = function (r_event) {
document
.getElementById("imagePreview")
.setAttribute("src", r_event.target.result);
document.getElementById("imagePreview").style.display = "block";
};
document
.getElementsByName("inputFile")[0]
.addEventListener("change", function (event) {
reader.readAsDataURL(this.files[0]);
});
</script>
</body>
</html>
view raw newrecipe.html hosted with ❤ by GitHub
Using th:field="*..." we bind the attribute properties to the required HTML controls. This form data will be posted to /recipe/save and is mentioned in th:action="@...".

Service to Save New Recipe

We'll write some logic in the service class to save the recipe details. So, in the RecipeService class, add the saveRecipe() method:
package com.raven.springbootthymeleafrecipeorganizer.service;
// imports are omitted
@Service
public class RecipeService {
private final String IMAGE_UPLOAD_DIR = "./image_uploads/";
private final IRecipeRepository recipeRepository;
@Autowired
public RecipeService(IRecipeRepository recipeRepository) {
this.recipeRepository = recipeRepository;
}
public Recipe saveRecipe(Recipe _recipe, MultipartFile multipartFile) {
String fileName;
Recipe recipe = null;
Path filePath;
try {
fileName = StringUtils.cleanPath(Objects.requireNonNull(multipartFile.getOriginalFilename()));
String[] splitName = fileName.split("\\.(?=[^\\.]+$)");
fileName = splitName[0].concat("_").concat(String.valueOf((new Date()).getTime())).concat(".")
.concat(splitName[1]);
filePath = Paths.get(IMAGE_UPLOAD_DIR + fileName);
Files.copy(multipartFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
RecipeImage recipeImage = new RecipeImage(fileName, filePath.toString());
_recipe.setRecipeImage(recipeImage);
recipe = this.recipeRepository.save(_recipe);
} catch (Exception e) {
e.printStackTrace();
}
return recipe;
}
public List<Recipe> findAll() {
return this.recipeRepository.findAll();
}
public Resource loadImage(String filename) {
try {
Path imageDirPath = Paths.get(this.IMAGE_UPLOAD_DIR);
Path file = imageDirPath.resolve(filename);
Resource resource = new UrlResource(file.toUri());
if (resource.exists() || resource.isReadable()) {
return resource;
} else {
throw new RuntimeException("Could not read the image file.");
}
} catch (MalformedURLException e) {
throw new RuntimeException("Error: " + e.getMessage());
}
}
}
In the saveRecipe() method, we first save the image in a local directory before saving the recipe details in the database alongside the image details.

Controller to Save New Recipe

Now, in the RestController class, add the save() method and map it to /save using @PostMapping:
package com.raven.springbootthymeleafrecipeorganizer.controller;
// imports are omitted
@Controller
@RequestMapping("/recipe")
public class RecipeController {
private final RecipeService recipeService;
@Autowired
public RecipeController(RecipeService recipeService) {
this.recipeService = recipeService;
}
@PostMapping("/save")
public String saveRecipe(@ModelAttribute("recipe") Recipe recipe, @RequestParam("inputFile") MultipartFile multipartFile) {
this.recipeService.saveRecipe(recipe, multipartFile);
return "redirect:/recipe/list";
}
@GetMapping("/new")
public ModelAndView newRecipe() {
// existing code
}
@GetMapping("/list")
public ModelAndView getRecipeList() {
// existing code
}
}
We simply use our service class's method to save the recipe details and redirect the user to the recipe list page.

Service to Fetch Recipe Details

Now we'll write some logic to retrieve the details of a specific recipe in the service class. In the RecipeService class, add the findById() method:
package com.raven.springbootthymeleafrecipeorganizer.service;
// imports are omitted
@Service
public class RecipeService {
private final String IMAGE_UPLOAD_DIR = "./image_uploads/";
private final IRecipeRepository recipeRepository;
@Autowired
public RecipeService(IRecipeRepository recipeRepository) {
this.recipeRepository = recipeRepository;
}
public Recipe findById(int _id) {
Optional<Recipe> _recipe = this.recipeRepository.findById(_id);
Recipe recipe;
if (_recipe.isPresent()) {
recipe = _recipe.get();
} else {
throw new RuntimeException("No recipe with this id :: " + _id);
}
return recipe;
}
public List<Recipe> findAll() {
return this.recipeRepository.findAll();
}
public Recipe saveRecipe(Recipe _recipe, MultipartFile multipartFile) {
String fileName;
Recipe recipe = null;
Path filePath;
try {
fileName = StringUtils.cleanPath(Objects.requireNonNull(multipartFile.getOriginalFilename()));
String[] splitName = fileName.split("\\.(?=[^\\.]+$)");
fileName = splitName[0].concat("_").concat(String.valueOf((new Date()).getTime())).concat(".")
.concat(splitName[1]);
filePath = Paths.get(IMAGE_UPLOAD_DIR + fileName);
Files.copy(multipartFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
RecipeImage recipeImage = new RecipeImage(fileName, filePath.toString());
_recipe.setRecipeImage(recipeImage);
recipe = this.recipeRepository.save(_recipe);
} catch (Exception e) {
e.printStackTrace();
}
return recipe;
}
public Resource loadImage(String filename) {
try {
Path imageDirPath = Paths.get(this.IMAGE_UPLOAD_DIR);
Path file = imageDirPath.resolve(filename);
Resource resource = new UrlResource(file.toUri());
if (resource.exists() || resource.isReadable()) {
return resource;
} else {
throw new RuntimeException("Could not read the image file.");
}
} catch (MalformedURLException e) {
throw new RuntimeException("Error: " + e.getMessage());
}
}
}
Here, we retrieve the recipe details from the database using their ID and then return them.

Controller to Fetch Recipe Details

In the RecipeController class, add the getRecipe() method and map it to /view/id using the @GetMapping annotation:
package com.raven.springbootthymeleafrecipeorganizer.controller;
// imports are omitted
@Controller
@RequestMapping("/recipe")
public class RecipeController {
private final RecipeService recipeService;
@Autowired
public RecipeController(RecipeService recipeService) {
this.recipeService = recipeService;
}
@GetMapping("/view/{id}")
public ModelAndView getRecipe(@PathVariable(name = "id") int id) {
Recipe recipe = this.recipeService.findById(id);
recipe.setImageName(recipe.getRecipeImage().getOriginalFileName());
recipe.setImageURL(MvcUriComponentsBuilder
.fromMethodName(RecipeController.class, "getImage",
recipe.getRecipeImage().getOriginalFileName())
.build().toString());
ModelAndView modelAndView = new ModelAndView("recipe/viewrecipe");
modelAndView.addObject("recipe", recipe);
return modelAndView;
}
@GetMapping("/list")
public ModelAndView getRecipeList() {
List<Recipe> recipes = new ArrayList<>();
List<Recipe> recipeList = this.recipeService.findAll();
recipeList.forEach(t -> {
t.setImageName(t.getRecipeImage().getOriginalFileName());
t.setImageURL(MvcUriComponentsBuilder
.fromMethodName(RecipeController.class, "getImage",
t.getRecipeImage().getOriginalFileName())
.build().toString());
recipes.add(t);
});
ModelAndView modelAndView = new ModelAndView("recipe/recipelist");
modelAndView.addObject("recipes", recipes);
return modelAndView;
}
@GetMapping("/new")
public ModelAndView newRecipe() {
ModelAndView modelAndView = new ModelAndView("recipe/newrecipe");
modelAndView.addObject("recipe", new Recipe());
return modelAndView;
}
@PostMapping("/save")
public String saveRecipe(@ModelAttribute("recipe") Recipe recipe,
@RequestParam("inputFile") MultipartFile multipartFile) {
Recipe savedRecipe = this.recipeService.saveRecipe(recipe, multipartFile);
return "redirect:/recipe/list";
}
@GetMapping("/images/{fileName}")
public ResponseEntity<Resource> getImage(@PathVariable(name = "fileName") String fileName) {
Resource file = this.recipeService.loadImage(fileName);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"")
.body(file);
}
}
In the getRecipe() method, we first use findById() to get the recipe details, then we create the image URL and assign it to the recipe details.
 
We need to create viewrecipe.html in the recipe directory because we passed recipe/viewrecipe as our view name in the ModelAndView object.

View Recipe Details Page

Now, in the /resources/templates/recipe directory, create an HTML page called viewrecipe.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="" />
<title>Recipe Organizer</title>
<link rel="stylesheet" th:href="@{/css/style.css}" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"/>
<meta name="theme-color" content="#712cf9" />
</head>
<body>
<header>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a
th:href="@{/recipe/list}"
class="navbar-brand d-flex align-items-center"
><strong>Recipe Organizer</strong>
</a>
</div>
</div>
</header>
<div class="container">
<form id="recipeForm" action="#" th:object="${recipe}">
<hr />
<p th:text="${recipe.recipeName}" class="fs-2 text-wrap"></p>
<hr />
<div class="row mb-3">
<div class="col-sm-6">
<img
th:src="@{${recipe.imageURL}}"
alt="${recipe.imageName}"
height="350"
width="100%"
/>
</div>
<div class="col-sm-6">
<p th:text="${recipe.description}" class="fs-5 text-wrap"></p>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4">
<label class="text-uppercase">Preparation Time</label>
<p th:text="${recipe.preparationTime}" class="fs-4 text-wrap"></p>
</div>
<div class="col-sm-4">
<label class="text-uppercase">Cooking Time</label>
<p th:text="${recipe.cookingTime}" class="fs-4 text-wrap"></p>
</div>
<div class="col-sm-4">
<label class="text-uppercase">Dish Type</label>
<p th:text="${recipe.dishType}" class="fs-4 text-wrap"></p>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-12">
<label class="text-uppercase">Ingredients</label>
<p th:text="${recipe.ingredients}" class="fs-5"></p>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-12">
<label class="text-uppercase">Directions</label>
<p th:text="${recipe.directions}" class="fs-5"></p>
</div>
</div>
<div class="col-sm-12">
<a th:href="@{/recipe/list}" class="btn btn-sm btn-outline-secondary"
>back to list</a
>
</div>
</form>
<hr />
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
crossorigin="anonymous"
></script>
</body>
</html>
view raw viewrecipe.html hosted with ❤ by GitHub
Using th:object="$recipe" we link the attribute object to the HTML form.

Recipe List Page

java,springboot,thymeleaf,programming,software development

New Recipe Page

java,springboot,thymeleaf,programming,software development

View Recipe Page

java,springboot,thymeleaf,programming,software development

You can download the source code from here.
Happy coding!!! 😊
in

Original recipes


No comments:

Post a Comment

Popular posts