RandomData.java

/*
 * Copyright 2000-2012 Namics AG. All rights reserved.
 */

package com.namics.commons.random;

import com.googlecode.gentyref.GenericTypeReflector;
import com.namics.commons.random.generator.InformedRandomGenerator;
import com.namics.commons.random.generator.RandomGenerator;
import com.namics.commons.random.generator.RandomGeneratorFactory;
import com.namics.commons.random.support.BarcodeUtils;
import com.namics.commons.random.support.BeanUtils;
import com.namics.commons.random.support.LoremIpsum;
import com.namics.commons.random.support.ValuePool;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.math.RandomUtils;
import org.joda.time.LocalDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.text.Normalizer;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;

import static com.namics.commons.random.support.BeanUtils.makeAccessible;

/**
 * Utility class to generate random data.
 *
 * Hint according to international chars:
 * You may enable international names by setting either
 * - system property "random.names.international" to "true"
 * - environment variable "RANDOM_NAMES_INTERNATIONAL" to "true"
 *
 * @author aschaefer
 * @author pnueesch
 * @since 21.02.14 13:43
 */
public final class RandomData {
	private static final Logger LOG = LoggerFactory.getLogger(RandomData.class);

	/**
	 * Creates a random instance of the class provided, if a suitable {@link RandomGenerator} is registered.
	 * A suitable generator can be registered in several ways:
	 * <li>automatically when placed in same package as the SupportedType</li>
	 * <li>Manually by registering either class or instance directly to {@link com.namics.commons.random.RandomData#addRandomGenerator}</li>
	 * <li>Manually by registering package for scanning  to {@link com.namics.commons.random.RandomData#addRandomGenerators(String)}</li>
	 *
	 * @param type class to create a random object for.
	 * @param info optional additional information for the object to create, e.g. a field name.
	 * @return Random instance of the class.
	 * @throws java.lang.IllegalArgumentException if no suitable {@link RandomGenerator} is registered.
	 */
	public static <T> T random(Type type, Object... info) throws IllegalArgumentException {
		if (type instanceof ParameterizedType) {
			ParameterizedType parameterizedType = (ParameterizedType) type;
			Type[] genericTypes = parameterizedType.getActualTypeArguments();
			Object[] information = new Object[0];
			information = ArrayUtils.addAll(information, genericTypes);
			information = ArrayUtils.addAll(information, info);
			return (T) random((Class) parameterizedType.getRawType(), information);
		} else if (type instanceof Class) {
			return (T) random((Class) type, info);
		}
		throw new IllegalArgumentException(type.toString() + " not supported, only ParameterizedType and Class are supported.");
	}

	/**
	 * Creates a random instance of the class provided, if a suitable {@link RandomGenerator} is registered.
	 * A suitable generator can be registered in several ways:
	 * <li>automatically when placed in same package as the SupportedType</li>
	 * <li>Manually by registering either class or instance directly to {@link com.namics.commons.random.RandomData#addRandomGenerator}</li>
	 * <li>Manually by registering package for scanning  to {@link com.namics.commons.random.RandomData#addRandomGenerators(String)}</li>
	 *
	 * @param clazz class to create a random object for.
	 * @param info
	 * @return Random instance of the class.
	 * @throws java.lang.IllegalArgumentException if no suitable {@link RandomGenerator} is registered.
	 */
	public static <T> T random(Class<T> clazz, Object... info) throws IllegalArgumentException {
		if (clazz == null) {
			return null;
		}

		// check for enum
		T[] enumConstants = clazz.getEnumConstants();
		if (enumConstants != null) {
			return RandomData.random(enumConstants);
		}

		RandomGenerator<T> generator = RandomGeneratorFactory.generator(clazz);
		if (generator != null) {
			if (info != null && generator instanceof InformedRandomGenerator) {
				return ((InformedRandomGenerator<T>) generator).random(info);
			}
			return generator.random();
		}
		if (Class.class != clazz) {
			if (clazz.isArray()) {
				int length = RandomData.randomInteger(1, 5);
				Object array = Array.newInstance(clazz.getComponentType(), length);
				for (int i = 0; i < length; i++) {
					Array.set(array, i, RandomData.random(clazz.getComponentType()));
				}
				return (T) array;
			} else {
				T bean = createInstance(clazz);
				populateBean(bean);
				return bean;
			}
		}
		throw new IllegalArgumentException("No random generator for type " + clazz + GENERATOR_REGISTER_INFO);

	}

