I am trying Spring MVC example. I have a testBean which has List type variable like
private List<HashMap<String, String>> books;
In my controller I have
#RequestMapping(value = "/booksList", method = RequestMethod.POST)
public String displayBooks(#ModelAttribute TestBean testBean, Model model, HttpSession session) {
// some code here
}
In my jsp page I have
<form:form action="booksList.html" method="post" modelAttribute="testBean">
<form:hidden path="books" />
<input type="submit" value="submit">
</form:form>
When I submit this form I am getting this error
Cannot convert value of type [java.lang.String] to required type [java.util.HashMap] for property 'books[0]'.
How can I solve this error ? please help
Updated:
I have seen that I can do this way also.
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(List.class, "testBean", new CustomCollectionEditor(List.class) {
#Override
protected Object convertElement(Object element) {
TestBean testBean = new TestBean();
if (element != null) {
List<HashMap<String, String>> id = (List<HashMap<String, String>>) element;
testBean.setFilters(id);
}
return testBean;
}
});
}
But I don't understand the above method fully there may be some mistakes in what I have written. And I don't know how and from where I can call convertElement(Object element) method. i do understand initBinder(WebDataBinder binder) will call jsut before my controller method public String displayBooks(...). Even I don't know if its the right way of doing this.
As you have it above, you're trying to represent an extremely complex type (a List of Maps) with a single text field. Your first step should be working out how you want to display that information in a web form. Then try using "canned data" (e.g. a TestBean that you've loaded up with fake books) and see if your form looks how you expect it to.
You might find it easier if you change the way your books collection is stored. Having such a complex type as part of a "bean" while allowed, is probably not recommended. Consider using a List<BookBean> where a BookBean holds a collection of BookDetailBean, and a BookDetailBean holds that String-to-String relationship that you previously had in your Map.
Related
Having this handler method:
#RequestMapping(method = RequestMethod.POST, value = "/myform1")
public String formPost(#ModelAttribute("myModel") #Valid MyModel myModel, BindingResult binder)
how can I ignore errors on certain (or all) fields?
Even if I omit the #Valid annotation, it still detects errors like "abc entered into a Number field" (ie binder.hasErrors() returns true). And the the error message (from the catalog) is displayed on the final web page, which I do not want.
If I omit the BindingResult binder, then the code never reaches this method but throws an exception/error before it.
Is there a #DontValidateAtAll annotation or some other method to achieve this?
Related problem: I can not override the bad value in the method, it keeps showing the old rejected value. For example, user enters "abc" into myModel.someNumber and submits the form, then even if I do myModel.setSomeNumber(22) in the method formPost(), after returning from it, the web page (JSP) will show "abc" (and the error text) instead of 22 in that field.
For the specific String-to-Number conversion exception you were referring to, you can use the below manipulation. This is a type conversion exception that occurs even before Spring MVC's form validation, ie, even before the validate() method.
If your only purpose is to NOT see the errors on your final web page, then you can write extra code in your Controller method.
#RequestMapping(method = RequestMethod.POST, value = "/myform1")
public ModelAndView formPost(#ModelAttribute("myModel") #Valid MyModel myModel, BindingResult binder){
List<ObjectError> errors = bindingResult.getAllErrors();
boolean hasIgnorableErrorsOnly = true;
if(bindingResult.hasErrors()){
for(ObjectError error : errors){
if(error.getCode().equals("myModel.someNumber.NotANumber")){ //or whatever your error code is
}else{
hasIgnorableErrorsOnly = false;
}
}
}
if(hasIgnorableErrorsOnly){
//You have not shown where your final view is. I am assuming your myModel form view is myform.jsp and final web page is myModel.jsp
// Notice that I have also changed this method signature to return ModelAndView instead of only String view.
if(myModel.getSomeNumber() == null)
myModel.setSomeNumber(22);
return new ModelAndView("myModel.jsp", myModel); //Take care of view Resolvers here
}else{
return new ModelAndView("myform.jsp", myModel); //Take care of view Resolvers here
}
}
Now, if your BindingResult has more than ignorable errors, then it would go to myModel.jsp and I believe you already have code in place for display of errors. But if, because of above code, you are forwarded to myModel.jsp, you would have to iterate over the ${errors} key on your jsp and write the someNumber element so that it does not show errors. For example,
<spring:hasBindErrors name="myModel">
<c:forEach items="${errors.allErrors}" var="error">
<c:if test="${error.code eq 'myModel.someNumber.NotANumber'}">
//skip your display of global errors
</c:if>
</c:forEach>
</spring:hasBindErrors>
This works:
add a (Model)Map parameter to the handler method (it is usually used anyway, I omitted it in the question for brevity)
overwrite the model attribute with a fresh copy
Code:
#RequestMapping(method = RequestMethod.POST, value = "/myform1")
public String formPost(#ModelAttribute("myModel") #Valid MyModel myModel, BindingResult binder, Map<String, Object> modmap) {
if(ignore_errors) {
modmap.put("myModel", new MyModel());
return "myForm.jsp";
} // else ... other things
}
Apparently this procedure makes the framework to "forget" about the validation errors.
Note: I use Spring 3.0.x, other versions might behave differently.
I'm developing a registration flow where user comes and fills 5 pages to complete a process. I decided to have multiple views and one controller and a ProcessNext action method to go step by step. Each time Process Next gets called it gets the origin view and next view. Since each view associated with there own view model i have created a base view model which all view specific view model derived from. Now the issue is, casting is throwing an exception.. here is the sample code
Base View Model
public class BaseViewModel
{
public string viewName;
}
Personal View Model
public class PersonalViewModel : BaseViewModel
{
public string FirstName;
// rest properties comes here
}
Index.cshtml
#Model PersonalViewModel
#using (Html.BeginForm("ProcessNext", "Wizard", FormMethod.Post, new { class = "form-horizontal", role = "form" }))
#Html.TextBoxFor(m => m.FirstName, new { #class = "form-control" })
<input type="submit" class="btn btn-default" value="Register" />
Basically, I'm binding the view with PersonalViewModel here
Now in Controller ProcessNext Action method looks like this.
public ActionResult ProcessNext(BaseViewModel viewModelData)
{
PersonalViewModel per = (PersonalViewModel) viewModelData;
}
This is failing and throwing a type case exception, why?..
My idea is to use only one action method to transform all these derived view model and send to a common class to validate and process. Please help me to get through this issue.. Thanks!
The reason that you see this exception is that your model type is BaseViewModel and not PersonalViewModel. Model binder is the one that creates a model and since your action's model is BaseViewModel it creates a BaseViewModel object.
I would recommend you to create separate actions for each one of your steps. Each action should have its corresponding model. I also think that you should prefer with composition instead of inheritance in this case.
public class FullModel
{
public FirstStepModel FirstStep {get;set;}
public SecondStepModel SecondStep {get;set;}
}
Then once you start your flow (on a first step for example) you can create a FullModel object and store it somewhere (session/cookie/serialize into a text and send to client - it is really up to you).
Then in controller you will have
[HttpGet]
public ActionResult ProcessFirst()
{
HttpContext.Session["FullModel"] = new FullModel(); //at the beginning store full model in session
var firstStepModel = new FirstsStepModel();
return View(firstStepModel) //return a view for first step
}
[HttpPost]
public ActionResult ProcessFirst(FirstStepModel model)
{
if(this.ModelState.IsValid)
{
var fullModel = HttpContext.Session["FullModel"] as FullModel; //assuming that you stored it in session variable with name "FullModel"
if(fullModel == null)
{
//something went wrong and your full model is not in session..
//return some error page
}
fullModel.FirstStep = model;
HttpContext.Session["FullModel"] = fullModel; // update your session with latest model
var secondStepModel = new SecondStepModel();
return View("SecondStepView", secondStepModel) //return a view for second step
}
// model is invalid ...
return View("FirstStepView", model);
}
[HttpPost]
public ActionResult ProcessSecond(SecondStepModel model)
{
var fullModel = HttpContext.Session["FullModel"] as FullModel; //assuming that you stored it in session variable with name "FullModel"
if(fullModel == null)
{
//something went wrong and your full model is not in session..
//return some error page
}
fullModel.SecondStep = model;
HttpContext.Session["FullModel"] = fullModel; // update your session with latest model
var thirdStepModel = new ThirdStepModel();
return View("ThirdStepModel", thirdStepModel); //return a view for a third step
}
Of course you should extract all the shared code to some reusable method.
And it is entirely up to you what persistence technique to use for passing FullModel between the request.
If you still prefer to go with one Action solution you need to create a custom model binder that is going create derived instances based on some data that is passed from the client. Take a look at this thread
I figured it out a generic way to handle this situation using Model Binders. Here it is..
You might need to have a extended model binder from DefaultBinder to implement to return your model type.
public class WizardModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var viewIdContext = bindingContext.ValueProvider.GetValue("ViewId");
int StepId = 0;
if (!int.TryParse(viewIdContext, out StepId))
throw new InvalidOperationException("Incorrect view identity");
//This is my factory who gave me child view based on the next view id you can play around with this logic to identify which view should be rendered next
var model = WizardFactory.GetViewModel(StepId);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, model.GetType());
bindingContext.ModelMetadata.Model = model;
return model;
}
}
You would register this binder from your gloab asx like
ModelBinders.Binders.Add(typeof(BaseViewModel), new WizardModelBinder());
Thanks to all who responsed to my query..!! Let me know if you guys have any questions.
Happy coding..!!
I have following POJO class as an input -
public class Input implements java.io.Serializable {
private String id;
private List<Inputbenefit> Inputbenefits;
//and getter and setter method
}
Now In the controller i have initbinder -
#InitBinder
public void initBinder(Object target,WebDataBinder binder) {
binder.registerCustomEditor(ArrayList.class, new CustomCollectionEditor(ArrayList.class) {
#Override
protected Object convertElement(Object element) {
Input input= new Input ();
if (element != null) {
ArrayList<Inputbenefit> id = (ArrayList<Inputbenefit>) element;
input.setInputbenefits(id);
}
return input;
}
});
Post method signature in controller is -
#RequestMapping(value = "/addDependentOutput.html", method = RequestMethod.POST)
public String OutputForm(#ModelAttribute("Input") Input input, BindingResult result, Model model)
In JSP - I have 5 fixed text boxes to take value for Input class -
<spring:bind path="inputbenefits.benefitId">
<form:input path="${status.expression}" size="10" value="Manisha"/>
</spring:bind>
I am not getting the values from html form to OutputForm i.e. not able to read the value of inputbenefits.benefitId filed in controller post method OutputForm.
In short - my List object values are not getting passed to controller method.
Please help. Thanks.
You shouldn't return host object from converElement method, you should return your collection object instead
#Override
protected Object convertElement(Object element) {
int benefitId = Integer.parseInt(element.toString());
return benefitService.getById(benefitId);
}
UPDATE
I suppose you trying to bind collection on benefits to your Input object and you have a form to create new Input and select control to select desired benefits. Right? If true, you need something like this
Use form spring tags
<form:select path="inputBenefits" items="${benefits}"
multiple="multiple" size="5" itemLabel="additionalAmt" itemValue="benefitId"/>
<form:errors path="inputBenefits"/>
where ${benefits} collection of available benefits that you path to the page
In your controller you need
#InitBinder
public void initBinder(ServletRequestDataBinder binder) {
binder.registerCustomEditor(List.class, "inputBenefits", new CustomCollectionEditor(List.class) {
protected Object convertElement(Object element) {
if (element != null) {
Integer benefitId = Integer.parseInt(element.toString());
Benefit benefit = benefitService.getById(benefitId); // something that able to get benefit object
return benefit;
}
return null;
}
});
}
This way method convertElement will be called for every value selected on page select control. After that collection of benefits will be pushed to command form object (Input i suppose)
simple example here http://www.tutorialspoint.com/spring/spring_mvc_form_handling_example.htm
I have a problem with a validation error displayed when i submit my form with an empty date like that in the resulting web page:
Failed to convert property value of type java.lang.String to required type
java.util.Date for property dateFin; nested exception is
java.lang.IllegalArgumentException: Could not parse date: Unparseable date: ""
My controller looks like this:
#Controller
#SessionAttributes
#Lazy
public class MyController extends AbstractMVPAction {
#RequestMapping(value = "/secured/cp/saveProgram")
public String enregistrerProgramme(#ModelAttribute Program program,
BindingResult bindingResult, ModelMap model){
if(bindingResult.hasErrors()){
model.put("program", program);
return "/secured/cp/showProgram"
}else{
// ... saves the programme
model.put("program", null);
return "/secured/cp/backToOtherPage"
}
}
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Date.class, new CustomDateEditor(
new SimpleDateFormat("dd/MM/yyyy"), false));
}
}
When I debug my method, I can see my object is fine, the modif I did are well reported, the date is null, but the bindingResult.hasErrors() returns true and according to me it shouldn't.
I used to have some validation annotations in the Program object and a #Valid annotation but I removed them all and still have the problem.
I have read lot's of similar issues and every time the solution is the #InitBinder/CustomDateEditor.
So it is there and I guess it's working, the dates are displayed the way I want (this was not the case before I add it) and I can submit them provided it's not empty.
Thank's in advance, I'm starting to go crazy...
You've constructed a CustomDateEditor that explicitly disallows the empty string. Check the Javadoc for the constructor you're using, when the boolean argument is false, passing an empty string to that editor causes the IllegalArgumentException you're seeing.
Try this:
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Date.class, new CustomDateEditor(
new SimpleDateFormat("dd/MM/yyyy"), false));
}
Let me introduce my issue by providing some of the code in question.
First my form object:
public class OrgChartForm {
List<OrgChartFormElement> orgChartFormElements;
public OrgChartForm() {
orgChartFormElements = new ArrayList<OrgChartFormElement>();
}
private OrgChartFormElement createOrgChartFormElementFromMprsStructureYear(MprsStructureYear structureYear){
OrgChartFormElement element = new OrgChartFormElement();
element.set.... // populate element based on attribute values from structureYear param
return element;
}
public void createOrgChartFormElements(List<MprsStructureYear> structureYears) {
orgChartFormElements = new ArrayList<OrgChartFormElement>();
for(MprsStructureYear structureYear:structureYears){
orgChartFormElements.add(createOrgChartFormElementFromMprsStructureYear(structureYear));
}
}
// expected getters and setters
}
The form contains a simple list of OrgChartFormElements
public class OrgChartFormElement {
private boolean selected;
private String elementLabel;
private Long id;
//default constructor, getters and setters
}
I am using context:component-scan and mvc:annotation-driven, so my controller looks like:
#Controller
public class OrganisationStatusController{
#Autowired(required=true)
// dependencies here
#RequestMapping(value="/finyear/{finyearId}/organisationstatus", method=RequestMethod.GET)
public String createRootOrg(#PathVariable(value="finyearId") Long finyearId, Model model) throws Exception {
List<MprsStructureYear> orgStructuure = getOrganisationService().getOrganisationStructureForFinyear(finyearId);
OrgChartForm orgChartForm = new OrgChartForm();
orgChartForm.createOrgChartFormElements(orgStructuure);
model.addAttribute("orgChartForm", orgChartForm);
return "finyear/organisationchart/view";
}
#RequestMapping(value="/finyear/{finyearId}/organisationstatus", method=RequestMethod.POST)
public String createRootOrg(#PathVariable(value="finyearId") Long finyearId,#ModelAttribute("orgChartForm") OrgChartForm orgChartForm, BindingResult result, Model model) throws Exception {
System.out.println("Found model attribute: " + model.containsAttribute("orgChartForm"));
List<OrgChartFormElement> elements = orgChartForm.getOrgChartFormElements();
System.out.println(elements);
return "redirect:/spring/finyear/" + finyearId + "/organisationstatus";
}
// expected getters and setters
}
The issue is with the POST handler. I realise that it isn't doing much now, but once I get it to work, I will be persisting the submitted values.
At the moment, the output i see from the two sysout statements are:
Found model attribute: true
[]
Here is my JSP snippet:
<sf:form modelAttribute="orgChartForm" method="post">
<c:forEach items="${orgChartForm.orgChartFormElements}" var="org" varStatus="status">
<sf:hidden id="${org.id}field" path="orgChartFormElements[${status.index}].id"/>
<sf:input id="${org.id}hidden" path="orgChartFormElements[${status.index}].selected"/>
<c:out value="${org.elementLabel}"/>(<c:out value="${org.id}"/>) - <c:out value="${status.index}"/>
</c:forEach>
<input type="submit" value="Submit" />
</sf:form>
When i make the GET request, the JSP renders, and i see my list of text input fields, with the expected values, which tells me that im using the spring-form tags properly. However, when i submit, the form backing object declared as a parameter (orgChartForm) in the POST handler method is initialised, but everything is null/default initialised. I don't know where the submitted data went! It seems that springMVC looses it, and simply constucts a new object.
I have used this pattern extensively in this application without a glitch. It just wont work here. I realise this is a special case in my application where the form field is not atomic but a list, However its really confusing me that the data binds in the GET request, but not on the POST.
Thanks in advance for any pointers!
I think the problem is that you are trying to bind an arbitrary number of form fields to an ArrayList, which is a list that has a predetermined size.
Spring has something called an AutoPopulatingList that is custom designed for this purpose. Please have a look at this link for more info on how to use it: http://blog.richardadamdean.com/?p=12
I think you will need to write PropertyEditorSupport for your class. Following is the example for your reference.
public class SampleEditor extends PropertyEditorSupport {
private final SampleService sampleService;
public SampleEditor(SampleService sampleService, Class collectionType) {
super(collectionType);
this.sampleService = sampleService;
}
#Override
public void setAsText(String text) throws IllegalArgumentException {
Object obj = getValue();
List list = (List) obj;
for (String str : text.split(",")) {
list.add(sampleService.get(Long.valueOf(str)));
}
}
#Override
public String getAsText() {
return super.getAsText();
}
}
In controller, you should bind it using #InitBinder as follows:
#InitBinder
protected void initBinder(HttpServletRequest request, WebDataBinder binder) {
binder.registerCustomEditor(List.class, "list", new SampleEditor(this.sampleService, List.class));
}
Hope this will solve your problem.