/*
* *********************************************************************************************************************
*
* TheseFoolishThings: Miscellaneous utilities
* http://tidalwave.it/projects/thesefoolishthings
*
* Copyright (C) 2009 - 2023 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 javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import it.tidalwave.util.NotFoundException;
import it.tidalwave.util.annotation.VisibleForTesting;
import it.tidalwave.role.ContextManager;
import it.tidalwave.role.spi.impl.DatumAndRole;
import it.tidalwave.role.spi.impl.MultiMap;
import lombok.extern.slf4j.Slf4j;
import static it.tidalwave.role.spi.impl.LogUtil.*;
/***********************************************************************************************************************
*
* A basic implementation of a {@link RoleManager}. 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 RoleManagerSupport implements RoleManager
{
@VisibleForTesting final MultiMap<DatumAndRole, Class<?>> roleMapByDatumAndRole = new MultiMap<>();
// FIXME: use ConcurrentHashMap
use ConcurrentHashMap
@VisibleForTesting final Set<DatumAndRole> 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();
Class<?> contextType = null;
Object context = null;
try
{
contextType = findContextTypeForRole(roleImplementationType);
// With DI frameworks such as Spring it's better to avoid eager initializations of references
final var contextManager = ContextManager.Locator.find();
log.trace(">>>> contexts: {}", shortIds(contextManager.getContexts()));
try
{
context = contextManager.findContextOfType(contextType);
}
catch (NotFoundException e)
{
log.trace(">>>> role {} discarded, can't find context: {}",
shortName(roleImplementationType), shortName(contextType));
continue outer;
}
}
catch (NotFoundException e)
{
// ok, no context
}
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,
@Nullable final Class<?> contextClass,
@Nullable final Object context)
{
final List<Object> values = new ArrayList<>();
for (final var parameterType : parameterTypes)
{
if (parameterType.isAssignableFrom(datumClass))
{
values.add(datum);
}
else if ((contextClass != null) && parameterType.isAssignableFrom(contextClass))
{
values.add(context);
}
else // generic injection
{
values.add(getBean(parameterType));
}
}
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 DatumAndRole(datumType, roleType);
if (!alreadyScanned.contains(datumAndRole))
{
alreadyScanned.add(datumAndRole);
final var before = new HashSet<>(roleMapByDatumAndRole.getValues(datumAndRole));
for (final var superDatumAndRole : datumAndRole.getSuper())
{
roleMapByDatumAndRole.addAll(datumAndRole, roleMapByDatumAndRole.getValues(superDatumAndRole));
}
final var after = new HashSet<>(roleMapByDatumAndRole.getValues(datumAndRole));
logChanges(datumAndRole, before, after);
}
return (Set<Class<? extends T>>)(Set)roleMapByDatumAndRole.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()))
{
roleMapByDatumAndRole.add(new DatumAndRole(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<>(Comparator.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
*
******************************************************************************************************************/
@Nullable
protected abstract <T> T getBean (@Nonnull Class<T> beanType);
/*******************************************************************************************************************
*
* Returns the type of the context associated to the given role implementation type.
*
* @param roleImplementationType the role type
* @return the context type
* @throws NotFoundException if no context is found
*
******************************************************************************************************************/
@Nonnull
protected abstract Class<?> findContextTypeForRole (@Nonnull Class<?> roleImplementationType)
throws NotFoundException;
/*******************************************************************************************************************
*
* Returns the valid datum types for the given role implementation type.
*
* @param roleImplementationType the role type
* @return the datum types
*
******************************************************************************************************************/
@Nonnull
protected abstract Class<?>[] findDatumTypesForRole (@Nonnull Class<?> roleImplementationType);
/*******************************************************************************************************************
*
*
******************************************************************************************************************/
private void logChanges (@Nonnull final DatumAndRole datumAndRole,
@Nonnull final Set<Class<?>> before,
@Nonnull final Set<Class<?>> after)
{
after.removeAll(before);
if (!after.isEmpty())
{
log.debug(">>>>>>> added implementations: {} -> {}", datumAndRole, shortNames(after));
if (log.isTraceEnabled()) // yes, trace
{
logRoles();
}
}
}
/*******************************************************************************************************************
*
*
******************************************************************************************************************/
public void logRoles()
{
log.debug("Configured roles:");
final List<Entry<DatumAndRole, Set<Class<?>>>> entries = new ArrayList<>(roleMapByDatumAndRole.entrySet());
entries.sort(Comparator.comparing((Entry<DatumAndRole, Set<Class<?>>> e) -> e.getKey()
.getDatumClass()
.getName())
.thenComparing(e -> e.getKey().getRoleClass().getName()));
for (final var entry : entries)
{
log.debug(">>>> {}: {} -> {}",
shortName(entry.getKey().getDatumClass()),
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;
}
}