Spring CrudRepository - validate before save in JSF project - spring

I can't figure out why validation errors raise instead of beign just displayed after submitting a form ? I'm using HIbernate, Spring and JSF. I declared validation as of JSR 303 and use Hibernate annotation as follows:
#Entity
#Table(name = "posts")
public class Post implements Serializable {
#Column(name = "title")
#NotEmpty(message = "Title should not be empty")
private String title;
... other fields are ommited
}
In the corresponding bean I'm trying to save a post as follows:
#Component
#ManagedBean
#RequestScoped
#URLMappings(mappings = {
#URLMapping(id = "posts", pattern = "/posts/", viewId = "/faces/posts/list.xhtml"),
#URLMapping(id = "new", pattern = "/posts/new", viewId = "/faces/posts/new.xhtml")
})
public class PostBean {
#Autowired
private PostService postService;
private List<Post> posts;
private Post post = new Post();
public List<Post> getPosts() {
return postService.findAll();
}
public Post getPost() {
return post;
}
public void setPost(Post post) {
this.post = post;
}
public String create(Post post) {
postService.save(post);
return "pretty:posts";
}
....
}
The Post related repository is declared as follows:
public interface PostRepository extends CrudRepository<Post, Long> {
public Post findByTitleIgnoreCase(String title);
}
When submitting a form I get the below error:
Caused by: javax.faces.el.EvaluationException: javax.validation.ConstraintViolationException: Validation failed for classes [com.airial.domain.Post] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='Title should not be empty', propertyPath=title, rootBeanClass=class com.airial.domain.Post, messageTemplate='Title should not be empty'}
]
at javax.faces.component.MethodBindingMethodExpressionAdapter.invoke(MethodBindingMethodExpressionAdapter.java:101)
at com.sun.faces.application.ActionListenerImpl.processAction(ActionListenerImpl.java:102)
... 39 more
Caused by: javax.validation.ConstraintViolationException: Validation failed for classes [com.airial.domain.Post] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='Title should not be empty', propertyPath=title, rootBeanClass=class com.airial.domain.Post, messageTemplate='Title should not be empty'}
]
at org.hibernate.cfg.beanvalidation.BeanValidationEventListener.validate(BeanValidationEventListener.java:161)
at org.hibernate.cfg.beanvalidation.BeanValidationEventListener.onPreInsert(BeanValidationEventListener.java:94)
at org.hibernate.action.EntityIdentityInsertAction.preInsert(EntityIdentityInsertAction.java:160)
at org.hibernate.action.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:65)
at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:273)
at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:320)
at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:203)
at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:129)
at org.hibernate.ejb.event.EJB3PersistEventListener.saveWithGeneratedId(EJB3PersistEventListener.java:69)
at org.hibernate.event.def.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:179)
at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:135)
at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:61)
at org.hibernate.impl.SessionImpl.firePersist(SessionImpl.java:808)
at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:782)
at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:786)
at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:672)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:241)
Any idea on how to fix it, i.e. to run validation before save ?
You can find the projet at github repo: https://github.com/Javix/blog-jsf.
Thank you

Related

Type = org.springframework.web.HttpMediaTypeNotSupportedException after adding JsonBackedReference

