AbstractRandomDataProviderStrategy.java

/**
 *
 */
package uk.co.jemos.podam.api;

import net.jcip.annotations.ThreadSafe;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import uk.co.jemos.podam.common.*;
import uk.co.jemos.podam.exceptions.PodamMockeryException;
import uk.co.jemos.podam.typeManufacturers.ArrayTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.BooleanTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.ByteTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.CharTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.CollectionTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.DoubleTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.EnumTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.FloatTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.IntTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.LongTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.MapTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.ShortTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.StringTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.TypeTypeManufacturerImpl;
import uk.co.jemos.podam.typeManufacturers.TypeManufacturer;

import jakarta.validation.constraints.Email;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Default abstract implementation of a {@link DataProviderStrategy}
 * <p>
 * This default implementation returns values based on a random generator.
 * Convinient for subclassing and redefining behaviour.
 * <b>Don't use this implementation if you seek deterministic values</b>
 * </p>
 *
 * <p>
 * All values returned by this implementation are <b>different from zero</b>.
 * </p>
 *
 * @author mtedone
 *
 * @since 1.0.0
 *
 */
@ThreadSafe
public abstract class AbstractRandomDataProviderStrategy implements RandomDataProviderStrategy {

	// ------------------->> Constants

	/** Application logger */
	private static final Logger LOG = LoggerFactory.getLogger(AbstractRandomDataProviderStrategy.class);

	/**
	 * How many times it is allowed to PODAM to create an instance of the same
	 * class in a recursive hierarchy
	 */
	private int maxDepth = 3;

	/** The number of collection elements. */
	private final AtomicInteger nbrOfCollectionElements = new AtomicInteger();

	/** Flag to enable/disable the memoization setting. */
	private final AtomicBoolean isMemoizationEnabled = new AtomicBoolean();

	/**
	 * A map to keep one object for each class. If memoization is enabled, the
	 * factory will use this table to avoid creating objects of the same class
	 * multiple times.
	 */
	private final Map<Class<?>, Map<Type[], Object>> memoizationTable = new HashMap<Class<?>, Map<Type[], Object>>();

	/**
	 * A mapping between types and their registered manufacturers
	 */
	private final ConcurrentHashMap<Class<?>, TypeManufacturer<?>> typeManufacturers
			= new ConcurrentHashMap<Class<?>, TypeManufacturer<?>>();

	/**
	 * A list of user-submitted specific implementations for interfaces and
	 * abstract classes
	 */
	private final Map<Class<?>, Class<?>> specificTypes = new ConcurrentHashMap<Class<?>, Class<?>>();

	/**
	 * A list of user-submitted factories to build interfaces and abstract classes
	 */
	private final Map<Class<?>, Class<?>> factoryTypes = new ConcurrentHashMap<Class<?>, Class<?>>();

	/**
	 * Mapping between annotations and attribute strategies
	 */
	private final Map<Class<? extends Annotation>, AttributeStrategy<?>> attributeStrategies
			= new ConcurrentHashMap<Class<? extends Annotation>, AttributeStrategy<?>>();

	/**
	 * Mapping between attributes and attribute strategies
	 */
	private final Map<Class<?>, Map<String,AttributeStrategy<?>>> attributeClassStrategies
			= new ConcurrentHashMap<Class<?>, Map<String,AttributeStrategy<?>>>();

	/** The constructor comparator */
	private AbstractConstructorComparator constructorHeavyComparator =
			ConstructorHeavyFirstComparator.INSTANCE;

	/** The constructor comparator */
	private AbstractConstructorComparator constructorLightComparator =
			ConstructorLightFirstComparator.INSTANCE;

	/** The constructor comparator */
	private AbstractMethodComparator methodHeavyComparator
			= MethodHeavyFirstComparator.INSTANCE;

	/** The constructor comparator */
	private AbstractMethodComparator methodLightComparator
			= MethodLightFirstComparator.INSTANCE;

	// ------------------->> Instance / Static variables

	// ------------------->> Constructors

	/**
	 * Implementation of the Singleton pattern
	 */
	public AbstractRandomDataProviderStrategy() {
		this(PodamConstants.DEFAULT_NBR_COLLECTION_ELEMENTS);
	}

