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 jakarta.annotation.Nonnull;
37 import jakarta.annotation.Nullable;
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 @Nullable
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 }