SystemRoleFactorySupport.java
- /*
- * *************************************************************************************************************************************************************
- *
- * TheseFoolishThings: Miscellaneous utilities
- * http://tidalwave.it/projects/thesefoolishthings
- *
- * Copyright (C) 2009 - 2024 by Tidalwave s.a.s. (http://tidalwave.it)
- *
- * *************************************************************************************************************************************************************
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * 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
- * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *
- * *************************************************************************************************************************************************************
- *
- * git clone https://bitbucket.org/tidalwave/thesefoolishthings-src
- * git clone https://github.com/tidalwave-it/thesefoolishthings-src
- *
- * *************************************************************************************************************************************************************
- */
- package it.tidalwave.role.spi;
- import java.lang.reflect.InvocationTargetException;
- import javax.annotation.Nonnull;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Map;
- import java.util.Optional;
- import java.util.Set;
- import java.util.SortedSet;
- import java.util.TreeSet;
- import it.tidalwave.util.ContextManager;
- import it.tidalwave.util.annotation.VisibleForTesting;
- import it.tidalwave.role.impl.MultiMap;
- import it.tidalwave.role.impl.OwnerAndRole;
- import it.tidalwave.dci.annotation.DciRole;
- import lombok.extern.slf4j.Slf4j;
- import static java.util.Comparator.*;
- import static it.tidalwave.util.ShortNames.*;
- /***************************************************************************************************************************************************************
- *
- * A basic implementation of a {@link SystemRoleFactory}. This class must be specialized to:
- *
- * <ol>
- * <li>discover roles (see {@link #scan(java.util.Collection)}</li>
- * <li>associate roles to a datum (see {@link #findDatumTypesForRole(java.lang.Class)}</li>
- * <li>associate roles to contexts (see {@link #findContextTypeForRole(java.lang.Class)}</li>
- * <li>eventually retrieve beans to inject in created roles (see {@link #getBean(java.lang.Class)}</li>
- * </ol>
- *
- * Specializations might use annotations or configuration files to accomplish these tasks.
- *
- * @author Fabrizio Giudici
- *
- **************************************************************************************************************************************************************/
- @Slf4j
- public abstract class SystemRoleFactorySupport implements SystemRoleFactory
- {
- @VisibleForTesting final MultiMap<OwnerAndRole, Class<?>> roleMapByOwnerAndRole = new MultiMap<>();
- // FIXME: use ConcurrentHashMap
- @VisibleForTesting final Set<OwnerAndRole> alreadyScanned = new HashSet<>();
- /***********************************************************************************************************************************************************
- * {@inheritDoc}
- **********************************************************************************************************************************************************/
- @Override @Nonnull
- public synchronized <T> List<T> findRoles (@Nonnull final Object datum, @Nonnull final Class<? extends T> roleType)
- {
- log.trace("findRoles({}, {})", shortId(datum), shortName(roleType));
- final Class<?> datumType = findTypeOf(datum);
- final List<T> roles = new ArrayList<>();
- final var roleImplementationTypes = findRoleImplementationsFor(datumType, roleType);
- outer: for (final var roleImplementationType : roleImplementationTypes)
- {
- for (final var constructor : roleImplementationType.getDeclaredConstructors())
- {
- log.trace(">>>> trying constructor {}", constructor);
- final var parameterTypes = constructor.getParameterTypes();
- Optional<?> context = Optional.empty();
- final var contextType = findContextTypeForRole(roleImplementationType);
- if (contextType.isPresent())
- {
- // With DI frameworks such as Spring it's better to avoid eager initializations of references
- final var contextManager = ContextManager.getInstance();
- log.trace(">>>> contexts: {}", shortIds(contextManager.getContexts()));
- context = contextManager.findContextOfType(contextType.get());
- if (context.isEmpty())
- {
- log.trace(">>>> role {} discarded, can't find context: {}",
- shortName(roleImplementationType), shortName(contextType.get()));
- continue outer;
- }
- }
- try
- {
- final var params = getParameterValues(parameterTypes, datumType, datum, contextType, context);
- roles.add(roleType.cast(constructor.newInstance(params)));
- break;
- }
- catch (InstantiationException | IllegalAccessException
- | IllegalArgumentException | InvocationTargetException e)
- {
- log.error("Could not instantiate role of type " + roleImplementationType, e);
- }
- }
- }
- if (log.isTraceEnabled())
- {
- log.trace(">>>> findRoles() returning: {}", shortIds(roles));
- }
- return roles;
- }
- /***********************************************************************************************************************************************************
- * Prepare the constructor parameters out of the given expected types. Parameters will be eventually made of the
- * given datum, context, and other objects returned by {@link #getBean(java.lang.Class)}.
- *
- * @param parameterTypes the expected types
- * @param datumClass the type of the datum
- * @param datum the datum
- * @param contextClass the type of the context
- * @param context the context
- **********************************************************************************************************************************************************/
- @Nonnull
- private Object[] getParameterValues (@Nonnull final Class<?>[] parameterTypes,
- @Nonnull final Class<?> datumClass,
- @Nonnull final Object datum,
- @Nonnull final Optional<Class<?>> contextClass,
- @Nonnull final Optional<?> context)
- {
- final var values = new ArrayList<>();
- for (final var parameterType : parameterTypes)
- {
- if (parameterType.isAssignableFrom(datumClass))
- {
- values.add(datum);
- }
- else if (contextClass.isPresent() && parameterType.isAssignableFrom(contextClass.get()))
- {
- values.add(context.orElse(null));
- }
- else // generic injection
- {
- // FIXME: it's injecting null, but perhaps should it throw exception?
- values.add(getBean(parameterType).orElse(null));
- }
- }
- log.trace(">>>> constructor parameters: {}", values);
- return values.toArray();
- }
- /***********************************************************************************************************************************************************
- * Finds the role implementations for the given owner type and role type. This method might discover new
- * implementations that weren't found during the initial scan, since the initial scan can't go down in a
- * hierarchy; that is, given a Base class or interface with some associated roles, it can't associate those roles
- * to subclasses (or implementations) of Base. Now we can navigate up the hierarchy and complete the picture.
- * Each new discovered role is added into the map, so the next time scanning will be faster.
- *
- * @param datumType the type of the datum
- * @param roleType the type of the role to find
- * @return the types of role implementations
- **********************************************************************************************************************************************************/
- @Nonnull
- @VisibleForTesting synchronized <T> Set<Class<? extends T>> findRoleImplementationsFor (
- @Nonnull final Class<?> datumType,
- @Nonnull final Class<T> roleType)
- {
- final var datumAndRole = new OwnerAndRole(datumType, roleType);
- if (!alreadyScanned.contains(datumAndRole))
- {
- alreadyScanned.add(datumAndRole);
- final var before = new HashSet<>(roleMapByOwnerAndRole.getValues(datumAndRole));
- for (final var superDatumAndRole : datumAndRole.getSuper())
- {
- roleMapByOwnerAndRole.addAll(datumAndRole, roleMapByOwnerAndRole.getValues(superDatumAndRole));
- }
- final var after = new HashSet<>(roleMapByOwnerAndRole.getValues(datumAndRole));
- logChanges(datumAndRole, before, after);
- }
- return (Set<Class<? extends T>>)(Set)roleMapByOwnerAndRole.getValues(datumAndRole);
- }
- /***********************************************************************************************************************************************************
- * Scans all the given role implementation classes and build a map of roles by owner class.
- *
- * @param roleImplementationTypes the types of role implementations to scan
- **********************************************************************************************************************************************************/
- protected synchronized void scan (@Nonnull final Collection<Class<?>> roleImplementationTypes)
- {
- log.debug("scan({})", shortNames(roleImplementationTypes));
- for (final var roleImplementationType : roleImplementationTypes)
- {
- for (final var datumType : findDatumTypesForRole(roleImplementationType))
- {
- for (final var roleType : findAllImplementedInterfacesOf(roleImplementationType))
- {
- if (!"org.springframework.beans.factory.aspectj.ConfigurableObject".equals(roleType.getName()))
- {
- roleMapByOwnerAndRole.add(new OwnerAndRole(datumType, roleType), roleImplementationType);
- }
- }
- }
- }
- logRoles();
- }
- /***********************************************************************************************************************************************************
- * Finds all the interfaces implemented by a given class, including those eventually implemented by superclasses
- * and interfaces that are indirectly implemented (e.g. C implements I1, I1 extends I2).
- *
- * @param clazz the class to inspect
- * @return the implemented interfaces
- **********************************************************************************************************************************************************/
- @Nonnull
- @VisibleForTesting static SortedSet<Class<?>> findAllImplementedInterfacesOf (@Nonnull final Class<?> clazz)
- {
- final SortedSet<Class<?>> interfaces = new TreeSet<>(comparing(Class::getName));
- interfaces.addAll(List.of(clazz.getInterfaces()));
- for (final var interface_ : interfaces)
- {
- interfaces.addAll(findAllImplementedInterfacesOf(interface_));
- }
- if (clazz.getSuperclass() != null)
- {
- interfaces.addAll(findAllImplementedInterfacesOf(clazz.getSuperclass()));
- }
- return interfaces;
- }
- /***********************************************************************************************************************************************************
- * Retrieves an extra bean.
- *
- * @param <T> the static type of the bean
- * @param beanType the dynamic type of the bean
- * @return the bean
- **********************************************************************************************************************************************************/
- @Nonnull
- protected <T> Optional<T> getBean (@Nonnull final Class<T> beanType)
- {
- return Optional.empty();
- }
- /***********************************************************************************************************************************************************
- * Returns the type of the context associated to the given role implementation type.
- *
- * @param roleImplementationType the role type
- * @return the context type
- **********************************************************************************************************************************************************/
- @Nonnull
- protected Optional<Class<?>> findContextTypeForRole (@Nonnull final Class<?> roleImplementationType)
- {
- final var contextClass = roleImplementationType.getAnnotation(DciRole.class).context();
- return (contextClass == DciRole.NoContext.class) ? Optional.empty() : Optional.of(contextClass);
- }
- /***********************************************************************************************************************************************************
- * Returns the valid datum types for the given role implementation type.
- *
- * @param roleImplementationType the role type
- * @return the datum types
- **********************************************************************************************************************************************************/
- @Nonnull
- protected Class<?>[] findDatumTypesForRole (@Nonnull final Class<?> roleImplementationType)
- {
- return roleImplementationType.getAnnotation(DciRole.class).datumType();
- }
- /***********************************************************************************************************************************************************
- **********************************************************************************************************************************************************/
- private void logChanges (@Nonnull final OwnerAndRole ownerAndRole,
- @Nonnull final Set<Class<?>> before,
- @Nonnull final Set<Class<?>> after)
- {
- after.removeAll(before);
- if (!after.isEmpty())
- {
- log.debug(">>>>>>> added implementations: {} -> {}", ownerAndRole, shortNames(after));
- if (log.isTraceEnabled()) // yes, trace
- {
- logRoles();
- }
- }
- }
- /***********************************************************************************************************************************************************
- **********************************************************************************************************************************************************/
- public void logRoles()
- {
- log.debug("Configured roles:");
- final var entries = new ArrayList<>(roleMapByOwnerAndRole.entrySet());
- entries.sort(comparing((Map.Entry<OwnerAndRole, Set<Class<?>>> e) -> e.getKey().getOwnerClass().getName())
- .thenComparing(e -> e.getKey().getRoleClass().getName()));
- for (final var entry : entries)
- {
- log.debug(">>>> {}: {} -> {}",
- shortName(entry.getKey().getOwnerClass()),
- shortName(entry.getKey().getRoleClass()),
- shortNames(entry.getValue()));
- }
- }
- /***********************************************************************************************************************************************************
- * Returns the type of an object, taking care of mocks created by Mockito, for which the implemented interface is
- * returned.
- *
- * @param object the object
- * @return the object type
- **********************************************************************************************************************************************************/
- @Nonnull
- @VisibleForTesting static <T> Class<T> findTypeOf (@Nonnull final T object)
- {
- var ownerClass = object.getClass();
- if (ownerClass.toString().contains("MockitoMock"))
- {
- ownerClass = ownerClass.getInterfaces()[0]; // 1st is the original class, 2nd is CGLIB proxy
- if (log.isTraceEnabled())
- {
- log.trace(">>>> owner is a mock {} implementing {}",
- shortName(ownerClass), shortNames(List.of(ownerClass.getInterfaces())));
- log.trace(">>>> owner class replaced with {}", shortName(ownerClass));
- }
- }
- return (Class<T>)ownerClass;
- }
- }