AnnotatedMethodGenerator.java

package dev.orne.test.rnd.generators;

/*-
 * #%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.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.WeakHashMap;

import javax.validation.constraints.NotNull;

import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import dev.orne.test.rnd.Generator;
import dev.orne.test.rnd.GeneratorMethod;
import dev.orne.test.rnd.Generators;
import dev.orne.test.rnd.Priority;
import dev.orne.test.rnd.UnsupportedValueTypeException;
import dev.orne.test.rnd.params.ExecutableGenerator;

/**
 * Generator of beans annotated with {@code Currency}.
 * 
 * @author <a href="mailto:wamphiry@orne.dev">(w) Iker Hernaez</a>
 * @version 1.0, 2022-12
 * @since 0.1
 */
@API(status=Status.EXPERIMENTAL, since="0.1")
@Priority(Priority.ANNOTATION_GENERATORS)
public class AnnotatedMethodGenerator
implements Generator {

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

    /** Required modifiers for instance generation constructors. */
    private static final int GENERATOR_CONSTRUCTOR_MODIFIERS =
            Modifier.PUBLIC;
    /** Required modifiers for instance generation methods. */
    private static final int GENERATOR_METHOD_MODIFIERS =
            Modifier.STATIC | Modifier.PUBLIC;
    /** Informational message for misconfigured identity types. */
    private static final String HELP_MISCONFIGURED =
            "Class '{}' generation is misconfigured."
            + " See GeneratorMethod javadoc for correct bean generation configuration."
            + " No further generation attempts will succed.";
    /** Error message for wrong generation constructor modifiers. */
    private static final String ERR_GENERATOR_CONSTRUCTOR_MODIFIERS =
            "Invalid modifiers in annotated class generation constructor '{}' for type '{}'."
            + " Method must be public.";
    /** Error message for wrong generation method modifiers. */
    private static final String ERR_GENERATOR_METHOD_MODIFIERS =
            "Invalid modifiers in annotated class generation method '{}' for type '{}'."
            + " Method must be public and static.";
    /** Error message for wrong generation method return type. */
    private static final String ERR_GENERATOR_METHOD_RETURN_TYPE =
            "Invalid return type in annotated class generation method '{}' for type '{}'."
            + " Method must return an instance of the class.";

    /** The cache of discovered annotated generators. */
    private final WeakHashMap<Class<?>, Generator> cache =
            new WeakHashMap<>();

    /**
     * Creates a new instance.
     */
    public AnnotatedMethodGenerator() {
        super();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean supports(
            final @NotNull Class<?> type) {
        return getAnnotatedGenerator(type) != null;
    }

    /**
     * Verifies that the specified type is supported by this instance.
     * If test fails throws an {@code IllegalArgumentException} exception
     * as documented in {@link #defaultValue(Class)},
     * {@link #nullableDefaultValue(Class)}, {@link #randomValue(Class)} and
     * {@link #nullableRandomValue(Class)}.
     * 
     * @param type The requested value type.
     * @throws UnsupportedValueTypeException If the specified type is not supported.
     */
    protected void assertSupported(
            final @NotNull Class<?> type) {
        if (!supports(type)) {
            throw new UnsupportedValueTypeException(String.format(
                    "Generators of type %s does not support generation of values of type %s",
                    getClass(),
                    type));
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T> @NotNull T defaultValue(
            final @NotNull Class<T> type) {
        assertSupported(type);
        return getAnnotatedGenerator(type).defaultValue(type);
    }

    @Override
    public <T> T nullableDefaultValue(
            final @NotNull Class<T> type) {
        assertSupported(type);
        return getAnnotatedGenerator(type).nullableDefaultValue(type);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T> @NotNull T randomValue(
            final @NotNull Class<T> type) {
        assertSupported(type);
        return getAnnotatedGenerator(type).randomValue(type);
    }

    @Override
    public <T> T nullableRandomValue(
            final @NotNull Class<T> type) {
        assertSupported(type);
        return getAnnotatedGenerator(type).nullableRandomValue(type);
    }

    /**
     * Returns annotated generator to be used to create random instances of
     * the specified target class.
     * <p>
     * This method caches the generator (found or not) for further calls.
     * 
     * @param targetType The target class
     * @return The found annotated generator, or {@code null}
     */
    protected Generator getAnnotatedGenerator(
            final @NotNull Class<?> targetType) {
        Validate.notNull(targetType);
        Generator generator;
        synchronized (this.cache) {
            generator = this.cache.computeIfAbsent(targetType, this::discoverAnnotatedGenerator);
        }
        if (generator == Generators.MissingGenerator.INSTANCE) {
            generator = null;
        }
        return generator;
    }

    /**
     * Clears the cache of discovered generators.
     */
    protected void clearCache() {
        synchronized (this.cache) {
            this.cache.clear();
        }
    }

    /**
     * Removes the cache of discovered generator for the specified class.
     * 
     * @param targetType The target class
     */
    protected void clearCache(
            final @NotNull Class<?> targetType) {
        synchronized (this.cache) {
            this.cache.remove(targetType);
        }
    }

    /**
     * Finds a constructor or static method annotated with
     * {@code GeneratorMethod} in the specified target class.
     * 
     * @param targetType The target class
     * @return The found annotated generator, or {@code null}
     */
    protected @NotNull Generator discoverAnnotatedGenerator(
            final @NotNull Class<?> targetType) {
        Generator generator = null;
        try {
            generator = findDeclaredConstructor(targetType);
            if (generator == null) {
                generator = findDeclaredMethod(targetType);
            }
        } catch (final RuntimeException re) {
            LOG.warn(HELP_MISCONFIGURED, targetType);
        }
        if (generator == null) {
            generator = Generators.MissingGenerator.INSTANCE;
        }
        return generator;
    }

    /**
     * Finds a constructor annotated with {@code GeneratorMethod} in the
     * specified target class.
     * The method must be public and return an instance of the class.
     * 
     * @param <T> The target class
     * @param targetType The target class
     * @return The found annotated generator, or {@code null}
     */
    protected <T> ExecutableGenerator<T> findDeclaredConstructor(
            final @NotNull Class<T> targetType) {
        Validate.notNull(targetType);
        ExecutableGenerator<T> generator = null;
        for (final Constructor<?> ctr : targetType.getDeclaredConstructors()) {
            if (ctr.isAnnotationPresent(GeneratorMethod.class)) {
                if ((ctr.getModifiers() & GENERATOR_CONSTRUCTOR_MODIFIERS) != GENERATOR_CONSTRUCTOR_MODIFIERS) {
                    LOG.warn(ERR_GENERATOR_CONSTRUCTOR_MODIFIERS,
                            ctr,
                            targetType);
                } else {
                    @SuppressWarnings("unchecked")
                    final Constructor<T> tCtr = (Constructor<T>) ctr;
                    generator = ExecutableGenerator.of(tCtr);
                }
                break;
            }
        }
        return generator;
    }

    /**
     * Finds a method annotated with {@code GeneratorMethod} in the
     * specified target class.
     * The method must be public, static and return an instance of the class.
     * 
     * @param <T> The target class
     * @param targetType The target class
     * @return The found annotated generator, or {@code null}
     */
    protected <T> ExecutableGenerator<T> findDeclaredMethod(
            final @NotNull Class<T> targetType) {
        Validate.notNull(targetType);
        ExecutableGenerator<T> generator = null;
        for (final Method method : targetType.getDeclaredMethods()) {
            if (method.isAnnotationPresent(GeneratorMethod.class)) {
                if ((method.getModifiers() & GENERATOR_METHOD_MODIFIERS) != GENERATOR_METHOD_MODIFIERS) {
                    LOG.warn(ERR_GENERATOR_METHOD_MODIFIERS,
                            method,
                            targetType);
                } else if (!(targetType.isAssignableFrom(method.getReturnType()))) {
                    LOG.warn(ERR_GENERATOR_METHOD_RETURN_TYPE,
                            method,
                            targetType);
                } else {
                    generator = ExecutableGenerator.of(targetType, method);
                }
                break;
            }
        }
        return generator;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return new HashCodeBuilder()
                .append(getClass())
                .build();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(final Object obj) {
        if (obj == null) { return false; }
        if (obj == this) { return true; }
        // Ignore cache
        return obj.getClass() == getClass();
    }
}