Recently I've rediscovered another wheel again, but this one is pretty interesting I think...
Simple Fluent Builder
Builder is rather simple desing pattern. It accumulates bunch of values and then uses them to construct some complex product.
Fluent builder is a builder with fluent interface. That means it returns reference to self from every method (except the build() method that returns product) so it allows method chaining.
Nice and simple example is StringBuilder from java standard library.
String hello = new StringBuilder() .append("h").append("e").append("l").append("l").append("o") .toString();
Generic getThis() trick
When Builder is single standalone class, using return this is dead simple solution, but when inheritance comes into play, builder base class methods cannot return this, because we need concrete builder to be returned to allow method chaining.Solution is the getThis() trick, that allows to return reference to concrete builder even from base class that builder is extending. The price is that every concrete builder must implement getThis() method.
Here is a nice article with some background and explanationGeneric parent trick
I've found another slightly different scenario, when similar generic trick can be used. It employs two builders and uses generic parent (instead of generic self) to keep track of builders used in chaining. It is quite simple JSON builder.
JSON message is built on two structures: object and array, therefore we will have two concrete builders working together. We don't want builder user to be confused by offering object building methods to him when he constructs array and contrariwise. When user completes constructing array or object, previous builder must be restored and returned to user.
/** | |
{ "f1" : "hello", | |
"f2" : [1, "2", 3], | |
"f3" : { "n" : 5.5 } | |
} | |
*/ | |
String json = new SimpleJsonBuilder() | |
.object() //switch to object context 1 | |
.field("f1", "hello") | |
.array("f2") //switch to array context | |
.element(1).element("2").element(3) | |
.end() //restore previous object context 1 | |
.object("f3") //switch to object context 2 | |
.field("n", 5.5) | |
.end() //restore previous object context 1 | |
.end() //restore root context | |
.getJson(); |
import java.lang.reflect.Array; | |
import java.util.Collection; | |
/** | |
* This is simplified version of | |
* https://github.com/anthavio/hatatitla/blob/master/src/main/java/com/anthavio/httl/util/JsonBuilder.java | |
* | |
* @author martin.vanek | |
* | |
*/ | |
public class SimpleJsonBuilder { | |
private StringBuilder sb = new StringBuilder(); | |
/** | |
* Start Root JSON object | |
*/ | |
public ObjectBuilder<SimpleJsonBuilder> object() { | |
return new ObjectBuilder<SimpleJsonBuilder>(this); | |
} | |
/** | |
* Start Root JSON array | |
*/ | |
public ArrayBuilder<SimpleJsonBuilder> array() { | |
return new ArrayBuilder<SimpleJsonBuilder>(this); | |
} | |
private void value(Object value) { | |
if (value == null) { | |
sb.append("null"); | |
} else { | |
if (value instanceof String) { | |
sb.append('"').append(value).append('"'); | |
} else if (value instanceof Boolean) { | |
sb.append(value.toString()); | |
} else if (value instanceof Number) { | |
sb.append(value.toString()); | |
} else if (value instanceof Collection<?>) { | |
Collection<?> collection = (Collection<?>) value; | |
sb.append("[ "); | |
boolean ff = false; | |
for (Object element : collection) { | |
if (ff) { | |
sb.append(", "); | |
} else { | |
ff = true; | |
} | |
value(element); //recursive | |
} | |
sb.append(" ]"); | |
} else if (value.getClass().isArray()) { | |
int length = Array.getLength(value); | |
sb.append("[ "); | |
for (int i = 0; i < length; i++) { | |
if (i != 0) { | |
sb.append(", "); | |
} | |
Object element = Array.get(value, i); | |
value(element); //recursive | |
} | |
sb.append(" ]"); | |
} else { | |
throw new IllegalArgumentException("Unsuported type " + value.getClass().getName() + " of the value " + value); | |
} | |
} | |
} | |
public String getJson() { | |
return sb.toString(); | |
} | |
@Override | |
public String toString() { | |
return getJson(); | |
} | |
public class ObjectBuilder<T> { | |
private final T parent; | |
private boolean ff; //first field flag | |
private ObjectBuilder(T parent) { | |
sb.append("{ "); | |
this.parent = parent; | |
} | |
public T end() { | |
sb.append(" }"); | |
return parent; | |
} | |
private void name(String name) { | |
if (name == null || name.length() == 0) { | |
throw new IllegalArgumentException("field name is empty"); | |
} | |
if (ff) { | |
sb.append(", "); | |
} else { | |
ff = true; | |
} | |
sb.append('"').append(name).append('"'); | |
sb.append(" : "); | |
} | |
/** | |
* Field @param name as simple value | |
*/ | |
public ObjectBuilder<T> field(String name, Object value) { | |
name(name); | |
value(value); | |
return this; | |
} | |
/** | |
* Field @param name as nested JSON object | |
*/ | |
public ObjectBuilder<ObjectBuilder<T>> object(String name) { | |
name(name); | |
return new ObjectBuilder<ObjectBuilder<T>>(this); | |
} | |
/** | |
* Field @param name as nested JSON array | |
*/ | |
public ArrayBuilder<ObjectBuilder<T>> array(String name) { | |
name(name); | |
return new ArrayBuilder<ObjectBuilder<T>>(this); | |
} | |
} | |
public class ArrayBuilder<T> { | |
private final T parent; | |
private boolean ff; //first element flag | |
private ArrayBuilder(T parent) { | |
sb.append('['); | |
this.parent = parent; | |
} | |
public T end() { | |
sb.append(']'); | |
return parent; | |
} | |
private void element() { | |
if (ff) { | |
sb.append(", "); | |
} else { | |
ff = true; | |
} | |
} | |
/** | |
* Simple value array element | |
*/ | |
public ArrayBuilder<T> element(Object value) { | |
element(); | |
value(value); | |
return this; | |
} | |
/** | |
* Nested object array element | |
*/ | |
public ObjectBuilder<ArrayBuilder<T>> object() { | |
element(); | |
return new ObjectBuilder<ArrayBuilder<T>>(this); | |
} | |
/** | |
* Nested array array element | |
*/ | |
public ArrayBuilder<ArrayBuilder<T>> array() { | |
element(); | |
return new ArrayBuilder<ArrayBuilder<T>>(this); | |
} | |
} | |
public static void main(String[] args) { | |
String json = new SimpleJsonBuilder().object().field("f1", "hello").array("f2").element(1).element("2").element(3) | |
.end().object("f3").field("n", 5.5).end().end().getJson(); | |
System.out.println(json); | |
} | |
} |
JSR-353 - JSON-P
Brand new Java API for JSON processing is going to be part of the upcoming JEE 7 specification also contains JSON builder.Surprisinly to me they came with simplest builder that is throwing exception when user calls object/array builder method out of correct object/array building context.
For example this code
JsonGenerator jg = javax.json.Json.createGenerator(System.out); jg.writeStartObject().write("array_element").writeEnd();executed will throw
javax.json.stream.JsonGenerationException: write(String) can only be called in array contextVery error prone API indeed! I've filed improvement into Jira, but since JSR already passed the Final Approval Ballot, I guess there is no hope.
No comments:
Post a Comment