IdentityResolver.java

package dev.orne.beans;

/*-
 * #%L
 * Orne Beans
 * %%
 * Copyright (C) 2020 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.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p>Identity resolver that converts an instance of {@code Identity}
 * of unknown type to an instance of a concrete type.</p>
 * 
 * <p>The target identity type must have a public constructor that accepts a
 * single {@code String} argument. If such constructor is not an identity token
 * resolution constructor a public, static method that accepts a single
 * {@code String} argument and returns instances of identity type can be
 * annotated with {@code IdentityTokenResolver} to provide an alternative
 * identity token resolution method.</p>
 * 
 * <p>If no constructor and no static method is valid a warning is logged and
 * every future resolution attempt for that identity type will fail.</p>
 * 
 * <p>Valid examples:</p>
 * 
 * <pre>
 * class MyIdentity
 * implements Identity {
 *   ...
 *   public MyIdentity(String token)
 *   throws UnrecognizedIdentityTokenException {
 *     ...
 *   }
 *   ...
 * }
 * </pre>
 * 
 * <pre>
 * class MyIdentity
 * implements Identity {
 *   ...
 *   public MyIdentity(String notAToken) {
 *     ...
 *   }
 *   ...
 *   {@code @}IdentityTokenResolver
 *   public static MyIdentity resolve(String identityToken)
 *   throws UnrecognizedIdentityTokenException {
 *     // Resolve identity token
 *   }
 *   ...
 * }
 * </pre>
 * 
 * @author <a href="mailto:wamphiry@orne.dev">(w) Iker Hernaez</a>
 * @version 1.0, 2020-05
 * @since 0.1
 * @see IdentityTokenResolver
 */
@API(status=Status.STABLE, since="0.1")
public class IdentityResolver {

    /** The class logger. */
    private static final Logger LOG = LoggerFactory.getLogger(IdentityResolver.class);
    /** The type level annotations shared cache. */
    private static final Cache SHARED_CACHE = new WeakHashMapCache();
    /** Error message for unresolvable identity token. */
    private static final String ERR_UNRESOLVABLE_TOKEN =
            "Identity token cannot be resolved";
    /** Error message for misconfigured identity types. */
    private static final String ERR_MISCONFIGURED =
            "Identity token cannot be resolved for misconfigured identity type";
    /** Informational message for misconfigured identity types. */
    private static final String HELP_MISCONFIGURED =
            "Identity type '{}' resolution is misconfigured."
            + " See IdentityResolver javadoc for correct Identity resolution configuration."
            + " No further resolution attempts will succed.";
    /** Error message for misconfigured identity types with no valid resolver. */
    private static final String ERR_NO_RESOLVER =
            "Identity type '%s' does not have any valid identity token resolution method or constructor";
    /** Error message for unexpected resolver type. */
    private static final String ERR_UNEXPECTED_RESOLVER_TYPE =
            "Unexpected Executable subtype for '%s' identity type: %s";
    /** Error message for unexpected resolver type. */
    private static final String ERR_GET_RESOLVER_ERROR =
            "Unexpected error analyzing '%s' identity type";
    /** Required modifiers for identity token resolution methods. */
    private static final int RESOLVER_METHOD_MODIFIERS =
            Modifier.STATIC | Modifier.PUBLIC;
    /** Error message for wrong resolution method modifiers. */
    private static final String ERR_RESOLVER_METHOD_MODIFIERS =
            "Invalid modifiers in annotated token resolution method '%s' for identity type '%s'."
            + " Method must be public and static.";
    /** Error message for wrong resolution method parameter types. */
    private static final String ERR_RESOLVER_METHOD_PARAMS =
            "Invalid parameter types in annotated token resolution method '%s' for identity type '%s'."
            + " Method must have one only argument of String type.";
    /** Error message for wrong resolution method return type. */
    private static final String ERR_RESOLVER_METHOD_RETURN_TYPE =
            "Invalid return type in annotated token resolution method '%s' for identity type '%s'."
            + " Method must return identity type.";
    /** Error message for no identity token constructor. */
    private static final String ERR_TOKEN_CONSTRUCTOR_NOT_FOUND =
            "No identity token contructor for identity type '%s'.";
    /**
     * The identity type resolve methods cache for this instance. By default
     * shared between all instances.
     */
    private @NotNull Cache cache = SHARED_CACHE;