I created my spring-boot app, and added JsonMangedReference and JsonBackReference on my DTO object to ignore infinite recursion but now my test case is failing to say, I assumed it just ignored deserializing JSON object on favorite DTO but this one is saying I am expecting JSON but it is not producing JSON
java.lang.AssertionError: Status expected:<200> but was:<415>
Expected :200
Actual :415
<Click to see difference>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:59)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:122)
at org.springframework.test.web.servlet.result.StatusResultMatchers.lambda$matcher$9(StatusResultMatchers.java:627)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:212)
at com.self.zoo.controller.AnimalControllerTest.addAnimal(AnimalControllerTest.java:65)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
<system-out><![CDATA[2022-05-25 12:00:20.996 WARN 444 --- [ main] .c.j.MappingJackson2HttpMessageConverter : Failed to evaluate Jackson deserialization for type [[simple type, class com.self.zoo.dto.AnimalDto]]: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot handle managed/back reference 'defaultReference': no back reference property found from type `java.util.Set<com.self.zoo.entity.Favorite>`
2022-05-25 12:00:21.003 WARN 444 --- [ main] .c.j.MappingJackson2HttpMessageConverter : Failed to evaluate Jackson deserialization for type [[simple type, class com.self.zoo.dto.AnimalDto]]: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot handle managed/back reference 'defaultReference': no back reference property found from type `java.util.Set<com.self.zoo.entity.Favorite>`
2022-05-25 12:00:21.008 WARN 444 --- [ main] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/json;charset=UTF-8' not supported]
Below is my test case:
#RunWith(SpringRunner.class)
#WebMvcTest(AnimalController.class)
class AnimalControllerTest {
#Autowired
MockMvc mvc;
#Autowired
ObjectMapper objectMapper;
#MockBean
AnimalService animalService;
#BeforeEach
public void setUp() throws InvalidRoomDetailException {
AnimalDto response = new AnimalDto();
response.setId(1l);
when(animalService.add(any())).thenReturn(response);
}
#Test
void addAnimal() throws Exception {
AnimalDto animalDto = createAnimalDtoObject();
mvc.perform(MockMvcRequestBuilders
.post("/animal/add")
.content(asJsonString(animalDto))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").exists());
}
private AnimalDto createAnimalDtoObject() {
AnimalDto animalDto = new AnimalDto();
RoomDto roomDto = new RoomDto();
roomDto.setSize(25l);
roomDto.setTitle("Green");
RoomDto roomDto1 = new RoomDto();
roomDto1.setSize(10l);
roomDto1.setTitle("Blue");
animalDto.setRoom(roomDto);
//animalDto.setFavoriteRooms();
FavoriteDto f1 = new FavoriteDto();
f1.setAnimalDto(animalDto);
f1.setRoomDto(roomDto);
FavoriteDto f2 = new FavoriteDto();
f2.setAnimalDto(animalDto);
f2.setRoomDto(roomDto1);
Set<FavoriteDto> favSet = new HashSet<>();
favSet.add(f2);
favSet.add(f1);
animalDto.setFavoriteRooms(favSet);
return animalDto;
}
}
below is my AnimalDto class
#Data
#FieldDefaults(level = AccessLevel.PRIVATE)
#NoArgsConstructor
#AllArgsConstructor
public class AnimalDto {
Long id;
String title;
LocalDateTime located;
String type;
Long preference;
#JsonManagedReference
Set<FavoriteDto> favoriteRooms;
RoomDto room;
}
and my favoriteDTO
#Data
#FieldDefaults(level = AccessLevel.PRIVATE)
#NoArgsConstructor
#AllArgsConstructor
public class FavoriteDto {
RoomDto roomDto;
#JsonBackReference
AnimalDto animalDto;
Long id;
}
your classes appear to be correct.
try to put one
System.out.println(asJsonString(animalDto));
to see what input the test is sending to the API, to try to validate it is correct.

Spring Data: Query By Example and Converter

I have an entity:
import javax.persistence.Convert;
#Entity(name = "my_entity")
public class MyEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#Convert(converter = StringListConverter.class)
private List<String> emails;
// next fields omitted...
}
Converter which is used by emails field in entity (typical implementation like LocalDateTimeConverter):
import javax.persistence.Converter;
#Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {
private static final String SPLIT_CHAR = ";";
#Override
public String convertToDatabaseColumn(List<String> stringList) {
if (CollectionUtils.isNotEmpty(stringList)) {
return String.join(SPLIT_CHAR, stringList);
} else {
return null;
}
}
#Override
public List<String> convertToEntityAttribute(String string) {
if (StringUtils.isNotBlank(string)) {
return Arrays.asList(string.split(SPLIT_CHAR));
} else {
return Collections.emptyList();
}
}
}
(I store emails separated by semicolons in one column. StringListConverter do that conversion.)
And Spring Data repository:
import org.springframework.data.domain.Example;
public interface MyRepository extends JpaRepository<MyEntity, Long> {
default List<MyEntity> findMatchingMyEntity(MyEntity myEntity) {
Example<MyEntity> example = Example.of(myEntity);
return findAll(example);
}
}
I use Query by Example mechanism from Spring Data. When I have fields without #Convert (like String name) it works. But when I have field with #Convert (AttributeConverter) like List<String> emails it causes InvalidDataAccessApiUsageException.
org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [abc#company.com] did not match expected type [java.util.List (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [abc#company.com] did not match expected type [java.util.List (n/a)]
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:374) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:257) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:528) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) ~[spring-tx-5.2.1.RELEASE.jar:5.2.1.RELEASE]
...
Caused by: java.lang.IllegalArgumentException: Parameter value [abc#company.com] did not match expected type [java.util.List (n/a)]
at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:54) ~[hibernate-core-5.4.8.Final.jar:5.4.8.Final]
at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:27) ~[hibernate-core-5.4.8.Final.jar:5.4.8.Final]
at org.hibernate.query.internal.QueryParameterBindingImpl.validate(QueryParameterBindingImpl.java:90) ~[hibernate-core-5.4.8.Final.jar:5.4.8.Final]
... 146 common frames omitted
(message is weird because I've tried search with that list: ["abc#company.com", "def#company.com"], but in message is only one email)
I've tried to implement transform in ExampleMatcher:
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
public interface MyRepository extends JpaRepository<MyEntity, Long> {
default List<MyEntity> findMatchingMyEntity(MyEntity myEntity) {
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("emails",
match -> match.transform(emailsOptional -> {
if (emailsOptional.isPresent()) {
List<String> emails = (List<String>) emailsOptional.get();
return Optional.ofNullable(new StringListConverter().convertToDatabaseColumn(emails));
}
return emailsOptional;
}));
Example<MyEntity> example = Example.of(myEntity, matcher);
return findAll(example);
}
}
But is causes InvalidDataAccessApiUsageException too, but with different message than previous one (there are two emails that I've set):
org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [abc#company.com;def#company.com] did not match expected type [java.util.List (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [abc#company.com;def#company.com] did not match expected type [java.util.List (n/a)]
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:374) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:257) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:528) ~[spring-orm-5.2.1.RELEASE.jar:5.2.1.RELEASE]
Caused by: java.lang.IllegalArgumentException: Parameter value [abc#company.com;def#company.com] did not match expected type [java.util.List (n/a)]
at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:54) ~[hibernate-core-5.4.8.Final.jar:5.4.8.Final]
at org.hibernate.query.spi.QueryParameterBindingValidator.validate(QueryParameterBindingValidator.java:27) ~[hibernate-core-5.4.8.Final.jar:5.4.8.Final]
... 146 common frames omitted
It seems that for some reason Hibernate is trying to split the array of emails into multiple conditions just like in IN query in SQL using expandListValuedParameters method.
Note - with your solution doing query like findAllByEmailsIn(List<String> emailsList) also won't work.
Method expandListValuedParameters is deprecated since Hibernate 5.2, so it may contains some problems and for sure will be implemented differently in Hibernate 6.0.
I haven't found a fix for your problem, but there are some workarounds:
Wrap your List<String> emails in another class
Wrapper class:
public class EmailList {
private List<String> emails;
// getters, setters, constructors ommited
}
Updated model class:
import javax.persistence.Convert;
#Entity(name = "my_entity")
public class MyEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#Convert(converter = StringEmailListConverter.class)
private EmailList emailList;
// next fields omitted...
}
Updated converter class:
import javax.persistence.Converter;
#Converter
public class StringEmailListConverter implements AttributeConverter<EmailList, String> {
private static final String SPLIT_CHAR = ";";
#Override
public String convertToDatabaseColumn(EmailList emailList) {
if (emailList != null && CollectionUtils.isNotEmpty(emailList.getEmails())) {
return String.join(SPLIT_CHAR, emailList.getEmails());
} else {
return null;
}
}
#Override
public EmailList convertToEntityAttribute(String string) {
if (StringUtils.isNotBlank(string)) {
return new EmailList(Arrays.asList(string.split(SPLIT_CHAR)));
} else {
return new EmailList(Collections.emptyList());
}
}
}
And Spring Data repository will work fine with this code - no need for using transform:
import org.springframework.data.domain.Example;
public interface MyRepository extends JpaRepository<MyEntity, Long> {
default List<MyEntity> findMatchingMyEntity(MyEntity myEntity) {
Example<MyEntity> example = Example.of(myEntity);
return findAll(example);
}
}
Use String[] emails instead of List<String> emails
You need to change MyEntity and Converter respectively to use String[]. Of course using String[] sometimes is not an option because you specifically need a List.

