RoleManagerSupport.java

  1. /*
  2.  * *********************************************************************************************************************
  3.  *
  4.  * TheseFoolishThings: Miscellaneous utilities
  5.  * http://tidalwave.it/projects/thesefoolishthings
  6.  *
  7.  * Copyright (C) 2009 - 2023 by Tidalwave s.a.s. (http://tidalwave.it)
  8.  *
  9.  * *********************************************************************************************************************
  10.  *
  11.  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  12.  * the License. You may obtain a copy of the License at
  13.  *
  14.  *     http://www.apache.org/licenses/LICENSE-2.0
  15.  *
  16.  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  17.  * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
  18.  * specific language governing permissions and limitations under the License.
  19.  *
  20.  * *********************************************************************************************************************
  21.  *
  22.  * git clone https://bitbucket.org/tidalwave/thesefoolishthings-src
  23.  * git clone https://github.com/tidalwave-it/thesefoolishthings-src
  24.  *
  25.  * *********************************************************************************************************************
  26.  */
  27. package it.tidalwave.role.spi;

  28. import java.lang.reflect.InvocationTargetException;
  29. import javax.annotation.Nonnull;
  30. import javax.annotation.Nullable;
  31. import java.util.ArrayList;
  32. import java.util.Collection;
  33. import java.util.Comparator;
  34. import java.util.HashSet;
  35. import java.util.List;
  36. import java.util.Map.Entry;
  37. import java.util.Set;
  38. import java.util.SortedSet;
  39. import java.util.TreeSet;
  40. import it.tidalwave.util.NotFoundException;
  41. import it.tidalwave.util.annotation.VisibleForTesting;
  42. import it.tidalwave.role.ContextManager;
  43. import it.tidalwave.role.spi.impl.DatumAndRole;
  44. import it.tidalwave.role.spi.impl.MultiMap;
  45. import lombok.extern.slf4j.Slf4j;
  46. import static it.tidalwave.role.spi.impl.LogUtil.*;

  47. /***********************************************************************************************************************
  48.  *
  49.  * A basic implementation of a {@link RoleManager}. This class must be specialized to:
  50.  *
  51.  * <ol>
  52.  * <li>discover roles (see {@link #scan(java.util.Collection)}</li>
  53.  * <li>associate roles to a datum (see {@link #findDatumTypesForRole(java.lang.Class)}</li>
  54.  * <li>associate roles to contexts (see {@link #findContextTypeForRole(java.lang.Class)}</li>
  55.  * <li>eventually retrieve beans to inject in created roles (see {@link #getBean(java.lang.Class)}</li>
  56.  * </ol>
  57.  *
  58.  * Specializations might use annotations or configuration files to accomplish these tasks.
  59.  *
  60.  * @author  Fabrizio Giudici
  61.  *
  62.  **********************************************************************************************************************/
  63. @Slf4j
  64. public abstract class RoleManagerSupport implements RoleManager
  65.   {
  66.     @VisibleForTesting final MultiMap<DatumAndRole, Class<?>> roleMapByDatumAndRole = new MultiMap<>();

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

  69.     /*******************************************************************************************************************
  70.      *
  71.      * {@inheritDoc}
  72.      *
  73.      ******************************************************************************************************************/
  74.     @Override @Nonnull
  75.     public synchronized <T> List<T> findRoles (@Nonnull final Object datum, @Nonnull final Class<? extends T> roleType)
  76.       {
  77.         log.trace("findRoles({}, {})", shortId(datum), shortName(roleType));
  78.         final Class<?> datumType = findTypeOf(datum);
  79.         final List<T> roles = new ArrayList<>();
  80.         final var roleImplementationTypes = findRoleImplementationsFor(datumType, roleType);

  81.         outer:  for (final var roleImplementationType : roleImplementationTypes)
  82.           {
  83.             for (final var constructor : roleImplementationType.getDeclaredConstructors())
  84.               {
  85.                 log.trace(">>>> trying constructor {}", constructor);
  86.                 final var parameterTypes = constructor.getParameterTypes();
  87.                 Class<?> contextType = null;
  88.                 Object context = null;

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

  95.                     try
  96.                       {
  97.                         context = contextManager.findContextOfType(contextType);
  98.                       }
  99.                     catch (NotFoundException e)
  100.                       {
  101.                         log.trace(">>>> role {} discarded, can't find context: {}",
  102.                                   shortName(roleImplementationType), shortName(contextType));
  103.                         continue outer;
  104.                       }
  105.                   }
  106.                 catch (NotFoundException e)
  107.                   {
  108.                     // ok, no context
  109.                   }

  110.                 try
  111.                   {
  112.                     final var params = getParameterValues(parameterTypes, datumType, datum, contextType, context);
  113.                     roles.add(roleType.cast(constructor.newInstance(params)));
  114.                     break;
  115.                   }
  116.                 catch (InstantiationException | IllegalAccessException
  117.                         | IllegalArgumentException | InvocationTargetException e)
  118.                   {
  119.                     log.error("Could not instantiate role of type " + roleImplementationType, e);
  120.                   }
  121.               }
  122.           }

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

  127.         return roles;
  128.       }

  129.     /*******************************************************************************************************************
  130.      *
  131.      * Prepare the constructor parameters out of the given expected types. Parameters will be eventually made of the
  132.      * given datum, context, and other objects returned by {@link #getBean(java.lang.Class)}.
  133.      *
  134.      * @param   parameterTypes      the expected types
  135.      * @param   datumClass          the type of the datum
  136.      * @param   datum               the datum
  137.      * @param   contextClass        the type of the context
  138.      * @param   context             the context
  139.      *
  140.      ******************************************************************************************************************/
  141.     @Nonnull
  142.     private Object[] getParameterValues (@Nonnull final Class<?>[] parameterTypes,
  143.                                          @Nonnull final Class<?> datumClass,
  144.                                          @Nonnull final Object datum,
  145.                                          @Nullable final Class<?> contextClass,
  146.                                          @Nullable final Object context)
  147.       {
  148.         final List<Object> values = new ArrayList<>();

  149.         for (final var parameterType : parameterTypes)
  150.           {
  151.             if (parameterType.isAssignableFrom(datumClass))
  152.               {
  153.                 values.add(datum);
  154.               }
  155.             else if ((contextClass != null) && parameterType.isAssignableFrom(contextClass))
  156.               {
  157.                 values.add(context);
  158.               }
  159.             else // generic injection
  160.               {
  161.                 values.add(getBean(parameterType));
  162.               }
  163.           }

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

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

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

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

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

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

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

  209.         for (final var roleImplementationType : roleImplementationTypes)
  210.           {
  211.             for (final var datumType : findDatumTypesForRole(roleImplementationType))
  212.               {
  213.                 for (final var roleType : findAllImplementedInterfacesOf(roleImplementationType))
  214.                   {
  215.                     if (!"org.springframework.beans.factory.aspectj.ConfigurableObject".equals(roleType.getName()))
  216.                       {
  217.                         roleMapByDatumAndRole.add(new DatumAndRole(datumType, roleType), roleImplementationType);
  218.                       }
  219.                   }
  220.               }
  221.           }

  222.         logRoles();
  223.       }

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

  238.         for (final var interface_ : interfaces)
  239.           {
  240.             interfaces.addAll(findAllImplementedInterfacesOf(interface_));
  241.           }

  242.         if (clazz.getSuperclass() != null)
  243.           {
  244.             interfaces.addAll(findAllImplementedInterfacesOf(clazz.getSuperclass()));
  245.           }

  246.         return interfaces;
  247.       }

  248.     /*******************************************************************************************************************
  249.      *
  250.      * Retrieves an extra bean.
  251.      *
  252.      * @param <T>           the static type of the bean
  253.      * @param beanType      the dynamic type of the bean
  254.      * @return              the bean
  255.      *
  256.      ******************************************************************************************************************/
  257.     @Nullable
  258.     protected abstract <T> T getBean (@Nonnull Class<T> beanType);

  259.     /*******************************************************************************************************************
  260.      *
  261.      * Returns the type of the context associated to the given role implementation type.
  262.      *
  263.      * @param   roleImplementationType      the role type
  264.      * @return                              the context type
  265.      * @throws NotFoundException            if no context is found
  266.      *
  267.      ******************************************************************************************************************/
  268.     @Nonnull
  269.     protected abstract Class<?> findContextTypeForRole (@Nonnull Class<?> roleImplementationType)
  270.             throws NotFoundException;

  271.     /*******************************************************************************************************************
  272.      *
  273.      * Returns the valid datum types for the given role implementation type.
  274.      *
  275.      * @param   roleImplementationType      the role type
  276.      * @return                              the datum types
  277.      *
  278.      ******************************************************************************************************************/
  279.     @Nonnull
  280.     protected abstract Class<?>[] findDatumTypesForRole (@Nonnull Class<?> roleImplementationType);

  281.     /*******************************************************************************************************************
  282.      *
  283.      *
  284.      ******************************************************************************************************************/
  285.     private void logChanges (@Nonnull final DatumAndRole datumAndRole,
  286.                              @Nonnull final Set<Class<?>> before,
  287.                              @Nonnull final Set<Class<?>> after)
  288.       {
  289.         after.removeAll(before);

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

  293.             if (log.isTraceEnabled()) // yes, trace
  294.               {
  295.                 logRoles();
  296.               }
  297.           }
  298.       }

  299.     /*******************************************************************************************************************
  300.      *
  301.      *
  302.      ******************************************************************************************************************/
  303.     public void logRoles()
  304.       {
  305.         log.debug("Configured roles:");

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

  311.         for (final var entry : entries)
  312.           {
  313.             log.debug(">>>> {}: {} -> {}",
  314.                       shortName(entry.getKey().getDatumClass()),
  315.                       shortName(entry.getKey().getRoleClass()),
  316.                       shortNames(entry.getValue()));
  317.           }
  318.       }

  319.     /*******************************************************************************************************************
  320.      *
  321.      * Returns the type of an object, taking care of mocks created by Mockito, for which the implemented interface is
  322.      * returned.
  323.      *
  324.      * @param  object   the object
  325.      * @return          the object type
  326.      *
  327.      ******************************************************************************************************************/
  328.     @Nonnull
  329.     @VisibleForTesting static <T> Class<T> findTypeOf (@Nonnull final T object)
  330.       {
  331.         var ownerClass = object.getClass();

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

  335.             if (log.isTraceEnabled())
  336.               {
  337.                 log.trace(">>>> owner is a mock {} implementing {}",
  338.                           shortName(ownerClass), shortNames(List.of(ownerClass.getInterfaces())));
  339.                 log.trace(">>>> owner class replaced with {}", shortName(ownerClass));
  340.               }
  341.           }

  342.         return (Class<T>)ownerClass;
  343.       }
  344.   }