Introduction

Bean Builder is intended to be a simple utility for creating JavaBeans and collections of JavaBeans from text. In particular, the original use was for building collections of JavaBeans from XML and CSV files.

Bean Builder's use mimics that of SAX in that it supports simple events:

  • push - starts a new node (a Collection, JavaBean or property)
  • pop - ends the current node (a Collection, JavaBean or property)
  • apply - sets a text value for the current node (only applies to properties)

While Bean Builder can be used programmatically, its intended use is via a file parser such as those in the net.sealisland.beanbuilder.csv and net.sealisland.beanbuilder.sax packages.

Why another JavaBean loader?

This library arose from the need to load JavaBeans into memory for a mock DAO implementation (used in unit testing). For homogeneous data like this CSV was the preferred format and I just wanted a few lines of code in my mock DAO to load the file - minimal configuration.

While there are several excellent XML and JavaBean frameworks such as XStream and Commons Digester, I struggled to find one that would load CSV files. No doubt I could have written a CSV parser that generated SAX events to adapt to an XML parser but that would mean more configuration - I just wanted to get the unit testing over with! I also got annoyed by those that used direct field manipulation (ie, bypassing setter methods); aside from the blatent violation of encapsulation this prevents any logic or validation built into these methods being applied during loading.

This library isn't intended to replace anything, cover all bases or be particularly fast. It's simply meant to be a useful way of loading beans from files - if it provides utility elsewhere then that's a bonus!

Concepts

Bean Builder uses the following concepts:

  • JavaBean - as defined by the JavaBean standard. In particular, a default constructor is required!
  • Property - a JavaBean property. Note that indexed properties are not supported.
  • Collection - something implementing the Collection interface. Note that Map is not a collection and is therefore not supported.
  • Type - a non-JavaBean class, such as java.lang.String
  • Alias - a mapping from a logical name to a class (ie, a JavaBean class or a type), such as "string" being mapped to java.lang.String
  • Format - an implementation of java.text.Format which converts from a text value to a type.

Bean Builder works by building a tree of nodes (JavaBean/Collection/Type instances) using push, pop and apply events.

Push events

A push event creates a new node on the tree: beanBuilder.push("name"); . The node that is created is based on the name used for the push event according to the following rules:

  • If the head node has a JavaBean property matching the name of the push event then a node representing the JavaBean property is added.
  • Otherwise a class mapped to the name by an alias is found and a new instance of that class added to the tree: if the class represents a Collection then it is recognised and anything pushed onto it is added to the collection.

JavaBean example

To create a new JButton we need to do the following:

  • Create a Bean Builder
  • Register an alias for the JButton class
  • Push a node with that alias onto the Bean Builder, thereby creating the bean
  • Pop the node from the Bean Builder, marking completion of bean modification
  • Get the root node from the Bean Builder

The following code implements these steps:

BeanBuilder beanBuilder = new BeanBuilder();
beanBuilder.registerType("button", JButton.class);
beanBuilder.push("button");
beanBuilder.pop("button"); 
JButton button = (JButton) beanBuilder.root();
			

This may seem like a lot of work just to create a new JButton. Things get a little smoother when we start setting properties and chaining methods:

BeanBuilder beanBuilder = new BeanBuilder();
beanBuilder.registerType("button", JButton.class);
beanBuilder.push("button"); 
// Set the text property of the button
beanBuilder.push("text").apply("This is the button text").pop("text"); 
// Set other properties of the button
beanBuilder.push("borderPainted").apply("false").pop("borderPainted");
beanBuilder.push("contentAreaFilled").apply("false").pop("contentAreaFilled");
beanBuilder.pop("button"); 
JButton button = (JButton) beanBuilder.root();
			

Nested properties

If a JavaBean property is another JavaBean there are several ways of accessing it. For example, consider the following beans:

public class User {

  private Status status;

  public void setStatus(Status status) { 
    this.status = status; 
  }

  public Status getStatus() { 
    return status; 
  } 
}

public class Status {
 
  private String code; 
  private Date modificationDate;

  // setters and getters... 
}
				

If we wanted to set the status for a user we could do the following:

