View Javadoc
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  
28  import java.lang.annotation.Annotation;
29  import java.lang.reflect.Array;
30  import java.lang.reflect.Field;
31  import java.lang.reflect.GenericArrayType;
32  import java.lang.reflect.InvocationTargetException;
33  import java.lang.reflect.ParameterizedType;
34  import java.lang.reflect.Type;
35  import java.lang.reflect.TypeVariable;
36  import javax.annotation.CheckForNull;
37  import javax.annotation.Nonnull;
38  import java.util.ArrayList;
39  import java.util.Arrays;
40  import java.util.HashMap;
41  import java.util.List;
42  import java.util.Map;
43  import lombok.extern.slf4j.Slf4j;
44  import static java.util.Objects.requireNonNull;
45  import static java.util.stream.Collectors.*;
46  import static it.tidalwave.util.ShortNames.*;
47  
48  /***************************************************************************************************************************************************************
49   *
50   * Adapted from <a href="http://www.artima.com/weblogs/viewpost.jsp?thread=208860">this article</a>
51   *
52   * @author Ian Robertson
53   * @author Fabrizio Giudici
54   *
55   **************************************************************************************************************************************************************/
56  @Slf4j
57  public class ReflectionUtils
58    {
59      private static final List<String> INJECT_CLASS_NAMES = List.of("javax.inject.Inject", "jakarta.inject.Inject");
60  
61      /***********************************************************************************************************************************************************
62       * 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
63       * if it is the first inherited interface in childClass.
64       *
65       * @param   <T>           the static type of the base class
66       * @param   baseClass     the base class
67       * @param   childClass    the subclass
68       * @return                a list of the raw classes for the actual type arguments.
69       **********************************************************************************************************************************************************/
70      @Nonnull
71      public static <T> List<Class<?>> getTypeArguments (@Nonnull final Class<T> baseClass,
72                                                         @Nonnull final Class<? extends T> childClass)
73        {
74          final Map<Type, Type> resolvedTypes = new HashMap<>();
75          Type type = childClass;
76  
77          // start walking up the inheritance hierarchy until we hit baseClass
78          while (!baseClass.equals(getClass(type)))
79            {
80              if (type instanceof Class<?>)
81                {
82                  if (baseClass.isInterface())
83                    {
84                      type = ((Class<?>)type).getGenericInterfaces()[0]; // FIXME: works only for one interface in hierarchy
85                    }
86                  else
87                    {
88                      type = ((Class<?>)type).getGenericSuperclass();
89                    }
90                  // there is no useful information for us in raw types, so just keep going.
91                }
92              else
93                {
94                  final var parameterizedType = (ParameterizedType) type;
95                  final var rawType = (Class<?>) parameterizedType.getRawType();
96                  final var actualTypeArguments = parameterizedType.getActualTypeArguments();
97                  final TypeVariable<?>[] typeParameters = rawType.getTypeParameters();
98  
99                  for (var i = 0; i < actualTypeArguments.length; i++)
100                   {
101                     resolvedTypes.put(typeParameters[i], actualTypeArguments[i]);
102                   }
103 
104                 if (!rawType.equals(baseClass))
105                   {
106                     type = rawType.getGenericSuperclass();
107                   }
108               }
109           }
110 
111         // finally, for each actual type argument provided to baseClass, determine (if possible)
112         // the raw class for that type argument.
113         final Type[] actualTypeArguments;
114 
115         if (type instanceof Class)
116           {
117             actualTypeArguments = ((Class<?>)type).getTypeParameters();
118           }
119         else
120           {
121             actualTypeArguments = ((ParameterizedType)type).getActualTypeArguments();
122           }
123 
124         final var typeArgumentsAsClasses = new ArrayList<Class<?>>();
125 
126         // resolve types by chasing down type variables.
127         for (var baseType : actualTypeArguments)
128           {
129             while (resolvedTypes.containsKey(baseType))
130               {
131                 baseType = resolvedTypes.get(baseType);
132               }
133 
134             typeArgumentsAsClasses.add(getClass(baseType));
135           }
136 
137         return typeArgumentsAsClasses;
138       }
139 
140     /***********************************************************************************************************************************************************
141      * Instantiates an object of the given class performing dependency injections through the constructor.
142      *
143      * @param <T>     the generic type of the object to instantiate
144      * @param type    the dynamic type of the object to instantiate; it is expected to have a single constructor
145      * @param beans   the bag of objects to instantiate
146      * @return        the new instance
147      * @throws        RuntimeException if something fails
148      * @since         3.2-ALPHA-17
149      **********************************************************************************************************************************************************/
150     public static <T> T instantiateWithDependencies (@Nonnull final Class<? extends T> type,
151                                                      @Nonnull final Map<Class<?>, Object> beans)
152       {
153         try
154           {
155             log.debug("instantiateWithDependencies({}, {})", shortName(type), shortIds(beans.values()));
156             final var constructors = type.getConstructors();
157 
158             if (constructors.length > 1)
159               {
160                 throw new RuntimeException("Multiple constructors in " + type);
161               }
162 
163             final var parameters = Arrays.stream(constructors[0].getParameterTypes()).map(beans::get).collect(toList());
164 
165             log.trace(">>>> ctor arguments: {}", shortIds(parameters));
166             return type.cast(constructors[0].newInstance(parameters.toArray()));
167           }
168         catch (InstantiationException | IllegalAccessException | InvocationTargetException e)
169           {
170             throw new RuntimeException(e);
171           }
172       }
173 
174     /***********************************************************************************************************************************************************
175      * Performs dependency injection to an object by means of field introspection.
176      *
177      * @param object  the object
178      * @param beans   the bag of objects to instantiate
179      * @since         3.2-ALPHA-17
180      **********************************************************************************************************************************************************/
181     public static void injectDependencies (@Nonnull final Object object, @Nonnull final Map<Class<?>, Object> beans)
182       {
183         for (final var field : object.getClass().getDeclaredFields())
184           {
185             if (hasInjectAnnotation(field))
186               {
187                 field.setAccessible(true);
188                 final var type = field.getType();
189                 final var dependency = beans.get(type);
190 
191                 if (dependency == null)
192                   {
193                     throw new RuntimeException("Can't inject " + object + "." + field.getName());
194                   }
195 
196                 try
197                   {
198                     field.set(object, dependency);
199                   }
200                 catch (IllegalArgumentException | IllegalAccessException e)
201                   {
202                     throw new RuntimeException(e);
203                   }
204               }
205           }
206       }
207 
208     /***********************************************************************************************************************************************************
209      * Returns the class literal associated to the given type.
210      *
211      * @param   type    the type to inspect
212      * @return          the class literal; it might be {@code null} if fails
213      **********************************************************************************************************************************************************/
214     @CheckForNull
215     public static Class<?> getClass (@Nonnull final Type type)
216       {
217         requireNonNull(type, "type");
218 
219         if (type instanceof Class<?>)
220           {
221             return (Class<?>)type;
222           }
223         else if (type instanceof ParameterizedType)
224           {
225             return getClass(((ParameterizedType)type).getRawType());
226           }
227         else if (type instanceof GenericArrayType)
228           {
229             final var componentType = ((GenericArrayType)type).getGenericComponentType();
230             final var componentClass = getClass(componentType);
231 
232             if (componentClass == null)
233               {
234                 return null;
235               }
236 
237             return Array.newInstance(componentClass, 0).getClass();
238           }
239         else
240           {
241 //            throw new IllegalArgumentException(type.toString());
242             return null;
243           }
244       }
245 
246     /***********************************************************************************************************************************************************
247      *
248      **********************************************************************************************************************************************************/
249     private static boolean hasInjectAnnotation (@Nonnull final Field field)
250       {
251         final var classLoader = Thread.currentThread().getContextClassLoader();
252 
253         for (final var className : INJECT_CLASS_NAMES)
254           {
255             try
256               {
257                 @SuppressWarnings("unchecked")
258                 final var clazz = (Class<? extends Annotation>)classLoader.loadClass(className);
259 
260                 if (field.getAnnotation(clazz) != null)
261                   {
262                     return true;
263                   }
264               }
265             catch (ClassNotFoundException ignored)
266               {
267                 // try next
268               }
269           }
270 
271         return false;
272       }
273   }