I'm a newbie in java spring. I have checked for solutions online and I can't seem to get my hands on anything helpful.
I have a form that has been bound to an Entity, however, I have a field in that form that was not bound, which I receive as a requestParam.
So when the field is submitted I need to validate that parameter to ensure it's not empty.
#PostMapping("/store")
public String store(#Valid Quote quote,BindingResult result, Model model,#RequestParam(required=false) String [] tagsFrom) {
if(result.hasErrors()) {
model.addAttribute("jsontags", returnTagsAsJson(tagRepo.findAll()));
return "admin/quotes/create";
}
List<Tag> listtags = new ArrayList<Tag> ();
for(String tag : tagsFrom) {
Tag theTag = new Tag();
theTag.setTag(tag);
theTag.setCreatedAt(new Date());
theTag.setUpdatedAt(new Date());
if(tagRepo.findByTag(tag) == null) {
tagRepo.save(theTag);
}
listtags.add(tagRepo.findByTag(tag));
}
quote.setTags(listtags);
quoteRepo.save(quote);
return "redirect:/dashboard/quotes";
}
What I have tried;
I created a custom validation and added the annotation to the parameter but that gave an error "The annotation #EmptyArrayString is disallowed for this location"
public String store(#Valid Quote quote,BindingResult result, Model model,
#RequestParam(required=false) #EmptyArrayString String [] tagsFrom)
I have tried using #NotEmpty on the parameter which throws NullPointerException
I need a solution that allows me to display the error on the HTML form like this
<span th:if="${#fields.hasErrors('tags')}"
th:errors="${quote.tags}" class="errors">
</span>
So when the field is submitted I need to validate that parameter to ensure it's not empty.
,#RequestParam(required=false) String [] tagsFrom
By default, required is set to true. So, if the URL must have the param, you shouldn't do required=false.
String [] tagsFrom implies you expect a bunch of tag params. But, is it of the form http://localhost:xxx?param=1,2,3 or
http://localhost:xxx?param1=1¶m2="stringvalue" ?
For the first one, you can have the mapping method as:
public String store(...#RequestParam List<String> param)
For the second one, you can do:
public String store(...#RequestParam Map<String,String> allQueryParameters)
You can then do your necessary validations.
Read more here
Related
I have a controller which will have 3 query strings.
Instead of having 3 fields in controller, I am defining them in class.
public class PassengerInformation{
String travellerAddress;
String travellerAge;
String travellerName;
}
Now in controller , I am able to accept them
#GetMapping("/passenger-info)
public TravelInformation getPassengerInfo(PassengerInformation info){
//Call a service
}
Now, this works as expected, if I pass the query string as is. eg: /passenger-info?travellerAge=21.
But, How do I accept the query parameter names different to it's corresponding fieldName.
I should be able to call it as below:
/passenger-info?traveller_age=21&traveller_name=JohnWick&traveller_address=ST.
Try to add the following constructor to your class
public class PassengerInformation{
String travellerAddress;
String travellerAge;
String travellerName;
#ConstructorProperties({"traveller_address", "traveller_age", "traveller_name"})
public PassengerInformation(String travellerAddress, String travellerAge, String travellerName) {
this.travellerAddress = travellerAddress;
...
}
}
The best you can do by the default features without any customisation is to use #ConstructorProperties :
public class PassengerInformation {
String travellerAddress;
String travellerAge;
String travellerName;
#ConstructorProperties({ "traveller_address", "traveller_age", "traveller_name" })
public PassengerInformation(String travellerAddress, String travellerAge, String travellerName) {
this.travellerAddress = travellerAddress;
this.travellerAge = travellerAge;
this.travellerName = travellerName;
}
}
This behaviour is mentioned at the docs as follows :
The #ModelAttribute parameter instance (i.e PassengerInformation)
is sourced in one of the following ways:
Retrieved from the model where it may have been added by a
#ModelAttribute method.
Retrieved from the HTTP session if the model attribute was listed in
the class-level #SessionAttributes annotation.
Obtained through a Converter where the model attribute name matches
the name of a request value such as a path variable or a request
parameter (see next example).
Instantiated using its default constructor.
Instantiated through a “primary constructor” with arguments that match
to Servlet request parameters. Argument names are determined through
JavaBeans #ConstructorProperties or through runtime-retained parameter
names in the bytecode.
The caveat here is that you need to make sure there are no default constructor in the PassengerInformation :
public class PassengerInformation {
public PassengerInformation(){}
}
I want to add a list of items to a Spring Model. It sometimes throws ConversionFailedException. The method in question:
private void addAuthoritiesToModel(Model model){
List<Authority> allAuthorities = Arrays.asList(
Authority.of("ROLE_ADMIN","Admin"),
Authority.of("ROLE_USER","User")
);
model.addAttribute("allAuthorities",allAuthorities);
}
The method throws on the last line. The curious thing it only does so, when called from a particular method and not others. For example, it works fine here:
#GetMapping("/users/new")
public String newUserForm(Model model){
model.addAttribute("user",User.blank());
model.addAttribute("newUser",true);
addAuthoritiesToModel(model);
return "user_details";
}
But it blows here:
#PostMapping(value = {"/users","/profile","/users/{any}"})
public String postUser(#Valid #ModelAttribute("user") User user,
BindingResult bindingResult,
#RequestParam("newPassword") Optional<String> newPassword,
#RequestParam("confirmPassword") Optional<String> confirmPassword,
RedirectAttributes redirectAttributes,
#PathVariable("any") String pathVariable
){
if(bindingResult.hasErrors()) {
if(user.getId()==null)
redirectAttributes.addAttribute("newUser",true);
addAuthoritiesToModel(redirectAttributes);
return "user_details";
}
...
}
I have tried exchanging Arrays.asList to another List implementation, but that doesn't solve the problem. And it wouldn't explain, why it doesn't work in the first case.
There is difference between Model and RedirectAttributes.
The values in RedirectAttributes are getting formatted as String.
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/support/RedirectAttributes.html
A specialization of the Model interface that controllers can use to select attributes for a redirect scenario. Since the intent of adding redirect attributes is very explicit -- i.e. to be used for a redirect URL, attribute values may be formatted as Strings and stored that way to make them eligible to be appended to the query string or expanded as URI variables in org.springframework.web.servlet.view.RedirectView.
You should not use unless required for redirecting and in such case should be string values.
I think in terms of REST, the ID should be placed into the URL, something like:
https://example.com/module/[ID]
and then I call GET, PUT, DELETE on that URL. That's kind of clear I think. In Spring MVC controllers, I'd get the ID with #PathVariable. Works.
Now, my practical problem with Spring MVC is, that if I do this, I have to NOT include the ID as part of the form (as well), Spring emits warnings of type
Skipping URI variable 'id' since the request contains a bind value with the same name.
otherwise. And it also makes kind of sense to only send it once, right? What would you do if they don't match??
That would be fine, but I do have a custom validator for my form backing bean, that needs to know the ID! (It needs to check if a certain unique name is already being used for a different entity instance, but cannot without knowing the ID of the submitted form).
I haven't found a good way to tell the validator that ID from #PathVariable, since the validation happens even before code in my controller method is executed.
How would you solve this dilemma?
This is my Controller (modified):
#Controller
#RequestMapping("/channels")
#RoleRestricted(resource = RoleResource.CHANNEL_ADMIN)
public class ChannelAdminController
{
protected ChannelService channelService;
protected ChannelEditFormValidator formValidator;
#Autowired
public ChannelAdminController(ChannelService channelService, ChannelEditFormValidator formValidator)
{
this.channelService = channelService;
this.formValidator = formValidator;
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.GET)
public String editChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") ChannelEditForm channelEditForm, Model model)
{
if (channelId > 0)
{
// Populate from persistent entity
}
else
{
// Prepare form with default values
}
return "channel/admin/channel-edit";
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
try
{
// Has to validate in controller if the name is already used by another channel, since in the validator, we don't know the channelId
Long nameChannelId = channelService.getChannelIdByName(channelEditForm.getName());
if (nameChannelId != null && !nameChannelId.equals(channelId))
result.rejectValue("name", "channel:admin.f1.error.name");
}
catch (EmptyResultDataAccessException e)
{
// That's fine, new valid unique name (not so fine using an exception for this, but you know...)
}
if (result.hasErrors())
{
return "channel/admin/channel-edit";
}
// Copy properties from form to ChannelEditRequest DTO
// ...
// Save
// ...
redirectAttributes.addFlashAttribute("successMessage", new SuccessMessage.Builder("channel:admin.f1.success", "Success!").build());
// POST-REDIRECT-GET
return "redirect:/channels/" + channelId + "/admin";
}
#InitBinder("channelForm")
protected void initBinder(WebDataBinder binder)
{
binder.setValidator(formValidator);
}
}
I think I finally found the solution.
As it turns out Spring binds path variables to form beans, too! I haven't found this documented anywhere, and wouldn't have expected it, but when trying to rename the path variable, like #DavidW suggested (which I would have expected to only have a local effect in my controller method), I realized that some things got broken, because of the before-mentioned.
So, basically, the solution is to have the ID property on the form-backing object, too, BUT not including a hidden input field in the HTML form. This way Spring will use the path variable and populate it on the form. The local #PathVariable parameter in the controller method can even be skipped.
The cleanest way to solve this, I think, is to let the database handle the duplicates: Add a unique constraint to the database column. (or JPA by adding a #UniqueConstraint)
But you still have to catch the database exception and transform it to a user friendly message.
This way you can keep the spring MVC validator simple: only validate fields, without needing to query the database.
Could you not simply disambiguate the 2 (URI template variables vs. parameters) by using a different name for your URI template variable?
#RequestMapping(value = "/{chanId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long chanId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
[...]
What ever you said is correct the correct way to design rest api is to mention the resource id in path variable if you look at some examples from the swagger now as open api you could find similar examples there
for you the correct solution would be to use a custom validator like this
import javax.validation.Validator;`
import org.apache.commons.lang3.StringUtils;`
import org.springframework.validation.Errors;`
importorg.springframework.validation.beanvalidation.CustomValidatorBean;`
public class MyValidator extends CustomValidatorBean {`
public void myvalidate(Object target,Errors errors,String flag,Profile profile){
super.validate(target,errors);
if(StringUtils.isEmpty(profile.name())){
errors.rejectValue("name", "NotBlank.profilereg.name", new Object[] { "name" }, "Missing Required Fields");
}
}
}
This would make sure all the fields are validated and you dont need to pass the id in the form.
I am trying to reload the same page with different content that varies depending on which link is clicked on the page. The url pattern for the page is "/owners", which triggers the running of this method in OwnerController.java:
#RequestMapping(value = "/owners", method = RequestMethod.GET)
public String processFindForm(Owner owner, BindingResult result, Map<String, Object> model) {
Collection<Owner> results = this.clinicService.findOwnerByLastName("");
model.put("selections", results);
model.put("sel_owner",this.clinicService.findOwnerById(ownerId));//ownerId is not defined yet
return "owners/ownersList";
}
The jsp includes a dynamically generated list of ownerId integer values, each of which can be used to send a unique ownerId back to the server. What do I need to add to my jsp in order to get ownerId to have a user-specified value when processFindForm() is run? In other words, what does the hyperlink need to look like?
You are using GET request type.If you want to send any parameter to controller then you have to use either #RequestParam or #PathParam annotations based on requirement as an arguments to controller method. In your case it will be something like...
public String processFindForm(#RequestParam("ownerID") String ownerId, BindingResult result, Map<String, Object> model) {... }
Take a look on this link as well: http://www.byteslounge.com/tutorials/spring-mvc-requestmapping-example
I am using Spring SimpleFormController for my forms and for some reason it won't go to the onSubmit method
Here's my code:
public class CreateProjectController extends SimpleFormController {
ProjectDao projectDao;
public CreateProjectController() {
setCommandClass(Project.class);
setCommandName("Project");
setSessionForm(true);
}
#Override
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
String id = request.getParameter("id");
Project project = projectDao.getProjectByOutsideId(id);
System.out.println("#formbacking object method");
System.out.println("the success view is "+getSuccessView());
return project;
}
#Override
protected ModelAndView onSubmit(Object command) throws Exception {
Project project = (Project) command;
System.out.println("this is the project title: "+project.getTitle());
System.out.println("the success view is "+getSuccessView());
projectDao.insert(project);
return new ModelAndView(getSuccessView());
}
I know because it prints "#formbacking object method" string but not the "the success view is..." string and the :"this is the pr..." string. I see "#formback.." string in the console but not the last two whenever I hit submit. I don't know where the problem is.
This is my jsp
<form:form method="POST" commandName="Project">
Name: <form:input path="title"/><br/>
Description: <form:input path="description"/><br/>
Link: <form:input path="url" disabled="true"/><br/>
Tags: <form:input path="tags"/><br/>
Assessors <form:input path="assessors"/><br/><br/>
<input type="submit" value="submit"/>
</form:form>
I am running on Google App Engine btw. Maybe the problem is there?
UPDATE: The problem seems to be with the formBackingObject method. When I removed it, the form now goes to the onSubmit when I click submit.
But I'd like to have values from of the command class from the database in my forms.
Another piece of code that doesn't work:
#Override
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
String id = request.getParameter("id");
Project projectFromConsumer = projectDao.getProjectByOutsideId(id);
Project project = new Project();
String title = projectFromConsumer.getTitle();
project.setTitle(title);
project.setUrl("projectUrl");
return project;
}
but this does work:
#Override
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
String id = request.getParameter("id");
Project projectFromConsumer = projectDao.getProjectByOutsideId(id);
Project project = new Project();
String title = projectFromConsumer.getTitle();
project.setTitle("projectTitle");
project.setUrl("projectUrl");
return project;
}
Now I am really confused. haha.
I was thinking along the same lines as axtavt. You are only going to have an id request parameter on updates, so you should add some code for creation forms:
FYI, formBackingObject requires a non-null object to be returned. To save some memory, you can have a final constant member variable that is the default return value. Your code satisfies this though since you're transferring objects, but I don't get why you're transferring data (creating an extra object) when you're not using a DTO. You could simply do this:
private final static Project PROJECT_INSTANCE = new Project();
static {
PROJECT_INSTANCE.setTitle("defaultProjectTitle");
}
#Override
protected Project formBackingObject(HttpServletRequest request) throws Exception {
String id = request.getParameter("id");
if(id == null || id.trim().length() == 0 || !id.matches("\\d+")) {
return PROJECT_INSTANCE;
}
return projectDao.getProjectByOutsideId(id);
}
You don't need a hidden id input field. You would use formBackingObject() for initializing the form input fields for updating (by navigating to page.jsp?id=111).
Look at the String id = request.getParameter("id");. There is no such field in your form, so probably you get an error there during submit process, maybe, getProjectByOutsideId returns null.
P.S. It's strange that your formBackingObject is executing when you press submit, it shouldn't if you really set setSessionForm(true).
Try turning the spring debugging up. It provides a lot of information, which can be helpful. Do this by editing the log4j.properties file.
log4j.logger.org.springframework=DEBUG
Have you added logging to make sure the formBackingObject is returning something?
System.out.println("#formbacking object method is returning: " + project);
It will make sure something is being returned. In general the formBackingObject should always return something.
EDIT:
Id is not being passed during submission in the snippet. Maybe it is during the load, e.g. /page.do?id=4, but it doesn't appear in the form.
Add <form:hidden path="id"/> to your form during on submit. Otherwise the id will not be a parameter and the getProjectByOutsideId will fail.