javaspring-bootthymeleafdtomodelattribute

Take input from Thymeleaf. Translate into ModelAttribute. Doesn't recognize the @ModelAttribute bean. Cannot redirect to new page


I am trying to create a new Movie. This means that I want to access the "addMovie" URL, to generate a form in which I add the title of a new movie. This title should be stored in a movieDTO already from the Thymeleaf form. Then, when I press "Submit" I want it to send me to a new page "/saveMovie" that receives the movieDTO from the previous form. Using this movieDTO, I create a new Movie and then "redirect:movie/movies-all" to show all the current movies (including the added one).

However, this does not happen. When I access the URL localhost:8081/addMovie I get these errors:

org.thymeleaf.exceptions.TemplateProcessingException: Error during execution of processor 'org.thymeleaf.spring5.processor.SpringInputGeneralFieldTagProcessor' (template: "movie/movie-add" - line 18, col 40)

and this one

Caused by: java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'movieDTO' available as request attribute at org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:153) ~[spring-webmvc-5.3.9.jar:5.3.9].

Thus, I don't understand what the issue is. Is it that it does not store the information in the movieDTO correctly (or at all)? or is it because the bean movieDTO is not sent to the post method?

The Project Structure: project_structure

The Controller:

package movie_API.movie.controllers;

import movie_API.movie.services.MovieService;
import movie_API.movie.services.beans.MovieDTO;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.validation.Valid;
import java.util.Arrays;
import java.util.List;

@Controller
//@RequestMapping("/something") <- if you want a general something before the other resources
public class MovieController {

    private final MovieService movieService;

    public MovieController(MovieService movieService) {
        this.movieService = movieService;
    }  

    @RequestMapping("/addMovie")
    public String add() {
        return "movie/movie-add";
    }

    @RequestMapping(value = "/saveMovie", method = RequestMethod.POST)
    public String create(@ModelAttribute("movieDTO") @Valid MovieDTO movieDTO, Model model, BindingResult result) {
        System.out.println("got into post");
        if (result.hasErrors()) {
            System.out.println("error in result");
            return "movie/movie-add";
        }

        //movieService.saveNewMovie(movieDTO);
        movieService.showMovies(model);
        return "redirect:movie/movies-all";
    }
}

The movieDTO + the corresponding getters and setters:

package movie_API.movie.services.beans;

import java.io.Serializable;

public class MovieDTO implements Serializable {

    private static final long serialVersionUID = -8040351309785589042L;

    private String metascore;
    private String title;
    private String year;
    private String description;
    private String genreId;

    public MovieDTO(String metascore, String title, String year, String description, String genreId) {
        this.metascore = metascore;
        this.title = title;
        this.year = year;
        this.description = description;
        this.genreId = genreId;
    }
}

The movie-add.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <title>Add Movie</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.1/css/all.css">
</head>
<body>
<div class="container my-5">
    <h2 class="mb-5">New Movie</h2>
    <div class="row">
        <div class="col-md-6">
            <form action="#" th:action="@{/saveMovie}" th:object="${movieDTO}" method="post">
                <label>
                    <input type="text" th:field="*{title}" name="title" class="form-control" placeholder="insert string here">
                </label>
                <input type="submit" class="btn btn-primary" value="Submit">
            </form>
        </div>
    </div>
</div>
</body>
</html>

The movies-all.html:

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org/" lang="en">
<head>
    <title> All Movies </title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>


<div class="container">
    <h2>Movies table</h2>
    <p>Here are all the movies we have in the database:</p>
    <table class="table table-bordered">
        <thead>
        <tr>
            <th>ID</th>
            <th>metascore</th>
            <th>title</th>
            <th>year</th>
            <th>description</th>
            <th>genre</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="movie, stat: ${allMovies}">

            <td th:text="${movie.id}"></td>
            <td th:text="${movie.metascore}"></td>
            <td th:text="${movie.title}"></td>
            <td th:text="${movie.year}"></td>
            <td th:text="${movie.description}"></td>
            <td th:text="${allGenres[stat.index]}"></td>

        </tr>
        </tbody>
    </table>
</div>

</body>
</html>

The movieService:

package movie_API.movie.services;

import movie_API.movie.models.Movie;
import movie_API.movie.repositories.GenreRepository;
import movie_API.movie.repositories.MovieRepository;
import movie_API.movie.services.beans.MovieDTO;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;

import java.util.List;

@Service
public class MovieService {

    private final MovieRepository movieRepository;
    private final GenreRepository genreRepository;
    private List<Movie> movies;
    private Movie movie;

    public MovieService(MovieRepository movieRepository, GenreRepository genreRepository) {
        this.movieRepository = movieRepository;
        this.genreRepository = genreRepository;
    }

    public void showMovies(Model m) {

        m.addAttribute("allMovies", getMovies());
        //System.out.println("done with movies");

        m.addAttribute("allGenres", getGenreNames());
        //System.out.println("done with genres");
    }

    public void showMovieByID(Model m, String id) {
        m.addAttribute("movie", getMovieById(id));
        m.addAttribute("genreName", getGenreNameByMovieId());
    }

    public List<Movie> getMovies() {
        movies = movieRepository.findAll();
        //System.out.println(movies.get(0).toString());
        return movies;
    }

    public Movie getMovieById(String id) {
        this.movie = movieRepository.findById(id).get();
        return movie;
    }

    public List<String> getGenreNames() {
        return movieRepository.getGenreNames(movies, genreRepository);
    }

    public String getGenreNameByMovieId() {
        String genreId = movie.getGenreId();
        return genreRepository.findById(genreId).get().getName();
    }

    public void saveNewMovie(MovieDTO movieDTO) {
        Movie movie = new Movie();

        movie.setTitle(movieDTO.getTitle());
        movie.setYear(movieDTO.getYear());
        movie.setMetascore(movieDTO.getMetascore());
        movie.setDescription(movieDTO.getDescription());
        movie.setGenreId(movieDTO.getGenreId());

        movieRepository.save(movie);
    }

    public void updateMovie(MovieDTO movieDTO, String id) {
        Movie movie = movieRepository.findById(id).get();

        movie.setTitle(movieDTO.getTitle());
        movie.setYear(movieDTO.getYear());
        movie.setMetascore(movieDTO.getMetascore());
        movie.setDescription(movieDTO.getDescription());
        movie.setGenreId(movieDTO.getGenreId());

        movieRepository.save(movie);
    }

    public void deleteMovie(String id) {
        movieRepository.deleteById(id);
    }

}

The 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.5.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>movie_API</groupId>
    <artifactId>movie</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>movie</name>
    <description>Movie REST API</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jersey</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <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-starter-web-services</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Main problems:

  1. What are the errors? Why is the movieDTO not created according to the given input from the User? (I can only assume it is not properly created because it's always null when I want to access it)
  2. Why is the redirect not recognizing the movies-all location? redirect

Solution

  • for the first error, just add an empty DTO to the model;

    @RequestMapping("/addMovie")
    public String add(Model model) {
        model.addAttribute("movieDTO", new MovieDTO());
        return "movie/movie-add";
    }
    

    For the second error, remove the "redirect:" part

        movieService.showMovies(model);
        return "movie/movies-all";
    

    Then you will get the pages you want;

    enter image description here

    enter image description here

    A warning;

    Please add default constructor, setters/getters and id field to the DTO.