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);
        }
    }
}