Values of #PathVariable and #ModelAttribute overlapping - spring

I have an User object stored in the session with #SessionAttributes. And a straight-forward method decorated with #ModelAttribute in order to initialize it whenever the session's value is null.
User class:
#Entity
#Table( name="USER")
public class User implements java.io.Serializable {
private Long id;
private String username;
private String password;
....
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name ="ID")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
...
Controller:
#RequestMapping("/item")
#Controller
#SessionAttributes({"user"})
public class MyController {
#ModelAttribute method:
#ModelAttribute("user")
public User createUser(Principal principal) {
return userService.findByUsername(principal.getName());
}
It all seems to work as expected except in this particular method:
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public String showItem(#PathVariable("id") Long id, #ModelAttribute("user") User user,
Model uiModel) {
...
}
The problem is that User.id is being set with #PathVariable("id"). I believe I ran into this with #RequestParam too. I'm assuming that's because both have the same name and type. After reading Spring's documentation (see below) I'm assuming this is expected behavior:
The next step is data binding. The WebDataBinder class matches request parameter names — including query string parameters and form fields — to model attribute fields by name. Matching fields are populated after type conversion (from String to the target field type) has been applied where necessary.
However, I would think this scenario is fairly common, how are other people handling this? If my findings are correct and this is expected behavior (or bug), this seems to be very error prone.
Possible solutions:
Change #PathVariable("id") to #PathVariable("somethingElse"). Works but it's not as straightforward with #RequestParam (e.g. I don't know how to change jqgrid's request parameter id to something else but this is another issue).
Change #PathVariable("id") type from Long to Int. This will make User.id and id types differ but the cast to Long looks ugly :)
Don't use #ModelAttribute here and query the DB for User again. Not consistent with other methods and involves redundant DB calls.
Any suggestions?

How about this approach -
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public String showItem(#PathVariable("id") Long id,
Model uiModel) {
User user = (User)uiModel.asMap().get("user");
...
}

use #SessionAttribute
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public String showItem(#PathVariable("id") Long id, #SessionAttribute("user") User user,
Model uiModel) {
...
}

Related

Spring Data Audit, CreatedBy lost on Update [duplicate]

I am using the auditing capabilities of Spring Data and have a class similar to this:
#Entity
#Audited
#EntityListeners(AuditingEntityListener.class)
#Table(name="Student")
public class Student {
#Id
#GeneratedValue (strategy = GenerationType.AUTO)
private Long id;
#CreatedBy
private String createdBy;
#CreatedDate
private Date createdDate;
#LastModifiedBy
private String lastModifiedBy;
#LastModifiedDate
private Date lastModifiedDate;
...
Now, I believe I have configured auditing fine because I can see that createdBy, createdDate, lastModifiedBy and lastModifiedDate all are getting the correct values when I update the domain objects.
However, my problem is that when I update an object I am losing the values of createdBy and createdDate. So, when I first create the object I have all four values, but when I update it createdBy and createdDate are nullified ! I am also using the Hibernate envers to keep a history of the domain objects.
Do you know why do I get this behavior ? Why do createdBy and createdDate are empty when I update the domain object ?
Update: To answer #m-deinum 's questions: Yes spring data JPA is configured correctly - everything else works fine - I really wouldn't like to post the configuration because as you udnerstand it will need a lot of space.
My AuditorAwareImpl is this
#Component
public class AuditorAwareImpl implements AuditorAware {
Logger logger = Logger.getLogger(AuditorAwareImpl.class);
#Autowired
ProfileService profileService;
#Override
public String getCurrentAuditor() {
return profileService.getMyUsername();
}
}
Finally, here's my update controller implementation:
#Autowired
private StudentFormValidator validator;
#Autowired
private StudentRepository studentRep;
#RequestMapping(value="/edit/{id}", method=RequestMethod.POST)
public String updateFromForm(
#PathVariable("id")Long id,
#Valid Student student, BindingResult result,
final RedirectAttributes redirectAttributes) {
Student s = studentRep.secureFind(id);
if(student == null || s == null) {
throw new ResourceNotFoundException();
}
validator.validate(student, result);
if (result.hasErrors()) {
return "students/form";
}
student.setId(id);
student.setSchool(profileService.getMySchool());
redirectAttributes.addFlashAttribute("message", "Επιτυχής προσθήκη!");
studentRep.save(student);
return "redirect:/students/list";
}
Update 2: Please take a look at a newer version
#RequestMapping(value="/edit/{id}", method=RequestMethod.GET)
public ModelAndView editForm(#PathVariable("id")Long id) {
ModelAndView mav = new ModelAndView("students/form");
Student student = studentRep.secureFind(id);
if(student == null) {
throw new ResourceNotFoundException();
}
mav.getModelMap().addAttribute(student);
mav.getModelMap().addAttribute("genders", GenderEnum.values());
mav.getModelMap().addAttribute("studentTypes", StudEnum.values());
return mav;
}
#RequestMapping(value="/edit/{id}", method=RequestMethod.POST)
public String updateFromForm(
#PathVariable("id")Long id,
#Valid #ModelAttribute Student student, BindingResult result,
final RedirectAttributes redirectAttributes, SessionStatus status) {
Student s = studentRep.secureFind(id);
if(student == null || s == null) {
throw new ResourceNotFoundException();
}
if (result.hasErrors()) {
return "students/form";
}
//student.setId(id);
student.setSchool(profileService.getMySchool());
studentRep.save(student);
redirectAttributes.addFlashAttribute("message", "Επιτυχής προσθήκη!");
status.setComplete();
return "redirect:/students/list";
}
This still leaves empty the createdBy and createdDate fields when I do an update :(
Also it does not get the School value (which is not contained in my form because it is related to the user currently editing) so I need to get it again from the SecurityContext... Have I done anything wrong ?
Update 3: For reference and to not miss it in the comments: The main problem was that I needed to include the #SessionAttributes annotation to my controller.
Use updatable attribute of #Column annotation like below.
#Column(name = "created_date", updatable = false)
private Date createdDate;
This will retain the created date on update operation.
Your method in your (#)Controller class is not that efficient. You don't want to (manually) retrieve the object and copy all the fields, relationships etc. over to it. Next to that with complex objects you will sooner or alter run into big trouble.
What you want is on your first method (the GET for showing the form) retrieve the user and store it in the session using #SessionAttributes. Next you want an #InitBinder annotated method to set your validator on the WebDataBinder so that spring will do the validation. This will leave your updateFromForm method nice and clean.
#Controller
#RequestMapping("/edit/{id}")
#SessionAttributes("student")
public EditStudentController
#Autowired
private StudentFormValidator validator;
#Autowired
private StudentRepository studentRep;
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.setValidator(validator);
}
#RequestMapping(method=RequestMethod.GET)
public String showUpdateForm(Model model) {
model.addObject("student", studentRep.secureFind(id));
return "students/form";
}
#RequestMapping(method=RequestMethod.POST)
public String public String updateFromForm(#Valid #ModelAttribute Student student, BindingResult result, RedirectAttributes redirectAttributes, SessionStatus status) {
// Optionally you could check the ids if they are the same.
if (result.hasErrors()) {
return "students/form";
}
redirectAttributes.addFlashAttribute("message", "?p?t???? p??s????!");
studentRep.save(student);
status.setComplete(); // Will remove the student from the session
return "redirect:/students/list";
}
}
You will need to add the SessionStatus attribute to the method and mark the processing complete, so that Spring can cleanup your model from the session.
This way you don't have to copy around objects, etc. and Spring will do all the heave lifting and all your fields/relations will be properly set.
In my case #CreatedDate and #CreatedBy fields were not removed from databse during update, but were not queried by #Repository findOne method.
Changing
#Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
into
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
on #Entity class helped with that.

Find the updated fields on update request

I have a user entity as
#Entity
public class User {
#Id
private Long id;
private String name;
private String address;
// getter / setter
}
And controller method like:
#PutMapping(value = "/user")
public ResponseEntity<?> updateUser(#RequestBody User user) {
userRepository.save(user);
// ...
}
Now What I am trying to do is finding the field that is being updated.
Example:
If the only name is present I need some message like "Updated field is name".
Is there any better way other than comparing the fields one by one with the database stored values.
you need getUser method. for example: userRepository.getUser(user.id)
then you return the result

Spring Request Mapping post vs put, same method, same logic, but

I have a 2 method:
first one create product:
#RequestMapping(method = RequestMethod.POST)
public ResponseEntity<?> create(#Validated ProductDTO productDTO){
productService.addProduct(productDTO);
return new ResponseEntity<>("Maxsulot ro'yhatga qo'shildi", HttpStatus.OK);
}
another one update product:
#RequestMapping(method = RequestMethod.PUT)
public ResponseEntity<?> update(#Validated ProductDTO productDTO){
productService.update(productDTO);
return new ResponseEntity<>("Maxsulot ma'lumotlari yangilandi", HttpStatus.OK);
}
Now, I am surprized that, if I sent same data post method works fine(screen1), but put(screen2) method return validation error.
screen1(post)
screen2(put)
What the problem is?
MyDTO class:
public class ProductDTO {
private Long id;
private MultipartFile file;
#NotNull
#Size(min = 2, max = 50)
private String productName;
#NotNull
private Long productPrice;
private String productInfo;
#NotNull
private Long categoryId;
private String unitOfMeasurement;
// getters and setters
}
I can see you have #Validated that should validate your request body according to JSR-303.
Seems like it is not consistent when you POST and PUT. It validates/not validating and return an error because your body does not match the validation rules you placed on ProductDTO.
In all the docs I saw you should do something like #Valid #RequestBody instead of just putting #Validated.
Try to change it to the above and see if it now work more consistently.

What does {id} mean/do in #RequestMapping(value = "/delete/{id}")

I'm currently in the process of learning Spring and I keep bumping into that in various examples but I haven't found any explanation anywhere
I've also found
return "redirect:/delete/{id}"
I understand id is a variable, but what does it do. what is the difference between
#RequestMapping(value = "/delete/{id}")
and
#RequestMapping(value = "/delete")
Taken from this example http://www.javainterviewpoint.com/spring-mvc-crud-example-mysql/
Controller
#RequestMapping(value = "/delete/{id}")
public ModelAndView deleteEmployee(#ModelAttribute("employee") Employee employee,#PathVariable("id") int id)
{
employeeDAO.deleteEmployee(id);
return new ModelAndView("redirect:/employees");
}
Model
package com.javainterviewpoint;
import java.io.Serializable;
public class Employee implements Serializable
{
private static final long serialVersionUID = -1280037900360314186L;
private Integer id;
private String name;
private Integer age;
private String dept;
public Employee()
{
super();
}
public Employee(Integer id, String name, Integer age, String dept)
{
super();
this.id = id;
this.name = name;
this.age = age;
this.dept = dept;
}
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
#RequestMapping(value = "/delete/{id}") mean that you can delete employee with specifc id. The full request can be for example: http://yourSite.com/delete/42
Then you can see #PathVariable("id") int id - it's mean variable id will be contain value from url.
With this aproach you can do something like this:
/doSomething/{someUserName}/{someValue}/{someId}
and you will have:
#PathVariable("someUserName") String someUserName, #PathVariable("someValue") String id, #PathVariable("id") int id
Another example:
It's a PathVariable and you can use it for example when you have users and you to need to edit or delete one user. This will tell to spring method on which user you have clicked.
You can read the section URI Template Pattern of Spring MVC guide for more clarifications.
With this approach you attach the id of the user to the url you are calling, and Spring will map the id found in url to the variable you define in method deleteEmployee (#PathVariable("id") int id)
For example you can invoke deleteEmployee method with this url:
http://yourUrl/delete/12345
Then employeeDAO.deleteEmployee(id); call will be executed with 12345 as id

Spring - How to use #RequestParam parameter of a Controller in a Custom Validator class?

I've got a problem about validation in Spring MVC with Hibernate.
I want a validator that valid user input, but the validation must be done out of the controller, so, in a separate validation class.
The situation: this is the head of my controller in which I want to do the validation. I need that id to retrieve a list of Booking of a specific car.
#PostMapping(value = "/rent")
public ModelAndView vehicleRent(#ModelAttribute("newBooking") Booking booking, BindingResult bindingResult, #RequestParam("id") long id) {
But if i want to separate the logic out of this controller creating a custom validator, i have this result:
public class BookingValidator implements Validator {
#Autowired
VehicleBO vehicleBo;
#Override
public boolean supports(Class<?> type) {
return Booking.class.isAssignableFrom(type);
}
#Override
public void validate(Object o, Errors errors) {
Booking booking = (Booking) o;
//other code
rejectIfBookingExists(booking, 0, errors, "validation.booking.startdate.exists");
}
}
public boolean rejectIfBookingExists(Booking booking, long id, Errors errors, String key){
boolean exists = false;
List<Booking> vehicleBookings = vehicleBo.getVehicleBookings(id);
if (booking != null || booking.getStartDate() != null || booking.getFinishDate() != null) {
for (Booking b : vehicleBookings) {
if (booking.getStartDate().before((b.getFinishDate())) || booking.getStartDate().equals(b.getFinishDate())) {
errors.rejectValue("startDate", key);
exists = true;
break;
}
}
}
return exists;
}
}
In this way I cannot retrieve the list because i don't have the required id, could you explain me how to do that? Or,there are other ways to solve this problem?
Thanks!
EDIT:
This is the Booking class, as you can see it has a Vehicle object mapped inside
#Entity
public class Booking implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne
#JoinTable(name="user_booking", joinColumns={#JoinColumn(name ="booking_id", referencedColumnName ="id")},
inverseJoinColumns={#JoinColumn(name ="user_id", referencedColumnName ="id")})
private User user;
#ManyToOne
#JoinColumn(name = "vehicle_id")
private Vehicle vehicle;
#DateTimeFormat(pattern = "dd/MM/yyyy")
private Date startDate;
#DateTimeFormat(pattern = "dd/MM/yyyy")
private Date finishDate;
public Booking() {
//getter and setter and other code
}
Any ideas?
Why don't you simply map the vehicle id as booking.vehicle.id in your form? Provided Vehicle has a no-arg constructor (which it probably does, being an entity), the Booking should come back in the POST request handler with an instantiated Vehicle, along with its id property set. You should then be able to access booking.vehicle.id from wihtin the validator.
You can use an input[type=hidden] for the booking.vehicle.id field. In your GET request for the view with the form, simply inject the vehicle id as a #PathVariable and copy it to your model, so that you could reference the value inside the form.

Resources