Loading a List from a file in Spring Application Context

Have you ever wanted to load a List in your application context from a file? Perhaps you want to define a variable number of settings to be injected in, or in my case I wanted to pull in the Hibernate mapping files into an AnnotationSessionFactoryBean. Spring doesn't appear to have a way to do this out of the box, so here is how I have done it.

So you want to load a list of strings defined in a file called names.txt such as:
James
Adam
Peter
Paul
Your application context would look something like:

<bean id="resourceList" class="com.fizzpod.spring.ResourceListFactoryBean">
<property name="targetListClass">
<value>java.util.ArrayList</value>
</property>
<property name="locations">
<list>
    <value>classpath:names.txt</value>
</list>
</property>
</bean>
The ResourceListFactoryBean is (obviously) a factory bean for creating a list from a Spring resource. Now this uses some of Springs magic to turn the classpath:names.txt into a spring Resource object. To take advantage of the normal ListFactoryBean by extending it so that you can define a list using the normal ListFactoryBean (why you would want to do that rather than using the util:list tag). Anyway the class looks like this:


/**
* Creates a List of values from the specified resources.
*/
public class ResourceListFactoryBean extends ListFactoryBean {

  /**
   * The List implementation class to create
   */
 private Class targetListClass = null;
 
  /**
   * The resources to create the list from
   */
 private Resource[] locations = null;

  /**
   * Flag for whether the source list is set
   */
 private boolean sourceListSet = false;
 
 @Override
 /**
  * Set the source List, just used to detect whether
   * a sourceList is set
   * @param sourceList the source list to create the list from.
  */
 public void setSourceList(List sourceList) {
  super.setSourceList(sourceList);
  sourceListSet = true;
 }
 
 @Override
  /**
   * To allow the creation of the list class
   * @param targetListClass the class of the list to instantiate
   */
 public void setTargetListClass(Class targetListClass) {
  super.setTargetListClass(targetListClass);
  this.targetListClass = targetListClass;
 }

 /**
  * Set a location of the list file to load.
   * @param location the resource to load the settings f
  */
 public void setLocation(Resource location) {
  this.locations  = new Resource[] {location};
 }

 /**
  * Set locations of list files to be loaded.
   * @param locations the locations of the resources
  */
 public void setLocations(Resource[] locations) {
  this.locations = locations;
 }
 
 @Override
 @SuppressWarnings("unchecked")
  /**
   * Create a list from the resources, delegating the
   * super class if any source list has been set.
   * @return the list instantiated from the context definition
   */
 protected List createInstance() {
  List result = null;
  if(this.sourceListSet) {
   result = super.createInstance();
  }
  if(this.locations != null) {
   List resourceList = null;
   if(result == null) {
    result = instantiateListClass();
   }
   
   Class valueType = null;
   
   for(Resource resource: locations) {
    List contents = this.resolveList(resource);
   
    if (this.targetListClass != null) {
     valueType = GenericCollectionTypeResolver.getCollectionType(this.targetListClass);
    }
    if (valueType != null) {
     TypeConverter converter = getBeanTypeConverter();
     for (Object elem : contents) {
      result.add(converter.convertIfNecessary(elem, valueType));
     }
    } else {
     result.addAll(contents);
    }
   }
  }
  if(result == null) {
   throw new IllegalArgumentException("You must set either the sourceList, locations or location parameters");
  }  return result;
 }

  /**
   * Instantiates the list class
   * @return the list class as defined by the context
   */
 private List instantiateListClass() {
  List result;
  if (this.targetListClass != null) {
   result = (List) BeanUtils.instantiateClass(this.targetListClass);
  } else {
   result = new ArrayList();
  }
  return result;
 }

 @SuppressWarnings("unchecked")
  /**
   * Loads the resource into a list of strings
   * @return the list of strings loaded from the resource
   */
 private List resolveList(Resource resource) {
  InputStream inStream = null;
  List lines = null;
  try {
   inStream = resource.getInputStream();
   lines = IOUtils.readLines(inStream);
  } catch (IOException e) {
   throw new IllegalArgumentException("Could not read resource " + resource.getDescription(), e);
  } finally {
   IOUtils.closeQuietly(inStream);
  }
  return lines;
 }

}


Note that the above code relies on the apache commons io to parse and load the resource into a list. Also note that the some of the ListFactoryBean methods are overriden so that we can detect if the user is just defining a normal list. Why, well like I said above if you just want a list use the util:list tag, but by doing this in the above code it allows for a mixed list of items from a resource and sources defined in the xml to be merged together. The following example shows that and as a bonus how to define a single resource for example:

<bean id="resourceList" class="com.fizzpod.ResourceListFactoryBean">
   <property name="targetListClass">
       <value>java.util.ArrayList</value>
   </property>
   <property name="sourceList">
           <list>
               <value>Sarah</value>
               <value>Helen</value>
               <value>Debbie</value>
           </list>
       </property>
   <property name="locations" value="classpath:names.txt"></property>
</bean>

And that's about all. Hope this is of help to someone.

Comments

Popular posts from this blog

Gradle and the parent pom

Pushing changes from Shippable to Github

Goodbye Shippable Hello ... CircleCI