Spring Data Rest Validation Confusion

Looking for some help with Spring data rest validation regarding proper handling of validation errors:
I'm so confused with the docs regarding spring-data-rest validation here: http://docs.spring.io/spring-data/rest/docs/current/reference/html/#validation
I am trying to properly deal with validation for a POST call that tries to save a new Company entity
I got this entity:
#Entity
public class Company implements Serializable {
#Id
#GeneratedValue
private Long id;
#NotNull
private String name;
private String address;
private String city;
private String country;
private String email;
private String phoneNumber;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "company")
private Set<Owner> owners = new HashSet<>();
public Company() {
super();
}
...
and this RestResource dao
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RestResource;
import com.domain.Company;
#RestResource
public interface CompanyDao extends PagingAndSortingRepository<Company, Long> {
}
POST Request to api/Companies:
{
"address" : "One Microsoft Way",
"city" : "Redmond",
"country" : "USA",
"email" : "info#microsoft.com",
"phoneNumber" : "(425) 703-6214"
}
When I issue a POST with a null name , I get the following rest response with httpcode 500
{"timestamp":1455131008472,"status":500,"error":"Internal Server Error","exception":"javax.validation.ConstraintViolationException","message":"Validation failed for classes [com.domain.Company] during persist time for groups [javax.validation.groups.Default, ]\nList of constraint violations:[\n\tConstraintViolationImpl{interpolatedMessage='may not be null', propertyPath=name, rootBeanClass=class com.domain.Company, messageTemplate='{javax.validation.constraints.NotNull.message}'}\n]","path":"/api/companies/"}
I tried creating the following bean, but it never seems to do anything:
#Component(value="beforeCreateCompanyValidator")
public class BeforeCreateCompanyValidator implements Validator{
#Override
public boolean supports(Class<?> clazz) {
return Company.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object arg0, Errors arg1) {
System.out.println("xxxxxxxx");
}
}
and even if it did work, how would it help me in developing a better error response with a proper http code and understandable json response ?
so confused
using 1.3.2.RELEASE
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
#Mathias
it seems the following is enough for jsr 303 annotations to be checked and for it to auto return a http code of 400 with nice messages (I dont even need BeforeCreateCompanyValidator or BeforeSaveCompanyValidator classes):
#Configuration
public class RestValidationConfiguration extends RepositoryRestConfigurerAdapter{
#Bean
#Primary
/**
* Create a validator to use in bean validation - primary to be able to autowire without qualifier
*/
Validator validator() {
return new LocalValidatorFactoryBean();
}
#Override
public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
Validator validator = validator();
//bean validation always before save and create
validatingListener.addValidator("beforeCreate", validator);
validatingListener.addValidator("beforeSave", validator);
}
}
400 response:
{
"errors": [{
"entity": "Company",
"message": "may not be null",
"invalidValue": "null",
"property": "name"
}, {
"entity": "Company",
"message": "may not be null",
"invalidValue": "null",
"property": "address"
}]
}
I think your problem is that the bean validation is happening too late - it is done on the JPA level before persist. I found that - unlike spring mvc - spring-data-rest is not doing bean validation when a controller method is invoked. You will need some extra configuration for this.
You want spring-data-rest to validate your bean - this will give you nice error messages responses and a proper http return code.
I configured my validation in spring-data-rest like this:
#Configuration
public class MySpringDataRestValidationConfiguration extends RepositoryRestConfigurerAdapter {
#Bean
#Primary
/**
* Create a validator to use in bean validation - primary to be able to autowire without qualifier
*/
Validator validator() {
return new LocalValidatorFactoryBean();
}
#Bean
//the bean name starting with beforeCreate will result into registering the validator before insert
public BeforeCreateCompanyValidator beforeCreateCompanyValidator() {
return new BeforeCreateCompanyValidator();
}
#Override
public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
Validator validator = validator();
//bean validation always before save and create
validatingListener.addValidator("beforeCreate", validator);
validatingListener.addValidator("beforeSave", validator);
}
}
When bean validation and/or my custom validator find errors I receive a 400 - bad request with a payload like this:
Status = 400
Error message = null
Headers = {Content-Type=[application/hal+json]}
Content type = application/hal+json
Body = {
"errors" : [ {
"entity" : "siteWithAdminUser",
"message" : "may not be null",
"invalidValue" : "null",
"property" : "adminUser"
} ]
}
The answers by #Mathias and #1977 is enough for regular Spring Data REST calls. However in cases when you need to write custom #RepositoryRestControllers using #RequestBody and #Valid JSR-303 annotations didn't work for me.
So, as an addition to the answer, in case of custom #RepositoryRestControllers with #RequestBody and #Valid annotation I've added the following #ControllerAdvice:
/**
* Workaround class for making JSR-303 annotation validation work for controller method parameters.
* Check the issue DATAREST-593
*/
#ControllerAdvice
public class RequestBodyValidationProcessor extends RequestBodyAdviceAdapter {
private final Validator validator;
public RequestBodyValidationProcessor(#Autowired #Qualifier("mvcValidator") final Validator validator) {
this.validator = validator;
}
#Override
public boolean supports(final MethodParameter methodParameter, final Type targetType, final Class<? extends
HttpMessageConverter<?>> converterType) {
final Annotation[] parameterAnnotations = methodParameter.getParameterAnnotations();
for (final Annotation annotation : parameterAnnotations) {
if (annotation.annotationType().equals(Valid.class)) {
return true;
}
}
return false;
}
#Override
public Object afterBodyRead(final Object body, final HttpInputMessage inputMessage, final MethodParameter
parameter, final Type targetType, final Class<? extends HttpMessageConverter<?>> converterType) {
final Object obj = super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
final BindingResult bindingResult = new BeanPropertyBindingResult(obj, obj.getClass().getCanonicalName());
validator.validate(obj, bindingResult);
if (bindingResult.hasErrors()) {
throw new RuntimeBindException(bindingResult);
}
return obj;
}
}

