All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.exparity.test.builder.BeanBuilder Maven / Gradle / Ivy

Go to download

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 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... 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... subtypes) { List> factories = new ArrayList>(); for (Class 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