org.exparity.test.builder.BeanBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of test-object-builder Show documentation
Show all versions of test-object-builder Show documentation
A library to support configurable instantiation and creation of random objects to use as test dummy's for model objects
The newest version!
package org.exparity.test.builder;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.co.it.modular.beans.BeanNamingStrategy;
import uk.co.it.modular.beans.BeanPropertyPath;
import uk.co.it.modular.beans.Type;
import uk.co.it.modular.beans.TypeProperty;
import uk.co.it.modular.beans.naming.ForceRootNameNamingStrategy;
import uk.co.it.modular.beans.naming.LowerCaseNamingStrategy;
import static java.lang.System.identityHashCode;
import static org.apache.commons.lang.StringUtils.lowerCase;
import static org.exparity.test.builder.ValueFactories.*;
import static uk.co.it.modular.beans.Type.type;
/**
* Builder object for instantiating and populating objects which follow the Java beans standards conventions for getter/setters
*
* @author Stewart Bissett
*/
@SuppressWarnings({
"rawtypes", "unchecked"
})
public class BeanBuilder {
private static final Logger LOG = LoggerFactory.getLogger(BeanBuilder.class);
/**
* Return an instance of a {@link BeanBuilder} for the given type which can then be populated with values either manually or automatically. For example:
*
*
*
* Person aPerson = BeanBuilder.anInstanceOf(Person.class)
* .path("person.firstName", "Bob")
* .build()
*
* @param type the type to return the {@link BeanBuilder} for
*/
public static BeanBuilder anInstanceOf(final Class type) {
return new BeanBuilder(type, BeanBuilderType.NULL, new LowerCaseNamingStrategy());
}
/**
* Return an instance of a {@link BeanBuilder} for the given type which can then be populated with values either manually or automatically. For example:
*
*
* Person aPerson = BeanBuilder.anInstanceOf(Person.class, "instance")
* .path("instance.firstName", "Bob")
* .build()
*
* @param type the type to return the {@link BeanBuilder} for
* @param rootName the name give to the root entity when referencing paths
*/
public static BeanBuilder anInstanceOf(final Class type, final String rootName) {
return new BeanBuilder(type, BeanBuilderType.NULL, new ForceRootNameNamingStrategy(new LowerCaseNamingStrategy(), rootName));
}
/**
* Return an instance of a {@link BeanBuilder} for the given type which is populated with empty objects but collections, maps, etc which have empty objects. For example:
*
*
*
* Person aPerson = BeanBuilder.anEmptyInstanceOf(Person.class)
* .path("person.firstName", "Bob")
* .build();
*
* @param type the type to return the {@link BeanBuilder} for
*/
public static BeanBuilder anEmptyInstanceOf(final Class type) {
return new BeanBuilder(type, BeanBuilderType.EMPTY, new LowerCaseNamingStrategy());
}
/**
* Return an instance of a {@link BeanBuilder} for the given type which is populated with empty objects but collections, maps, etc which have empty objects. For example:
*
*
* Person aPerson = BeanBuilder.anEmptyInstanceOf(Person.class, "instance")
* .path("instance.firstName", "Bob")
* .build()
*
* @param type the type to return the {@link BeanBuilder} for
* @param rootName the name give to the root entity when referencing paths
*/
public static BeanBuilder anEmptyInstanceOf(final Class type, final String rootName) {
return new BeanBuilder(type, BeanBuilderType.EMPTY, new ForceRootNameNamingStrategy(new LowerCaseNamingStrategy(), rootName));
}
/**
* Return an instance of a {@link BeanBuilder} for the given type which is populated with random values. For example:
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .path("person.firstName", "Bob")
* .build()
*
* @param type the type to return the {@link BeanBuilder} for
*/
public static BeanBuilder aRandomInstanceOf(final Class type) {
return new BeanBuilder(type, BeanBuilderType.RANDOM, new LowerCaseNamingStrategy());
}
/**
* Return an instance of a {@link BeanBuilder} for the given type which is populated with random values. For example:
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class,"instance")
* .path("instance.firstName", "Bob")
* .build()
*
* @param type the type to return the {@link BeanBuilder} for
*/
public static BeanBuilder aRandomInstanceOf(final Class type, final String rootName) {
return new BeanBuilder(type, BeanBuilderType.RANDOM, new ForceRootNameNamingStrategy(new LowerCaseNamingStrategy(), rootName));
}
private final Set excludedProperties = new HashSet();
private final Set excludedPaths = new HashSet();
private final Map paths = new HashMap();
private final Map properties = new HashMap();
private final Map, ValueFactory> types = new HashMap, ValueFactory>();
private final Class type;
private final BeanBuilderType builderType;
private final BeanNamingStrategy naming;;
private CollectionSize defaultCollectionSize = new CollectionSize(1, 5);
private final Map collectionSizeForPaths = new HashMap();
private final Map collectionSizeForProperties = new HashMap();
private BeanBuilder(final Class type, final BeanBuilderType builderType, final BeanNamingStrategy naming) {
this.type = type;
this.builderType = builderType;
this.naming = naming;
}
/**
* Configure the builder to populate the given property or path with the supplied value. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .with("firstName", "Bob")
* .build()
*
* @param propertyOrPathName the property or path name to set the value on
* @param value the value to assign the property or path
*/
public BeanBuilder with(final String propertyOrPathName, final Object value) {
return with(propertyOrPathName, theValue(value));
}
/**
* Configure the builder to populate any properties of the given type with a value created by the supplied value factory. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .with(Date.class, ValueFactories.oneOf(APR(5,1975), APR(5,1985)))
* .build()
*
* @param type the type of property to use the factory for
* @param factory the factory to use to create the value
*/
public BeanBuilder with(final Class type, final ValueFactory factory) {
this.types.put(type, factory);
return this;
}
/**
* Configure the builder to populate the given property or path with a value created by the supplied value factory. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .with("firstName", ValueFactories.oneOf("Bob", "Alice"))
* .build()
*
* @param propertyOrPathName the property or path name to set the value on
* @param factory the factory to use to create the value
*/
public BeanBuilder with(final String propertyOrPathName, final ValueFactory> factory) {
path(propertyOrPathName, factory);
property(propertyOrPathName, factory);
return this;
}
/**
* Configure the builder to populate the given property with the supplied value. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .property("firstName", "Bob")
* .build()
*
* @param propertyName the property to set the value on
* @param value the value to assign the property
*/
public BeanBuilder property(final String propertyName, final Object value) {
return property(propertyName, theValue(value));
}
/**
* Configure the builder to populate the given property with a value created by the supplied value factory. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .property("firstName", ValueFactories.oneOf("Bob", "Alice"))
* .build()
*
* @param propertyName the property to set the value on
* @param factory the factory to use to create the value
*/
public BeanBuilder property(final String propertyName, final ValueFactory> factory) {
properties.put(lowerCase(propertyName), factory);
return this;
}
/**
* Configure the builder to populate any properties of the given type with a value created by the supplied value factory. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .factory(Date.class, ValueFactories.oneOf(APR(5,1975), APR(5,1985)))
* .build()
*
* @param type the type of property to use the factory for
* @param factory the factory to use to create the value
*/
public BeanBuilder factory(final Class type, final ValueFactory factory) {
types.put(type, factory);
return this;
}
/**
* Configure the builder to exclude the given property from being populated. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .excludeProperty("firstName")
* .build()
*
* @param propertyName the property to exclude
*/
public BeanBuilder excludeProperty(final String propertyName) {
this.excludedProperties.add(lowerCase(propertyName));
return this;
}
/**
* Configure the builder to populate the given path with the supplied value. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .path("person.firstName", "Bob")
* .build()
*
* @param path the path to set the value on
* @param value the value to assign the path
*/
public BeanBuilder path(final String path, final Object value) {
return path(path, theValue(value));
}
/**
* Configure the builder to populate the given path with a value created by the supplied value factory. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .path("person.firstName", ValueFactories.oneOf("Bob", "Alice"))
* .build()
*
* @param path the path to set the value on
* @param factory the factory to use to create the value
*/
public BeanBuilder path(final String path, final ValueFactory> factory) {
this.paths.put(lowerCase(path), factory);
return this;
}
/**
* Configure the builder to exclude the given path from being populated. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .excludeProperty("person.firstName")
* .build()
*
* @param path the path to exclude
*/
public BeanBuilder excludePath(final String path) {
this.excludedPaths.add(lowerCase(path));
return this;
}
/**
* Configure the builder to set the size of a collections. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .collectionSizeOf(5)
* .build()
*
* @param size the size to create the collections
*/
public BeanBuilder collectionSizeOf(final int size) {
return collectionSizeRangeOf(size, size);
}
/**
* Configure the builder to set the size of a collections to within a given range. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .collectionSizeRangeOf(2,10)
* .build()
*
* @param min the minimum size to create the collections
* @param max the maximum size to create the collections
*/
public BeanBuilder collectionSizeRangeOf(final int min, final int max) {
this.defaultCollectionSize = new CollectionSize(min, max);
return this;
}
/**
* Configure the builder to set the size of a collection at a path to within a given range. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .collectionSizeRangeForPropertyOf("sublings", 1,3)
* .build()
*
* @param property the name of the property to limit the collection size of
* @param min the minimum size to create the collections
* @param max the maximum size to create the collections
*/
public BeanBuilder collectionSizeRangeForPropertyOf(final String property, final int min, final int max) {
this.collectionSizeForProperties.put(property, new CollectionSize(min, max));
return this;
}
/**
* Configure the builder to set the size of a collection at a path. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .collectionSizeForPropertyOf("sublings", 3)
* .build()
*
* @param property the name of the property to limit the collection size of
* @param size the size to create the collection
*/
public BeanBuilder collectionSizeForPropertyOf(final String property, final int size) {
return collectionSizeRangeForPropertyOf(property, size, size);
}
/**
* Configure the builder to set the size of a collection for a path to within a given range. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .collectionSizeForPathOf("person.sublings", 1, 3)
* .build()
*
* @param path the path to limit the collection size of
* @param size the size to create the collection
*/
public BeanBuilder collectionSizeForPathOf(final String path, final int size) {
return collectionSizeRangeForPathOf(path, size, size);
}
/**
* Configure the builder to set the size of a collection at a path to within a given range. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .collectionSizeRangeForPathOf("person.siblings", 1,3)
* .build()
*
* @param path the name of the path to limit the collection size of
* @param min the minimum size to create the collection
* @param max the maximum size to create the collection
*/
public BeanBuilder collectionSizeRangeForPathOf(final String path, final int min, final int max) {
this.collectionSizeForPaths.put(path, new CollectionSize(min, max));
return this;
}
/**
* Configure the builder to use a particular subtype when instantiating a super type. For example
*
*
* ShapeSorter aSorter = BeanBuilder.aRandomInstanceOf(ShapeSorter.class)
* .subtype(Shape.class, Square.class)
* .build()
*
* @param supertype the type of the super type
* @param subtype the subtype to use when instantiating the super type
*/
public BeanBuilder subtype(final Class supertype, final Class extends X> subtype) {
return with(supertype, oneOf(createInstanceOfFactoriesForTypes(subtype)));
}
/**
* Configure the builder to use any of of a particular subtype when instantiating a super type. For example
*
*
* ShapeSorter aSorter = BeanBuilder.aRandomInstanceOf(ShapeSorter.class)
* .subtype(Shape.class, Square.class, Circle.class, Triangle.class)
* .build()
*
* @param supertype the type of the super type
* @param subtypes the subtypes to pick from when instantiating the super type
*/
public BeanBuilder subtype(final Class supertype, final Class extends X>... subtypes) {
return with(supertype, oneOf(createInstanceOfFactoriesForTypes(subtypes)));
}
/**
* Build the configured instance. For example
*
*
* Person aPerson = BeanBuilder.aRandomInstanceOf(Person.class)
* .path("person.firstName", "Bob")
* .path("person.age", oneOf(25,35))
* .build()
*
*/
public T build() {
return populate(createNewInstance(), new BeanPropertyPath(naming.describeRoot(type)), new Stack(type(type)));
}
private I populate(final I instance, final BeanPropertyPath path, final Stack stack) {
if (instance != null) {
for (TypeProperty property : type(instance).setNamingStrategy(naming).propertyList()) {
populateProperty(instance, property, path.append(property.getName()), stack);
}
return instance;
} else {
return instance;
}
}
private void populateProperty(final Object instance, final TypeProperty property, final BeanPropertyPath path, final Stack stack) {
if (isExcludedPath(path) || isExcludedProperty(property)) {
LOG.trace("Ignore [{}]. Explicity excluded", path);
return;
}
ValueFactory factory = factoryForPath(property, path);
if (factory != null) {
assignValue(instance, property, path, createValue(factory, type), stack);
return;
}
if (isPropertySet(instance, property) || isChildOfAssignedPath(path) || isOverflowing(property, path, stack)) {
return;
}
if (property.isArray()) {
property.setValue(instance, createArray(property.getType().getComponentType(), path, stack));
} else if (property.isMap()) {
property.setValue(instance, createMap(property.getTypeParameter(0), property.getTypeParameter(1), collectionSize(path), path, stack));
} else if (property.isSet()) {
property.setValue(instance, createSet(property.getTypeParameter(0), collectionSize(path), path, stack));
} else if (property.isList() || property.isCollection()) {
property.setValue(instance, createList(property.getTypeParameter(0), collectionSize(path), path, stack));
} else {
assignValue(instance, property, path, createValue(property.getType()), stack);
}
}
private boolean isPropertySet(final Object instance, final TypeProperty property) {
if (property.getValue(instance) == null) {
return false;
} else if (property.isCollection()) {
return !property.getValue(instance, Collection.class).isEmpty();
} else if (property.isMap()) {
return !property.getValue(instance, Map.class).isEmpty();
} else {
return true;
}
}
private boolean isOverflowing(final TypeProperty property, final BeanPropertyPath path, final Stack stack) {
if (stack.contains(property.getType())) {
LOG.trace("Ignore {}. Avoids stack overflow caused by type {}", path, property.getTypeSimpleName());
return true;
}
for (Class> genericType : property.getTypeParameters()) {
if (stack.contains(genericType)) {
LOG.trace("Ignore {}. Avoids stack overflow caused by type {}", path, genericType.getSimpleName());
return true;
}
}
return false;
}
private ValueFactory factoryForPath(final TypeProperty property, final BeanPropertyPath path) {
return selectNotNull(paths.get(path.fullPath()), paths.get(path.fullPathWithNoIndexes()), properties.get(property.getName()));
}
private boolean isExcludedProperty(final TypeProperty property) {
return excludedProperties.contains(property.getName());
}
private boolean isExcludedPath(final BeanPropertyPath path) {
return excludedPaths.contains(path.fullPath()) || excludedPaths.contains(path.fullPathWithNoIndexes());
}
private boolean isChildOfAssignedPath(final BeanPropertyPath path) {
for (String assignedPath : paths.keySet()) {
if (path.startsWith(assignedPath + ".")) {
LOG.trace("Ignore {}. Child of assigned path {}", path, assignedPath);
return true;
}
}
return false;
}
private void assignValue(final Object instance, final TypeProperty property, final BeanPropertyPath path, final Object value, final Stack stack) {
if (value != null) {
LOG.trace("Assign {} value [{}:{}]", new Object[] {
path, value.getClass().getSimpleName(), identityHashCode(value)
});
property.setValue(instance, populate(value, path, stack.append(type(value))));
} else {
LOG.trace("Assign {} value [null]", path);
}
}
private E createValue(final Class type) {
for (Entry, ValueFactory> keyedFactory : types.entrySet()) {
if (type.isAssignableFrom(keyedFactory.getKey())) {
ValueFactory factory = keyedFactory.getValue();
return (E) createValue(factory, type);
}
}
switch (builderType) {
case RANDOM: {
ValueFactory factory = RANDOM_FACTORIES.get(type);
if (factory != null) {
return (E) createValue(factory, type);
} else if (type.isEnum()) {
return createValue(aRandomEnum(type), type);
} else {
return createValue(aNewInstanceOf(type), type);
}
}
case EMPTY: {
ValueFactory factory = EMPTY_FACTORIES.get(type);
if (factory != null) {
return (E) createValue(factory, type);
} else if (type.isEnum()) {
return null;
} else {
return createValue(aNewInstanceOf(type), type);
}
}
default:
return null;
}
}
private E createValue(final ValueFactory factory, final Class type) {
E value = factory != null ? factory.createValue() : null;
LOG.trace("Create Value [{}] for Type [{}]", value, type(type).simpleName());
return value;
}
private Object createArray(final Class type, final BeanPropertyPath path, final Stack stack) {
switch (builderType) {
case EMPTY:
case RANDOM:
Object array = Array.newInstance(type, collectionSize(path));
for (int i = 0; i < Array.getLength(array); ++i) {
Array.set(array, i, populate(createValue(type), path.appendIndex(i), stack.append(type)));
}
return array;
default:
return null;
}
}
private Set createSet(final Class type, final int length, final BeanPropertyPath path, final Stack stack) {
switch (builderType) {
case EMPTY:
case RANDOM:
Set set = new HashSet();
for (int i = 0; i < length; ++i) {
E value = populate(createValue(type), path.appendIndex(i), stack.append(type));
if (value != null) {
set.add(value);
}
}
return set;
default:
return null;
}
}
private List createList(final Class type, final int length, final BeanPropertyPath path, final Stack stack) {
switch (builderType) {
case EMPTY:
case RANDOM:
List list = new ArrayList();
for (int i = 0; i < length; ++i) {
E value = populate(createValue(type), path.appendIndex(i), stack.append(type));
if (value != null) {
list.add(value);
}
}
return list;
default:
return null;
}
}
private Map createMap(final Class keyType, final Class valueType, final int length, final BeanPropertyPath path, final Stack stack) {
switch (builderType) {
case EMPTY:
case RANDOM:
Map map = new HashMap();
for (int i = 0; i < length; ++i) {
K key = populate(createValue(keyType), path.appendIndex(i), stack.append(keyType).append(valueType));
if (key != null) {
map.put(key, createValue(valueType));
}
}
return map;
default:
return null;
}
}
private T createNewInstance() {
try {
return type.newInstance();
} catch (InstantiationException e) {
throw new BeanBuilderException("Failed to instantiate '" + type + "'. Error [" + e.getMessage() + "]", e);
} catch (IllegalAccessException e) {
throw new BeanBuilderException("Failed to instantiate '" + type + "'. Error [" + e.getMessage() + "]", e);
}
}
private int collectionSize(final BeanPropertyPath path) {
return selectNotNull(collectionSizeForPaths.get(path.fullPath()),
collectionSizeForPaths.get(path.fullPathWithNoIndexes()),
collectionSizeForProperties.get(propertyName(path)),
defaultCollectionSize).aRandomSize();
}
private String propertyName(final BeanPropertyPath path) {
return StringUtils.substringAfterLast(path.fullPathWithNoIndexes(), ".");
}
private List> createInstanceOfFactoriesForTypes(final Class extends X>... subtypes) {
List> factories = new ArrayList>();
for (Class extends X> subtype : subtypes) {
factories.add((ValueFactory) aNewInstanceOf(subtype));
}
return factories;
}
private static class Stack {
private final Type[] stack;
private Stack(final Type type) {
this(new Type[] {
type
});
}
private Stack(final Type[] stack) {
this.stack = stack;
}
public boolean contains(final Class> type) {
int hits = 0;
for (Type entry : stack) {
if (entry.is(type)) {
if ((++hits) > 1) {
return true;
}
}
}
return false;
}
public Stack append(final Class> value) {
return append(type(value));
}
public Stack append(final Type value) {
Type[] newStack = Arrays.copyOf(stack, stack.length + 1);
newStack[stack.length] = value;
return new Stack(newStack);
}
}
private static class CollectionSize {
private final int min, max;
public CollectionSize(final int min, final int max) {
this.min = min;
this.max = max;
}
public int aRandomSize() {
if (min == max) {
return min;
} else {
return RandomBuilder.aRandomInteger(min, max);
}
}
}
@SuppressWarnings("serial")
private static final Map, ValueFactory> RANDOM_FACTORIES = new HashMap, ValueFactory>() {
{
put(Short.class, aRandomShort());
put(short.class, aRandomShort());
put(Integer.class, aRandomInteger());
put(int.class, aRandomInteger());
put(Long.class, aRandomLong());
put(long.class, aRandomLong());
put(Double.class, aRandomDouble());
put(double.class, aRandomDouble());
put(Float.class, aRandomFloat());
put(float.class, aRandomFloat());
put(Boolean.class, aRandomBoolean());
put(boolean.class, aRandomBoolean());
put(Byte.class, aRandomByte());
put(byte.class, aRandomByte());
put(Character.class, aRandomChar());
put(char.class, aRandomChar());
put(String.class, aRandomString());
put(BigDecimal.class, aRandomDecimal());
put(Date.class, aRandomDate());
}
};
private static final Map, ValueFactory> EMPTY_FACTORIES = new HashMap, ValueFactory>() {
private static final long serialVersionUID = 1L;
{
put(Short.class, aNullValue());
put(short.class, theValue((short) 0));
put(Integer.class, aNullValue());
put(int.class, theValue(0));
put(Long.class, aNullValue());
put(long.class, theValue(0));
put(Double.class, aNullValue());
put(double.class, theValue(0.0));
put(Float.class, aNullValue());
put(float.class, theValue((float) 0.0));
put(Boolean.class, aNullValue());
put(boolean.class, theValue(false));
put(Byte.class, aNullValue());
put(byte.class, theValue((byte) 0));
put(Character.class, aNullValue());
put(char.class, theValue((char) 0));
put(String.class, aNullValue());
put(BigDecimal.class, aNullValue());
put(Date.class, aNullValue());
}
};
private static enum BeanBuilderType {
RANDOM, EMPTY, NULL
}
private T selectNotNull(final T... options) {
for (T option : options) {
if (option != null) {
return option;
}
}
return null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy