ParametersExtractors.java

package dev.orne.test.rnd.params;

/*-
 * #%L
 * Orne Test Generators
 * %%
 * Copyright (C) 2022 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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;

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.Generators;

/**
 * Registry of generation parameters source extractors.
 * <p>
 * Registers extractors declared in
 * {@code /META-INF/services/dev.orne.test.rnd.params.ParametersSourceExtractor}
 * SPI files in the class path.
 * 
 * @author <a href="mailto:wamphiry@orne.dev">(w) Iker Hernaez</a>
 * @version 1.0, 2022-11
 * @since 0.1
 */
@API(status=Status.EXPERIMENTAL, since="0.1")
public final class ParametersExtractors {

    /**
     * The parameter source extractors comparator by priority.
     */
    @SuppressWarnings("rawtypes")
    public static final Comparator<ParametersSourceExtractor> COMPARATOR =
            Collections.reverseOrder(Comparator.comparingInt(ParametersSourceExtractor::getPriority));
    /**
     * The default parameters source extractors filter.
     * <p>
     * Default filter returns all the registered generation parameters source
     * extractors that accept parameters of the specified type
     */
    public static final SourceExtractorFilter DEFAULT_FILTER =
            ParametersExtractors::filterSourceExtractors;
    /**
     * The default parameters extractor builder.
     * <p>
     * Default builder creates a instance of {@code DefaultParametersExtractor}.
     * 
     * @see DefaultParametersExtractor
     */
    public static final ExtractorBuilder DEFAULT_BUILDER =
            DefaultParametersExtractor::new;

    /** The parameter extractors cache by parameters type. */
    private static final Map<Class<?>, ParametersExtractor<?>> CACHE =
            new HashMap<>();
    /** The registered generation parameters source extractors. */
    private static List<ParametersSourceExtractor<?, ?>> registeredSourceExtractors;
    /** The generation parameters source extractors filter. */
    private static SourceExtractorFilter filter = DEFAULT_FILTER;
    /** The parameters extractor builder. */
    private static ExtractorBuilder builder = DEFAULT_BUILDER;

    /**
     * Private constructor.
     */
    private ParametersExtractors() {
        // Utility class
    }

    /**
     * Returns an unmodifiable list with the registered parameter source
     * extractors.
     * 
     * @return The registered parameter source extractors
     */
    @SuppressWarnings("java:S1452")
    public static @NotNull List<ParametersSourceExtractor<?, ?>> getRegisteredSourceExtractors() {
        return Collections.unmodifiableList(getSourceExtractorsInt());
    }

    /**
     * Adds the specified parameter source extractors to the registered
     * extractors.
     * 
     * @param extractors The parameter source extractors to register
     */
    public static void register(
            final @NotNull ParametersSourceExtractor<?, ?>... extractors) {
        Validate.notNull(extractors);
        register(Arrays.asList(extractors));
    }

    /**
     * Adds the specified parameter source extractors to the registered
     * extractors.
     * 
     * @param extractors The parameter source extractors to register
     */
    public static void register(
            final @NotNull Collection<ParametersSourceExtractor<?, ?>> extractors) {
        Validate.notNull(extractors);
        Validate.noNullElements(extractors);
        synchronized (Generators.class) {
            final List<ParametersSourceExtractor<?, ?>> intList = getSourceExtractorsInt();
            intList.addAll(extractors);
            Collections.sort(intList, COMPARATOR);
            CACHE.clear();
        }
    }

    /**
     * Removes the specified parameter source extractors from the registered
     * extractors.
     * 
     * @param extractors The parameter source extractors to remove
     */
    public static void remove(
            final @NotNull ParametersSourceExtractor<?, ?>... extractors) {
        Validate.notNull(extractors);
        remove(Arrays.asList(extractors));
    }

    /**
     * Removes the specified parameter source extractors from the registered
     * extractors.
     * 
     * @param extractors The parameter source extractors to remove
     */
    public static void remove(
            final @NotNull Collection<ParametersSourceExtractor<?, ?>> extractors) {
        Validate.notNull(extractors);
        Validate.noNullElements(extractors);
        synchronized (Generators.class) {
            final List<ParametersSourceExtractor<?, ?>> intList = getSourceExtractorsInt();
            intList.removeAll(extractors);
            Collections.sort(intList, COMPARATOR);
            CACHE.clear();
        }
    }

    /**
     * Resets the loaded and cached extractors. Next call will regenerate the
     * parameter source extractors list (including SPI extractor) and restart
     * parameter extractors caching.
     */
    public static void reset() {
        synchronized (Generators.class) {
            registeredSourceExtractors = null;
            CACHE.clear();
        }
    }

    /**
     * Returns a generation parameters extractor for the specified parameters
     * type.
     * <p>
     * The extractor will contain all parameter source extractors suitable for
     * the given parameters type.
     * 
     * @param <P> The target parameters type
     * @param parametersType The target parameters type
     * @return The parameters extractor for the target parameters type
     */
    @SuppressWarnings("unchecked")
    public static <P> dev.orne.test.rnd.params.ParametersExtractor<P> getExtractor(
            final @NotNull Class<P> parametersType) {
        return (ParametersExtractor<P>) CACHE.computeIfAbsent(
                parametersType,
                ParametersExtractors::createExtractor);
    }

    /**
     * Returns the generation parameters source extractors filter.
     * 
     * @return The generation parameters source extractors filter
     */
    public static @NotNull SourceExtractorFilter getFilter() {
        return ParametersExtractors.filter;
    }

    /**
     * Sets the generation parameters source extractors filter.
     * 
     * @param filter The generation parameters source extractors filter
     */
    public static void setFilter(
            final @NotNull SourceExtractorFilter filter) {
        ParametersExtractors.filter = Validate.notNull(filter);
        reset();
    }

