I am learning spring and I've encountered some strange behavior I can't explain.
Here I have simple class:
public class Customer {
private Integer id;
public void setId(Integer id) {
this.id = id;
}
Simple controller:
#Controller
#RequestMapping("/customer")
public class CustomerController {
#InitBinder
public void initBinder(WebDataBinder dataBinder){
StringTrimmerEditor editor = new StringTrimmerEditor(true);
dataBinder.registerCustomEditor(String.class,editor);
}
#RequestMapping("/showForm")
public String customerPage(Model model){
model.addAttribute("customer",new Customer());
return "customerPage";
}
#RequestMapping("/showConfirmation")
public String customerConfirmation(#Valid #ModelAttribute("customer") Customer customer, BindingResult result){
System.out.println(customer.getLastName());
if(result.hasErrors())
return "customerPage";
else
return "customerConfirmation";
}
}
And here's a part of customerPage.jsp:
<body>
<form:form action="showConfirmation" modelAttribute="customer">
Id: <form:input path="id"/>
<form:errors path="id"/>
<input type="submit" value="Submit">
</form:form>
</body>
I run it, leave id field empty and press Submit button.
If my Customer.id property is Integer, then I get following sequence of actions: initBinder method triggers and sets my empty string to null. Then the setterMethod triggers, sets id to null, all good.
But if Customer.id is int, then in the browser I get this: NumberFormatException: For input string: "".
The question is why input string in exception is just empty? Isn't initBinder should have set input string to null by this moment? I thought that the sequence always looks like this: #InitBinder methods→setter methods→validating settled values.
result→errors→source→cause→stackTrace:
"java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:68)"
"java.base/java.lang.Integer.parseInt(Integer.java:668)"
"java.base/java.lang.Integer.valueOf(Integer.java:989)"
"org.springframework.util.NumberUtils.parseNumber(NumberUtils.java:211)"
"org.springframework.beans.propertyeditors.CustomNumberEditor.setAsText(CustomNumberEditor.java:115)"
"org.springframework.beans.TypeConverterDelegate.doConvertTextValue(TypeConverterDelegate.java:429)"
"org.springframework.beans.TypeConverterDelegate.doConvertValue(TypeConverterDelegate.java:402)"
"org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:155)"
"org.springframework.beans.AbstractNestablePropertyAccessor.convertIfNecessary(AbstractNestablePropertyAccessor.java:585)"
"org.springframework.beans.AbstractNestablePropertyAccessor.convertForProperty(AbstractNestablePropertyAccessor.java:604)"
"org.springframework.beans.AbstractNestablePropertyAccessor.processLocalProperty(AbstractNestablePropertyAccessor.java:453)"
"org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:278)"
"org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:266)"
"org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:97)"
"org.springframework.validation.DataBinder.applyPropertyValues(DataBinder.java:848)"
"org.springframework.validation.DataBinder.doBind(DataBinder.java:744)"
"org.springframework.web.bind.WebDataBinder.doBind(WebDataBinder.java:197)"
"org.springframework.web.bind.ServletRequestDataBinder.bind(ServletRequestDataBinder.java:107)"
"org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor.bindRequestParameters(ServletModelAttributeMethodProcessor.java:158)"
"org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:160)"
"org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)"
"org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)"
"org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)"
"org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)"
"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:888)"
"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)"
"org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)"
"org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)"
"org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)"
"org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)"
"org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)"
"javax.servlet.http.HttpServlet.service(HttpServlet.java:660)"
"org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)"
"javax.servlet.http.HttpServlet.service(HttpServlet.java:741)"
"org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)"
"org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)"
"org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)"
"org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)"
"org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)"
"org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)"
"org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)"
"org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:526)"
"org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)"
"org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)"
"org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:678)"
"org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)"
"org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)"
"org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)"
"org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)"
"org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861)"
"org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1579)"
"org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)"
"java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)"
"java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)"
"org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)"
"java.base/java.lang.Thread.run(Thread.java:830)"
Looking at the code for the setAsText in CustomNumberEditor at the time of writing:
#Override
public void setAsText(String text) throws IllegalArgumentException {
if (this.allowEmpty && !StringUtils.hasText(text)) {
// Treat empty String as null value.
setValue(null);
}
else if (this.numberFormat != null) {
// Use given NumberFormat for parsing text.
setValue(NumberUtils.parseNumber(text, this.numberClass, this.numberFormat));
}
else {
// Use default valueOf methods for parsing text.
setValue(NumberUtils.parseNumber(text, this.numberClass));
}
}
We see that if the allowEmpty property is true it will set the value as null when the text is null or an empty string. If the value is false it will just attempt to parse the number value from the String regardless, resulting in the error. Without digging further into the code it is reasonable that the allowEmpty flag is set to false presumably because an int is a primitive and can not be null. This may not seem like desirable behaviour but when an int is unassigned it will default to zero. You would prefer an exception to be thrown than an int set to 0 as this will just obscure errors.
Related
I am new to the Java Play Framework and I'm trying to get the authentication to work. So I am following this tutorial: https://www.playframework.com/documentation/2.1.0/JavaGuide4
Here is my code:
public static Result authenticate()
{
Form<Login> loginForm = form(Login.class).bindFromRequest();
return ok(loginForm.toString());
}
public static class Login
{
public String email;
public String password;
public String validate()
{
return "VALIDATE "+email+password;
}
}
In the method autheticate() I can see the submitted values of the form, but the method validate() in the Login class does not see them (the variables are always null).. The output of loginForm.toString() contains:
Form(of=class controllers.Application$Login, data={email=asdf#asdf, password=asdf}, value=None, errors={=[ValidationError(,[VALIDATE nullnull],[])]})
As you can see, the data is received.. But in the validate method the data suddenly is equal to null. So how do I fix this?
You don't mention how you are calling validate() however I think this might do the trick, do something along the lines of:
public static Result authenticate() {
Form<Login> form = form(Login.class).bindFromRequest();
// handle errors
if (!form.hasErrors()) {
Login login = form.get();
Logger.debug(login.validate());
} else {
// bad request
}
}
This works for me.
Method validate in your model should return null if you think that validation has passed, otherwise you should return error message text. Then you need to check form if it contains error by "hasGlobalError" method. globalError is filled when validate() method returns String instead of null. But in your case you should use some model field annotations - https://www.playframework.com/documentation/2.3.x/api/java/play/data/validation/Constraints.html.
If you want to check if form fails on those - then you use "hasErrors" method.
public static class Login {
#Constraints.Email
public String email;
#Constraints.MinLength(value = 6)
public String password;
}
Such model will check if provided emails is really email and if password is longer or equal 6 characters.
ps. Do not use toString on template, you should use render()
Here is a sample skeleton of DTO object.
public class MyDTO
{
List<Student> students=new ArrayList<>();
}
public class Student
{
String name;
Integer age;
// setter and getter methods
}
Now, the user has a chance to enter a lot of students into the list and any student detail might contain an error. The possible errors are student age being greater than 25, and name containing special characters etc.
For example, students[2].name has a special character and student[4].age > 25, then they are errors. Now, I would like to display the error below those fields and also highlight the corresponding fields.
<form th:field="${myDTO}">
<input type="text" th:field="*{students[0].name}" th:errorclass="fieldError"/>
<span class="error" th:if="${#fields.hasErrors('students[0].name')}" th:errors="*{students[0].name}"></span>
<input type="number" th:field="*{students[0].age}" min="15" max="25" th:errorclass="fieldError"/>
<span class="error" th:if="${#fields.hasErrors('students[0].age')}" th:errors="*{students[0].age}"></span>
</form>
I am confused on what to put in th:field attribute? When I write as above, such type of error is the result
Neither BindingResult nor plain target object for bean name
'students+'['+0+']'' available as request attribute.
In my validators, I have such type of code..
int idx=0;
for(Student st: students)
{
errors.pushNestedPath("students["+idx+"]");
ValidationUtils.invokeValidator(studentValidator, st, errors);
errors.popNestedPath();
idx++;
}
and in the StudentValidator class..
#Override
public void validate(Object obj, Errors errors) {
Student s=(Student) obj;
if(containsSpecialCharacters(s.name))
{
errors.rejectValue("name","name.containsSpecialCharacters",null,null);
}
if(s.age>25 || s.age<15)
{
errors.rejectValue("age","age.invalid",null,null);
}
}
Now, my problems are
How do I show those errors, highlight the corresponding fields?
What to put in the th:field tag?
Next, the student records are added dynamically, that is the student rows doesn't exist previously, by clicking on Add student button, the user will be able to add the student. Now, even the th:field must also be updated. How to do that, because it is related to thymeleaf template processing which is done previously but not after the page is loaded?
Hope you will reply as soon as possible.
your validation seems to be right, but maybe you need to pass a BindingResult as parameter in your controller so that the error can be retrieved in your view layer.
#PostMapping("/students")
public String saveStudent(#Valid Student, BindingResult bindingResult, RedirectAttributes redirAttrs) {
if (bindingResult.hasErrors()) {
// Show errors here
bindingResult.getAllErrors().stream().forEach(System.out::println);
return "student-edit";
} else {
Long id = releaseLogService.save(student).getId();
redirAttrs.addFlashAttribute("message", "Success");
return "redirect:/student/edit/" + id;
}
}
I defined the following drop down box in my page:
...
<td>
<h:selectOneMenu id="bootenvironment1" value="#{detailModel.selectedBootenvironment1}"
disabled="#{detailModel.mode == detailModel.viewMode}">
<f:selectItems value="#{detailModel.availableBootenvironments}"/>
</h:selectOneMenu>
</td>
In my model I have:
...
private Map<String, Bootenvironment> availableBootenvironments;
public DefinitionDetailModel()
{
super();
}
public String getSelectedBootenvironment1()
{
if (((Definition) getAfterObject()).getBootenvironment1() != null)
{
return ((Definition) getAfterObject()).getBootenvironment1().getEnvironmentName();
}
return "--Please select one--";
}
public void setSelectedBootenvironment1( String selectedBootenvironment )
{
((Definition) getAfterObject()).setBootenvironment1(availableBootenvironments.get(selectedBootenvironment));
}
...
And in the controller I set the availableBootenvironments map:
private void fetchBootenvironments()
{
...
#SuppressWarnings( "unchecked" )
List<Bootenvironment> bootenvironments = (List<Bootenvironment>) ...
Map<String, Bootenvironment> availableBootenvironments = new HashMap<String, Bootenvironment>();
availableBootenvironments.put("--Please select one--", null);
for(Bootenvironment bootenvironment : bootenvironments)
{
availableBootenvironments.put(bootenvironment.getEnvironmentName(), bootenvironment);
}
((DefinitionDetailModel) detailModel).setAvailableBootenvironments(availableBootenvironments);
}
The problem is that when I click a button in the page (which is bound to an action), I get the error:
detailForm:bootenvironment1: Validation error: value is not valid.
I don't understand where the error is; the value for selectItems is a map with the object's name-field(so a string) as key and the object itself as value. Then the value for the default selected (value="#{detailModel.selectedBootenvironment1}") is a string too as you can see in the getter/setter method of the model.
Another problem (maybe related to the previous one) is that when the page first loads, the default selected value should be --Please select one--- as the getBootenvironment1() returns null, but this does not happen: another one from the list is selected.
Can you please help me understanding what/where am I doing wrong?
EDIT
I implemented the Converter as you said:
#FacesConverter( forClass = Bootenvironment.class )
public class BootenvironmentConverter implements Converter
{
#Override
public String getAsString( FacesContext context, UIComponent component, Object modelValue ) throws ConverterException
{
return String.valueOf(((Bootenvironment) modelValue).getDbId());
}
#Override
public Object getAsObject( FacesContext context, UIComponent component, String submittedValue ) throws ConverterException
{
List<Bootenvironment> bootenvironments = ... (get from DB where dbid=submittedValue)
return bootenvironments.get(0);
}
}
But now I have the following error:
java.lang.ClassCastException: java.lang.String cannot be cast to
ch.ethz.id.wai.bootrobot.bo.Bootenvironment
You will get this error when the selected item value doesn't pass the equals() test on any of the available item values.
And indeed, you've there a list of Bootenvironment item values, but you've bound the property to a String which indicates that you're relying on the Bootenvironment#toString() value being passed as submitted value and that you aren't using a normal JSF Converter at all. A String can never return true on an equals() test with an Bootenvironment object.
You'd need to provide a Converter which converts between Bootenvironment and its unique String representation. Usually, the technical ID (such as the autogenerated PK from the database) is been used as the unique String representation.
#FacesConverter(forClass=Bootenvironment.class)
public class BootenvironmentConverter implements Converter {
#Override
public void getAsString(FacesContext context, UIComponent component, Object modelValue) throws ConverterException {
// Write code to convert Bootenvironment to its unique String representation. E.g.
return String.valueOf(((Bootenvironment) modelValue).getId());
}
#Override
public void getAsObject(FacesContext context, UIComponent component, Object submittedValue) throws ConverterException {
// Write code to convert unique String representation of Bootenvironment to concrete Bootenvironment. E.g.
return someBootenvironmentService.find(Long.valueOf(submittedValue));
}
}
Finally, after implementing the converter accordingly, you should be able to fix the selectedBootenvironment1 property to be a normal property without any mess in getter/setter:
private Bootenvironment selectedBootenvironment1;
public Bootenvironment getSelectedBootenvironment1() {
return selectedBootenvironment1;
}
public void setSelectedBootenvironment1(Bootenvironment selectedBootenvironment1) {
this.selectedBootenvironment1 = selectedBootenvironment1;
}
I have a model class in the following structure:
public class User {
public String name;
public Long id;
}
public class Play {
public String name;
public User user;
}
Now i want to have a form based on Play class. So I have an editPlay view which takes Form[Play] as an input.
In the view I have a form which calls an update action on submit:
#form (routes.PlayController.update())
{..}
but I cannot find the right way to bind the user field in a way I'll receive it properly in the controller:
Form<Play> formPlay = form(Play.class).bindFromRequest();
Play playObj = formPlay.get();
According to the API, Form.Field value is always a string. Is there some other way to automatic bind an input to the User Object?
Thanks
You can make use of custom DataBinder
In the play.scla.html:
#form (routes.PlayController.update())
{
<input type="hidden" name="user" id="user" value="#play.user.id"/>
}
in your method in the controller
public static Result update()
{
// add a formatter which takes you field and convert it to the proper object
// this will be called automatically when you call bindFromRequest()
Formatters.register(User.class, new Formatters.SimpleFormatter<User>(){
#Override
public User parse(String input, Locale arg1) throws ParseException {
// here I extract It from the DB
User user = User.find.byId(new Long(input));
return user;
}
#Override
public String print(User user, Locale arg1) {
return user.id.toString();
}
});
Form<Play> formPlay = form(Play.class).bindFromRequest();
Play playObj = formPlay.get();
}
I'm not quite sure I understand your question, but basically I have been handling forms like this:
final static Form<Play> playForm = form(Play.class);
...
public static Result editPlay(){
Form<Play> newPlayForm = form(User.class).bindFromRequest();
Play newPlay = newPlayForm.get();
....
}
I serve and render the template from an action using:
return ok(play_form_template.render(playForm));
Then in the template:
#(playForm: Form[Play])
#import helper._
#helper.form(action = routes.Application.editPlay()) {
#helper.inputText(playForm("name"))
...
}
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.