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