    /**
     * Returns the parameters extractor builder.
     * 
     * @return The parameters extractor builder
     */
    public static @NotNull ExtractorBuilder getBuilder() {
        return ParametersExtractors.builder;
    }

    /**
     * Sets the parameters extractor builder.
     * 
     * @param builder The parameters extractor builder
     */
    public static void setBuilder(
            final @NotNull ExtractorBuilder builder) {
        ParametersExtractors.builder = Validate.notNull(builder);
        reset();
    }

    /**
     * Creates a new parameter extractors with all the parameter source
     * extractors suitable for the given parameters type.
     * 
     * @param <P> The target parameters type
     * @param parametersType The target parameters type
     * @return The parameters extractor for the target parameters type
     */
    static <P> dev.orne.test.rnd.params.ParametersExtractor<P> createExtractor(
            final @NotNull Class<P> parametersType) {
        final List<ParametersSourceExtractor<? super P, ?>> extractors =
                filter.findSuitable(
                        getRegisteredSourceExtractors(),
                        parametersType);
        return builder.create(extractors);
    }

    /**
     * Returns all the registered generation parameters source extractors that
     * accept parameters of the specified type.
     * 
     * @param <P> The target parameter type
     * @param extractors The registered source extractors, in priority order
     * @param parametersType The target parameter type
     * @return The source extractors that accept the specified parameters typet,
     * in priority order
     */
    @SuppressWarnings("java:S1452")
    static <P> @NotNull List<ParametersSourceExtractor<? super P, ?>> filterSourceExtractors(
            final @NotNull List<ParametersSourceExtractor<?, ?>> extractors,
            final @NotNull Class<P> parametersType) {
        final List<ParametersSourceExtractor<? super P, ?>> result =
                new ArrayList<>();
        for (final ParametersSourceExtractor<?, ?> sourceExtractor : extractors) {
            if (sourceExtractor.getParametersType().isAssignableFrom(parametersType)) {
                @SuppressWarnings("unchecked")
                final ParametersSourceExtractor<? super P, ?> tExtractor =
                        (ParametersSourceExtractor<? super P, ?>) sourceExtractor;
                result.add(tExtractor);
            }
        }
        return result;
    }

    /**
     * Returns a modifiable list with the registered parameter source
     * extractors.
     * If extractors has not been loaded loads the default extractors,
     * including extractors registered through SPI.
     * 
     * @return The registered parameter source extractors
     */
    @SuppressWarnings("java:S1452")
    static @NotNull List<ParametersSourceExtractor<?, ?>> getSourceExtractorsInt() {
        synchronized (ParametersExtractors.class) {
            if (registeredSourceExtractors == null) {
                registeredSourceExtractors = new ArrayList<>();
                registeredSourceExtractors.addAll(loadSpiExtractors());
                Collections.sort(registeredSourceExtractors, COMPARATOR);
            }
            return registeredSourceExtractors;
        }
    }

    /**
     * Returns the internal by type parameters extractor cache.
     * 
     * @return The internal by type parameters extractor cache
     */
    @SuppressWarnings("java:S1452")
    static @NotNull Map<Class<?>, ParametersExtractor<?>> getCacheInt() {
        return CACHE;
    }

    /**
     * Loads the parameter source extractors declared through SPI for interface
     * {@code dev.orne.test.rnd.ParametersSourceExtractor}
     * 
     * @return The SPI declared parameter source extractors
     * @see ServiceLoader
     */
    @SuppressWarnings({ "rawtypes", "java:S1452" })
    static @NotNull List<ParametersSourceExtractor<?, ?>> loadSpiExtractors() {
        final List<ParametersSourceExtractor<?, ?>> result = new ArrayList<>();
        final ServiceLoader<ParametersSourceExtractor> loader =
                ServiceLoader.load(ParametersSourceExtractor.class);
        final Iterator<ParametersSourceExtractor> it = loader.iterator();
        while (it.hasNext()) {
            final ParametersSourceExtractor<?, ?> generator = it.next();
            result.add(generator);
        }
        return result;
    }

    /**
     * Functional interface for generation parameters source extractors filter.
     * 
     * @author <a href="mailto:wamphiry@orne.dev">(w) Iker Hernaez</a>
     * @version 1.0, 2022-11
     * @since ParametersExtractors 1.0
     */
    @FunctionalInterface
    public interface SourceExtractorFilter {

        /**
         * Returns the generation parameters source extractors to be used when
         * extracting parameters of the specified type.
         * 
         * @param <P> The target parameter type
         * @param extractors The registered source extractors, in priority order
         * @param parametersType The target parameter type
         * @return The source extractors to use, in priority order
         */
        @SuppressWarnings("java:S1452")
        <P> @NotNull List<ParametersSourceExtractor<? super P, ?>> findSuitable(
                @NotNull List<ParametersSourceExtractor<?, ?>> extractors,
                @NotNull Class<P> parametersType);
    }

    /**
     * Functional interface for parameters extractor builder.
     * 
     * @author <a href="mailto:wamphiry@orne.dev">(w) Iker Hernaez</a>
     * @version 1.0, 2022-11
     * @since ParametersExtractors 1.0
     */
    @FunctionalInterface
    public interface ExtractorBuilder {

        /**
         * Creates a new parameters extractor from the specified suitable
         * parameter source extractors.
         * 
         * @param <P> The parameters type
         * @param sourceExtractors The suitable parameter source extractors,
         * in priority order
         * @return The created parameters extractor
         */
        <P> dev.orne.test.rnd.params.ParametersExtractor<P> create(
                @NotNull Collection<ParametersSourceExtractor<? super P, ?>> sourceExtractors);
    }
}