Sunday, 17 January 2016

IndexOutOfBoundsException when using commons BeanUtils in Java 8

When migrating from Java 7 to Java 8, we noticed a strange issue with apache commons BeanUtils.populate(target, properties) method, when collections were populated from properties. They started failing with the following exception


Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.get(ArrayList.java:429)
at org.apache.commons.beanutils.PropertyUtilsBean.getIndexedProperty(PropertyUtilsBean.java:513)
at org.apache.commons.beanutils.PropertyUtilsBean.getIndexedProperty(PropertyUtilsBean.java:410)
at org.apache.commons.beanutils.PropertyUtilsBean.getNestedProperty(PropertyUtilsBean.java:768)
at org.apache.commons.beanutils.PropertyUtilsBean.getProperty(PropertyUtilsBean.java:846)
at org.apache.commons.beanutils.BeanUtilsBean.setProperty(BeanUtilsBean.java:903)
at org.apache.commons.beanutils.BeanUtilsBean.populate(BeanUtilsBean.java:830)
at org.apache.commons.beanutils.BeanUtils.populate(BeanUtils.java:433)
at sample.test.BeanPopulateTest.main(BeanPopulateTest.java:16)

To make things clear, I am going to use the example below.

Employee has a collection of Address objects. Note that Employee object has additional methods, to get the address at a given index and set address at an index. These methods are used by the BeanPopulator, to populate the values from OGNL expressions.

Employee.java

import java.util.ArrayList;
import java.util.List;

public class Employee {

  private List addresses = new ArrayList<Address>();

  public List<Address> getAddresses() {
    return addresses;
  }

  public void setAddresses(List<Address> addresses) {
    this.addresses = addresses;
  }

  public Address getAddresses(final int index) {
    if (index <= addresses.size()) {
       for (int i = addresses.size(); i <= index; i++) {
         addresses.add(new Address());
       }
    }
    return addresses.get(index);
  }

  public void setAddresses(final int index, final Address address) {
    this.addresses.add(index, address);
  }
}

Address.java

public class Address {
 private String postCode;

 public Address() {
 }

 public String getPostCode() {
  return postCode;
 }

 public void setPostCode(String postCode) {
  this.postCode = postCode;
 }
} 

A simple test to populate the Address collection revealed what was happening

import java.util.HashMap;
import java.util.Map;
import org.apache.commons.beanutils.BeanUtils;

public class BeanPopulateTest {
   public static void main(String[] args) throws Exception {
 
        Employee employee = new Employee();
 
        Map<String,String> properties = new HashMap<String,String>();
 properties.put("addresses[0].postCode", "TES456");

 BeanUtils.populate(employee, properties);

 System.out.println(employee.getAddresses().get(0).getPostCode());
   }
}

After a few hours of investigation and looking at the BeanUtils code, I realized that Java 8 property descriptor for Employee.addresses (collections) field is different from Java 7.

BeanInfo info = Introspector.getBeanInfo(Employee.class);
PropertyDescriptor[] descriptors = info.getPropertyDescriptors();
for (int i = 0; i < descriptors.length; i++) {
  System.out.println(descriptors[i].getName() + " " + descriptors[i].getClass().getName());
}

The output from the above code is

Java 8
java.beans.PropertyDescriptor:addresses

Java 7
java.beans.IndexedPropertyDescriptor:addresses

This causes BeanUtils to process the address collection as a non-indexed property and invoke the get(index) method on the collection, causing the IndexOutOfBoundsException.

A simple workaround for the issue is to change the names of the default getter/setter methods for the addresses collection, from getAddresses/setAddresses to getAddressList/setAddressList. All references to the existing method calls will have to be replaced with the new method names.

Employee.java (fix)

import java.util.ArrayList;
import java.util.List;

public class Employee {

 private List addresses = new ArrayList<Address>();

 public List<Address> getAddressList() {
  return addresses;
 }

 public void setAddressList(List<Address> addresses) {
  this.addresses = addresses;
 }

 public Address getAddresses(final int index) {
  if (index <= addresses.size()) {
   for (int i = addresses.size(); i <= index; i++) {
    addresses.add(new Address());
   }
  }
  return addresses.get(index);
 }

 public void setAddresses(final int index, final Address address) {
  this.addresses.add(index, address);
 }
}