1/* 2 * Copyright (C) 2012 The Guava Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.google.common.reflect; 18 19import static com.google.common.base.Preconditions.checkNotNull; 20 21import com.google.common.annotations.Beta; 22import com.google.common.annotations.VisibleForTesting; 23import com.google.common.base.CharMatcher; 24import com.google.common.base.Predicate; 25import com.google.common.base.Splitter; 26import com.google.common.collect.FluentIterable; 27import com.google.common.collect.ImmutableMap; 28import com.google.common.collect.ImmutableSet; 29import com.google.common.collect.ImmutableSortedSet; 30import com.google.common.collect.Maps; 31import com.google.common.collect.Ordering; 32import com.google.common.collect.Sets; 33 34import java.io.File; 35import java.io.IOException; 36import java.net.URI; 37import java.net.URISyntaxException; 38import java.net.URL; 39import java.net.URLClassLoader; 40import java.util.Enumeration; 41import java.util.LinkedHashMap; 42import java.util.Map; 43import java.util.Set; 44import java.util.jar.Attributes; 45import java.util.jar.JarEntry; 46import java.util.jar.JarFile; 47import java.util.jar.Manifest; 48import java.util.logging.Logger; 49 50import javax.annotation.Nullable; 51 52/** 53 * Scans the source of a {@link ClassLoader} and finds all loadable classes and resources. 54 * 55 * @author Ben Yu 56 * @since 14.0 57 */ 58@Beta 59public 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 /** Separator for the Class-Path manifest attribute value in jar files. */ 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 * Returns a {@code ClassPath} representing all classes and resources loadable from {@code 82 * classloader} and its parent class loaders. 83 * 84 * <p>Currently only {@link URLClassLoader} and only {@code file://} urls are supported. 85 * 86 * @throws IOException if the attempt to read class path resources (jar files or directories) 87 * failed. 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 * Returns all resources loadable from the current class path, including the class files of all 99 * loadable classes but excluding the "META-INF/MANIFEST.MF" file. 100 */ 101 public ImmutableSet<ResourceInfo> getResources() { 102 return resources; 103 } 104 105 /** 106 * Returns all classes loadable from the current class path. 107 * 108 * @since 16.0 109 */ 110 public ImmutableSet<ClassInfo> getAllClasses() { 111 return FluentIterable.from(resources).filter(ClassInfo.class).toSet(); 112 } 113 114 /** Returns all top level classes loadable from the current class path. */ 115 public ImmutableSet<ClassInfo> getTopLevelClasses() { 116 return FluentIterable.from(resources).filter(ClassInfo.class).filter(IS_TOP_LEVEL).toSet(); 117 } 118 119 /** Returns all top level classes whose package name is {@code packageName}. */ 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 * Returns all top level classes whose package name is {@code packageName} or starts with 133 * {@code packageName} followed by a '.'. 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 * Represents a class path resource that can be either a class file or any other resource file 149 * loadable from the class path. 150 * 151 * @since 14.0 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 /** Returns the url identifying the resource. */ 172 public final URL url() { 173 return checkNotNull(loader.getResource(resourceName), 174 "Failed to load resource: %s", resourceName); 175 } 176 177 /** Returns the fully qualified name of the resource. Such as "com/mycomp/foo/bar.txt". */ 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 // Do not change this arbitrarily. We rely on it for sorting ResourceInfo. 196 @Override public String toString() { 197 return resourceName; 198 } 199 } 200 201 /** 202 * Represents a class that can be loaded through {@link #load}. 203 * 204 * @since 14.0 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 * Returns the package name of the class, without attempting to load the class. 217 * 218 * <p>Behaves identically to {@link Package#getName()} but does not require the class (or 219 * package) to be loaded. 220 */ 221 public String getPackageName() { 222 return Reflection.getPackageName(className); 223 } 224 225 /** 226 * Returns the simple name of the underlying class as given in the source code. 227 * 228 * <p>Behaves identically to {@link Class#getSimpleName()} but does not require the class to be 229 * loaded. 230 */ 231 public String getSimpleName() { 232 int lastDollarSign = className.lastIndexOf('$'); 233 if (lastDollarSign != -1) { 234 String innerClassName = className.substring(lastDollarSign + 1); 235 // local and anonymous classes are prefixed with number (1,2,3...), anonymous classes are 236 // entirely numeric whereas local classes have the user supplied name as a suffix 237 return CharMatcher.DIGIT.trimLeadingFrom(innerClassName); 238 } 239 String packageName = getPackageName(); 240 if (packageName.isEmpty()) { 241 return className; 242 } 243 244 // Since this is a top level class, its simple name is always the part after package name. 245 return className.substring(packageName.length() + 1); 246 } 247 248 /** 249 * Returns the fully qualified name of the class. 250 * 251 * <p>Behaves identically to {@link Class#getName()} but does not require the class to be 252 * loaded. 253 */ 254 public String getName() { 255 return className; 256 } 257 258 /** 259 * Loads (but doesn't link or initialize) the class. 260 * 261 * @throws LinkageError when there were errors in loading classes that this class depends on. 262 * For example, {@link NoClassDefFoundError}. 263 */ 264 public Class<?> load() { 265 try { 266 return loader.loadClass(className); 267 } catch (ClassNotFoundException e) { 268 // Shouldn't happen, since the class name is read from the class path. 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 // Search parent first, since it's the order ClassLoader#loadClass() uses. 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 // A cycle in the filesystem, for example due to a symbolic link. 341 return; 342 } 343 File[] files = directory.listFiles(); 344 if (files == null) { 345 logger.warning("Cannot read directory " + directory); 346 // IO error, just skip the directory 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 // Not a jar file 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 * Returns the class path URIs specified by the {@code Class-Path} manifest attribute, according 395 * to <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html#Main%20Attributes"> 396 * JAR File Specification</a>. If {@code manifest} is null, it means the jar file has no 397 * manifest, and an empty set will be returned. 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 // Ignore bad entry 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 * Returns the absolute uri of the Class-Path entry value as specified in 425 * <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html#Main%20Attributes"> 426 * JAR File Specification</a>. Even though the specification only talks about relative urls, 427 * absolute urls are actually supported too (for example, in Maven surefire plugin). 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} 445