Generators.java
package dev.orne.test.rnd;
/*-
* #%L
* Orne Test Generators
* %%
* Copyright (C) 2021 Orne Developments
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/lgpl-3.0.html>.
* #L%
*/
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.WeakHashMap;
import javax.validation.constraints.NotNull;
import org.apache.commons.lang3.Validate;
import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;
import dev.orne.test.rnd.params.ConstructorParameterTypeGenerator;
import dev.orne.test.rnd.params.GeneratorNotParameterizableException;
import dev.orne.test.rnd.params.MethodParameterTypeGenerator;
import dev.orne.test.rnd.params.MethodReturnTypeGenerator;
import dev.orne.test.rnd.params.ParameterTypeGenerator;
import dev.orne.test.rnd.params.ParameterizableGenerator;
import dev.orne.test.rnd.params.PropertyTypeGenerator;
/**
* Main entry point for random value generation system. Allows generation
* of random values with dynamic generators addition.
* <p>
* Registers generators declared in
* {@code /META-INF/services/dev.orne.test.rnd.Generator}
* SPI files in the class path.
* <p>
* Further registered generator adjustment can be performed with
* {@link #getRegisteredGenerators()}, {@link #getGenerator(Class)},
* {@link #register(Generator...)}, {@link #register(Collection)},
* {@link #remove(Generator...)}, {@link #remove(Collection)}
* and {@link #reset()}.
*
* @author <a href="mailto:wamphiry@orne.dev">(w) Iker Hernaez</a>
* @version 1.1, 2023-11
* @since 0.1
*/
@API(status=Status.STABLE, since="0.1")
public final class Generators {
/**
* The generator comparator by priority.
*/
public static final Comparator<Generator> COMPARATOR =
Comparator.comparingInt(Generator::getPriority).reversed();
/** The by class generator cache. */
private static final Map<Class<?>, Generator> CACHE = new WeakHashMap<>();
/** The registered generators. */
private static List<Generator> registeredGenerators;
/**
* Private constructor.
*/
private Generators() {
// Utility class
}
/**
* Checks if values of the specified type can be generated by any
* registered generator.
*
* @param type The type to check.
* @return If values of the specified type can be generated.
*/
public static boolean supports(
final @NotNull Class<?> type) {
return getGenerator(type) != null;
}
/**
* Returns the default value for the specified type.
*
* @param <T> The requested value type.
* @param type The requested value type.
* @return The default value for the specified type.
* @throws GeneratorNotFoundException If no generator supports the
* requested value type.
*/
public static <T> T defaultValue(
final @NotNull Class<T> type) {
final Generator generator = getGeneratorInt(type);
return generator.defaultValue(type);
}
/**
* Returns the default value for the specified type.
*
* @param <T> The requested value type.
* @param type The requested value type.
* @param params The generation parameter sources.
* @return The default value for the specified type.
* @throws GeneratorNotFoundException If no generator supports the
* requested value type.
* @throws GeneratorNotParameterizableException If the generator registered
* for the requested value type is not parameterizable.
*/
@API(status=Status.EXPERIMENTAL, since = "0.1")
public static <T> T defaultValue(
final @NotNull Class<T> type,
final @NotNull Object... params) {
return requireParameterizableGenerator(type).defaultValue(type, params);
}
/**
* Returns the default value for the specified type allowing {@code null}
* values.
* <p>
* This method should return {@code null} except for native types.
*
* @param <T> The requested value type.
* @param type The requested value type.
* @return The nullable default value for the specified type.
* @throws GeneratorNotFoundException If no generator supports the
* requested value type.
*/
public static <T> T nullableDefaultValue(
final @NotNull Class<T> type) {
final Generator generator = getGeneratorInt(type);
return generator.nullableDefaultValue(type);
}
/**
* Returns the default value for the specified type allowing {@code null}
* values.
* <p>
* This method should return {@code null} except for native types.
*
* @param <T> The requested value type.
* @param type The requested value type.
* @param params The generation parameter sources.
* @return The nullable default value for the specified type.
* @throws GeneratorNotFoundException If no generator supports the
* requested value type.
* @throws GeneratorNotParameterizableException If the generator registered
* for the requested value type is not parameterizable.
*/
@API(status=Status.EXPERIMENTAL, since = "0.1")
public static <T> T nullableDefaultValue(
final @NotNull Class<T> type,
final @NotNull Object... params) {
return requireParameterizableGenerator(type).nullableDefaultValue(type, params);
}
/**
* Returns a random value of the specified type.
*
* @param <T> The requested value type.
* @param type The requested value type.
* @return A random value for the specified type.
* @throws GeneratorNotFoundException If no generator supports the
* requested value type.
*/
public static <T> T randomValue(
final @NotNull Class<T> type) {
final Generator generator = getGeneratorInt(type);
return generator.randomValue(type);
}
/**
* Returns a random value of the specified type.
*
* @param <T> The requested value type.
* @param type The requested value type.
* @param params The generation parameter sources.
* @return A random value for the specified type.
* @throws GeneratorNotFoundException If no generator supports the
* requested value type.
* @throws GeneratorNotParameterizableException If the generator registered
* for the requested value type is not parameterizable.
*/
@API(status=Status.EXPERIMENTAL, since = "0.1")
public static <T> T randomValue(
final @NotNull Class<T> type,
final @NotNull Object... params) {
return requireParameterizableGenerator(type).randomValue(type, params);
}
/**
* Returns a random value of the specified type allowing {@code null}
* values.
* <p>
* The returned value has a probability of be {@code null} except for
* native types. If not {@code null} behaves as {@code randomValue()}.
*
* @param <T> The requested value type.
* @param type The requested value type.
* @return A random nullable value for the specified type.
* @throws GeneratorNotFoundException If no generator supports the
* requested value type.
* @see #randomValue(Class)
*/
public static <T> T nullableRandomValue(
final @NotNull Class<T> type) {
final Generator generator = getGeneratorInt(type);
return generator.nullableRandomValue(type);
}
/**
* Returns a random value of the specified type allowing {@code null}
* values.
* <p>
* The returned value has a probability of be {@code null} except for
* native types. If not {@code null} behaves as {@code randomValue()}.
*
* @param <T> The requested value type.
* @param type The requested value type.
* @param params The generation parameter sources.
* @return A random nullable value for the specified type.
* @throws GeneratorNotFoundException If no generator supports the
* requested value type.
* @throws GeneratorNotParameterizableException If the generator registered
* for the requested value type is not parameterizable.
* @see #randomValue(Class)
*/
@API(status=Status.EXPERIMENTAL, since = "0.1")
public static <T> T nullableRandomValue(
final @NotNull Class<T> type,
final @NotNull Object... params) {
return requireParameterizableGenerator(type).nullableRandomValue(type, params);
}
/**
* Returns the generator to use for the specified value type.
*
* @param type The value type to generate
* @return The generator to use. Returns {@code null} is no one is
* suitable.
*/
public static Generator getGenerator(
final @NotNull Class<?> type) {
Validate.notNull(type);
final Generator result = getGeneratorInt(type);
return result == MissingGenerator.INSTANCE ? null : result;
}
/**
* Returns the parameterizable generator to use for the specified value
* type. If no generator supports the requested value type or the
* generator is not parameterizable returns {@code null}.
*
* @param type The value type to generate
* @return The generator to use. Returns {@code null} is no one is
* suitable.
*/
public static ParameterizableGenerator getParameterizableGenerator(
final @NotNull Class<?> type) {
final Generator result = getGenerator(type);
if (result instanceof ParameterizableGenerator) {
return (ParameterizableGenerator) result;
} else {
return null;
}
}
/**
* Returns the generator to use for the specified value type.
*
* @param type The value type to generate
* @return The generator to use. Returns {@code MissingGenerator.INSTANCE}
* if no one is suitable.
*/
static @NotNull Generator getGeneratorInt(
final @NotNull Class<?> type) {
return CACHE.computeIfAbsent(type, Generators::findGenerator);
}
/**
* Return the parameterizable generator for the specified value type.
*
* @param type The value type to generate
* @return The generator to use.
* @throws GeneratorNotFoundException If no generator supports the
* requested value type.
* @throws GeneratorNotParameterizableException If the registered generator
* is not parameterizable.
*/
static @NotNull ParameterizableGenerator requireParameterizableGenerator(
final @NotNull Class<?> type) {
final Generator generator = getGeneratorInt(type);
if (generator == MissingGenerator.INSTANCE) {
throw new GeneratorNotFoundException(MissingGenerator.ERR_MSG);
} else {
return generator.asParameterizable();
}
}
/**
* Finds the generator to use for the specified value type.
*
* @param type The value type to generate
* @return The generator to use. Returns {@code MissingGenerator.INSTANCE}
* if no one is suitable.
*/
static @NotNull Generator findGenerator(
final @NotNull Class<?> type) {
Generator result = MissingGenerator.INSTANCE;
synchronized (Generators.class) {
for (final Generator generator : getGeneratorsInt()) {
if (generator.supports(type)) {
result = generator;
break;
}
}
}
return result;
}
/**
* Returns a targeted generator for the specified field.
* <p>
* If a parameterizable generator has been registered for the specified type
* extracts the generation parameters from the field declaration.
*
* @param <T> The type of the generated values
* @param field The bean field
* @return A generator for the type of the specified field
* @since 0.2
*/
@API(status=Status.EXPERIMENTAL, since = "0.2")
public static <T> @NotNull PropertyTypeGenerator<T> forField(
final @NotNull Field field) {
return PropertyTypeGenerator.<T>targeting(field);
}
/**
* Returns a targeted generator for the specified field.
* <p>
* If a parameterizable generator has been registered for the specified type
* extracts the generation parameters from the field declaration.
*
* @param <T> The type of the generated values
* @param beanType The bean type
* @param field The bean field
* @return A generator for the type of the specified field
* @since 0.2
*/
@API(status=Status.EXPERIMENTAL, since = "0.2")
public static <T> @NotNull PropertyTypeGenerator<T> forField(
final @NotNull Class<?> beanType,
final @NotNull Field field) {
return PropertyTypeGenerator.<T>targeting(beanType, field);
}
/**
* Returns a targeted generator for the specified bean property.
* <p>
* If a parameterizable generator has been registered for the specified type
* extracts the generation parameters from the property declaration.
*
* @param <T> The type of the generated values
* @param beanType The bean type
* @param property The property of the bean
* @return A generator for the type of the specified property
*/
@API(status=Status.EXPERIMENTAL, since = "0.1")
public static <T> @NotNull PropertyTypeGenerator<T> forProperty(
final @NotNull Class<?> beanType,
final @NotNull String property) {
return PropertyTypeGenerator.<T>targeting(beanType, property);
}
/**
* Returns a targeted generator for the specified parameter.
* <p>
* If a parameterizable generator has been registered for the specified type
* extracts the generation parameters from the parameter declaration.
*
* @param <T> The type of the generated values
* @param parameter The parameter
* @return A generator for the type of the specified parameter
* @since 0.2
*/
@API(status=Status.EXPERIMENTAL, since = "0.2")
public static <T> @NotNull ParameterTypeGenerator<T> forParameter(
final @NotNull Parameter parameter) {
return ParameterTypeGenerator.<T>targeting(parameter);
}
/**
* Returns a targeted generator for the specified method parameter.
* <p>
* If a parameterizable generator has been registered for the specified type
* extracts the generation parameters from the method declaration.
*
* @param <T> The type of the generated values
* @param method The method
* @param parameterIndex The parameter index
* @return A generator for the type of the specified method parameter
*/
@API(status=Status.EXPERIMENTAL, since = "0.1")
public static <T> @NotNull MethodParameterTypeGenerator<T> forParameter(
final @NotNull Method method,
final @NotNull int parameterIndex) {
return MethodParameterTypeGenerator.<T>targeting(method, parameterIndex);
}
/**
* Returns a targeted generator for the specified method parameter.
* <p>
* If a parameterizable generator has been registered for the specified type
* extracts the generation parameters from the method declaration.
*
* @param <T> The type of the generated values
* @param cls The method's class
* @param method The method name
* @param parameterTypes The method parameter types
* @param parameterIndex The parameter index
* @return A generator for the type of the specified method parameter
*/
@API(status=Status.EXPERIMENTAL, since = "0.1")
public static <T> @NotNull MethodParameterTypeGenerator<T> forParameter(
final @NotNull Class<?> cls,
final @NotNull String method,
final @NotNull Class<?>[] parameterTypes,
final @NotNull int parameterIndex) {
return MethodParameterTypeGenerator.<T>targeting(cls, method, parameterIndex, parameterTypes);
}
/**
* Returns a targeted generator for the specified method return type.
* <p>
* If a parameterizable generator has been registered for the specified type
* extracts the generation parameters from the method declaration.
*
* @param <T> The type of the generated values
* @param method The method
* @return A generator for the type of the specified method return type
*/
@API(status=Status.EXPERIMENTAL, since = "0.1")
public static <T> @NotNull MethodReturnTypeGenerator<T> forReturnType(
final @NotNull Method method) {
return MethodReturnTypeGenerator.<T>targeting(method);
}
/**
* Returns a targeted generator for the specified method return type.
* <p>
* If a parameterizable generator has been registered for the specified type
* extracts the generation parameters from the method declaration.
*
* @param <T> The type of the generated values
* @param cls The method's class
* @param parameterTypes The method parameter types
* @param method The method name
* @return A generator for the type of the specified method return type
*/
@API(status=Status.EXPERIMENTAL, since = "0.1")
public static <T> @NotNull MethodReturnTypeGenerator<T> forReturnType(
final @NotNull Class<?> cls,
final @NotNull String method,
final @NotNull Class<?>[] parameterTypes) {
return MethodReturnTypeGenerator.<T>targeting(cls, method, parameterTypes);
}
/**
* Returns a targeted generator for the specified constructor parameter.
* <p>
* If a parameterizable generator has been registered for the specified type
* extracts the generation parameters from the constructor declaration.
*
* @param <T> The type of the generated values
* @param constructor The constructor
* @param parameterIndex The parameter index
* @return A generator for the type of the specified constructor parameter
*/
@API(status=Status.EXPERIMENTAL, since = "0.1")
public static <T> @NotNull ConstructorParameterTypeGenerator<T> forParameter(
final @NotNull Constructor<?> constructor,
final @NotNull int parameterIndex) {
return ConstructorParameterTypeGenerator.<T>targeting(constructor, parameterIndex);
}
/**
* Returns a targeted generator for the specified constructor parameter.
* <p>
* If a parameterizable generator has been registered for the specified type
* extracts the generation parameters from the constructor declaration.
*
* @param <T> The type of the generated values
* @param cls The class of the constructor
* @param parameterTypes The constructor parameter types
* @param parameterIndex The parameter index
* @return A generator for the type of the specified constructor parameter
*/
@API(status=Status.EXPERIMENTAL, since = "0.1")
public static <T> @NotNull ConstructorParameterTypeGenerator<T> forParameter(
final @NotNull Class<?> cls,
final @NotNull Class<?>[] parameterTypes,
final @NotNull int parameterIndex) {
return ConstructorParameterTypeGenerator.<T>targeting(cls, parameterIndex, parameterTypes);
}
/**
* Returns an unmodifiable list with the registered generators.
*
* @return The registered generators
*/
public static @NotNull List<Generator> getRegisteredGenerators() {
return Collections.unmodifiableList(getGeneratorsInt());
}
/**
* Adds the specified generators to the registered generators.
*
* @param generators The generators to register
*/
public static void register(
final @NotNull Generator... generators) {
Validate.notNull(generators);
register(Arrays.asList(generators));
}
/**
* Adds the specified generators to the registered generators.
*
* @param generators The generators to register
*/
public static void register(
final @NotNull Collection<Generator> generators) {
Validate.notNull(generators);
Validate.noNullElements(generators);
synchronized (Generators.class) {
final List<Generator> intList = getGeneratorsInt();
intList.addAll(generators);
Collections.sort(intList, COMPARATOR);
CACHE.clear();
}
}
/**
* Removes the specified generators from the registered generators.
*
* @param generators The generators to remove
*/
public static void remove(
final @NotNull Generator... generators) {
Validate.notNull(generators);
remove(Arrays.asList(generators));
}
/**
* Removes the specified generators from the registered generators.
*
* @param generators The generators to remove
*/
public static void remove(
final @NotNull Collection<Generator> generators) {
Validate.notNull(generators);
Validate.noNullElements(generators);
synchronized (Generators.class) {
final List<Generator> intList = getGeneratorsInt();
intList.removeAll(generators);
Collections.sort(intList, COMPARATOR);
CACHE.clear();
}
}
/**
* Resets the loaded and cached generators. Next call will regenerate the
* generator list (including SPI generators) and restart generator caching.
*/
public static void reset() {
synchronized (Generators.class) {
registeredGenerators = null;
CACHE.clear();
}
}
/**
* Returns a modifiable list with the registered generators.
* If generators has not been loaded loads the default generators,
* including generators registered through SPI.
*
* @return The registered generators
*/
static @NotNull List<Generator> getGeneratorsInt() {
synchronized (Generators.class) {
if (registeredGenerators == null) {
registeredGenerators = new ArrayList<>();
registeredGenerators.addAll(loadSpiGenerators());
Collections.sort(registeredGenerators, COMPARATOR);
}
return registeredGenerators;
}
}
/**
* Returns the internal by class generator cache.
*
* @return The internal by class generator cache
*/
static @NotNull Map<Class<?>, Generator> getCacheInt() {
return CACHE;
}
/**
* Loads the value generators declared through SPI for interface
* {@code dev.orne.test.rnd.Generator}
*
* @return The SPI declared generators
* @see ServiceLoader
*/
static @NotNull List<Generator> loadSpiGenerators() {
final List<Generator> result = new ArrayList<>();
final ServiceLoader<Generator> loader =
ServiceLoader.load(Generator.class);
final Iterator<Generator> it = loader.iterator();
while (it.hasNext()) {
final Generator generator = it.next();
result.add(generator);
}
return result;
}
/**
* Cache value for missing generators for a value type.
*
* @author <a href="mailto:wamphiry@orne.dev">(w) Iker Hernaez</a>
* @version 1.0, 2022-10
* @since Generators 1.0
*/
@API(status=Status.INTERNAL, since="0.1")
public static final class MissingGenerator
implements Generator {
/** The error message. */
public static final String ERR_MSG = "No suitable generator found";
/** The generator placeholder for missing generators. */
public static final MissingGenerator INSTANCE = new MissingGenerator();
/**
* Private constructor.
*/
private MissingGenerator() {
super();
}
/**
* {@inheritDoc}
*/
@Override
public boolean supports(
final @NotNull Class<?> type) {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public <T> @NotNull T defaultValue(
final @NotNull Class<T> type) {
throw new GeneratorNotFoundException(ERR_MSG);
}
/**
* {@inheritDoc}
*/
@Override
public <T> T nullableDefaultValue(
final @NotNull Class<T> type) {
throw new GeneratorNotFoundException(ERR_MSG);
}
/**
* {@inheritDoc}
*/
@Override
public <T> @NotNull T randomValue(
final @NotNull Class<T> type) {
throw new GeneratorNotFoundException(ERR_MSG);
}
/**
* {@inheritDoc}
*/
@Override
public <T> T nullableRandomValue(
final @NotNull Class<T> type) {
throw new GeneratorNotFoundException(ERR_MSG);
}
}
}