RoleManagerSupport.java

/*
 * *********************************************************************************************************************
 *
 * TheseFoolishThings: Miscellaneous utilities
 * http://tidalwave.it/projects/thesefoolishthings
 *
 * Copyright (C) 2009 - 2023 by Tidalwave s.a.s. (http://tidalwave.it)
 *
 * *********************************************************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations under the License.
 *
 * *********************************************************************************************************************
 *
 * git clone https://bitbucket.org/tidalwave/thesefoolishthings-src
 * git clone https://github.com/tidalwave-it/thesefoolishthings-src
 *
 * *********************************************************************************************************************
 */
package it.tidalwave.role.spi;

import java.lang.reflect.InvocationTargetException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import it.tidalwave.util.NotFoundException;
import it.tidalwave.util.annotation.VisibleForTesting;
import it.tidalwave.role.ContextManager;
import it.tidalwave.role.spi.impl.DatumAndRole;
import it.tidalwave.role.spi.impl.MultiMap;
import lombok.extern.slf4j.Slf4j;
import static it.tidalwave.role.spi.impl.LogUtil.*;

/***********************************************************************************************************************
 *
 * A basic implementation of a {@link RoleManager}. This class must be specialized to:
 *
 * <ol>
 * <li>discover roles (see {@link #scan(java.util.Collection)}</li>
 * <li>associate roles to a datum (see {@link #findDatumTypesForRole(java.lang.Class)}</li>
 * <li>associate roles to contexts (see {@link #findContextTypeForRole(java.lang.Class)}</li>
 * <li>eventually retrieve beans to inject in created roles (see {@link #getBean(java.lang.Class)}</li>
 * </ol>
 *
 * Specializations might use annotations or configuration files to accomplish these tasks.
 *
 * @author  Fabrizio Giudici
 *
 **********************************************************************************************************************/
