View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.di.impl;
20  
21  import java.lang.annotation.Annotation;
22  import java.lang.reflect.AnnotatedElement;
23  import java.lang.reflect.Constructor;
24  import java.lang.reflect.Executable;
25  import java.lang.reflect.Field;
26  import java.lang.reflect.InvocationTargetException;
27  import java.lang.reflect.Member;
28  import java.lang.reflect.Method;
29  import java.lang.reflect.Modifier;
30  import java.lang.reflect.Parameter;
31  import java.lang.reflect.Type;
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.Collections;
35  import java.util.HashSet;
36  import java.util.List;
37  import java.util.function.Consumer;
38  import java.util.function.Function;
39  import java.util.function.Supplier;
40  import java.util.regex.Pattern;
41  import java.util.stream.Stream;
42  
43  import org.apache.maven.api.annotations.Nullable;
44  import org.apache.maven.api.di.Inject;
45  import org.apache.maven.api.di.Named;
46  import org.apache.maven.api.di.Priority;
47  import org.apache.maven.api.di.Qualifier;
48  import org.apache.maven.di.Key;
49  
50  import static java.util.stream.Collectors.toList;
51  
52  public final class ReflectionUtils {
53      private static final String IDENT = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*";
54  
55      private static final Pattern PACKAGE = Pattern.compile("(?:" + IDENT + "\\.)*");
56      private static final Pattern PACKAGE_AND_PARENT = Pattern.compile(PACKAGE.pattern() + "(?:" + IDENT + "\\$\\d*)?");
57      private static final Pattern ARRAY_SIGNATURE = Pattern.compile("\\[L(.*?);");
58  
59      public static @Nullable Object getOuterClassInstance(Object innerClassInstance) {
60          if (innerClassInstance == null) {
61              return null;
62          }
63          Class<?> cls = innerClassInstance.getClass();
64          Class<?> enclosingClass = cls.getEnclosingClass();
65          if (enclosingClass == null) {
66              return null;
67          }
68          for (Field field : cls.getDeclaredFields()) {
69              if (!field.isSynthetic() || !field.getName().startsWith("this$") || field.getType() != enclosingClass) {
70                  continue;
71              }
72              field.setAccessible(true);
73              try {
74                  return field.get(innerClassInstance);
75              } catch (IllegalAccessException e) {
76                  throw new RuntimeException(e);
77              }
78          }
79          return null;
80      }
81  
82      public static @Nullable Object qualifierOf(AnnotatedElement annotatedElement) {
83          Object qualifier = null;
84          for (Annotation annotation : annotatedElement.getDeclaredAnnotations()) {
85              if (annotation.annotationType().isAnnotationPresent(Qualifier.class)) {
86                  if (qualifier != null) {
87                      throw new DIException("More than one qualifier annotation on " + annotatedElement);
88                  }
89                  if (annotation instanceof Named named) {
90                      qualifier = named.value();
91                  } else {
92                      Class<? extends Annotation> annotationType = annotation.annotationType();
93                      qualifier = annotationType.getDeclaredMethods().length == 0 ? annotationType : annotation;
94                  }
95              }
96          }
97          return qualifier;
98      }
99  
100     public static @Nullable Annotation scopeOf(AnnotatedElement annotatedElement) {
101         Annotation scope = null;
102         for (Annotation annotation : annotatedElement.getDeclaredAnnotations()) {
103             if (annotation.annotationType().isAnnotationPresent(org.apache.maven.api.di.Scope.class)) {
104                 if (scope != null) {
105                     throw new DIException("More than one scope annotation on " + annotatedElement);
106                 }
107                 scope = annotation;
108             }
109         }
110         return scope;
111     }
112 
113     public static <T> Key<T> keyOf(@Nullable Type container, Type type, AnnotatedElement annotatedElement) {
114         return Key.ofType(
115                 container != null ? Types.bind(type, Types.getAllTypeBindings(container)) : type,
116                 qualifierOf(annotatedElement));
117     }
118 
119     public static <T extends AnnotatedElement & Member> List<T> getAnnotatedElements(
120             Class<?> cls,
121             Class<? extends Annotation> annotationType,
122             Function<Class<?>, T[]> extractor,
123             boolean allowStatic) {
124         List<T> result = new ArrayList<>();
125         while (cls != null) {
126             for (T element : extractor.apply(cls)) {
127                 if (element.isAnnotationPresent(annotationType)) {
128                     if (!allowStatic && Modifier.isStatic(element.getModifiers())) {
129                         throw new DIException(
130                                 "@" + annotationType.getSimpleName() + " annotation is not allowed on " + element);
131                     }
132                     result.add(element);
133                 }
134             }
135             cls = cls.getSuperclass();
136         }
137         return result;
138     }
139 
140     public static <T> @Nullable Binding<T> generateImplicitBinding(Key<T> key) {
141         Binding<T> binding = generateConstructorBinding(key);
142         if (binding != null) {
143             Annotation scope = scopeOf(key.getRawType());
144             if (scope != null) {
145                 binding = binding.scope(scope);
146             }
147             binding = binding.initializeWith(generateInjectingInitializer(key));
148         }
149         return binding;
150     }
151 
152     @SuppressWarnings("unchecked")
153     public static <T> @Nullable Binding<T> generateConstructorBinding(Key<T> key) {
154         Class<?> cls = key.getRawType();
155 
156         List<Constructor<?>> constructors = Arrays.asList(cls.getDeclaredConstructors());
157         List<Constructor<?>> injectConstructors = constructors.stream()
158                 .filter(c -> c.isAnnotationPresent(Inject.class))
159                 .toList();
160 
161         List<Method> factoryMethods = Arrays.stream(cls.getDeclaredMethods())
162                 .filter(method -> method.getReturnType() == cls && Modifier.isStatic(method.getModifiers()))
163                 .toList();
164         List<Method> injectFactoryMethods = factoryMethods.stream()
165                 .filter(method -> method.isAnnotationPresent(Inject.class))
166                 .toList();
167 
168         if (!injectConstructors.isEmpty()) {
169             if (injectConstructors.size() > 1) {
170                 throw failedImplicitBinding(key, "more than one inject constructor");
171             }
172             if (!injectFactoryMethods.isEmpty()) {
173                 throw failedImplicitBinding(key, "both inject constructor and inject factory method are present");
174             }
175             return bindingFromConstructor(
176                     key, (Constructor<T>) injectConstructors.iterator().next());
177         }
178 
179         if (!injectFactoryMethods.isEmpty()) {
180             if (injectFactoryMethods.size() > 1) {
181                 throw failedImplicitBinding(key, "more than one inject factory method");
182             }
183             return bindingFromMethod(injectFactoryMethods.iterator().next());
184         }
185 
186         if (constructors.isEmpty()) {
187             throw failedImplicitBinding(key, "inject annotation on interface");
188         }
189         if (constructors.size() > 1) {
190             throw failedImplicitBinding(key, "inject annotation on class with multiple constructors");
191         }
192         Constructor<T> declaredConstructor =
193                 (Constructor<T>) constructors.iterator().next();
194 
195         Class<?> enclosingClass = cls.getEnclosingClass();
196         if (enclosingClass != null
197                 && !Modifier.isStatic(cls.getModifiers())
198                 && declaredConstructor.getParameterCount() != 1) {
199             throw failedImplicitBinding(
200                     key,
201                     "inject annotation on local class that closes over outside variables and/or has no default constructor");
202         }
203         return bindingFromConstructor(key, declaredConstructor);
204     }
205 
206     private static DIException failedImplicitBinding(Key<?> requestedKey, String message) {
207         return new DIException(
208                 "Failed to generate implicit binding for " + requestedKey.getDisplayString() + ", " + message);
209     }
210 
211     public static <T> BindingInitializer<T> generateInjectingInitializer(Key<T> container) {
212         Class<T> rawType = container.getRawType();
213         List<BindingInitializer<T>> initializers = Stream.concat(
214                         getAnnotatedElements(rawType, Inject.class, Class::getDeclaredFields, false).stream()
215                                 .map(field -> fieldInjector(container, field)),
216                         getAnnotatedElements(rawType, Inject.class, Class::getDeclaredMethods, true).stream()
217                                 .filter(method -> !Modifier.isStatic(
218                                         method.getModifiers())) // we allow them and just filter out to allow
219                                 // static factory methods
220                                 .map(method -> methodInjector(container, method)))
221                 .collect(toList());
222         return BindingInitializer.combine(initializers);
223     }
224 
225     public static <T> BindingInitializer<T> fieldInjector(Key<T> container, Field field) {
226         field.setAccessible(true);
227         Key<Object> key = keyOf(container.getType(), field.getGenericType(), field);
228         boolean optional = field.isAnnotationPresent(Nullable.class);
229         Dependency<Object> dep = new Dependency<>(key, optional);
230         return new BindingInitializer<T>(Collections.singleton(dep)) {
231             @Override
232             public Consumer<T> compile(Function<Dependency<?>, Supplier<?>> compiler) {
233                 Supplier<?> binding = compiler.apply(dep);
234                 return (T instance) -> {
235                     Object arg = binding.get();
236                     try {
237                         field.set(instance, arg);
238                     } catch (IllegalAccessException e) {
239                         throw new DIException("Not allowed to set injectable field " + field, e);
240                     }
241                 };
242             }
243         };
244     }
245 
246     public static <T> BindingInitializer<T> methodInjector(Key<T> container, Method method) {
247         method.setAccessible(true);
248         Dependency<?>[] dependencies = toDependencies(container.getType(), method);
249         return new BindingInitializer<T>(new HashSet<>(Arrays.asList(dependencies))) {
250             @Override
251             public Consumer<T> compile(Function<Dependency<?>, Supplier<?>> compiler) {
252                 return instance -> {
253                     Object[] args = getDependencies().stream()
254                             .map(compiler)
255                             .map(Supplier::get)
256                             .toArray();
257                     try {
258                         method.invoke(instance, args);
259                     } catch (IllegalAccessException e) {
260                         throw new DIException("Not allowed to call injectable method " + method, e);
261                     } catch (InvocationTargetException e) {
262                         throw new DIException("Failed to call injectable method " + method, e.getCause());
263                     }
264                 };
265             }
266         };
267     }
268 
269     public static Dependency<?>[] toDependencies(@Nullable Type container, Executable executable) {
270         Dependency<?>[] keys = toArgDependencies(container, executable);
271         if (executable instanceof Constructor || Modifier.isStatic(executable.getModifiers())) {
272             return keys;
273         } else {
274             Dependency<?>[] nkeys = new Dependency[keys.length + 1];
275             nkeys[0] = new Dependency<>(Key.ofType(container), false);
276             System.arraycopy(keys, 0, nkeys, 1, keys.length);
277             return nkeys;
278         }
279     }
280 
281     private static Dependency<?>[] toArgDependencies(@Nullable Type container, Executable executable) {
282         Parameter[] parameters = executable.getParameters();
283         Dependency<?>[] dependencies = new Dependency<?>[parameters.length];
284         if (parameters.length == 0) {
285             return dependencies;
286         }
287 
288         Type[] genericParameterTypes = executable.getGenericParameterTypes();
289         for (int i = 0; i < dependencies.length; i++) {
290             Type type = genericParameterTypes[i];
291             Parameter parameter = parameters[i];
292             boolean optional = parameter.isAnnotationPresent(Nullable.class);
293             dependencies[i] = new Dependency<>(keyOf(container, type, parameter), optional);
294         }
295         return dependencies;
296     }
297 
298     @SuppressWarnings("unchecked")
299     public static <T> Binding<T> bindingFromMethod(Method method) {
300         method.setAccessible(true);
301         Binding<T> binding = Binding.to(
302                 Key.ofType(method.getGenericReturnType(), ReflectionUtils.qualifierOf(method)),
303                 args -> {
304                     try {
305                         Object instance;
306                         Object[] params;
307                         if (Modifier.isStatic(method.getModifiers())) {
308                             instance = null;
309                             params = args;
310                         } else {
311                             instance = args[0];
312                             params = Arrays.copyOfRange(args, 1, args.length);
313                         }
314                         T result = (T) method.invoke(instance, params);
315                         if (result == null) {
316                             throw new NullPointerException(
317                                     "@Provides method must return non-null result, method " + method);
318                         }
319                         return result;
320                     } catch (IllegalAccessException e) {
321                         throw new DIException("Not allowed to call method " + method, e);
322                     } catch (InvocationTargetException e) {
323                         throw new DIException("Failed to call method " + method, e.getCause());
324                     }
325                 },
326                 toDependencies(method.getDeclaringClass(), method));
327 
328         Priority priority = method.getAnnotation(Priority.class);
329         if (priority != null) {
330             binding = binding.prioritize(priority.value());
331         }
332 
333         return binding;
334     }
335 
336     public static <T> Binding<T> bindingFromConstructor(Key<T> key, Constructor<T> constructor) {
337         constructor.setAccessible(true);
338 
339         Dependency<?>[] dependencies = toDependencies(key.getType(), constructor);
340 
341         Binding<T> binding = Binding.to(
342                 key,
343                 args -> {
344                     try {
345                         return constructor.newInstance(args);
346                     } catch (InstantiationException e) {
347                         throw new DIException(
348                                 "Cannot instantiate object from the constructor " + constructor
349                                         + " to provide requested key " + key,
350                                 e);
351                     } catch (IllegalAccessException e) {
352                         throw new DIException(
353                                 "Not allowed to call constructor " + constructor + " to provide requested key " + key,
354                                 e);
355                     } catch (InvocationTargetException e) {
356                         throw new DIException(
357                                 "Failed to call constructor " + constructor + " to provide requested key " + key,
358                                 e.getCause());
359                     }
360                 },
361                 dependencies);
362 
363         Priority priority = constructor.getDeclaringClass().getAnnotation(Priority.class);
364         if (priority != null) {
365             binding = binding.prioritize(priority.value());
366         }
367 
368         return binding.withKey(key);
369     }
370 
371     public static void getDisplayString(StringBuilder sb, Object object) {
372         if (object instanceof Class<?> clazz && clazz.isAnnotation()) {
373             //noinspection unchecked
374             getDisplayString(sb, (Class<? extends Annotation>) object, null);
375         } else if (object instanceof Annotation annotation) {
376             getDisplayString(sb, annotation.annotationType(), annotation);
377         } else {
378             sb.append(object.toString());
379         }
380     }
381 
382     public static String getDisplayName(Type type) {
383         Class<?> raw = Types.getRawType(type);
384         String typeName;
385         if (raw.isAnonymousClass()) {
386             Type superclass = raw.getGenericSuperclass();
387             typeName = "? extends " + superclass.getTypeName();
388         } else {
389             typeName = type.getTypeName();
390         }
391 
392         return PACKAGE_AND_PARENT
393                 .matcher(ARRAY_SIGNATURE.matcher(typeName).replaceAll("$1[]"))
394                 .replaceAll("");
395     }
396 
397     private static void getDisplayString(
398             StringBuilder sb, Class<? extends Annotation> annotationType, @Nullable Annotation annotation) {
399         if (annotation == null) {
400             sb.append("@").append(ReflectionUtils.getDisplayName(annotationType));
401         } else {
402             String typeName = annotationType.getName();
403             String str = annotation.toString();
404             if (str.startsWith("@" + typeName)) {
405                 sb.append("@").append(getDisplayName(annotationType)).append(str.substring(typeName.length() + 1));
406             } else {
407                 sb.append(str);
408             }
409         }
410     }
411 }