Can I add any object to Model in Spring MVC?

I wanted to add a bean to model in Spring MVC Controller. But, validator exception is thrown:
Illegalstate exception.
Can anyone guide me to submit a form and display the content which I get after form submission? In this case, I need to use a bean to display all my information in view.
Like:
model.addAttribute("simple", new Student());
But, I am keep getting IllegaStateException from validator.
Download:
https://sites.google.com/site/jimjicky/SpringFormValidation.rar?attredirects=0&d=1
Controller:
#Controller
public class EmployeeController {
private static final Logger logger = LoggerFactory
.getLogger(EmployeeController.class);
private Map<Integer, Employee> emps = null;
#Autowired
#Qualifier("employeeValidator")
private Validator validator;
#InitBinder
private void initBinder(WebDataBinder binder) {
binder.setValidator(validator);
}
public EmployeeController() {
emps = new HashMap<Integer, Employee>();
}
#ModelAttribute("employee")
public Employee createEmployeeModel() {
// ModelAttribute value should be same as used in the empSave.jsp
return new Employee();
}
#ModelAttribute("student")
public Student createStudentModel() {
// ModelAttribute value should be same as used in the empSave.jsp
return new Student();
}
#RequestMapping(value = "/emp/save", method = RequestMethod.GET)
public String saveEmployeePage(Model model) {
logger.info("Returning empSave.jsp page");
return "empSave";
}
#RequestMapping(value = "/emp/save.do", method = RequestMethod.POST)
public String saveEmployeeAction(
#ModelAttribute("employee") #Validated Employee employee,#ModelAttribute("student")Student student,
BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
logger.info("Returning empSave.jsp page");
return "empSave";
}
logger.info("Returning empSaveSuccess.jsp page");
model.addAttribute("emp", employee);
model.addAttribute("student", createStudentModel());
emps.put(employee.getId(), employee);
return "empSaveSuccess";
}
}
Validator:
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.journaldev.spring.form.model.Employee;
public class EmployeeFormValidator implements Validator {
//which objects can be validated by this validator
#Override
public boolean supports(Class<?> paramClass) {
return Employee.class.equals(paramClass);
}
#Override
public void validate(Object obj, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id", "id.required");
Employee emp = (Employee) obj;
if(emp.getId() <=0){
errors.rejectValue("id", "negativeValue", new Object[]{"'id'"}, "id can't be negative");
}
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "name.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "role", "role.required");
}
}
Stack Trace
java.lang.IllegalStateException: Invalid target for Validator [com.journaldev.spring.form.validator.EmployeeFormValidator#1625cd8]: com.journaldev.spring.form.model.Student#bd8550
org.springframework.validation.DataBinder.assertValidators(DataBinder.java:495)
org.springframework.validation.DataBinder.setValidator(DataBinder.java:486)
com.journaldev.spring.form.controllers.EmployeeController.initBinder(EmployeeController.java:38)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:606)
org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:215)
org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132)
org.springframework.web.method.annotation.InitBinderDataBinderFactory.initBinder(InitBinderDataBinderFactory.java:62)
org.springframework.web.bind.support.DefaultDataBinderFactory.createBinder(DefaultDataBinderFactory.java:53)
org.springframework.web.method.annotation.ModelFactory.updateBindingResult(ModelFactory.java:222)
org.springframework.web.method.annotation.ModelFactory.updateModel(ModelFactory.java:206)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.getModelAndView(RequestMappingHandlerAdapter.java:852)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:755)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:690)
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:83)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:945)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:876)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:961)
org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:852)
javax.servlet.http.HttpServlet.service(HttpServlet.java:621)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837)
javax.servlet.http.HttpServlet.service(HttpServlet.java:728)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
Based on the information provided try changing your InitBinder to following:
#InitBinder("employee")
private void initBinder(WebDataBinder binder) {
binder.setValidator(validator);
}
The reason this works is because of the value provided, in this case the value being "employee".
public abstract String[] value
The names of command/form attributes and/or request parameters that this init-binder method is supposed to apply to.
In your form you have multiple objects, without advising the #InitBinder what object to validate it attempted to validate your Student object as well, thus failing as it didn't meet the class requirements.
By specifying "employee" it basically ensured it only applied the validation against the Employee object.

