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 }