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;
}
}