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

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

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

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

  84.         outer:  for (final Class<? extends ROLE_TYPE> roleImplementationType : roleImplementationTypes)
  85.           {
  86.             for (final Constructor<?> constructor : roleImplementationType.getDeclaredConstructors())
  87.               {
  88.                 log.trace(">>>> trying constructor {}", constructor);
  89.                 final Class<?>[] parameterTypes = constructor.getParameterTypes();
  90.                 Class<?> contextType = null;
  91.                 Object context = null;

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

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

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

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

  130.         return roles;
  131.       }

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

  152.         for (final Class<?> parameterType : parameterTypes)
  153.           {
  154.             if (parameterType.isAssignableFrom(datumClass))
  155.               {
  156.                 values.add(datum);
  157.               }
  158.             else if ((contextClass != null) && parameterType.isAssignableFrom(contextClass))
  159.               {
  160.                 values.add(context);
  161.               }
  162.             else // generic injection
  163.               {
  164.                 values.add(getBean(parameterType));
  165.               }
  166.           }

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

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

  189.         if (!alreadyScanned.contains(datumAndRole))
  190.           {
  191.             alreadyScanned.add(datumAndRole);
  192.             final Set<Class<?>> before = new HashSet<>(roleMapByDatumAndRole.getValues(datumAndRole));

  193.             for (final DatumAndRole superDatumAndRole : datumAndRole.getSuper())
  194.               {
  195.                 roleMapByDatumAndRole.addAll(datumAndRole, roleMapByDatumAndRole.getValues(superDatumAndRole));
  196.               }

  197.             final Set<Class<?>> after = new HashSet<>(roleMapByDatumAndRole.getValues(datumAndRole));
  198.             logChanges(datumAndRole, before, after);
  199.           }

  200.         return (Set<Class<? extends RT>>)(Set)roleMapByDatumAndRole.getValues(datumAndRole);
  201.       }

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

  212.         for (final Class<?> roleImplementationType : roleImplementationTypes)
  213.           {
  214.             for (final Class<?> datumType : findDatumTypesForRole(roleImplementationType))
  215.               {
  216.                 for (final Class<?> roleType : findAllImplementedInterfacesOf(roleImplementationType))
  217.                   {
  218.                     if (!"org.springframework.beans.factory.aspectj.ConfigurableObject".equals(roleType.getName()))
  219.                       {
  220.                         roleMapByDatumAndRole.add(new DatumAndRole(datumType, roleType), roleImplementationType);
  221.                       }
  222.                   }
  223.               }
  224.           }

  225.         logRoles();
  226.       }

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

  241.         for (final Class<?> interface_ : interfaces)
  242.           {
  243.             interfaces.addAll(findAllImplementedInterfacesOf(interface_));
  244.           }

  245.         if (clazz.getSuperclass() != null)
  246.           {
  247.             interfaces.addAll(findAllImplementedInterfacesOf(clazz.getSuperclass()));
  248.           }

  249.         return interfaces;
  250.       }

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

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

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

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

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

  296.             if (log.isTraceEnabled()) // yes, trace
  297.               {
  298.                 logRoles();
  299.               }
  300.           }
  301.       }

  302.     /*******************************************************************************************************************
  303.      *
  304.      *
  305.      ******************************************************************************************************************/
  306.     public void logRoles()
  307.       {
  308.         log.debug("Configured roles:");

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

  314.         for (final Entry<DatumAndRole, Set<Class<?>>> entry : entries)
  315.           {
  316.             log.debug(">>>> {}: {} -> {}",
  317.                       shortName(entry.getKey().getDatumClass()),
  318.                       shortName(entry.getKey().getRoleClass()),
  319.                       shortNames(entry.getValue()));
  320.           }
  321.       }

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

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

  338.             if (log.isTraceEnabled())
  339.               {
  340.                 log.trace(">>>> owner is a mock {} implementing {}",
  341.                           shortName(ownerClass), shortNames(Arrays.asList(ownerClass.getInterfaces())));
  342.                 log.trace(">>>> owner class replaced with {}", shortName(ownerClass));
  343.               }
  344.           }

  345.         return (Class<T>)ownerClass;
  346.       }
  347.   }