ReflectionUtils.java

  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.util;

  27. import java.lang.annotation.Annotation;
  28. import java.lang.reflect.Array;
  29. import java.lang.reflect.Field;
  30. import java.lang.reflect.GenericArrayType;
  31. import java.lang.reflect.InvocationTargetException;
  32. import java.lang.reflect.ParameterizedType;
  33. import java.lang.reflect.Type;
  34. import java.lang.reflect.TypeVariable;
  35. import jakarta.annotation.Nonnull;
  36. import jakarta.annotation.Nullable;
  37. import java.util.ArrayList;
  38. import java.util.Arrays;
  39. import java.util.HashMap;
  40. import java.util.List;
  41. import java.util.Map;
  42. import lombok.extern.slf4j.Slf4j;
  43. import static java.util.Objects.requireNonNull;
  44. import static java.util.stream.Collectors.*;
  45. import static it.tidalwave.util.ShortNames.*;

  46. /***************************************************************************************************************************************************************
  47.  *
  48.  * Adapted from <a href="http://www.artima.com/weblogs/viewpost.jsp?thread=208860">this article</a>
  49.  *
  50.  * @author Ian Robertson
  51.  * @author Fabrizio Giudici
  52.  *
  53.  **************************************************************************************************************************************************************/
  54. @Slf4j
  55. public class ReflectionUtils
  56.   {
  57.     private static final List<String> INJECT_CLASS_NAMES = List.of("javax.inject.Inject", "jakarta.inject.Inject");

  58.     /***********************************************************************************************************************************************************
  59.      * Get the actual type arguments a subclass has used to extend a generic base class. Note: if the base class is an interface, this method will work only
  60.      * if it is the first inherited interface in childClass.
  61.      *
  62.      * @param   <T>           the static type of the base class
  63.      * @param   baseClass     the base class
  64.      * @param   childClass    the subclass
  65.      * @return                a list of the raw classes for the actual type arguments.
  66.      **********************************************************************************************************************************************************/
  67.     @Nonnull
  68.     public static <T> List<Class<?>> getTypeArguments (@Nonnull final Class<T> baseClass,
  69.                                                        @Nonnull final Class<? extends T> childClass)
  70.       {
  71.         final Map<Type, Type> resolvedTypes = new HashMap<>();
  72.         Type type = childClass;

  73.         // start walking up the inheritance hierarchy until we hit baseClass
  74.         while (!baseClass.equals(getClass(type)))
  75.           {
  76.             if (type instanceof Class<?>)
  77.               {
  78.                 if (baseClass.isInterface())
  79.                   {
  80.                     type = ((Class<?>)type).getGenericInterfaces()[0]; // FIXME: works only for one interface in hierarchy
  81.                   }
  82.                 else
  83.                   {
  84.                     type = ((Class<?>)type).getGenericSuperclass();
  85.                   }
  86.                 // there is no useful information for us in raw types, so just keep going.
  87.               }
  88.             else
  89.               {
  90.                 final var parameterizedType = (ParameterizedType) type;
  91.                 final var rawType = (Class<?>) parameterizedType.getRawType();
  92.                 final var actualTypeArguments = parameterizedType.getActualTypeArguments();
  93.                 final TypeVariable<?>[] typeParameters = rawType.getTypeParameters();

  94.                 for (var i = 0; i < actualTypeArguments.length; i++)
  95.                   {
  96.                     resolvedTypes.put(typeParameters[i], actualTypeArguments[i]);
  97.                   }

  98.                 if (!rawType.equals(baseClass))
  99.                   {
  100.                     type = rawType.getGenericSuperclass();
  101.                   }
  102.               }
  103.           }

  104.         // finally, for each actual type argument provided to baseClass, determine (if possible)
  105.         // the raw class for that type argument.
  106.         final Type[] actualTypeArguments;

  107.         if (type instanceof Class)
  108.           {
  109.             actualTypeArguments = ((Class<?>)type).getTypeParameters();
  110.           }
  111.         else
  112.           {
  113.             actualTypeArguments = ((ParameterizedType)type).getActualTypeArguments();
  114.           }

  115.         final var typeArgumentsAsClasses = new ArrayList<Class<?>>();

  116.         // resolve types by chasing down type variables.
  117.         for (var baseType : actualTypeArguments)
  118.           {
  119.             while (resolvedTypes.containsKey(baseType))
  120.               {
  121.                 baseType = resolvedTypes.get(baseType);
  122.               }

  123.             typeArgumentsAsClasses.add(getClass(baseType));
  124.           }

  125.         return typeArgumentsAsClasses;
  126.       }

  127.     /***********************************************************************************************************************************************************
  128.      * Instantiates an object of the given class performing dependency injections through the constructor.
  129.      *
  130.      * @param <T>     the generic type of the object to instantiate
  131.      * @param type    the dynamic type of the object to instantiate; it is expected to have a single constructor
  132.      * @param beans   the bag of objects to instantiate
  133.      * @return        the new instance
  134.      * @throws        RuntimeException if something fails
  135.      * @since         3.2-ALPHA-17
  136.      **********************************************************************************************************************************************************/
  137.     public static <T> T instantiateWithDependencies (@Nonnull final Class<? extends T> type,
  138.                                                      @Nonnull final Map<Class<?>, Object> beans)
  139.       {
  140.         try
  141.           {
  142.             log.debug("instantiateWithDependencies({}, {})", shortName(type), shortIds(beans.values()));
  143.             final var constructors = type.getConstructors();

  144.             if (constructors.length > 1)
  145.               {
  146.                 throw new RuntimeException("Multiple constructors in " + type);
  147.               }

  148.             final var parameters = Arrays.stream(constructors[0].getParameterTypes()).map(beans::get).collect(toList());

  149.             log.trace(">>>> ctor arguments: {}", shortIds(parameters));
  150.             return type.cast(constructors[0].newInstance(parameters.toArray()));
  151.           }
  152.         catch (InstantiationException | IllegalAccessException | InvocationTargetException e)
  153.           {
  154.             throw new RuntimeException(e);
  155.           }
  156.       }

  157.     /***********************************************************************************************************************************************************
  158.      * Performs dependency injection to an object by means of field introspection.
  159.      *
  160.      * @param object  the object
  161.      * @param beans   the bag of objects to instantiate
  162.      * @since         3.2-ALPHA-17
  163.      **********************************************************************************************************************************************************/
  164.     public static void injectDependencies (@Nonnull final Object object, @Nonnull final Map<Class<?>, Object> beans)
  165.       {
  166.         for (final var field : object.getClass().getDeclaredFields())
  167.           {
  168.             if (hasInjectAnnotation(field))
  169.               {
  170.                 field.setAccessible(true);
  171.                 final var type = field.getType();
  172.                 final var dependency = beans.get(type);

  173.                 if (dependency == null)
  174.                   {
  175.                     throw new RuntimeException("Can't inject " + object + "." + field.getName());
  176.                   }

  177.                 try
  178.                   {
  179.                     field.set(object, dependency);
  180.                   }
  181.                 catch (IllegalArgumentException | IllegalAccessException e)
  182.                   {
  183.                     throw new RuntimeException(e);
  184.                   }
  185.               }
  186.           }
  187.       }

  188.     /***********************************************************************************************************************************************************
  189.      * Returns the class literal associated to the given type.
  190.      *
  191.      * @param   type    the type to inspect
  192.      * @return          the class literal; it might be {@code null} if fails
  193.      **********************************************************************************************************************************************************/
  194.     @Nullable
  195.     public static Class<?> getClass (@Nonnull final Type type)
  196.       {
  197.         requireNonNull(type, "type");

  198.         if (type instanceof Class<?>)
  199.           {
  200.             return (Class<?>)type;
  201.           }
  202.         else if (type instanceof ParameterizedType)
  203.           {
  204.             return getClass(((ParameterizedType)type).getRawType());
  205.           }
  206.         else if (type instanceof GenericArrayType)
  207.           {
  208.             final var componentType = ((GenericArrayType)type).getGenericComponentType();
  209.             final var componentClass = getClass(componentType);

  210.             if (componentClass == null)
  211.               {
  212.                 return null;
  213.               }

  214.             return Array.newInstance(componentClass, 0).getClass();
  215.           }
  216.         else
  217.           {
  218. //            throw new IllegalArgumentException(type.toString());
  219.             return null;
  220.           }
  221.       }

  222.     /***********************************************************************************************************************************************************
  223.      *
  224.      **********************************************************************************************************************************************************/
  225.     private static boolean hasInjectAnnotation (@Nonnull final Field field)
  226.       {
  227.         final var classLoader = Thread.currentThread().getContextClassLoader();

  228.         for (final var className : INJECT_CLASS_NAMES)
  229.           {
  230.             try
  231.               {
  232.                 @SuppressWarnings("unchecked")
  233.                 final var clazz = (Class<? extends Annotation>)classLoader.loadClass(className);

  234.                 if (field.getAnnotation(clazz) != null)
  235.                   {
  236.                     return true;
  237.                   }
  238.               }
  239.             catch (ClassNotFoundException ignored)
  240.               {
  241.                 // try next
  242.               }
  243.           }

  244.         return false;
  245.       }
  246.   }