SystemRoleFactorySupport.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 java.util.ArrayList;
  31. import java.util.Collection;
  32. import java.util.Comparator;
  33. import java.util.HashSet;
  34. import java.util.List;
  35. import java.util.Map;
  36. import java.util.Optional;
  37. import java.util.Set;
  38. import java.util.SortedSet;
  39. import java.util.TreeSet;
  40. import it.tidalwave.util.ContextManager;
  41. import it.tidalwave.util.annotation.VisibleForTesting;
  42. import it.tidalwave.role.impl.OwnerAndRole;
  43. import it.tidalwave.role.impl.MultiMap;
  44. import it.tidalwave.dci.annotation.DciRole;
  45. import lombok.extern.slf4j.Slf4j;
  46. import static java.util.Comparator.*;
  47. import static it.tidalwave.util.ShortNames.*;

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

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

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

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

  90.                 if (contextType.isPresent())
  91.                   {
  92.                     // With DI frameworks such as Spring it's better to avoid eager initializations of references
  93.                     final var contextManager = ContextManager.getInstance();
  94.                     log.trace(">>>> contexts: {}", shortIds(contextManager.getContexts()));
  95.                     context = contextManager.findContextOfType(contextType.get());

  96.                     if (context.isEmpty())
  97.                       {
  98.                         log.trace(">>>> role {} discarded, can't find context: {}",
  99.                                   shortName(roleImplementationType), shortName(contextType.get()));
  100.                         continue outer;
  101.                       }
  102.                   }

  103.                 try
  104.                   {
  105.                     final var params = getParameterValues(parameterTypes, datumType, datum, contextType, context);
  106.                     roles.add(roleType.cast(constructor.newInstance(params)));
  107.                     break;
  108.                   }
  109.                 catch (InstantiationException | IllegalAccessException
  110.                         | IllegalArgumentException | InvocationTargetException e)
  111.                   {
  112.                     log.error("Could not instantiate role of type " + roleImplementationType, e);
  113.                   }
  114.               }
  115.           }

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

  120.         return roles;
  121.       }

  122.     /*******************************************************************************************************************
  123.      *
  124.      * Prepare the constructor parameters out of the given expected types. Parameters will be eventually made of the
  125.      * given datum, context, and other objects returned by {@link #getBean(java.lang.Class)}.
  126.      *
  127.      * @param   parameterTypes      the expected types
  128.      * @param   datumClass          the type of the datum
  129.      * @param   datum               the datum
  130.      * @param   contextClass        the type of the context
  131.      * @param   context             the context
  132.      *
  133.      ******************************************************************************************************************/
  134.     @Nonnull
  135.     private Object[] getParameterValues (@Nonnull final Class<?>[] parameterTypes,
  136.                                          @Nonnull final Class<?> datumClass,
  137.                                          @Nonnull final Object datum,
  138.                                          @Nonnull final Optional<Class<?>> contextClass,
  139.                                          @Nonnull final Optional<?> context)
  140.       {
  141.         final var values = new ArrayList<>();

  142.         for (final var parameterType : parameterTypes)
  143.           {
  144.             if (parameterType.isAssignableFrom(datumClass))
  145.               {
  146.                 values.add(datum);
  147.               }
  148.             else if (contextClass.isPresent() && parameterType.isAssignableFrom(contextClass.get()))
  149.               {
  150.                 values.add(context.orElse(null));
  151.               }
  152.             else // generic injection
  153.               {
  154.                 // FIXME: it's injecting null, but perhaps should it throw exception?
  155.                 values.add(getBean(parameterType).orElse(null));
  156.               }
  157.           }

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

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

  180.         if (!alreadyScanned.contains(datumAndRole))
  181.           {
  182.             alreadyScanned.add(datumAndRole);
  183.             final var before = new HashSet<>(roleMapByOwnerAndRole.getValues(datumAndRole));

  184.             for (final var superDatumAndRole : datumAndRole.getSuper())
  185.               {
  186.                 roleMapByOwnerAndRole.addAll(datumAndRole, roleMapByOwnerAndRole.getValues(superDatumAndRole));
  187.               }

  188.             final var after = new HashSet<>(roleMapByOwnerAndRole.getValues(datumAndRole));
  189.             logChanges(datumAndRole, before, after);
  190.           }

  191.         return (Set<Class<? extends T>>)(Set)roleMapByOwnerAndRole.getValues(datumAndRole);
  192.       }

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

  203.         for (final var roleImplementationType : roleImplementationTypes)
  204.           {
  205.             for (final var datumType : findDatumTypesForRole(roleImplementationType))
  206.               {
  207.                 for (final var roleType : findAllImplementedInterfacesOf(roleImplementationType))
  208.                   {
  209.                     if (!"org.springframework.beans.factory.aspectj.ConfigurableObject".equals(roleType.getName()))
  210.                       {
  211.                         roleMapByOwnerAndRole.add(new OwnerAndRole(datumType, roleType), roleImplementationType);
  212.                       }
  213.                   }
  214.               }
  215.           }

  216.         logRoles();
  217.       }

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

  232.         for (final var interface_ : interfaces)
  233.           {
  234.             interfaces.addAll(findAllImplementedInterfacesOf(interface_));
  235.           }

  236.         if (clazz.getSuperclass() != null)
  237.           {
  238.             interfaces.addAll(findAllImplementedInterfacesOf(clazz.getSuperclass()));
  239.           }

  240.         return interfaces;
  241.       }

  242.     /*******************************************************************************************************************
  243.      *
  244.      * Retrieves an extra bean.
  245.      *
  246.      * @param <T>           the static type of the bean
  247.      * @param beanType      the dynamic type of the bean
  248.      * @return              the bean
  249.      *
  250.      ******************************************************************************************************************/
  251.     @Nonnull
  252.     protected <T> Optional<T> getBean (@Nonnull final Class<T> beanType)
  253.       {
  254.         return Optional.empty();
  255.       }

  256.     /*******************************************************************************************************************
  257.      *
  258.      * Returns the type of the context associated to the given role implementation type.
  259.      *
  260.      * @param   roleImplementationType      the role type
  261.      * @return                              the context type
  262.      *
  263.      ******************************************************************************************************************/
  264.     @Nonnull
  265.     protected Optional<Class<?>> findContextTypeForRole (@Nonnull final Class<?> roleImplementationType)
  266.       {
  267.         final var contextClass = roleImplementationType.getAnnotation(DciRole.class).context();
  268.         return (contextClass == DciRole.NoContext.class) ? Optional.empty() : Optional.of(contextClass);
  269.       }

  270.     /*******************************************************************************************************************
  271.      *
  272.      * Returns the valid datum types for the given role implementation type.
  273.      *
  274.      * @param   roleImplementationType      the role type
  275.      * @return                              the datum types
  276.      *
  277.      ******************************************************************************************************************/
  278.     @Nonnull
  279.     protected Class<?>[] findDatumTypesForRole (@Nonnull final Class<?> roleImplementationType)
  280.       {
  281.         return roleImplementationType.getAnnotation(DciRole.class).datumType();
  282.       }

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

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

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

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

  308.         final var entries = new ArrayList<>(roleMapByOwnerAndRole.entrySet());
  309.         entries.sort(comparing((Map.Entry<OwnerAndRole, Set<Class<?>>> e) -> e.getKey().getOwnerClass().getName())
  310.                                .thenComparing(e -> e.getKey().getRoleClass().getName()));

  311.         for (final var entry : entries)
  312.           {
  313.             log.debug(">>>> {}: {} -> {}",
  314.                       shortName(entry.getKey().getOwnerClass()),
  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.   }