001/* 002 * Copyright (C) 2012 The Guava Authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.google.common.reflect; 018 019import static com.google.common.base.Preconditions.checkNotNull; 020 021import com.google.common.annotations.Beta; 022import com.google.common.annotations.VisibleForTesting; 023import com.google.common.base.CharMatcher; 024import com.google.common.base.Predicate; 025import com.google.common.base.Splitter; 026import com.google.common.collect.FluentIterable; 027import com.google.common.collect.ImmutableMap; 028import com.google.common.collect.ImmutableSet; 029import com.google.common.collect.ImmutableSortedSet; 030import com.google.common.collect.Maps; 031import com.google.common.collect.Ordering; 032import com.google.common.collect.Sets; 033 034import java.io.File; 035import java.io.IOException; 036import java.net.URI; 037import java.net.URISyntaxException; 038import java.net.URL; 039import java.net.URLClassLoader; 040import java.util.Enumeration; 041import java.util.LinkedHashMap; 042import java.util.Map; 043import java.util.Set; 044import java.util.jar.Attributes; 045import java.util.jar.JarEntry; 046import java.util.jar.JarFile; 047import java.util.jar.Manifest; 048import java.util.logging.Logger; 049 050import javax.annotation.Nullable; 051 052/** 053 * Scans the source of a {@link ClassLoader} and finds all loadable classes and resources. 054 * 055 * @author Ben Yu 056 * @since 14.0 057 */ 058@Beta 059public final class ClassPath { 060 private static final Logger logger = Logger.getLogger(ClassPath.class.getName()); 061 062 private static final Predicate<ClassInfo> IS_TOP_LEVEL = new Predicate<ClassInfo>() { 063 @Override public boolean apply(ClassInfo info) { 064 return info.className.indexOf('$') == -1; 065 } 066 }; 067 068 /** Separator for the Class-Path manifest attribute value in jar files. */ 069 private static final Splitter CLASS_PATH_ATTRIBUTE_SEPARATOR = 070 Splitter.on(" ").omitEmptyStrings(); 071 072 private static final String CLASS_FILE_NAME_EXTENSION = ".class"; 073 074 private final ImmutableSet<ResourceInfo> resources; 075 076 private ClassPath(ImmutableSet<ResourceInfo> resources) { 077 this.resources = resources; 078 } 079 080 /** 081 * Returns a {@code ClassPath} representing all classes and resources loadable from {@code 082 * classloader} and its parent class loaders. 083 * 084 * <p>Currently only {@link URLClassLoader} and only {@code file://} urls are supported. 085 * 086 * @throws IOException if the attempt to read class path resources (jar files or directories) 087 * failed. 088 */ 089 public static ClassPath from(ClassLoader classloader) throws IOException { 090 Scanner scanner = new Scanner(); 091 for (Map.Entry<URI, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) { 092 scanner.scan(entry.getKey(), entry.getValue()); 093 } 094 return new ClassPath(scanner.getResources()); 095 } 096 097 /** 098 * Returns all resources loadable from the current class path, including the class files of all 099 * 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}