    /**
     * Creates a new instance.
     */
    protected IdentityResolver() {
        super();
    }

    /**
     * Returns the shared, singleton instance.
     * 
     * @return The singleton instance.
     */
    public static @NotNull IdentityResolver getInstance() {
        return InstanceHolder.INSTANCE;
    }

    /**
     * Returns the cache to be used by this instance.
     * 
     * @return The cache to be used by this instance
     */
    protected @NotNull Cache getCache() {
        return this.cache;
    }

    /**
     * <p>Sets the identity type resolve methods cache for this instance.
     * If {@code cache} is {@code null} shared cache will be used.</p>
     * 
     * @param cache The cache to be used by this instance
     * @return This instance for method chaining
     */
    protected @NotNull IdentityResolver setCache(
            final Cache cache) {
        if (cache == null) {
            this.cache = SHARED_CACHE;
        } else {
            this.cache = cache;
        }
        return this;
    }

    /**
     * Resolves the specified source identity to an identity of the specified
     * target type.
     * 
     * @param <T> The target identity type
     * @param identity The source identity
     * @param targetType The target identity type
     * @return An instance of target identity type or {@code null}
     * if source identity is null or invalid
     * @throws UnrecognizedIdentityTokenException If the source identity's
     * token cannot be resolved to target identity type
     */
    public <T extends Identity> T resolve(
            final Identity identity,
            final @NotNull Class<T> targetType)
    throws UnrecognizedIdentityTokenException {
        Validate.notNull(targetType);
        if (identity == null || targetType.isInstance(identity)) {
            return targetType.cast(identity);
        }
        return resolve(identity.getIdentityToken(), targetType);
    }

    /**
     * Resolves the specified source identity token to an identity of the
     * specified target type.
     * 
     * @param <T> The target identity type
     * @param identityToken The source identity token
     * @param targetType The target identity type
     * @return An instance of target identity type or {@code null}
     * if source identity token is null
     * @throws UnrecognizedIdentityTokenException If the source identity
     * token cannot be resolved to target identity type
     */
    public <T extends Identity> T resolve(
            final String identityToken,
            final @NotNull Class<T> targetType)
    throws UnrecognizedIdentityTokenException {
        Validate.notNull(targetType);
        if (identityToken == null) {
            return null;
        }
        try {
            final Executable resolver = getResolver(targetType);
            if (resolver instanceof Method) {
                return targetType.cast(((Method) resolver).invoke(null, identityToken));
            } else if (resolver instanceof Constructor) {
                return targetType.cast(((Constructor<?>) resolver).newInstance(identityToken));
            } else {
                throw new UnresolvableIdentityException(String.format(
                        ERR_UNEXPECTED_RESOLVER_TYPE,
                        targetType,
                        resolver.getClass()));
            }
        } catch (final UnresolvableIdentityException uie) {
            throw new UnrecognizedIdentityTokenException(ERR_MISCONFIGURED, uie);
        } catch (final InvocationTargetException ite) {
            if (!(ite.getTargetException() instanceof UnrecognizedIdentityTokenException)) {
                throw new UnrecognizedIdentityTokenException(
                        ERR_UNRESOLVABLE_TOKEN,
                        ite.getTargetException());
            } else {
                throw ((UnrecognizedIdentityTokenException) ite.getTargetException());
            }
        } catch (final IllegalAccessException
                | IllegalArgumentException
                | InstantiationException e) {
            throw new UnrecognizedIdentityTokenException(ERR_UNRESOLVABLE_TOKEN, e);
        }
    }

