Search code examples
spring-mvcapache-commons-fileupload

Property value lost (null/empty) on web form reload


Using spring mvc 3.0.5 I encountered a weird (almost sporadic) bug with data binding. In one page of the application a model bean with some primitive values (few strings and longs) and two List's (let's name them A and B) are used. The lists are displayed in two tables (using displaytag). The user can edit some of the primitive values and submit the from, that the display the updated data (same page is redisplayed). The list data is stored in the page in hidden input fields.

The problem is that sometimes one of the hidden fields is lost on a POST/display cycle. The Java value becomes null, in HTML is becomes value="". (it happened with one Long type and one Boolean)

Until now it happened only on two specific set of data values in the model attribute. In one case the one value vanished in Nth iteration (N about 3 or 4) while in other it happens on first POST. In many other data sets no problem has been observed. As if it were a case of uninitialized variable use, unsynchronized cross thread data access or similar.

The questions are:

  • Does this sound like some known bug?
  • How to proceed? Debug step by step? Where to look?

More information (rudimentary as even with exact copy of the code the issue doesn't reproduce):

The JSP:

<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<form:form  id="form" method="post" modelAttribute="myRequest" action="spremembaUporabnika" enctype="multipart/form-data">
  <display:table id="table1" name="myRequest.listA"  decorator="si.comtrade.vs.web.decorators.users.UserRightsDecorator">   
    <display:setProperty name="paging.banner.all_items_found" value=""/>
    <display:setProperty name="paging.banner.onepage" value=""/>

    <display:column title="Column1">
      <c:out value="${myRequest.listA[table1_rowNum-1].orgUnitName}"/>
      <form:hidden path="listA[${table1_rowNum-1}].string1" id="string1[${table1_rowNum-1}]"/>
      <form:hidden path="listA[${table1_rowNum-1}].long1" id="long1[${table1_rowNum-1}]"/>
      <form:hidden path="listA[${table1_rowNum-1}].long2" id="long2[${table1_rowNum-1}]"/>
      <form:hidden path="listA[${table1_rowNum-1}].string2" id="string2[${table1_rowNum-1}]"/>
    </display:column>
    <display:column escapeXml="true" property="prop2" title="Column2" />
    ...
  </display:table>

The model class:

public class MyRequest {
    private List<TypeA> listA = new AutoPopulatingList<TypeA>(TypeA.class);

    private List<TypeB> listB = new AutoPopulatingList<TypeB>(TypeB.class);

  // ... other fields and getters/setters omitted

}

public class TypeA {
    Long long1, long2;  // <---- one of these loses the value on POST/reload
    String string1, string2;
  // ... other fields and getters/setters omitted
}

The controller class:

@Controller
@PreAuthorize("hasAnyRole('fooRole')")
public class MyController {

  @InitBinder
  protected void initBinder(WebDataBinder binder) {
    binder.setValidator(new MyValidator());
    binder.registerCustomEditor(Date.class, "listB.date1", new CustomDateEditor(new SimpleDateFormat("dd.MM.yyyy HH:mm"), true));
    binder.registerCustomEditor(Date.class, "listB.date2", new CustomDateEditor(new SimpleDateFormat("dd.MM.yyyy HH:mm"), true));
  }

  @ModelAttribute("myRequest")
  public MyRequest getTheRequest() {
    MyRequest request = new MyRequest();
    List<BarType> barList = getSomeBars(null, null);
    request.setBarList(barList);
    return request;
  }

  @RequestMapping(value = "/myPage", method = RequestMethod.GET)
  public String myPage(@ModelAttribute("myRequest") MyRequest request,
    @RequestParam(value = "userId", required = false) String userId,
    HttpSession p_session, Map<String, Object> map) {
    ... values into myRequest are filled here

    return "id_for_tiles";
}

  @RequestMapping(value = "/doSomething", method = RequestMethod.POST)
  public String doMethod(@ModelAttribute("myRequest") @Valid MyRequest request, BindingResult binder, Map<String, Object> map) {
    ... do some processing, then redisplay the same page

    return "id_for_tiles"; // same as in other method
  }

New findings:

  • the problem happens when the form is POST-ed: the values in the HTML page are OK, but in the controller they wind up empty
  • I found out the value is an empty string when it enters the public void setPropertyValue(PropertyValue pv) method in BeanWrapperImpl

An important factor as it turned out is that the form supports file uploads. (see answer)


Solution

  • Apparently it is a bug in org.apache.commons.fileupload.MultipartStream from commons-fileupload-1.2

    The code in method public int read(byte[] b, int off, int len) (line 878) tries to read the parameter value but when no data is available in the buffer (checked by available()), it calls makeAvailable() to read more data from the HTTP request stream. That can return 0, 1 or similarly few bytes which is not enough to contain the separator string, so there are still 0 byte available() and the read method quits with -1 instead of reading more data from the HTTP request stream.

    For reference:

    /* this is line 877 */
        public int read(byte[] b, int off, int len) throws IOException {
            if (closed) {
                throw new FileItemStream.ItemSkippedException();
            }
            if (len == 0) {
                return 0;
            }
            int res = available();
            if (res == 0) {
                res = makeAvailable();
                if (res == 0) {
                    return -1;
                }
            }
            res = Math.min(res, len);
            System.arraycopy(buffer, head, b, off, res);
            head += res;
            total += res;
            return res;
        }
    

    It is a known bug: https://issues.apache.org/jira/browse/FILEUPLOAD-135 Fixed in version 1.2.1.