@Slf4j
public abstract class RoleManagerSupport implements RoleManager
  {
    @VisibleForTesting final MultiMap<DatumAndRole, Class<?>> roleMapByDatumAndRole = new MultiMap<>();

    // FIXME: use ConcurrentHashMap
    @VisibleForTesting final Set<DatumAndRole> alreadyScanned = new HashSet<>();

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public synchronized <T> List<T> findRoles (@Nonnull final Object datum, @Nonnull final Class<? extends T> roleType)
      {
        log.trace("findRoles({}, {})", shortId(datum), shortName(roleType));
        final Class<?> datumType = findTypeOf(datum);
        final List<T> roles = new ArrayList<>();
        final var roleImplementationTypes = findRoleImplementationsFor(datumType, roleType);

        outer:  for (final var roleImplementationType : roleImplementationTypes)
          {
            for (final var constructor : roleImplementationType.getDeclaredConstructors())
              {
                log.trace(">>>> trying constructor {}", constructor);
                final var parameterTypes = constructor.getParameterTypes();
                Class<?> contextType = null;
                Object context = null;

                try
                  {
                    contextType = findContextTypeForRole(roleImplementationType);
                    // With DI frameworks such as Spring it's better to avoid eager initializations of references
                    final var contextManager = ContextManager.Locator.find();
                    log.trace(">>>> contexts: {}", shortIds(contextManager.getContexts()));

                    try
                      {
                        context = contextManager.findContextOfType(contextType);
                      }
                    catch (NotFoundException e)
                      {
                        log.trace(">>>> role {} discarded, can't find context: {}",
                                  shortName(roleImplementationType), shortName(contextType));
                        continue outer;
                      }
                  }
                catch (NotFoundException e)
                  {
                    // ok, no context
                  }

                try
                  {
                    final var params = getParameterValues(parameterTypes, datumType, datum, contextType, context);
                    roles.add(roleType.cast(constructor.newInstance(params)));
                    break;
                  }
                catch (InstantiationException | IllegalAccessException
                        | IllegalArgumentException | InvocationTargetException e)
                  {
                    log.error("Could not instantiate role of type " + roleImplementationType, e);
                  }
              }
          }

        if (log.isTraceEnabled())
          {
            log.trace(">>>> findRoles() returning: {}", shortIds(roles));
          }

        return roles;
      }

    /*******************************************************************************************************************
     *
     * Prepare the constructor parameters out of the given expected types. Parameters will be eventually made of the
     * given datum, context, and other objects returned by {@link #getBean(java.lang.Class)}.
     *
     * @param   parameterTypes      the expected types
     * @param   datumClass          the type of the datum
     * @param   datum               the datum
     * @param   contextClass        the type of the context
     * @param   context             the context
     *
     ******************************************************************************************************************/
    @Nonnull
    private Object[] getParameterValues (@Nonnull final Class<?>[] parameterTypes,
                                         @Nonnull final Class<?> datumClass,
                                         @Nonnull final Object datum,
                                         @Nullable final Class<?> contextClass,
                                         @Nullable final Object context)
      {
        final List<Object> values = new ArrayList<>();

        for (final var parameterType : parameterTypes)
          {
            if (parameterType.isAssignableFrom(datumClass))
              {
                values.add(datum);
              }
            else if ((contextClass != null) && parameterType.isAssignableFrom(contextClass))
              {
                values.add(context);
              }
            else // generic injection
              {
                values.add(getBean(parameterType));
              }
          }

        log.trace(">>>> constructor parameters: {}", values);
        return values.toArray();
      }

    /*******************************************************************************************************************
     *
     * Finds the role implementations for the given owner type and role type. This method might discover new
     * implementations that weren't found during the initial scan, since the initial scan can't go down in a
     * hierarchy; that is, given a Base class or interface with some associated roles, it can't associate those roles
     * to subclasses (or implementations) of Base. Now we can navigate up the hierarchy and complete the picture.
     * Each new discovered role is added into the map, so the next time scanning will be faster.
     *
     * @param   datumType       the type of the datum
     * @param   roleType        the type of the role to find
     * @return                  the types of role implementations
     *
     ******************************************************************************************************************/
    @Nonnull
    @VisibleForTesting synchronized <T> Set<Class<? extends T>> findRoleImplementationsFor (
            @Nonnull final Class<?> datumType,
            @Nonnull final Class<T> roleType)
      {
        final var datumAndRole = new DatumAndRole(datumType, roleType);

        if (!alreadyScanned.contains(datumAndRole))
          {
            alreadyScanned.add(datumAndRole);
            final var before = new HashSet<>(roleMapByDatumAndRole.getValues(datumAndRole));

            for (final var superDatumAndRole : datumAndRole.getSuper())
              {
                roleMapByDatumAndRole.addAll(datumAndRole, roleMapByDatumAndRole.getValues(superDatumAndRole));
              }

            final var after = new HashSet<>(roleMapByDatumAndRole.getValues(datumAndRole));
            logChanges(datumAndRole, before, after);
          }

        return (Set<Class<? extends T>>)(Set)roleMapByDatumAndRole.getValues(datumAndRole);
      }

    /*******************************************************************************************************************
     *
     * Scans all the given role implementation classes and build a map of roles by owner class.
     *
     * @param   roleImplementationTypes     the types of role implementations to scan
     *
     ******************************************************************************************************************/
    protected synchronized void scan (@Nonnull final Collection<Class<?>> roleImplementationTypes)
      {
        log.debug("scan({})", shortNames(roleImplementationTypes));

        for (final var roleImplementationType : roleImplementationTypes)
          {
            for (final var datumType : findDatumTypesForRole(roleImplementationType))
              {
                for (final var roleType : findAllImplementedInterfacesOf(roleImplementationType))
                  {
                    if (!"org.springframework.beans.factory.aspectj.ConfigurableObject".equals(roleType.getName()))
                      {
                        roleMapByDatumAndRole.add(new DatumAndRole(datumType, roleType), roleImplementationType);
                      }
                  }
              }
          }

        logRoles();
      }

    /*******************************************************************************************************************
     *
     * Finds all the interfaces implemented by a given class, including those eventually implemented by superclasses
     * and interfaces that are indirectly implemented (e.g. C implements I1, I1 extends I2).
     *
     * @param  clazz    the class to inspect
     * @return          the implemented interfaces
     *
     ******************************************************************************************************************/
    @Nonnull
    @VisibleForTesting static SortedSet<Class<?>> findAllImplementedInterfacesOf (@Nonnull final Class<?> clazz)
      {
        final SortedSet<Class<?>> interfaces = new TreeSet<>(Comparator.comparing(Class::getName));
        interfaces.addAll(List.of(clazz.getInterfaces()));

        for (final var interface_ : interfaces)
          {
            interfaces.addAll(findAllImplementedInterfacesOf(interface_));
          }

        if (clazz.getSuperclass() != null)
          {
            interfaces.addAll(findAllImplementedInterfacesOf(clazz.getSuperclass()));
          }

        return interfaces;
      }

    /*******************************************************************************************************************
     *
     * Retrieves an extra bean.
     *
     * @param <T>           the static type of the bean
     * @param beanType      the dynamic type of the bean
     * @return              the bean
     *
     ******************************************************************************************************************/
    @Nullable
    protected abstract <T> T getBean (@Nonnull Class<T> beanType);

    /*******************************************************************************************************************
     *
     * Returns the type of the context associated to the given role implementation type.
     *
     * @param   roleImplementationType      the role type
     * @return                              the context type
     * @throws NotFoundException            if no context is found
     *
     ******************************************************************************************************************/
    @Nonnull
    protected abstract Class<?> findContextTypeForRole (@Nonnull Class<?> roleImplementationType)
            throws NotFoundException;

    /*******************************************************************************************************************
     *
     * Returns the valid datum types for the given role implementation type.
     *
     * @param   roleImplementationType      the role type
     * @return                              the datum types
     *
     ******************************************************************************************************************/
    @Nonnull
    protected abstract Class<?>[] findDatumTypesForRole (@Nonnull Class<?> roleImplementationType);

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    private void logChanges (@Nonnull final DatumAndRole datumAndRole,
                             @Nonnull final Set<Class<?>> before,
                             @Nonnull final Set<Class<?>> after)
      {
        after.removeAll(before);

        if (!after.isEmpty())
          {
            log.debug(">>>>>>> added implementations: {} -> {}", datumAndRole, shortNames(after));

            if (log.isTraceEnabled()) // yes, trace
              {
                logRoles();
              }
          }
      }

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    public void logRoles()
      {
        log.debug("Configured roles:");

        final List<Entry<DatumAndRole, Set<Class<?>>>> entries = new ArrayList<>(roleMapByDatumAndRole.entrySet());
        entries.sort(Comparator.comparing((Entry<DatumAndRole, Set<Class<?>>> e) -> e.getKey()
                                                                                     .getDatumClass()
                                                                                     .getName())
                               .thenComparing(e -> e.getKey().getRoleClass().getName()));

        for (final var entry : entries)
          {
            log.debug(">>>> {}: {} -> {}",
                      shortName(entry.getKey().getDatumClass()),
                      shortName(entry.getKey().getRoleClass()),
                      shortNames(entry.getValue()));
          }
      }

    /*******************************************************************************************************************
     *
     * Returns the type of an object, taking care of mocks created by Mockito, for which the implemented interface is
     * returned.
     *
     * @param  object   the object
     * @return          the object type
     *
     ******************************************************************************************************************/
    @Nonnull
    @VisibleForTesting static <T> Class<T> findTypeOf (@Nonnull final T object)
      {
        var ownerClass = object.getClass();

        if (ownerClass.toString().contains("MockitoMock"))
          {
            ownerClass = ownerClass.getInterfaces()[0]; // 1st is the original class, 2nd is CGLIB proxy

            if (log.isTraceEnabled())
              {
                log.trace(">>>> owner is a mock {} implementing {}",
                          shortName(ownerClass), shortNames(List.of(ownerClass.getInterfaces())));
                log.trace(">>>> owner class replaced with {}", shortName(ownerClass));
              }
          }

        return (Class<T>)ownerClass;
      }
  }