1
votes

I'm having a hard time trying to show validation errors in my Spring MVC application using a form-backing bean and complex objects in that form-backing bean.

It seems that the "hasErrors" functionality does not support "." characters or can not resolve complex objects?

When loading the following code, I'm able to show the form. However, after the validator finds a null Author's name and reloads the create.html page, I get a stacktrace about how Thymeleaf/Spring can not evaluate my SPel expression.

Does anyone know why it's trying to load "author.author" when the expression states "author.name"? It's as if the property is getting "author." prepended to the expression.

UPDATE: After a lot of troubleshooting, I found that the validator was the issue, although I don't know why yet.

I've included the validators.

AuthorCommandObject.java

public class AuthorCommandObject {
    private Author _author;
    private Book _book;

    public Author getAuthor() {
        return _author;
    }

    public void setAuthor(Author author) {
        _author = author;
    }

    public Author getBook() {
        return _book;
    }

    public void setBook(Book book) {
        _book = book;
    }
}

Author.java

public class Author {
    private long _authorId;
    private String _name;

    public long getAuthorId() {
        return _authorId;
    }

    public void setAuthorId(long authorId) {
        _authorId = authorId;
    }

    public String getName() {
        return _name;
    }

    public void setName(String name) {
        _name = name;
    }
}

Book.java

public class Book {
    private long _bookId;
    private String _bookName;

    public long getBookId() {
        return _bookId;
    }

    public void setBookId(long bookId) {
        _bookId = bookId;
    }

    public String getName() {
        return _name;
    }

    public void setName(String name) {
        _name = name;
    }
}

AuthorFormController.java

    @Controller
    @RequestMapping("/author")
    public class AuthorFormController {

        @InitBinder("authorCommandObject")
        public void initBinder(WebDataBinder binder) {
            binder.setValidator(new AutherCommandObjectValidator(new AuthorValidator(), new BookValidator()));      
        }

        @RequestMapping(method=RequestMethod.GET, value ="/create", produces = "text/html")
        public String createForm(Model model) {
            AuthorCommandObject authorCommandObject = new AuthorCommandObject();
            authorCommandObject.setAuthor(new Author());
            authorCommandObject.setBook(new Book());
            model.addAttribute("authorCommandObject", authorCommandObject);
            return "/author/create";
        }

        @RequestMapping(method=RequestMethod.POST, value ="/save", produces = "text/html")
        public String save(@Valid @ModelAttribute("authorCommandObject") AuthorCommandObject authorCommandObject, BindingResult result, Model model) {


        if (result.hasErrors()) {
            return "/author/create";
        }

        // the rest of the logic here

        }
    }

create.html

    <form th:action="@{/author/save}" method="POST" class="form-horizontal" th:object="${authorCommandObject}">
        Author Name:<input type="text" th:field="*{author.name}" />
        <ul th:if="${#fields.hasErrors('author.name')}">
            <li th:each="err : ${#fields.errors('author.name')}" th:text="${err}" />
        </ul>
        <input type="submit" value="Submit" class="btn btn-primary btn-lg" />
    </form>

AuthorCommandObjectValidator.java

public class AuthorCommandObjectValidator implements Validator {

    private final Validator _authorValidator;
    private final Validator _bookValidator;

    public AuthorCommandObjectValidator(Validator authorValidator, Validator bookValidator) {
        _authorValidator = authorValidator;
        _bookValidator = bookValidator;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return AuthorCommandObject.class.equals(clazz);
    }

    @Override
    public void validate(Object obj, Errors errors) {
        AuthorCommandObject authorCommandObject = (AuthorCommandObject)obj; 
        try {
            errors.pushNestedPath("author");
            ValidationUtils.invokeValidator(_authorValidator, authorCommandObject.getAuthor(), errors);

            errors.pushNestedPath("book");
            ValidationUtils.invokeValidator(_bookValidator, authorCommandObject.getBook(), errors);
        } finally {
            errors.popNestedPath();
        }
    }
}

AuthorValidator.java

public class AuthorValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Author.class.equals(clazz);
    }

    @Override
    public void validate(Object obj, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "field.required");
    }
}

Partial Stacktrace (last line is the most helpful)

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "#fields.hasErrors('author.name')"
org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "#fields.hasErrors('author.name')"
org.springframework.beans.NotReadablePropertyException: Invalid property 'author.author' of bean class [com.sample.application.AuthorCommandObject]: Bean property 'author.author' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
1
post getters and setters here too, that may be the problem - libik
I think that you should be evaluating #fields.hasErrors('name') becouse you are "inside" form which has th:object="${authorCommandObject}" so the expresion evaluating is traing to find property author on form object which is itself author ? - Josef Procházka
This wouldn't work because authorCommandObject contains an author, not a name. So I should be able to use author.name rather than just "name". - arcdegree

1 Answers

0
votes

You need this kind of tag before:

<div th:object="${form}" th:remove="tag">
    <ul>
        <li th:each="err : ${#fields.errors('*')}" th:text="${err}" />
    </ul>
</div>