Lazy loading of child throwing session error

I'm the following error when calling purchaseService.updatePurchase(purchase) inside my TagController:
SEVERE: Servlet.service() for servlet [PurchaseAPIServer] in context with path [/PurchaseAPIServer] threw exception [Request processing failed; nested exception is org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.app.model.Purchase.tags, no session or session was closed] with root cause
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.app.model.Purchase.tags, no session or session was closed
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:383)
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(AbstractPersistentCollection.java:375)
at org.hibernate.collection.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:368)
at org.hibernate.collection.PersistentSet.add(PersistentSet.java:212)
at com.app.model.Purchase.addTags(Purchase.java:207)
at com.app.controller.TagController.createAll(TagController.java:79)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:212)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:126)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:96)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:617)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:578)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:900)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:827)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:882)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:789)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:641)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:225)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:169)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:168)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:98)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:927)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:999)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:565)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:309)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
TagController:
#RequestMapping(value = "purchases/{purchaseId}/tags", method = RequestMethod.POST, params = "manyTags")
#ResponseStatus(HttpStatus.CREATED)
public void createAll(#PathVariable("purchaseId") final Long purchaseId, #RequestBody final Tag[] entities)
{
Purchase purchase = purchaseService.getById(purchaseId);
Set<Tag> tags = new HashSet<Tag>(Arrays.asList(entities));
purchase.addTags(tags);
purchaseService.updatePurchase(purchase);
}
Purchase:
#Entity
#XmlRootElement
public class Purchase implements Serializable
{
/**
*
*/
private static final long serialVersionUID = 6603477834338392140L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToMany(mappedBy = "purchase", fetch = FetchType.LAZY, cascade={CascadeType.ALL})
private Set<Tag> tags;
#JsonIgnore
public Set<Tag> getTags()
{
if (tags == null)
{
tags = new LinkedHashSet<Tag>();
}
return tags;
}
public void setTags(Set<Tag> tags)
{
this.tags = tags;
}
public void addTag(Tag tag)
{
tag.setPurchase(this);
this.tags.add(tag);
}
public void addTags(Set<Tag> tags)
{
Iterator<Tag> it = tags.iterator();
while (it.hasNext())
{
Tag tag = it.next();
tag.setPurchase(this);
this.tags.add(tag);
}
}
...
}
Tag:
#Entity
#XmlRootElement
public class Tag implements Serializable
{
/**
*
*/
private static final long serialVersionUID = 5165922776051697002L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumns({#JoinColumn(name = "PURCHASEID", referencedColumnName = "ID")})
private Purchase purchase;
#JsonIgnore
public Purchase getPurchase()
{
return purchase;
}
public void setPurchase(Purchase purchase)
{
this.purchase = purchase;
}
}
PurchaseService:
#Service
public class PurchaseService implements IPurchaseService
{
#Autowired
private IPurchaseDAO purchaseDAO;
public PurchaseService()
{
}
#Transactional
public List<Purchase> getAll()
{
return purchaseDAO.findAll();
}
#Transactional
public Purchase getById(Long id)
{
return purchaseDAO.findOne(id);
}
#Transactional
public void addPurchase(Purchase purchase)
{
purchaseDAO.save(purchase);
}
#Transactional
public void updatePurchase(Purchase purchase)
{
purchaseDAO.update(purchase);
}
}
TagService:
#Service
public class TagService implements ITagService
{
#Autowired
private ITagDAO tagDAO;
public TagService()
{
}
#Transactional
public List<Tag> getAll()
{
return tagDAO.findAll();
}
#Transactional
public Tag getById(Long id)
{
return tagDAO.findOne(id);
}
#Transactional
public void addTag(Tag tag)
{
tagDAO.save(tag);
}
#Transactional
public void updateTag(Tag tag)
{
tagDAO.update(tag);
}
}
Any ideas on how I can fix this? (I want to avoid using EAGER loading).
Do I need to setup some form of session management for transactions?
Thanks
Updates based on JB suggestions
TagController
#RequestMapping(value = "purchases/{purchaseId}/tags", method = RequestMethod.POST, params = "manyTags")
#ResponseStatus(HttpStatus.CREATED)
public void createAll(#PathVariable("purchaseId") final Long purchaseId, #RequestBody final Tag[] entities)
{
Purchase purchase = purchaseService.getById(purchaseId);
// Validation
RestPreconditions.checkRequestElementNotNull(purchase);
RestPreconditions.checkRequestElementIsNumeric(purchaseId);
Set<Tag> tags = new HashSet<Tag>(Arrays.asList(entities));
purchaseService.addTagsToPurchase(purchaseId, tags);
}
PurchaseService
#Transactional
public void addTagsToPurchase(Long purchaseId, Set<Tag> tags)
{
Purchase p = purchaseDAO.findOne(purchaseId);
p.addTags(tags);
}
You load the Purchase without initializing its tags collection and then, in your controller, once the transaction is committed and the session closed, you're adding a tag to this non-initialized collection. So obviously, this exception is thrown.
You have two possibilities:
Load the tags with the Purchase, either by calling Hibernate.initialize(purchase.getTags()), or by executing a JPQL query that loads the tag along with its tags, in a single query (select distinct p from Purchase p left join fetch p.tags).
Instead of having a service class that offers noting more than the DAO, add a real service method addTagsToPurchase(Long purchaseId, Set<Tag> tags) which reloads the purchase and adds the tags to this purchase. This is in fact the service method needed by your controller.
I of course prefer the second solution by far:
it shows why a service layer is useful,
it removes business code from the presentation layer to put it in the service layer,
it's more efficient
it's safer because it uses attached entities, and thus avoids the kind of exception you're having.
In general, if your controller, to execute a single atomic action, needs several calls to the service layer, it means that the service layer doesn't offer the needed functionality, and that the service is implemented in the presentation layer instead.

Resources