1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.google.common.reflect;
18
19 import static com.google.common.base.Preconditions.checkNotNull;
20
21 import com.google.common.annotations.Beta;
22 import com.google.common.annotations.VisibleForTesting;
23 import com.google.common.base.CharMatcher;
24 import com.google.common.base.Predicate;
25 import com.google.common.base.Splitter;
26 import com.google.common.collect.FluentIterable;
27 import com.google.common.collect.ImmutableMap;
28 import com.google.common.collect.ImmutableSet;
29 import com.google.common.collect.ImmutableSortedSet;
30 import com.google.common.collect.Maps;
31 import com.google.common.collect.Ordering;
32 import com.google.common.collect.Sets;
33
34 import java.io.File;
35 import java.io.IOException;
36 import java.net.URI;
37 import java.net.URISyntaxException;
38 import java.net.URL;
39 import java.net.URLClassLoader;
40 import java.util.Enumeration;
41 import java.util.LinkedHashMap;
42 import java.util.Map;
43 import java.util.Set;
44 import java.util.jar.Attributes;
45 import java.util.jar.JarEntry;
46 import java.util.jar.JarFile;
47 import java.util.jar.Manifest;
48 import java.util.logging.Logger;
49
50 import javax.annotation.Nullable;
51
52
53
54
55
56
57
58 @Beta
59 public final class ClassPath {
60 private static final Logger logger = Logger.getLogger(ClassPath.class.getName());
61
62 private static final Predicate<ClassInfo> IS_TOP_LEVEL = new Predicate<ClassInfo>() {
63 @Override public boolean apply(ClassInfo info) {
64 return info.className.indexOf('$') == -1;
65 }
66 };
67
68
69 private static final Splitter CLASS_PATH_ATTRIBUTE_SEPARATOR =
70 Splitter.on(" ").omitEmptyStrings();
71
72 private static final String CLASS_FILE_NAME_EXTENSION = ".class";
73
74 private final ImmutableSet<ResourceInfo> resources;
75
76 private ClassPath(ImmutableSet<ResourceInfo> resources) {
77 this.resources = resources;
78 }
79
80
81
82
83
84
85
86
87
88
89 public static ClassPath from(ClassLoader classloader) throws IOException {
90 Scanner scanner = new Scanner();
91 for (Map.Entry<URI, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) {
92 scanner.scan(entry.getKey(), entry.getValue());
93 }
94 return new ClassPath(scanner.getResources());
95 }
96
97
98
99
100
101 public ImmutableSet<ResourceInfo> getResources() {
102 return resources;
103 }
104
105
106
107
108
109
110 public ImmutableSet<ClassInfo> getAllClasses() {
111 return FluentIterable.from(resources).filter(ClassInfo.class).toSet();
112 }
113
114
115 public ImmutableSet<ClassInfo> getTopLevelClasses() {
116 return FluentIterable.from(resources).filter(ClassInfo.class).filter(IS_TOP_LEVEL).toSet();
117 }
118
119
120 public ImmutableSet<ClassInfo> getTopLevelClasses(String packageName) {
121 checkNotNull(packageName);
122 ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
123 for (ClassInfo classInfo : getTopLevelClasses()) {
124 if (classInfo.getPackageName().equals(packageName)) {
125 builder.add(classInfo);
126 }
127 }
128 return builder.build();
129 }
130
131
132
133
134
135 public ImmutableSet<ClassInfo> getTopLevelClassesRecursive(String packageName) {
136 checkNotNull(packageName);
137 String packagePrefix = packageName + '.';
138 ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
139 for (ClassInfo classInfo : getTopLevelClasses()) {
140 if (classInfo.getName().startsWith(packagePrefix)) {
141 builder.add(classInfo);
142 }
143 }
144 return builder.build();
145 }
146
147
148
149
150
151
152
153 @Beta
154 public static class ResourceInfo {
155 private final String resourceName;
156 final ClassLoader loader;
157
158 static ResourceInfo of(String resourceName, ClassLoader loader) {
159 if (resourceName.endsWith(CLASS_FILE_NAME_EXTENSION)) {
160 return new ClassInfo(resourceName, loader);
161 } else {
162 return new ResourceInfo(resourceName, loader);
163 }
164 }
165
166 ResourceInfo(String resourceName, ClassLoader loader) {
167 this.resourceName = checkNotNull(resourceName);
168 this.loader = checkNotNull(loader);
169 }
170
171
172 public final URL url() {
173 return checkNotNull(loader.getResource(resourceName),
174 "Failed to load resource: %s", resourceName);
175 }
176
177
178 public final String getResourceName() {
179 return resourceName;
180 }
181
182 @Override public int hashCode() {
183 return resourceName.hashCode();
184 }
185
186 @Override public boolean equals(Object obj) {
187 if (obj instanceof ResourceInfo) {
188 ResourceInfo that = (ResourceInfo) obj;
189 return resourceName.equals(that.resourceName)
190 && loader == that.loader;
191 }
192 return false;
193 }
194
195
196 @Override public String toString() {
197 return resourceName;
198 }
199 }
200
201
202
203
204
205
206 @Beta
207 public static final class ClassInfo extends ResourceInfo {
208 private final String className;
209
210 ClassInfo(String resourceName, ClassLoader loader) {
211 super(resourceName, loader);
212 this.className = getClassName(resourceName);
213 }
214
215
216
217
218
219
220
221 public String getPackageName() {
222 return Reflection.getPackageName(className);
223 }
224
225
226
227
228
229
230
231 public String getSimpleName() {
232 int lastDollarSign = className.lastIndexOf('$');
233 if (lastDollarSign != -1) {
234 String innerClassName = className.substring(lastDollarSign + 1);
235
236
237 return CharMatcher.DIGIT.trimLeadingFrom(innerClassName);
238 }
239 String packageName = getPackageName();
240 if (packageName.isEmpty()) {
241 return className;
242 }
243
244
245 return className.substring(packageName.length() + 1);
246 }
247
248
249
250
251
252
253
254 public String getName() {
255 return className;
256 }
257
258
259
260
261
262
263
264 public Class<?> load() {
265 try {
266 return loader.loadClass(className);
267 } catch (ClassNotFoundException e) {
268
269 throw new IllegalStateException(e);
270 }
271 }
272
273 @Override public String toString() {
274 return className;
275 }
276 }
277
278 @VisibleForTesting static ImmutableMap<URI, ClassLoader> getClassPathEntries(
279 ClassLoader classloader) {
280 LinkedHashMap<URI, ClassLoader> entries = Maps.newLinkedHashMap();
281
282 ClassLoader parent = classloader.getParent();
283 if (parent != null) {
284 entries.putAll(getClassPathEntries(parent));
285 }
286 if (classloader instanceof URLClassLoader) {
287 URLClassLoader urlClassLoader = (URLClassLoader) classloader;
288 for (URL entry : urlClassLoader.getURLs()) {
289 URI uri;
290 try {
291 uri = entry.toURI();
292 } catch (URISyntaxException e) {
293 throw new IllegalArgumentException(e);
294 }
295 if (!entries.containsKey(uri)) {
296 entries.put(uri, classloader);
297 }
298 }
299 }
300 return ImmutableMap.copyOf(entries);
301 }
302
303 @VisibleForTesting static final class Scanner {
304
305 private final ImmutableSortedSet.Builder<ResourceInfo> resources =
306 new ImmutableSortedSet.Builder<ResourceInfo>(Ordering.usingToString());
307 private final Set<URI> scannedUris = Sets.newHashSet();
308
309 ImmutableSortedSet<ResourceInfo> getResources() {
310 return resources.build();
311 }
312
313 void scan(URI uri, ClassLoader classloader) throws IOException {
314 if (uri.getScheme().equals("file") && scannedUris.add(uri)) {
315 scanFrom(new File(uri), classloader);
316 }
317 }
318
319 @VisibleForTesting void scanFrom(File file, ClassLoader classloader)
320 throws IOException {
321 if (!file.exists()) {
322 return;
323 }
324 if (file.isDirectory()) {
325 scanDirectory(file, classloader);
326 } else {
327 scanJar(file, classloader);
328 }
329 }
330
331 private void scanDirectory(File directory, ClassLoader classloader) throws IOException {
332 scanDirectory(directory, classloader, "", ImmutableSet.<File>of());
333 }
334
335 private void scanDirectory(
336 File directory, ClassLoader classloader, String packagePrefix,
337 ImmutableSet<File> ancestors) throws IOException {
338 File canonical = directory.getCanonicalFile();
339 if (ancestors.contains(canonical)) {
340
341 return;
342 }
343 File[] files = directory.listFiles();
344 if (files == null) {
345 logger.warning("Cannot read directory " + directory);
346
347 return;
348 }
349 ImmutableSet<File> newAncestors = ImmutableSet.<File>builder()
350 .addAll(ancestors)
351 .add(canonical)
352 .build();
353 for (File f : files) {
354 String name = f.getName();
355 if (f.isDirectory()) {
356 scanDirectory(f, classloader, packagePrefix + name + "/", newAncestors);
357 } else {
358 String resourceName = packagePrefix + name;
359 if (!resourceName.equals(JarFile.MANIFEST_NAME)) {
360 resources.add(ResourceInfo.of(resourceName, classloader));
361 }
362 }
363 }
364 }
365
366 private void scanJar(File file, ClassLoader classloader) throws IOException {
367 JarFile jarFile;
368 try {
369 jarFile = new JarFile(file);
370 } catch (IOException e) {
371
372 return;
373 }
374 try {
375 for (URI uri : getClassPathFromManifest(file, jarFile.getManifest())) {
376 scan(uri, classloader);
377 }
378 Enumeration<JarEntry> entries = jarFile.entries();
379 while (entries.hasMoreElements()) {
380 JarEntry entry = entries.nextElement();
381 if (entry.isDirectory() || entry.getName().equals(JarFile.MANIFEST_NAME)) {
382 continue;
383 }
384 resources.add(ResourceInfo.of(entry.getName(), classloader));
385 }
386 } finally {
387 try {
388 jarFile.close();
389 } catch (IOException ignored) {}
390 }
391 }
392
393
394
395
396
397
398
399 @VisibleForTesting static ImmutableSet<URI> getClassPathFromManifest(
400 File jarFile, @Nullable Manifest manifest) {
401 if (manifest == null) {
402 return ImmutableSet.of();
403 }
404 ImmutableSet.Builder<URI> builder = ImmutableSet.builder();
405 String classpathAttribute = manifest.getMainAttributes()
406 .getValue(Attributes.Name.CLASS_PATH.toString());
407 if (classpathAttribute != null) {
408 for (String path : CLASS_PATH_ATTRIBUTE_SEPARATOR.split(classpathAttribute)) {
409 URI uri;
410 try {
411 uri = getClassPathEntry(jarFile, path);
412 } catch (URISyntaxException e) {
413
414 logger.warning("Invalid Class-Path entry: " + path);
415 continue;
416 }
417 builder.add(uri);
418 }
419 }
420 return builder.build();
421 }
422
423
424
425
426
427
428
429 @VisibleForTesting static URI getClassPathEntry(File jarFile, String path)
430 throws URISyntaxException {
431 URI uri = new URI(path);
432 if (uri.isAbsolute()) {
433 return uri;
434 } else {
435 return new File(jarFile.getParentFile(), path.replace('/', File.separatorChar)).toURI();
436 }
437 }
438 }
439
440 @VisibleForTesting static String getClassName(String filename) {
441 int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length();
442 return filename.substring(0, classNameEnd).replace('/', '.');
443 }
444 }