beanBuilder.registerType("user", User.class");
beanBuilder.push("user");
beanBuilder.push("status");
beanBuilder.push("code").apply("active").pop("code");
beanBuilder.push("modificationDate").apply("01-01-1970").pop("modificationDate");
beanBuilder.pop("status");
beanBuilder.pop("user");
				

Note that we don't need to specify an alias for status. It is the name of a property of User - aliases are only needed when no matching property exists.

It is important to note here that Bean Builder will only create a new instance for a JavaBean property if it needs to. Therefore we can also push a new status property as follows - a Status instance will only be created on the first push/pop of that property:

beanBuilder.registerType("user", User.class");
beanBuilder.push("user");
beanBuilder.push("status").push("code").apply("active").pop("code").pop("status");
beanBuilder.push("status").push("modificationDate").apply("01-01-1970").pop("modificationDate").pop("status");
beanBuilder.pop("user");
				

This may not seem very useful (or logical). It relates to the desire to load beans from CSV files where column names might be "status.code" and "status.modificationDate" - we don't want to create a new Status for each column, only when necessary.

Collection example

Collection types can also be registered and created. Pushing onto a collection adds an entry rather than setting a property (as it would for a JavaBean node):

BeanBuilder beanBuilder = new BeanBuilder();
beanBuilder.registerType("button", JButton.class); 
// Start a list
beanBuilder.push("list"); 
// Add two buttons to the list
beanBuilder.push("button").pop("button");
beanBuilder.push("button").pop("button");
// Finish the list 
beanBuilder.pop("list");
List list = (List) beanBuilder.root();
			

Default collection types

The following aliases are registered by default:

  • set => java.util.HashSet
  • list => java.util.ArrayList

Non-string properties

If a JavaBean property isn't of String type a java.text.Format can be specified to handle the conversion from the applied text to the appropriate datatype:

public class MyBean {
  ...
  public void setCreationDate(Date creationDate) { 
    this.creationDate = creationDate; 
  } 
  ... 
}

BeanBuilder beanBuilder = new BeanBuilder();
beanBuilder.registerType("mybean", MyBean.class); 
beanBuilder.registerFormat(Date.class, new SimpleDateFormat("dd-MM-yyyy"));
beanBuilder.push("list"); 
// Create a new MyBean and set its creation date property
beanBuilder.push("mybean").push("creationDate").apply("01-02-1980").pop("creationDate").pop("mybean");
beanBuilder.pop("list"); 
List list =	(List)beanBuilder.root();
			

Default formats

The following formats are registered by default:

  • java.lang.String => StringFormat (direct mapping from string value)
  • java.lang.Integer => IntegerFormat (uses new Integer(String) )
  • java.lang.Long => LongFormat (uses new Long(String) )
  • java.lang.Float => FloatFormat (uses new Float(String) )
  • java.lang.Double => DoubleFormat (uses new Double(String) )
  • java.util.Date => SimpleDateFormat (uses yyyy-MM-dd'T'HH:mm:ss date/time pattern)
  • java.math.BigDecimal => BigDecimlFormat (uses new BigDecimal(String) )

Non-JavaBean values

Even non-JavaBean values can be created by registering an alias for the type. If a format is registered for that type then it will be used to create the node when a value is applied:

BeanBuilder beanBuilder = new BeanBuilder();
beanBuilder.registerType("date", Date.class):
beanBuilder.registerFormat(Date.class, new
SimpleDateFormat("dd-MM-yyyy"); 
// Create a list 
beanBuilder.push("list"); 
// Push two dates onto the list
beanBuilder.push("date").apply("31-12-2004").pop("date");
beanBuilder.push("date").apply("01-01-1970").pop("date");
// Finish a list beanBuilder.pop("list"); 
List list = (List)beanBuilder.root();
			

Default types

The following aliases are registered by default:

  • string => java.lang.String
  • boolean => java.lang.Boolean
  • integer => java.lang.Integer
  • long => java.lang.Long
  • float => java.lang.Float
  • double => java.lang.Double
  • date => java.lang.Date
  • decimal => java.math.BigDecimal