View Javadoc
1   /*
2    * *************************************************************************************************************************************************************
3    *
4    * TheseFoolishThings: Miscellaneous utilities
5    * http://tidalwave.it/projects/thesefoolishthings
6    *
7    * Copyright (C) 2009 - 2025 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 the License.
12   * 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 an "AS IS" BASIS, WITHOUT WARRANTIES OR
17   * CONDITIONS OF ANY KIND, either express or implied.  See the License for the specific language governing permissions and limitations under the License.
18   *
19   * *************************************************************************************************************************************************************
20   *
21   * git clone https://bitbucket.org/tidalwave/thesefoolishthings-src
22   * git clone https://github.com/tidalwave-it/thesefoolishthings-src
23   *
24   * *************************************************************************************************************************************************************
25   */
26  package it.tidalwave.role.spi;
27  
28  import java.lang.reflect.InvocationTargetException;
29  import jakarta.annotation.Nonnull;
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.HashSet;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Optional;
36  import java.util.Set;
37  import java.util.SortedSet;
38  import java.util.TreeSet;
39  import it.tidalwave.util.ContextManager;
40  import it.tidalwave.util.annotation.VisibleForTesting;
41  import it.tidalwave.role.impl.MultiMap;
42  import it.tidalwave.role.impl.OwnerAndRole;
43  import it.tidalwave.dci.annotation.DciRole;
44  import lombok.extern.slf4j.Slf4j;
45  import static java.util.Comparator.*;
46  import static it.tidalwave.util.ShortNames.*;
47  
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  
69      // FIXME: use ConcurrentHashMap
70      @VisibleForTesting final Set<OwnerAndRole> alreadyScanned = new HashSet<>();
71  
72      /***********************************************************************************************************************************************************
73       * {@inheritDoc}
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  
83          outer:  for (final var roleImplementationType : roleImplementationTypes)
84            {
85              for (final var constructor : roleImplementationType.getDeclaredConstructors())
86                {
87                  log.trace(">>>> trying constructor {}", constructor);
88                  final var parameterTypes = constructor.getParameterTypes();
89                  Optional<?> context = Optional.empty();
90                  final var contextType = findContextTypeForRole(roleImplementationType);
91  
92                  if (contextType.isPresent())
93                    {
94                      // With DI frameworks such as Spring it's better to avoid eager initializations of references
95                      final var contextManager = ContextManager.getInstance();
96                      log.trace(">>>> contexts: {}", shortIds(contextManager.getContexts()));
97                      context = contextManager.findContextOfType(contextType.get());
98  
99                      if (context.isEmpty())
100                       {
101                         log.trace(">>>> role {} discarded, can't find context: {}",
102                                   shortName(roleImplementationType), shortName(contextType.get()));
103                         continue outer;
104                       }
105                   }
106 
107                 try
108                   {
109                     final var params = getParameterValues(parameterTypes, datumType, datum, contextType, context);
110                     roles.add(roleType.cast(constructor.newInstance(params)));
111                     break;
112                   }
113                 catch (InstantiationException | IllegalAccessException
114                         | IllegalArgumentException | InvocationTargetException e)
115                   {
116                     log.error("Could not instantiate role of type " + roleImplementationType, e);
117                   }
118               }
119           }
120 
121         if (log.isTraceEnabled())
122           {
123             log.trace(">>>> findRoles() returning: {}", shortIds(roles));
124           }
125 
126         return roles;
127       }
128 
129     /***********************************************************************************************************************************************************
130      * Prepare the constructor parameters out of the given expected types. Parameters will be eventually made of the
131      * given datum, context, and other objects returned by {@link #getBean(java.lang.Class)}.
132      *
133      * @param   parameterTypes      the expected types
134      * @param   datumClass          the type of the datum
135      * @param   datum               the datum
136      * @param   contextClass        the type of the context
137      * @param   context             the context
138      **********************************************************************************************************************************************************/
139     @Nonnull
140     private Object[] getParameterValues (@Nonnull final Class<?>[] parameterTypes,
141                                          @Nonnull final Class<?> datumClass,
142                                          @Nonnull final Object datum,
143                                          @Nonnull final Optional<Class<?>> contextClass,
144                                          @Nonnull final Optional<?> context)
145       {
146         final var values = new ArrayList<>();
147 
148         for (final var parameterType : parameterTypes)
149           {
150             if (parameterType.isAssignableFrom(datumClass))
151               {
152                 values.add(datum);
153               }
154             else if (contextClass.isPresent() && parameterType.isAssignableFrom(contextClass.get()))
155               {
156                 values.add(context.orElse(null));
157               }
158             else // generic injection
159               {
160                 // FIXME: it's injecting null, but perhaps should it throw exception?
161                 values.add(getBean(parameterType).orElse(null));
162               }
163           }
164 
165         log.trace(">>>> constructor parameters: {}", values);
166         return values.toArray();
167       }
168 
169     /***********************************************************************************************************************************************************
170      * Finds the role implementations for the given owner type and role type. This method might discover new
171      * implementations that weren't found during the initial scan, since the initial scan can't go down in a
172      * hierarchy; that is, given a Base class or interface with some associated roles, it can't associate those roles
173      * to subclasses (or implementations) of Base. Now we can navigate up the hierarchy and complete the picture.
174      * Each new discovered role is added into the map, so the next time scanning will be faster.
175      *
176      * @param   datumType       the type of the datum
177      * @param   roleType        the type of the role to find
178      * @return                  the types of role implementations
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 OwnerAndRole(datumType, roleType);
186 
187         if (!alreadyScanned.contains(datumAndRole))
188           {
189             alreadyScanned.add(datumAndRole);
190             final var before = new HashSet<>(roleMapByOwnerAndRole.getValues(datumAndRole));
191 
192             for (final var superDatumAndRole : datumAndRole.getSuper())
193               {
194                 roleMapByOwnerAndRole.addAll(datumAndRole, roleMapByOwnerAndRole.getValues(superDatumAndRole));
195               }
196 
197             final var after = new HashSet<>(roleMapByOwnerAndRole.getValues(datumAndRole));
198             logChanges(datumAndRole, before, after);
199           }
200 
201         return (Set<Class<? extends T>>)(Set)roleMapByOwnerAndRole.getValues(datumAndRole);
202       }
203 
204     /***********************************************************************************************************************************************************
205      * Scans all the given role implementation classes and build a map of roles by owner class.
206      *
207      * @param   roleImplementationTypes     the types of role implementations to scan
208      **********************************************************************************************************************************************************/
209     protected synchronized void scan (@Nonnull final Collection<Class<?>> roleImplementationTypes)
210       {
211         log.debug("scan({})", shortNames(roleImplementationTypes));
212 
213         for (final var roleImplementationType : roleImplementationTypes)
214           {
215             for (final var datumType : findDatumTypesForRole(roleImplementationType))
216               {
217                 for (final var roleType : findAllImplementedInterfacesOf(roleImplementationType))
218                   {
219                     if (!"org.springframework.beans.factory.aspectj.ConfigurableObject".equals(roleType.getName()))
220                       {
221                         roleMapByOwnerAndRole.add(new OwnerAndRole(datumType, roleType), roleImplementationType);
222                       }
223                   }
224               }
225           }
226 
227         logRoles();
228       }
229 
230     /***********************************************************************************************************************************************************
231      * Finds all the interfaces implemented by a given class, including those eventually implemented by superclasses
232      * and interfaces that are indirectly implemented (e.g. C implements I1, I1 extends I2).
233      *
234      * @param  clazz    the class to inspect
235      * @return          the implemented interfaces
236      **********************************************************************************************************************************************************/
237     @Nonnull
238     @VisibleForTesting static SortedSet<Class<?>> findAllImplementedInterfacesOf (@Nonnull final Class<?> clazz)
239       {
240         final SortedSet<Class<?>> interfaces = new TreeSet<>(comparing(Class::getName));
241         interfaces.addAll(List.of(clazz.getInterfaces()));
242 
243         for (final var interface_ : interfaces)
244           {
245             interfaces.addAll(findAllImplementedInterfacesOf(interface_));
246           }
247 
248         if (clazz.getSuperclass() != null)
249           {
250             interfaces.addAll(findAllImplementedInterfacesOf(clazz.getSuperclass()));
251           }
252 
253         return interfaces;
254       }
255 
256     /***********************************************************************************************************************************************************
257      * Retrieves an extra bean.
258      *
259      * @param <T>           the static type of the bean
260      * @param beanType      the dynamic type of the bean
261      * @return              the bean
262      **********************************************************************************************************************************************************/
263     @Nonnull
264     protected <T> Optional<T> getBean (@Nonnull final Class<T> beanType)
265       {
266         return Optional.empty();
267       }
268 
269     /***********************************************************************************************************************************************************
270      * Returns the type of the context associated to the given role implementation type.
271      *
272      * @param   roleImplementationType      the role type
273      * @return                              the context type
274      **********************************************************************************************************************************************************/
275     @Nonnull
276     protected Optional<Class<?>> findContextTypeForRole (@Nonnull final Class<?> roleImplementationType)
277       {
278         final var contextClass = roleImplementationType.getAnnotation(DciRole.class).context();
279         return (contextClass == DciRole.NoContext.class) ? Optional.empty() : Optional.of(contextClass);
280       }
281 
282     /***********************************************************************************************************************************************************
283      * Returns the valid datum types for the given role implementation type.
284      *
285      * @param   roleImplementationType      the role type
286      * @return                              the datum types
287      **********************************************************************************************************************************************************/
288     @Nonnull
289     protected Class<?>[] findDatumTypesForRole (@Nonnull final Class<?> roleImplementationType)
290       {
291         return roleImplementationType.getAnnotation(DciRole.class).datumType();
292       }
293 
294     /***********************************************************************************************************************************************************
295      **********************************************************************************************************************************************************/
296     private void logChanges (@Nonnull final OwnerAndRole ownerAndRole,
297                              @Nonnull final Set<Class<?>> before,
298                              @Nonnull final Set<Class<?>> after)
299       {
300         after.removeAll(before);
301 
302         if (!after.isEmpty())
303           {
304             log.debug(">>>>>>> added implementations: {} -> {}", ownerAndRole, shortNames(after));
305 
306             if (log.isTraceEnabled()) // yes, trace
307               {
308                 logRoles();
309               }
310           }
311       }
312 
313     /***********************************************************************************************************************************************************
314      **********************************************************************************************************************************************************/
315     public void logRoles()
316       {
317         log.debug("Configured roles:");
318 
319         final var entries = new ArrayList<>(roleMapByOwnerAndRole.entrySet());
320         entries.sort(comparing((Map.Entry<OwnerAndRole, Set<Class<?>>> e) -> e.getKey().getOwnerClass().getName())
321                                .thenComparing(e -> e.getKey().getRoleClass().getName()));
322 
323         for (final var entry : entries)
324           {
325             log.debug(">>>> {}: {} -> {}",
326                       shortName(entry.getKey().getOwnerClass()),
327                       shortName(entry.getKey().getRoleClass()),
328                       shortNames(entry.getValue()));
329           }
330       }
331 
332     /***********************************************************************************************************************************************************
333      * Returns the type of an object, taking care of mocks created by Mockito, for which the implemented interface is
334      * returned.
335      *
336      * @param  object   the object
337      * @return          the object type
338      **********************************************************************************************************************************************************/
339     @Nonnull
340     @VisibleForTesting static <T> Class<T> findTypeOf (@Nonnull final T object)
341       {
342         var ownerClass = object.getClass();
343 
344         if (ownerClass.toString().contains("MockitoMock"))
345           {
346             ownerClass = ownerClass.getInterfaces()[0]; // 1st is the original class, 2nd is CGLIB proxy
347 
348             if (log.isTraceEnabled())
349               {
350                 log.trace(">>>> owner is a mock {} implementing {}",
351                           shortName(ownerClass), shortNames(List.of(ownerClass.getInterfaces())));
352                 log.trace(">>>> owner class replaced with {}", shortName(ownerClass));
353               }
354           }
355 
356         return (Class<T>)ownerClass;
357       }
358   }