	public AbstractRandomDataProviderStrategy(int nbrOfCollectionElements) {
		this.nbrOfCollectionElements.set(nbrOfCollectionElements);

		TypeManufacturer<?> byteManufacturer = new ByteTypeManufacturerImpl();
		typeManufacturers.put(byte.class, byteManufacturer);
		typeManufacturers.put(Byte.class, byteManufacturer);

		TypeManufacturer<?> booleanManufacturer = new BooleanTypeManufacturerImpl();
		typeManufacturers.put(boolean.class, booleanManufacturer);
		typeManufacturers.put(Boolean.class, booleanManufacturer);

		TypeManufacturer<?> charManufacturer = new CharTypeManufacturerImpl();
		typeManufacturers.put(char.class, charManufacturer);
		typeManufacturers.put(Character.class, charManufacturer);

		TypeManufacturer<?> shortManufacturer = new ShortTypeManufacturerImpl();
		typeManufacturers.put(short.class, shortManufacturer);
		typeManufacturers.put(Short.class, shortManufacturer);

		TypeManufacturer<?> intManufacturer = new IntTypeManufacturerImpl();
		typeManufacturers.put(int.class, intManufacturer);
		typeManufacturers.put(Integer.class, intManufacturer);

		TypeManufacturer<?> longManufacturer = new LongTypeManufacturerImpl();
		typeManufacturers.put(long.class, longManufacturer);
		typeManufacturers.put(Long.class, longManufacturer);

		TypeManufacturer<?> floatManufacturer = new FloatTypeManufacturerImpl();
		typeManufacturers.put(float.class, floatManufacturer);
		typeManufacturers.put(Float.class, floatManufacturer);

		TypeManufacturer<?> doubleManufacturer = new DoubleTypeManufacturerImpl();
		typeManufacturers.put(double.class, doubleManufacturer);
		typeManufacturers.put(Double.class, doubleManufacturer);

		TypeManufacturer<?> stringManufacturer = new StringTypeManufacturerImpl();
		typeManufacturers.put(CharSequence.class, stringManufacturer);

		TypeManufacturer<?> enumManufacturer = new EnumTypeManufacturerImpl();
		typeManufacturers.put(Enum.class, enumManufacturer);

		TypeManufacturer<?> typeManufacturer = new TypeTypeManufacturerImpl();
		typeManufacturers.put(Type.class, typeManufacturer);

		TypeManufacturer<?> collectionManufacturer = new CollectionTypeManufacturerImpl();
		typeManufacturers.put(Collection.class, collectionManufacturer);

		TypeManufacturer<?> mapManufacturer = new MapTypeManufacturerImpl();
		typeManufacturers.put(Map.class, mapManufacturer);

		TypeManufacturer<?> arrayManufacturer = new ArrayTypeManufacturerImpl();
		typeManufacturers.put(Cloneable.class, arrayManufacturer);

        addOrReplaceAttributeStrategy(Email.class, new EmailStrategy());
	}

	// ------------------->> Public methods