    /**
     * Returns the method or constructor to be used to resolve identity tokens
     * for the specified target identity type.
     * 
     * @param targetType The target identity type
     * @return The identity type resolution method or constructor
     * @throws UnresolvableIdentityException If the identity type is
     * misconfigured
     */
    protected @NotNull Executable getResolver(
            final @NotNull Class<? extends Identity> targetType)
    throws UnresolvableIdentityException {
        Validate.notNull(targetType);
        Executable resolver;
        synchronized (this.cache) {
            if (this.cache.contains(targetType)) {
                resolver = this.cache.get(targetType);
            } else {
                try {
                    resolver = findTokenResolverMethod(targetType);
                    if (resolver == null) {
                        resolver = findTokenConstructor(targetType);
                    }
                    this.cache.put(targetType, resolver);
                } catch (final UnresolvableIdentityException uie) {
                    LOG.warn(HELP_MISCONFIGURED, targetType);
                    this.cache.put(targetType, null);
                    throw uie;
                } catch (final RuntimeException re) {
                    LOG.warn(HELP_MISCONFIGURED, targetType);
                    this.cache.put(targetType, null);
                    throw new UnresolvableIdentityException(
                            ERR_GET_RESOLVER_ERROR, re);
                }
            }
        }
        if (resolver == null) {
            throw new UnresolvableIdentityException(String.format(
                    ERR_NO_RESOLVER,
                    targetType));
        }
        return resolver;
    }

    /**
     * Finds a method annotated with {@code IdentityTokenResolver} in the
     * specified target identity type. The method must be public and static,
     * have a single {@code String} argument and return a
     * 
     * @param targetType The target identity type
     * @return The found identity token resolution method
     * @throws UnresolvableIdentityException If the annotated method doesn't
     * fulfill the requirements
     * @throws SecurityException If a security exception occurs accessing the
     * class methods
     * @see IdentityTokenResolver
     */
    protected Method findTokenResolverMethod(
            final @NotNull Class<?> targetType)
    throws UnresolvableIdentityException {
        Validate.notNull(targetType);
        Method resolver = null;
        for (final Method method : targetType.getDeclaredMethods()) {
            if (method.isAnnotationPresent(IdentityTokenResolver.class)) {
                if ((method.getModifiers() & RESOLVER_METHOD_MODIFIERS) != RESOLVER_METHOD_MODIFIERS) {
                    throw new UnresolvableIdentityException(String.format(
                            ERR_RESOLVER_METHOD_MODIFIERS,
                            method,
                            targetType));
                }
                if (!Arrays.equals(new Class<?>[] { String.class }, method.getParameterTypes())) {
                    throw new UnresolvableIdentityException(String.format(
                            ERR_RESOLVER_METHOD_PARAMS,
                            method,
                            targetType));
                }
                if (!(targetType.isAssignableFrom(method.getReturnType()))) {
                    throw new UnresolvableIdentityException(String.format(
                            ERR_RESOLVER_METHOD_RETURN_TYPE,
                            method,
                            targetType));
                }
                resolver = method;
                break;
            }
        }
        return resolver;
    }

    /**
     * Finds an identity token constructor in the specified target identity
     * type. The constructor must be public and have a single {@code String}
     * argument.
     * 
     * @param <T> The target identity type
     * @param targetType The target identity type
     * @return The found identity token constructor
     * @throws UnresolvableIdentityException If no constructor is found or it
     * doesn't fulfill the requirements
     * @throws SecurityException If a security exception occurs accessing the
     * constructor
     */
    protected <T> Constructor<T> findTokenConstructor(
            final @NotNull Class<T> targetType)
    throws UnresolvableIdentityException {
        Validate.notNull(targetType);
        try {
            return targetType.getConstructor(String.class);
        } catch (final NoSuchMethodException nsme) {
            throw new UnresolvableIdentityException(String.format(
                        ERR_TOKEN_CONSTRUCTOR_NOT_FOUND,
                        targetType),
                    nsme);
        }
    }

    /**
     * Holder of shared {@code IdentityResolver} instance.
     */
    @API(status=Status.INTERNAL, since="0.1")
    private static class InstanceHolder{

        /** The shared instance. */
        private static final IdentityResolver INSTANCE = new IdentityResolver();
    }

    /**
     * Interface for identity resolve methods cache.
     */
    @API(status=Status.INTERNAL, since="0.1")
    protected static interface Cache {

        /**
         * Returns {@code true} if this instance contains an entry for
         * the specified identity type.
         * 
         * @param key The identity type
         * @return If this instance contains an entry for the identity type
         */
        boolean contains(
                @NotNull Class<? extends Identity> key);

        /**
         * Returns the cached identity token resolution executable for the
         * specified identity type, if any.
         * 
         * @param key The identity type
         * @return The identity token resolution executable, or
         * {@code null} if not cached o cache expired
         */
        Executable get(
                @NotNull Class<? extends Identity> key);