	private static <T> T createInstance(Class<T> clazz) {
		try {
			Constructor<T> constructor = clazz.getDeclaredConstructor();
			constructor.setAccessible(true);
			return constructor.newInstance();
		} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
			throw new IllegalArgumentException(
					"Could not create random instance for " + clazz + ", maybe there is no default constructor available, see exception cause for details." + GENERATOR_REGISTER_INFO, e);
		} catch (NoSuchMethodException e) {
			LOG.debug("No constructor without arguments {}", e, null);
			return createWithNoDefaultConstructor(clazz);
		}
	}

	@SuppressWarnings("unchecked")
	private static <T> T createWithNoDefaultConstructor(Class<T> clazz) {
		try {
			Constructor<T>[] constructors = (Constructor<T>[]) clazz.getConstructors();
			for (Constructor<T> constructor : constructors) {
				if (constructor != null) {
					Parameter[] parameters = constructor.getParameters();
					Object[] args = new Object[parameters.length];
					for (int i = 0; i < parameters.length; i++) {
						args[i] = random(parameters[i].getType(), parameters[i].getName());
					}
					constructor.setAccessible(true);
					return constructor.newInstance(args);
				}
			}
		} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
			throw new IllegalArgumentException(
					"Could not create random instance for " + clazz + ", maybe there is no default constructor available, see exception cause for details." + GENERATOR_REGISTER_INFO, e);
		}
		throw new IllegalArgumentException(
				"Could not create random instance for " + clazz + ", maybe there is no default constructor available, see exception cause for details." + GENERATOR_REGISTER_INFO);
	}

	/**
	 * Pickes a random value of the array / vararg provided.
	 *
	 * @param items array / vararg to pick an items from
	 * @param <I>   Item class
	 * @return Randomly picked items
	 */
	public static <I> I random(I[] items) {
		return random(Arrays.asList(items));
	}

	/**
	 * Pickes a random value of the collection provided.
	 *
	 * @param items collection to pick an items from
	 * @param <I>   Item class
	 * @return Randomly picked items
	 */
	public static <I> I random(Collection<I> items) {
		if (items == null || items.size() == 0) {
			return null;
		}
		int size = items.size();
		int random = randomInteger(0, size);
		int current = 0;
		for (I i : items) {
			if (current == random) {
				return i;
			}
			current++;
		}
		return items.iterator().next();
	}

	public static boolean randomBoolean() {
		return random(Boolean.class);
	}

	public static Integer randomInteger() {
		return random(Integer.class);
	}

	public static Long randomLong() {
		return random(Long.class);
	}

	public static float randomFloat() {
		return random(Float.class);
	}

	public static BigDecimal randomBigDecimal() {
		return random(BigDecimal.class);
	}

	public static String randomString() {
		return random(String.class);
	}

	public static Date date() {
		return random(Date.class);
	}

	public static LocalDateTime dateTime() {
		return random(LocalDateTime.class);
	}

	public static String alphabetic(int count) {
		return RandomStringUtils.randomAlphabetic(count);
	}

	public static String alphanumeric(int count) {
		return RandomStringUtils.randomAlphanumeric(count);
	}


	public static int randomInteger(int min, int max) {
		return min + RandomUtils.nextInt(max - min + 1);
	}

	public static long randomLong(int min, int max) {
		return min + RandomUtils.nextInt(max - min + 1);
	}

	public static float randomFloat0to1() {
		return RandomUtils.nextFloat();
	}

	public static float randomFloat(int min, int max) {
		return RandomUtils.nextFloat() + RandomUtils.nextInt(max - min);
	}

	public static float randomFloat(float min, float max) {
		if (Float.isInfinite(max - min)) {
			throw new IllegalArgumentException("range of float is infinty");
		}
		return min + RandomUtils.nextFloat() * (max - min);
	}

	public static double randomDouble(int min, int max) {
		return RandomUtils.nextDouble() + RandomUtils.nextInt(max - min);
	}

	public static double randomDouble(double min, double max) {
		if (Double.isInfinite(max - min)) {
			throw new IllegalArgumentException("range of double is infinty");
		}
		return min + RandomUtils.nextDouble() * (max - min);
	}


	public static String lang() {
		return languageCode();
	}


	public static String htmlParagraphs(int min, int max) {
		int count = randomInteger(min, max);
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < count; i++) {
			sb.append("<p>");
			sb.append(paragraphs(1, 1));
			sb.append("</p>");
		}
		return sb.toString().trim();
	}

	public static String paragraphs(int min, int max) {
		int count = randomInteger(min, max);
		StringBuilder sb = new StringBuilder();
		for (int j = 0; j < count; j++) {
			int sentences = randomInteger(2, 10);
			for (int i = 0; i < sentences; i++) {
				sb.append(words(1, 20, false));
				sb.append(". ");
			}
			sb.append("\n");
		}
		return sb.toString().trim();
	}

	public static String words(int min, int max, boolean title) {
		int count = randomInteger(min, max);
		return words(count, title);
	}

	public static String words(int count, boolean title) {
		StringBuilder sb = new StringBuilder();
		int wordCount = 0;
		while (wordCount < count) {
			String word = random(LoremIpsum.getWords());
			if (title) {
				if (wordCount == 0 || word.length() > 3) {
					word = word.substring(0, 1).toUpperCase() + word.substring(1);
				}
			}
			sb.append(word);
			sb.append(" ");
			wordCount++;
		}
		return sb.toString().trim();
	}


	public static String languageCode() {
		return locale().getLanguage();
	}

	public static Locale locale() {
		return random(Locale.class);
	}

	public static String countryCode() {
		return random(Locale.getISOCountries());
	}


	public static String email() {
		return email(firstname(), lastname());
	}

	public static String email(String firstname, String lastname) {
		return email(firstname, lastname, lastname + "." + random(ValuePool.getCountryTLDs()));
	}

	public static String email(String firstname, String lastname, String domain) {
		String email = firstname + "." + lastname + "@" + domain;
		return removeAccents(email).toLowerCase().replaceAll("[^A-Za-z\\.@\\-\\+]+", "");
	}

	/**
	 * Generate a Hex encoded block formatted type 4 (pseudo randomly generated) UUID.
	 *
	 * @see {@link UUID#randomUUID}
	 * @return uuid string representation.
	 */
	public static String uuid() {
		return UUID.randomUUID().toString();
	}

	/**
	 * Generate a short UUID - 22 digit base64 representation of a UUID.
	 * Details: http://www.shortguid.com
	 *
	 * @return 22 digit base64 representation of a UUID
	 */
	public static String shortGuid() {
		UUID uuid = UUID.randomUUID();
		byte[] array = toByteArray(uuid);
		return Base64.getEncoder().withoutPadding().encodeToString(array);
	}

	/**
	 * Convert UUID to 16 byte array.
	 *
	 * @param uuid uuid to get byte representation
	 * @return 16 byte array, null for null
	 */
	private static byte[] toByteArray(UUID uuid) {
		if (uuid == null) {
			return null;
		}
		ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
		bb.putLong(uuid.getMostSignificantBits());
		bb.putLong(uuid.getLeastSignificantBits());
		return bb.array();
	}

	public static String firstname() {
		return random(ValuePool.getFirstNames());
	}

	public static String firstnameMale() {
		return random(ValuePool.getMaleNames());
	}

	public static String firstnameFemale() {
		return random(ValuePool.getFemaleNames());
	}

	public static String lastname() {
		return random(ValuePool.getSurnames());
	}

	public static String username() {
		return removeAccents(firstname().toLowerCase()) + randomInteger(0, 9999);
	}

	public static String name() {
		return firstname() + " " + lastname();
	}

	public static String nameMale() {
		return firstnameMale() + " " + lastname();
	}

	public static String nameFemale() {
		return firstnameFemale() + " " + lastname();
	}

	public static String street() {
		return randomInteger(0, 1) == 0 ? lastname() + RandomData.random(ValuePool.getStreetSuffix()) : name() + RandomData.random(ValuePool.getStreetSuffixesExtra());
	}

	public static String streetNumber() {
		return Integer.toString(randomInteger(1, 999)) + (randomInteger(0, 4) == 0 ? alphabetic(1) : "");
	}

	public static String address() {
		return street() + " " + streetNumber();
	}

	public static String city() {
		return random(ValuePool.getCities());
	}

	public static String zipCH() {
		return Integer.toString(randomInteger(1000, 9999));
	}

	public static String zipDE() {
		return String.format("%05d", randomInteger(0, 99999));
	}

	public static String zip() {
		final int type = randomInteger(0, 1);
		switch (type) {
		case 0:
			return zipCH();
		default:
			return zipDE();
		}
	}

	public static String company() {
		return random(ValuePool.getCompanies());
	}

	public static String manufacturer() {
		return random(ValuePool.getManufacturers());
	}

	public static String title(int count) {
		return words(count, count, true);
	}

	public static String title(int min, int max) {
		return words(min, max, true);
	}

	public static String words(int count) {
		return words(count, count, false);
	}

	public static String words(int min, int max) {
		return words(min, max, false);
	}

	public static String isbn10() {
		StringBuilder isbn10 = new StringBuilder();
		isbn10.append(randomInteger(10, 99)); // group (2 digits)
		isbn10.append(randomInteger(1000, 9999)); // publisher (4 digits)
		isbn10.append(randomInteger(100, 999)); // title (3 digits)
		isbn10.append(BarcodeUtils.createIsbn10CheckSumDigit(isbn10.toString())); // checksum (1 digit)
		return isbn10.toString();
	}

	public static String eanIsbn() {
		StringBuilder eanIsbn = new StringBuilder();
		eanIsbn.append("978"); // bookland (3 digits)
		eanIsbn.append(randomInteger(10, 99)); // group (2 digits)
		eanIsbn.append(randomInteger(1000, 9999)); // publisher (4 digits)
		eanIsbn.append(randomInteger(100, 999)); // title (3 digits)
		eanIsbn.append(BarcodeUtils.createEanCheckSumDigit(eanIsbn.toString())); // checksum (1 digit)
		return eanIsbn.toString();
	}

	public static String ean13() {
		StringBuilder ean13 = new StringBuilder();
		ean13.append(randomInteger(100, 999)); // country (3 digits)
		ean13.append(randomInteger(1000, 9999)); // company (4 digits)
		ean13.append(randomInteger(10000, 99999)); // item (5 digits)
		ean13.append(BarcodeUtils.createEanCheckSumDigit(ean13.toString())); // checksum (1 digit)
		return ean13.toString();
	}

	public static String ean8() {
		StringBuilder ean8 = new StringBuilder();
		ean8.append(randomInteger(100, 999)); // country (3 digits)
		ean8.append(randomInteger(1000, 9999)); // item (4 digits)
		ean8.append(BarcodeUtils.createEanCheckSumDigit(ean8.toString())); // checksum (1 digit)
		return ean8.toString();
	}

	/**
	 * Populates a bean instance with random values for properties of a supported types of RandomGenerators.
	 *
	 * @param bean bean to be populated
	 * @param <T>  Type of the bean to be populated
	 * @return populated bean, the original object is populated, so the returned object is the same instance as provided as parameter.
	 * @see com.namics.commons.random.generator.RandomGenerator
	 * @see RandomData#random(Class, Object...)
	 */
	public static <T> T populateBean(T bean) {
		Class<?> beanClass = bean.getClass();
		List<PropertyDescriptor> propertyDescriptors = BeanUtils.getPropertyDescriptors(beanClass);
		for (PropertyDescriptor descriptor : propertyDescriptors) {
			Class<?> propertyType = descriptor.getPropertyType();

			if (Class.class != propertyType) {
				try {
					Method writeMethod = descriptor.getWriteMethod();
					if (writeMethod != null) {
						LOG.info("Property {} ", descriptor);
						String propertyName = descriptor.getName();

						Type[] genericParameterTypes = writeMethod.getGenericParameterTypes();
						Type genericFieldType = genericParameterTypes[0];
						Object random;
						if (beanClass == propertyType) {
							random = bean;
						} else if (genericFieldType instanceof TypeVariable) {
							Type[] exactGenericTypes = GenericTypeReflector.getExactParameterTypes(writeMethod, beanClass);
							random = random(exactGenericTypes[0], propertyName);
						} else if (genericFieldType instanceof ParameterizedType) {
							random = random(genericFieldType, propertyName);
						} else {
							random = random(propertyType, propertyName);
						}
						if (random != null) {
							makeAccessible(writeMethod);
							writeMethod.invoke(bean, random);
						}
					}
				} catch (Throwable e) {
					LOG.info("Property {} could not be processed", descriptor, e);
				}
			}
		}
		return bean;
	}

	/**
	 * Creates a HashMap, key and value class must be provided, since Generics are unknown at runtime.
	 *
	 * @param keyClass   class of key
	 * @param valueClass class of value
	 * @return Map populated with random values.
	 */

	public static Map map(Class<? extends Comparable> keyClass, Class<?> valueClass) {
		Map map = new HashMap();
		for (int i = 0; i < 10; i++) {
			Object key = random(keyClass);
			Object value = random(valueClass);
			if (key != null && value != null) {
				map.put(key, value);
			}
		}
		return map;
	}

	public static String removeAccents(final String value) {
		if (value == null) {
			return null;
		}
		String result = Normalizer.normalize(value, Normalizer.Form.NFD);
		result = result.replaceAll("\\p{M}", "");
		return result;
	}


	/**
	 * Register the instance of the {@link RandomGenerator} implementation.
	 *
	 * @param generator {@link RandomGenerator} implementations instance to register.
	 */
	public static void addRandomGenerator(RandomGenerator generator) {
		RandomGeneratorFactory.addRandomGenerator(generator);
	}

	/**
	 * Create and register an instance of the {@link RandomGenerator} implementation.
	 *
	 * @param generatorClazz {@link RandomGenerator} implementation to register.
	 */
	public static void addRandomGenerator(Class<? extends RandomGenerator> generatorClazz) {
		RandomGeneratorFactory.addRandomGenerator(generatorClazz);
	}

	/**
	 * Registers {@link com.namics.commons.random.generator.RandomGenerator} implementations found in the package provided.
	 *
	 * @param scanpackage package to scan for implementation of {@link RandomGenerator} to be registered.
	 */
	public static void addRandomGenerators(String scanpackage) {
		RandomGeneratorFactory.addRandomGenerators(scanpackage);
	}

	private static final String GENERATOR_REGISTER_INFO = ".\nA suitable generator can be registered in several ways:\n"
	                                                      + "\t- automatically when placed in same package as the SupportedType\n"
	                                                      + "\t- Manually by registering either class or instance directly toRandomData.addRandomGenerator\n"
	                                                      + "\t- Manually by registering package for scanning  to RandomData.addRandomGenerators";

	private RandomData() {
		// hide
	}
}