org.dummycreator.DummyCreator Maven / Gradle / Ivy
Show all versions of dummy-creator Show documentation
/*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at http://www.opensource.org/licenses/cddl1.php
* or http://www.opensource.org/licenses/cddl1.txt.
*
* When distributing Covered Code, include this CDDL Header Notice in each file
* and include the License file at http://www.opensource.org/licenses/cddl1.php.
* If applicable, add the following below the CDDL Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* The Original Software is dummyCreator. The Initial Developer of the Original
* Software is Alexander Muthmann .
*/
package org.dummycreator;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import org.codemonkey.javareflection.FieldUtils;
import org.codemonkey.javareflection.FieldUtils.BeanRestriction;
import org.codemonkey.javareflection.FieldUtils.Visibility;
import org.codemonkey.javareflection.FieldWrapper;
/**
* Tool to create populated dummy objects of a given class. This tool will recursively run through its setters and try to come up with newly
* populated objects for those fields.
*
* To avoid recursive infinite loops, this tool keeps track of previously populated instances of a certain type and reuse that instead.
*
* For numbers being generated a random number is being using, for strings a lorem ipsum generated is being used.
*
* @see #create(Class)
*
* @author Alexander Muthmann
* @author Benny Bottema
*/
public class DummyCreator {
private final Logger logger = Logger.getLogger(getClass());
/**
* A cache for previously found {@link Constructor} instances and preferred constructor for previous successfully invoked constructors.
*/
private final ConstructorCache constructorCache;
/**
* A cache for previously found {@link Method} instances and preferred constructor for previous successfully invoked constructors.
*/
private final MethodCache methodCache;
/**
* A map that contains deferred class types for a given type. With this you can defer the creation of a dummy instance to another type.
* This is useful if you need to instance dummy objects for an interface or abstract class.
*
* @see ClassBindings
*/
private final ClassBindings classBindings;
/**
* Default constructor: configures the Dummy Creator with vanilla new bindings and caches.
*/
public DummyCreator() {
this(new ClassBindings());
}
/**
* Constructor: configures the Dummy Creator with a given {@link ClassBindings} instance and new caches.
*/
public DummyCreator(ClassBindings classBindings) {
this.classBindings = classBindings;
this.constructorCache = new ConstructorCache();
this.methodCache = new MethodCache();
}
/**
* Main method, creates a dummy object of a given type.
*
* Provide your own {@link ClassBindings} in {@link #DummyCreator(ClassBindings)} to control how objects are created for specific types
* (such as the abstract List class). This is the main-method used to create a dummy of a certain class. It's called with the needed
* class. e.g. Integer i = createDummyOfClass(Integer.class)
*
* @param The type to be created and returned (returned type can be a sub type of T
).
* @param clazz The type that should be created
* @return The instantiated and populated object (can be a sub type, depending how the {@link ClassBindings} are configured!).
* @throws IllegalArgumentException Thrown if an abstract type or interface was given for which no binding could be found in the
* provided {@link ClassBindings}.
*/
public T create(final Class clazz) {
Map, ClassUsageInfo>> used_classes = new HashMap, ClassUsageInfo>>();
if (Modifier.isAbstract(clazz.getModifiers()) || Modifier.isInterface(clazz.getModifiers())) {
if (classBindings.find(clazz) == null) {
throw new IllegalArgumentException(String.format("Unable to instantiate object of type [%s] and no binding found for this type", clazz));
}
}
return create(clazz, used_classes);
}
/**
* Will try to create a new object for the given type, while maintaining a track record of already created - and - populated objects to
* avoid recursive loops.
*
* @param The type to be created and returned (returned type can be a sub type of T
).
* @param clazz The type that should be created
* @param knownInstances A list of previously created and populated objects for a specific type.
* @return The instantiated and populated object (can be a sub type, depending how the {@link ClassBindings} are configured).
* @throws IllegalArgumentException Thrown if class could not be instantiated. Possible constructor invocation exceptions are logged separately.
*/
@SuppressWarnings("unchecked")
private T create(final Class clazz, final Map, ClassUsageInfo>> knownInstances) {
// List of Classes, we already used for population. By remembering, we can avoid looping
if (knownInstances.get(clazz) == null || !knownInstances.get(clazz).isPopulated()) {
Object bind = classBindings.find(clazz);
if (bind != null && bind.getClass() == clazz) {
return (T) bind;
}
T ret = checkPrimitivesAndArray(clazz);
// If it was a special, we got ret != null
if (ret != null) {
return ret;
}
ClassUsageInfo usedInfo = new ClassUsageInfo();
usedInfo.setInstance(ret);
knownInstances.put(clazz, usedInfo);
List constructorExceptions = new ArrayList();
// Try to defer instantiation to a binding, if available
ret = findClassBindings(clazz, knownInstances, constructorExceptions);
if (ret != null) {
return ret;
}
// Do we need to create a string?
if (clazz == String.class) {
return (T) RandomCreator.getRandomString();
}
// Is the class an enum?
if (clazz.isEnum()) {
T[] enums = clazz.getEnumConstants();
return enums[RandomCreator.getRandomInt(enums.length - 1)];
}
// Load the constructors
List> consts = constructorCache.getCachedConstructors(clazz);
if (consts == null) {
consts = new ArrayList>();
Constructor>[] _con = clazz.getConstructors();
// Sort the constructors by their parameter-count
java.util.Arrays.sort(_con, new ConstructorComparator());
consts.addAll(Arrays.asList(_con));
// Add to cache
constructorCache.addConstructors(clazz, consts);
}
// Check if we have a prefered Constructor and try it
Constructor preferedConstructor = (Constructor) constructorCache.getPreferedConstructor(clazz);
if (preferedConstructor != null) {
ret = tryConstructor(preferedConstructor, knownInstances, constructorExceptions);
}
if (ret == null) {
for (Constructor> co : consts) {
ret = (T) tryConstructor(co, knownInstances, constructorExceptions);
if (ret != null) {
constructorCache.setPreferedConstructor(clazz, co);
// Worked
break;
}
}
}
if (ret == null) {
for (Exception e : constructorExceptions) {
logger.error("tried but failed to use constructor: ", e);
}
throw new IllegalArgumentException(String.format("Could not instantiate object for type [%s]", clazz));
}
usedInfo.setInstance(ret);
usedInfo.setPopulated(true);
populateObject(ret, knownInstances);
return ret;
} else {
return (T) knownInstances.get(clazz).getInstance();
}
}
/**
* Tries to instantiate an object with the given constructor.
*/
private T tryConstructor(final Constructor c, final Map, ClassUsageInfo>> knownInstances, List constructorExceptions) {
@SuppressWarnings("unchecked")
Class[] parameters = (Class[]) c.getParameterTypes();
try {
if (parameters.length > 0) {
final Object[] params = new Object[parameters.length];
for (int i = 0; i < params.length; i++) {
params[i] = create(parameters[i], knownInstances);
}
return c.newInstance(params);
} else {
return c.newInstance();
}
} catch (InvocationTargetException e) {
constructorExceptions.add(e);
} catch (InstantiationException e) {
constructorExceptions.add(e);
} catch (IllegalAccessException e) {
constructorExceptions.add(e);
}
return null;
}
/**
* Populates given object with dummy value. The behavior can vary depending on the type of object, as follows:
*
* - Collection: if the type is a subtype of {@link Collection}, a random number (2 or 3) of items will be added
* - Map: if the type is a subtype of {@link Map}, a random number (2 or 3) of key/value entries will be added
* - Other types: The setter methods will be retrieved from the object and will be invoked with a new dummy
* value.
*
*
* note: In case of a Collection
or Map
, the objects created are String
* instances, unless a generic type can be derived with java reflection.
*
* For example, List
or HashMap
will both result in an instance containing only strings,
* but a type declared as class FooList extends ArrayList
will result in an instance containing Foo
* elements. The behavior of the first case is a result of runtime type erasure. Only declared generic types in the
* class or interface signature, as in the latter case, are discoverable in runtime (the latter case).
*
* @param subject The object to populate with dummy values.
* @param knownInstances A list of known instances to keep track of already processed classes (to avoid infinite loop)
* @see #create(Class, Map)
*/
@SuppressWarnings({ "unchecked" })
private void populateObject(final T subject, final Map, ClassUsageInfo>> knownInstances) {
final Class> clazz = subject.getClass();
if (subject instanceof Collection) {
for (int i = 0; i < RandomCreator.getRandomInt(2) + 2; i++) {
// detect generic declarations
Type[] genericTypes = ((ParameterizedType) subject.getClass().getGenericSuperclass()).getActualTypeArguments();
if (genericTypes.length > 0 && (genericTypes[0] instanceof Class)) {
// uses generic type if available, Integer for '>'
((Collection