• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.util.inject;
2 
3 import static java.util.Collections.reverseOrder;
4 import static java.util.Comparator.comparing;
5 
6 import java.io.BufferedReader;
7 import java.io.IOException;
8 import java.io.InputStreamReader;
9 import java.net.URL;
10 import java.nio.charset.StandardCharsets;
11 import java.util.ArrayList;
12 import java.util.Comparator;
13 import java.util.Enumeration;
14 import java.util.HashSet;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.ServiceConfigurationError;
18 import java.util.Set;
19 import java.util.function.Predicate;
20 import java.util.stream.Collectors;
21 import javax.annotation.Nonnull;
22 import javax.annotation.Nullable;
23 import javax.annotation.Priority;
24 import org.robolectric.util.PerfStatsCollector;
25 
26 @SuppressWarnings({"NewApi", "AndroidJdkLibsChecker"})
27 class PluginFinder {
28 
29   private final ServiceFinderAdapter serviceFinderAdapter;
30 
PluginFinder()31   public PluginFinder() {
32     this(new ServiceFinderAdapter(null));
33   }
34 
35   /**
36    * @param classLoader the classloader to be used to load provider-configuration files and provider
37    *     classes, or null if the system classloader (or, failing that, the bootstrap classloader) is
38    *     to be used
39    */
PluginFinder(ClassLoader classLoader)40   public PluginFinder(ClassLoader classLoader) {
41     this(new ServiceFinderAdapter(classLoader));
42   }
43 
PluginFinder(ServiceFinderAdapter serviceFinderAdapter)44   PluginFinder(ServiceFinderAdapter serviceFinderAdapter) {
45     this.serviceFinderAdapter = serviceFinderAdapter;
46   }
47 
48   /**
49    * Returns an implementation class for the specified plugin.
50    *
51    * <p>If there is more than such one candidate, the classes will be sorted by {@link Priority} and
52    * the one with the highest priority will be returned. If multiple classes claim the same
53    * priority, a {@link ServiceConfigurationError} will be thrown. Classes without a Priority are
54    * treated as {@code @Priority(0)}.
55    *
56    * @param pluginType the class of the plugin type
57    * @param <T> the class of the plugin type
58    * @return the implementing class with the highest priority
59    */
60   @Nullable
findPlugin(Class<T> pluginType)61   <T> Class<? extends T> findPlugin(Class<T> pluginType) {
62     return best(pluginType, findPlugins(pluginType));
63   }
64 
65   /**
66    * Returns a list of implementation classes for the specified plugin, ordered from highest to
67    * lowest priority. If no implementing classes can be found, an empty list is returned.
68    *
69    * @param pluginType the class of the plugin type
70    * @param <T> the class of the plugin type
71    * @return a prioritized list of implementation classes
72    */
73   @Nonnull
findPlugins(Class<T> pluginType)74   <T> List<Class<? extends T>> findPlugins(Class<T> pluginType) {
75     return prioritize(filter(serviceFinderAdapter.load(pluginType)));
76   }
77 
filter(Iterable<Class<? extends T>> classes)78   private <T> Iterable<Class<? extends T>> filter(Iterable<Class<? extends T>> classes) {
79     Set<Class<?>> superceded = new HashSet<>();
80     for (Class<? extends T> clazz : classes) {
81       Supercedes supercedes = clazz.getAnnotation(Supercedes.class);
82       if (supercedes != null) {
83         superceded.add(supercedes.value());
84       }
85     }
86     if (superceded.isEmpty()) {
87       return classes;
88     } else {
89       return () -> new Filterator<>(classes.iterator(), o -> !superceded.contains(o));
90     }
91   }
92 
93   @Nullable
best(Class<T> pluginType, List<Class<? extends T>> serviceClasses)94   private <T> Class<? extends T> best(Class<T> pluginType,
95       List<Class<? extends T>> serviceClasses) {
96     if (serviceClasses.isEmpty()) {
97       return null;
98     }
99 
100     Class<? extends T> first = serviceClasses.get(0);
101     if (serviceClasses.size() == 1) {
102       return first;
103     }
104 
105     int topPriority = priority(first);
106     serviceClasses = serviceClasses.stream()
107         .filter(it -> priority(it) == topPriority)
108         .collect(Collectors.toList());
109 
110     if (serviceClasses.size() == 1) {
111       return serviceClasses.get(0);
112     } else {
113       throw new InjectionException(pluginType, "too many implementations: " + serviceClasses);
114     }
115   }
116 
117   static class ServiceFinderAdapter {
118 
119     private final ClassLoader classLoader;
120 
ServiceFinderAdapter(ClassLoader classLoader)121     ServiceFinderAdapter(ClassLoader classLoader) {
122       this.classLoader = classLoader;
123     }
124 
125     @Nonnull
load(Class<T> pluginType)126     <T> Iterable<Class<? extends T>> load(Class<T> pluginType) {
127       return PerfStatsCollector.getInstance()
128           .measure(
129               "loadPlugins",
130               () -> {
131                 ClassLoader serviceClassLoader = classLoader;
132                 if (serviceClassLoader == null) {
133                   serviceClassLoader = Thread.currentThread().getContextClassLoader();
134                 }
135                 HashSet<Class<? extends T>> result = new HashSet<>();
136 
137                 try {
138                   Enumeration<URL> urls =
139                       serviceClassLoader.getResources("META-INF/services/" + pluginType.getName());
140                   while (urls.hasMoreElements()) {
141                     URL url = urls.nextElement();
142                     BufferedReader reader =
143                         new BufferedReader(
144                             new InputStreamReader(url.openStream(), StandardCharsets.UTF_8));
145                     while (reader.ready()) {
146                       String s = reader.readLine();
147                       result.add(
148                           Class.forName(s, false, serviceClassLoader).asSubclass(pluginType));
149                     }
150                     reader.close();
151                   }
152                   return result;
153                 } catch (IOException | ClassNotFoundException e) {
154                   throw new AssertionError(e);
155                 }
156               });
157     }
158   }
159 
160   @Nonnull
prioritize(Iterable<Class<? extends T>> iterable)161   private <T> List<Class<? extends T>> prioritize(Iterable<Class<? extends T>> iterable) {
162     List<Class<? extends T>> serviceClasses = new ArrayList<>();
163 
164     for (Class<? extends T> serviceClass : iterable) {
165       serviceClasses.add(serviceClass);
166     }
167 
168     Comparator<Class<? extends T>> c = reverseOrder(comparing(PluginFinder::priority));
169     c = c.thenComparing(Class::getName);
170     serviceClasses.sort(c);
171 
172     return serviceClasses;
173   }
174 
priority(Class<? extends T> pluginClass)175   private static <T> int priority(Class<? extends T> pluginClass) {
176     Priority priority = pluginClass.getAnnotation(Priority.class);
177     return priority == null ? 0 : priority.value();
178   }
179 
180   private static class Filterator<T> implements Iterator<T> {
181 
182     private final Iterator<T> delegate;
183     private final Predicate<T> predicate;
184     private T next;
185 
Filterator(Iterator<T> delegate, Predicate<T> predicate)186     public Filterator(Iterator<T> delegate, Predicate<T> predicate) {
187       this.delegate = delegate;
188       this.predicate = predicate;
189       findNext();
190     }
191 
findNext()192     void findNext() {
193       while (delegate.hasNext()) {
194         next = delegate.next();
195         if (predicate.test(next)) {
196           return;
197         }
198       }
199       next = null;
200     }
201 
202     @Override
hasNext()203     public boolean hasNext() {
204       return next != null;
205     }
206 
207     @Override
next()208     public T next() {
209       try {
210         return next;
211       } finally {
212         findNext();
213       }
214     }
215   }
216 }
217