	// ------------------->> Getters / Setters

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getNumberOfCollectionElements(Class<?> type) {
		return nbrOfCollectionElements.get();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setDefaultNumberOfCollectionElements(int newNumberOfCollectionElements) {
		nbrOfCollectionElements.set(newNumberOfCollectionElements);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getMaxDepth(Class<?> type) {
		return maxDepth;
	}

	/**
	 * Max depth setter
	 *
	 * @param maxDepth
	 *            defines new max depth
	 */
	public void setMaxDepth(int maxDepth) {
		this.maxDepth = maxDepth;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean isMemoizationEnabled() {
		return isMemoizationEnabled.get();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setMemoization(boolean isMemoizationEnabled) {
		this.isMemoizationEnabled.set(isMemoizationEnabled);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized Object getMemoizedObject(AttributeMetadata attributeMetadata) {

		if (isMemoizationEnabled.get()) {
			/* No memoization for arrays, collections and maps */
			Class<?> pojoClass = attributeMetadata.getPojoClass();
			if (pojoClass == null ||
					(!pojoClass.isArray() &&
					!Collection.class.isAssignableFrom(pojoClass) &&
					!Map.class.isAssignableFrom(pojoClass))) {

				Map<Type[], Object> map = memoizationTable.get(attributeMetadata.getAttributeType());
				if (map != null) {
					for (Entry<Type[], Object> entry : map.entrySet()) {
						if (Arrays.equals(entry.getKey(), attributeMetadata.getAttrGenericArgs())) {
							LOG.trace("Found memoized {}<{}>", attributeMetadata.getAttributeType(), attributeMetadata.getAttrGenericArgs());
							return entry.getValue();
						}
					}
				}
			}
		}
		return null;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized void cacheMemoizedObject(AttributeMetadata attributeMetadata,
			Object instance) {

		if (isMemoizationEnabled.get()) {
			Map<Type[], Object> map = memoizationTable.get(attributeMetadata.getAttributeType());
			if (map == null) {
				map = new HashMap<Type[], Object>();
				memoizationTable.put(attributeMetadata.getAttributeType(), map);
			}
			LOG.trace("Saving memoized {}<{}>", attributeMetadata.getAttributeType(), attributeMetadata.getAttrGenericArgs());
			map.put(attributeMetadata.getAttrGenericArgs(), instance);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized void clearMemoizationCache() {

		memoizationTable.clear();

	}

	/**
	 * Rearranges POJO's constructors in order they will be tried to produce the
	 * POJO. Default strategy consist of putting constructors with less
	 * parameters to be tried first.
	 *
	 * @param constructors
	 *            Array of POJO's constructors
	 * @param order
	 *            {@link uk.co.jemos.podam.api.DataProviderStrategy.Order} how to sort constructors
	 */
	@Override
	public void sort(Constructor<?>[] constructors, Order order) {
		AbstractConstructorComparator constructorComparator;
		switch(order) {
		case HEAVY_FIRST:
			constructorComparator = constructorHeavyComparator;
			break;
		default:
			constructorComparator = constructorLightComparator;
			break;
		}
		Arrays.sort(constructors, constructorComparator);
	}

	/**
	 * Rearranges POJO's methods in order they will be tried to produce the
	 * POJO. Default strategy consist of putting methods with more
	 * parameters to be tried first.
	 *
	 * @param methods
	 *            Array of POJO's methods
	 * @param order
	 *            {@link uk.co.jemos.podam.api.DataProviderStrategy.Order} how to sort constructors
	 */
	@Override
	public void sort(Method[] methods, Order order) {
		AbstractMethodComparator methodComparator;
		switch(order) {
		case HEAVY_FIRST:
			methodComparator = methodHeavyComparator;
			break;
		default:
			methodComparator = methodLightComparator;
			break;
		}
		Arrays.sort(methods, methodComparator);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public <T> DataProviderStrategy addOrReplaceTypeManufacturer(
			Class<? extends T> type, TypeManufacturer<T> typeManufacturer) {

		typeManufacturers.put(type, typeManufacturer);
		return this;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public <T> DataProviderStrategy removeTypeManufacturer(
			Class<T> type) {

		typeManufacturers.remove(type);
		return this;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public <T> T getTypeValue(AttributeMetadata attributeMetadata,
			ManufacturingContext manufacturingCtx,
			Class<T> pojoType) {

		if (null == attributeMetadata) {
			throw new IllegalArgumentException(
					"The attribute metadata inside the wrapper cannot be null");
		}

		if (null == attributeMetadata.getAttributeAnnotations()) {
			throw new IllegalArgumentException(
					"The annotations list within the attribute metadata cannot be null, although it can be empty");
		}

		Deque<Class<?>> types = new ArrayDeque<Class<?>>();
		types.add(pojoType);
		while (!types.isEmpty()) {

			Class<?> type = types.remove();
			TypeManufacturer<?> manufacturer = typeManufacturers.get(type);
			if (null != manufacturer) {
				try {
					@SuppressWarnings("unchecked")
					T tmp = (T) manufacturer.getType(this, attributeMetadata,
							manufacturingCtx);
					if (null != tmp) {
						log(attributeMetadata);
						return tmp;
					} else {
						LOG.debug("{} cannot manufacture {}", manufacturer, pojoType);
					}
				} catch (Exception e) {
					throw new PodamMockeryException(
							"Unable to instantiate " + pojoType, e);
				}
			}

			for (Class<?> iface : type.getInterfaces()) {
				types.add(iface);
			}
			type = type.getSuperclass();
			if (null != type) {
				types.add(type);
			}
		}

		LOG.debug("Failed to find suitable manufacturer for type {}", pojoType);
		return null;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public <T> AbstractRandomDataProviderStrategy addOrReplaceFactory(
			final Class<T> abstractClass, final Class<?> factoryClass) {

		factoryTypes.put(abstractClass, factoryClass);
		return this;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public <T> AbstractRandomDataProviderStrategy removeFactory(
			final Class<T> abstractClass) {

		factoryTypes.remove(abstractClass);
		return this;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Class<?> getFactoryClass(Class<?> nonInstantiatableClass) {

		return factoryTypes.get(nonInstantiatableClass);
	}

	/**
	 * Bind an interface/abstract class to a specific implementation. If the
	 * strategy previously contained a binding for the interface/abstract class,
	 * the old value will not be replaced by the new value. If you want to force the
	 * value replacement, invoke removeSpecific before invoking this method.
	 * If you want to implement more sophisticated binding strategy, override this class.
	 *
	 * @param <T> return type
	 * @param abstractClass
	 *            the interface/abstract class to bind
	 * @param specificClass
	 *            the specific class implementing or extending
	 *            {@code abstractClass}.
	 * @return itself
	 */
	@Override
	public <T> DataProviderStrategy addOrReplaceSpecific(
			final Class<T> abstractClass, final Class<? extends T> specificClass) {

		specificTypes.put(abstractClass, specificClass);

		return this;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public <T> DataProviderStrategy removeSpecific(
			final Class<T> abstractClass) {

		specificTypes.remove(abstractClass);
		return this;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public <T> Class<? extends T> getSpecificClass(
			Class<T> nonInstantiatableClass) {

		@SuppressWarnings("unchecked")
		Class<? extends T> found = (Class<? extends T>) specificTypes
				.get(nonInstantiatableClass);
		if (found == null) {
			found = nonInstantiatableClass;
		}
		return found;

	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public RandomDataProviderStrategy addOrReplaceAttributeStrategy(
			final Class<? extends Annotation> annotationClass,
			final AttributeStrategy<?> attributeStrategy) {

		attributeStrategies.put(annotationClass, attributeStrategy);

		return this;
	}

	/**
	 * Remove binding of an annotation to attribute strategy
	 *
	 * @param annotationClass
	 *            the annotation class to remove binding
	 * @return itself
	 */
	@Override
	public RandomDataProviderStrategy removeAttributeStrategy(
			final Class<? extends Annotation> annotationClass) {

		attributeStrategies.remove(annotationClass);

		return this;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public AttributeStrategy<?> getStrategyForAnnotation(
			final Class<? extends Annotation> annotationClass) {

		return attributeStrategies.get(annotationClass);

	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public RandomDataProviderStrategy addOrReplaceAttributeStrategy(
			final Class<?> type, final String attributeName,
			final AttributeStrategy<?> attributeStrategy) {

		Map<String,AttributeStrategy<?>> classStrategies = attributeClassStrategies.get(type);
		if (null == classStrategies) {
			classStrategies = new ConcurrentHashMap<String,AttributeStrategy<?>>();
			attributeClassStrategies.put(type, classStrategies);
		}
		classStrategies.put(attributeName, attributeStrategy);

		return this;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public RandomDataProviderStrategy removeAttributeStrategy(
			final Class<?> type, String attributeName) {

		Map<String,AttributeStrategy<?>> classStrategies = attributeClassStrategies.get(type);
		if (null != classStrategies) {
			classStrategies.remove(attributeName);
		}

		return this;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public AttributeStrategy<?> getStrategyForAttribute(
			final ClassAttribute attribute) {

		AttributeStrategy<?> attributeStrategy = null;
		Field field = attribute.getAttribute();
		if (null != field) {
			Class<?> type = field.getDeclaringClass();
			Map<String,AttributeStrategy<?>> classStrategies = attributeClassStrategies.get(type);
			if (null != classStrategies) {
				attributeStrategy = classStrategies.get(attribute.getName());
			}
		}
		return attributeStrategy;

	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public AbstractConstructorComparator getConstructorLightComparator() {
		return constructorLightComparator;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setConstructorLightComparator(AbstractConstructorComparator constructorLightComparator) {
		this.constructorLightComparator = constructorLightComparator;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public AbstractConstructorComparator getConstructorHeavyComparator() {
		return constructorHeavyComparator;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setConstructorHeavyComparator(AbstractConstructorComparator constructorHeavyComparator) {
		this.constructorHeavyComparator = constructorHeavyComparator;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public AbstractMethodComparator getMethodLightComparator() {
		return methodLightComparator;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setMethodLightComparator(AbstractMethodComparator methodLightComparator) {
		this.methodLightComparator = methodLightComparator;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public AbstractMethodComparator getMethodHeavyComparator() {
		return methodHeavyComparator;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setMethodHeavyComparator(AbstractMethodComparator methodHeavyComparator) {
		this.methodHeavyComparator = methodHeavyComparator;
	}

	// ------------------->> Private methods

	private void log(AttributeMetadata attributeMetadata) {
		LOG.trace("Providing data for attribute {}.{}",
				attributeMetadata.getPojoClass() != null ? attributeMetadata.getPojoClass().getName() : "",
				attributeMetadata.getAttributeName() != null ? attributeMetadata.getAttributeName() : "");
	}

	// ------------------->> equals() / hashcode() / toString()

	// ------------------->> Inner classes

}