        /**
         * Puts the specified identity token resolution executable for the
         * specified identity type.
         * 
         * @param key The identity type
         * @param value The identity token resolution executable
         */
        void put(
                @NotNull Class<? extends Identity> key,
                Executable value);
    }

    /**
     * Implementation of {@code Cache} based on {@code WeakHashMap}.
     * 
     * @see Cache
     * @see WeakHashMap
     */
    @API(status=Status.INTERNAL, since="0.1")
    protected static class WeakHashMapCache
    implements Cache {

        /** The cache entries. */
        private final WeakHashMap<Class<? extends Identity>, Executable> entries =
                new WeakHashMap<>();

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

        /**
         * {@inheritDoc}
         */
        @Override
        public synchronized boolean contains(
                final @NotNull Class<? extends Identity> key) {
            return this.entries.containsKey(key);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public synchronized Executable get(
                final @NotNull Class<? extends Identity> key) {
            return this.entries.get(key);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public synchronized void put(
                final @NotNull Class<? extends Identity> key,
                final Executable value) {
            this.entries.put(key, value);
        }
    }

    /**
     * Exception for misconfigured identity types. Caused when an {@code Identity}
     * doesn't have a {@code String} constructor and no valid static method has
     * been annotated with {@code IdentityTokenResolver}.
     * 
     * @author <a href="mailto:wamphiry@orne.dev">(w) Iker Hernaez</a>
     * @version 1.0, 2020-05
     * @since 0.1
     * @see IdentityResolver
     * @see IdentityTokenResolver
     */
    @API(status=Status.STABLE, since="0.1")
    public static class UnresolvableIdentityException
    extends Exception {

        /** The Serial Version UID. */
        private static final long serialVersionUID = 1L;

        /**
         * Constructs a new exception with {@code null} as its detail message.
         * The cause is not initialized, and may subsequently be initialized by a
         * call to {@link #initCause}.
         */
        public UnresolvableIdentityException() {
            super();
        }

        /**
         * Constructs a new exception with the specified detail message.  The
         * cause is not initialized, and may subsequently be initialized by
         * a call to {@link #initCause}.
         *
         * @param   message   the detail message. The detail message is saved for
         *          later retrieval by the {@link #getMessage()} method.
         */
        public UnresolvableIdentityException(
                final String message) {
            super(message);
        }

        /**
         * Constructs a new exception with the specified cause and a detail
         * message of {@code (cause==null ? null : cause.toString())} (which
         * typically contains the class and detail message of {@code cause}).
         * This constructor is useful for exceptions that are little more than
         * wrappers for other throwables (for example, {@link
         * java.security.PrivilegedActionException}).
         *
         * @param  cause the cause (which is saved for later retrieval by the
         *         {@link #getCause()} method).  (A {@code null} value is
         *         permitted, and indicates that the cause is nonexistent or
         *         unknown.)
         */
        public UnresolvableIdentityException(
                final Throwable cause) {
            super(cause);
        }

        /**
         * Constructs a new exception with the specified detail message and
         * cause.  <p>Note that the detail message associated with
         * {@code cause} is <i>not</i> automatically incorporated in
         * this exception's detail message.
         *
         * @param  message the detail message (which is saved for later retrieval
         *         by the {@link #getMessage()} method).
         * @param  cause the cause (which is saved for later retrieval by the
         *         {@link #getCause()} method).  (A {@code null} value is
         *         permitted, and indicates that the cause is nonexistent or
         *         unknown.)
         */
        public UnresolvableIdentityException(
                final String message,
                final Throwable cause) {
            super(message, cause);
        }

        /**
         * Constructs a new exception with the specified detail message,
         * cause, suppression enabled or disabled, and writable stack
         * trace enabled or disabled.
         *
         * @param  message the detail message.
         * @param cause the cause.  (A {@code null} value is permitted,
         * and indicates that the cause is nonexistent or unknown.)
         * @param enableSuppression whether or not suppression is enabled
         *                          or disabled
         * @param writableStackTrace whether or not the stack trace should
         *                           be writable
         */
        public UnresolvableIdentityException(
                final String message,
                final Throwable cause,
                final boolean enableSuppression,
                final boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }
    }
}