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.Splitter; 024import com.google.common.collect.FluentIterable; 025import com.google.common.collect.ImmutableMap; 026import com.google.common.collect.ImmutableSet; 027import com.google.common.collect.ImmutableSortedSet; 028import com.google.common.collect.Maps; 029import com.google.common.collect.Ordering; 030import com.google.common.collect.Sets; 031 032import java.io.File; 033import java.io.IOException; 034import java.net.URI; 035import java.net.URISyntaxException; 036import java.net.URL; 037import java.net.URLClassLoader; 038import java.util.Enumeration; 039import java.util.LinkedHashMap; 040import java.util.Map; 041import java.util.Set; 042import java.util.jar.Attributes; 043import java.util.jar.JarEntry; 044import java.util.jar.JarFile; 045import java.util.jar.Manifest; 046import java.util.logging.Logger; 047 048import javax.annotation.Nullable; 049 050/** 051 * Scans the source of a {@link ClassLoader} and finds all loadable classes and resources. 052 * 053 * @author Ben Yu 054 * @since 14.0 055 */ 056@Beta 057public final class ClassPath { 058 059 private static final Logger logger = Logger.getLogger(ClassPath.class.getName()); 060 061 /** Separator for the Class-Path manifest attribute value in jar files. */ 062 private static final Splitter CLASS_PATH_ATTRIBUTE_SEPARATOR = 063 Splitter.on(" ").omitEmptyStrings(); 064 065 private static final String CLASS_FILE_NAME_EXTENSION = ".class"; 066 067 private final ImmutableSet<ResourceInfo> resources; 068 069 private ClassPath(ImmutableSet<ResourceInfo> resources) { 070 this.resources = resources; 071 } 072 073 /** 074 * Returns a {@code ClassPath} representing all classes and resources loadable from {@code 075 * classloader} and its parent class loaders. 076 * 077 * <p>Currently only {@link URLClassLoader} and only {@code file://} urls are supported. 078 * 079 * @throws IOException if the attempt to read class path resources (jar files or directories) 080 * failed. 081 */ 082 public static ClassPath from(ClassLoader classloader) throws IOException { 083 Scanner scanner = new Scanner(); 084 for (Map.Entry<URI, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) { 085 scanner.scan(entry.getKey(), entry.getValue()); 086 } 087 return new ClassPath(scanner.getResources()); 088 } 089 090 /** 091 * Returns all resources loadable from the current class path, including the class files of all 092 * loadable classes but excluding the "META-INF/MANIFEST.MF" file. 093 */ 094 public ImmutableSet<ResourceInfo> getResources() { 095 return resources; 096 } 097 098 /** Returns all top level classes loadable from the current class path. */ 099 public ImmutableSet<ClassInfo> getTopLevelClasses() { 100 return FluentIterable.from(resources).filter(ClassInfo.class).toSet(); 101 } 102 103 /** Returns all top level classes whose package name is {@code packageName}. */ 104 public ImmutableSet<ClassInfo> getTopLevelClasses(String packageName) { 105 checkNotNull(packageName); 106 ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder(); 107 for (ClassInfo classInfo : getTopLevelClasses()) { 108 if (classInfo.getPackageName().equals(packageName)) { 109 builder.add(classInfo); 110 } 111 } 112 return builder.build(); 113 } 114 115 /** 116 * Returns all top level classes whose package name is {@code packageName} or starts with 117 * {@code packageName} followed by a '.'. 118 */ 119 public ImmutableSet<ClassInfo> getTopLevelClassesRecursive(String packageName) { 120 checkNotNull(packageName); 121 String packagePrefix = packageName + '.'; 122 ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder(); 123 for (ClassInfo classInfo : getTopLevelClasses()) { 124 if (classInfo.getName().startsWith(packagePrefix)) { 125 builder.add(classInfo); 126 } 127 } 128 return builder.build(); 129 } 130 131 /** 132 * Represents a class path resource that can be either a class file or any other resource file 133 * loadable from the class path. 134 * 135 * @since 14.0 136 */ 137 @Beta 138 public static class ResourceInfo { 139 private final String resourceName; 140 final ClassLoader loader; 141 142 static ResourceInfo of(String resourceName, ClassLoader loader) { 143 if (resourceName.endsWith(CLASS_FILE_NAME_EXTENSION) && !resourceName.contains("$")) { 144 return new ClassInfo(resourceName, loader); 145 } else { 146 return new ResourceInfo(resourceName, loader); 147 } 148 } 149 150 ResourceInfo(String resourceName, ClassLoader loader) { 151 this.resourceName = checkNotNull(resourceName); 152 this.loader = checkNotNull(loader); 153 } 154 155 /** Returns the url identifying the resource. */ 156 public final URL url() { 157 return checkNotNull(loader.getResource(resourceName), 158 "Failed to load resource: %s", resourceName); 159 } 160 161 /** Returns the fully qualified name of the resource. Such as "com/mycomp/foo/bar.txt". */ 162 public final String getResourceName() { 163 return resourceName; 164 } 165 166 @Override public int hashCode() { 167 return resourceName.hashCode(); 168 } 169 170 @Override public boolean equals(Object obj) { 171 if (obj instanceof ResourceInfo) { 172 ResourceInfo that = (ResourceInfo) obj; 173 return resourceName.equals(that.resourceName) 174 && loader == that.loader; 175 } 176 return false; 177 } 178 179 // Do not change this arbitrarily. We rely on it for sorting ResourceInfo. 180 @Override public String toString() { 181 return resourceName; 182 } 183 } 184 185 /** 186 * Represents a class that can be loaded through {@link #load}. 187 * 188 * @since 14.0 189 */ 190 @Beta 191 public static final class ClassInfo extends ResourceInfo { 192 private final String className; 193 194 ClassInfo(String resourceName, ClassLoader loader) { 195 super(resourceName, loader); 196 this.className = getClassName(resourceName); 197 } 198 199 /** Returns the package name of the class, without attempting to load the class. */ 200 public String getPackageName() { 201 return Reflection.getPackageName(className); 202 } 203 204 /** Returns the simple name of the underlying class as given in the source code. */ 205 public String getSimpleName() { 206 String packageName = getPackageName(); 207 if (packageName.isEmpty()) { 208 return className; 209 } 210 // Since this is a top level class, its simple name is always the part after package name. 211 return className.substring(packageName.length() + 1); 212 } 213 214 /** Returns the fully qualified name of the class. */ 215 public String getName() { 216 return className; 217 } 218 219 /** 220 * Loads (but doesn't link or initialize) the class. 221 * 222 * @throws LinkageError when there were errors in loading classes that this class depends on. 223 * For example, {@link NoClassDefFoundError}. 224 */ 225 public Class<?> load() { 226 try { 227 return loader.loadClass(className); 228 } catch (ClassNotFoundException e) { 229 // Shouldn't happen, since the class name is read from the class path. 230 throw new IllegalStateException(e); 231 } 232 } 233 234 @Override public String toString() { 235 return className; 236 } 237 } 238 239 @VisibleForTesting static ImmutableMap<URI, ClassLoader> getClassPathEntries( 240 ClassLoader classloader) { 241 LinkedHashMap<URI, ClassLoader> entries = Maps.newLinkedHashMap(); 242 // Search parent first, since it's the order ClassLoader#loadClass() uses. 243 ClassLoader parent = classloader.getParent(); 244 if (parent != null) { 245 entries.putAll(getClassPathEntries(parent)); 246 } 247 if (classloader instanceof URLClassLoader) { 248 URLClassLoader urlClassLoader = (URLClassLoader) classloader; 249 for (URL entry : urlClassLoader.getURLs()) { 250 URI uri; 251 try { 252 uri = entry.toURI(); 253 } catch (URISyntaxException e) { 254 throw new IllegalArgumentException(e); 255 } 256 if (!entries.containsKey(uri)) { 257 entries.put(uri, classloader); 258 } 259 } 260 } 261 return ImmutableMap.copyOf(entries); 262 } 263 264 @VisibleForTesting static final class Scanner { 265 266 private final ImmutableSortedSet.Builder<ResourceInfo> resources = 267 new ImmutableSortedSet.Builder<ResourceInfo>(Ordering.usingToString()); 268 private final Set<URI> scannedUris = Sets.newHashSet(); 269 270 ImmutableSortedSet<ResourceInfo> getResources() { 271 return resources.build(); 272 } 273 274 void scan(URI uri, ClassLoader classloader) throws IOException { 275 if (uri.getScheme().equals("file") && scannedUris.add(uri)) { 276 scanFrom(new File(uri), classloader); 277 } 278 } 279 280 @VisibleForTesting void scanFrom(File file, ClassLoader classloader) 281 throws IOException { 282 if (!file.exists()) { 283 return; 284 } 285 if (file.isDirectory()) { 286 scanDirectory(file, classloader); 287 } else { 288 scanJar(file, classloader); 289 } 290 } 291 292 private void scanDirectory(File directory, ClassLoader classloader) throws IOException { 293 scanDirectory(directory, classloader, "", ImmutableSet.<File>of()); 294 } 295 296 private void scanDirectory( 297 File directory, ClassLoader classloader, String packagePrefix, 298 ImmutableSet<File> ancestors) throws IOException { 299 File canonical = directory.getCanonicalFile(); 300 if (ancestors.contains(canonical)) { 301 // A cycle in the filesystem, for example due to a symbolic link. 302 return; 303 } 304 File[] files = directory.listFiles(); 305 if (files == null) { 306 logger.warning("Cannot read directory " + directory); 307 // IO error, just skip the directory 308 return; 309 } 310 ImmutableSet<File> newAncestors = ImmutableSet.<File>builder() 311 .addAll(ancestors) 312 .add(canonical) 313 .build(); 314 for (File f : files) { 315 String name = f.getName(); 316 if (f.isDirectory()) { 317 scanDirectory(f, classloader, packagePrefix + name + "/", newAncestors); 318 } else { 319 String resourceName = packagePrefix + name; 320 if (!resourceName.equals(JarFile.MANIFEST_NAME)) { 321 resources.add(ResourceInfo.of(resourceName, classloader)); 322 } 323 } 324 } 325 } 326 327 private void scanJar(File file, ClassLoader classloader) throws IOException { 328 JarFile jarFile; 329 try { 330 jarFile = new JarFile(file); 331 } catch (IOException e) { 332 // Not a jar file 333 return; 334 } 335 try { 336 for (URI uri : getClassPathFromManifest(file, jarFile.getManifest())) { 337 scan(uri, classloader); 338 } 339 Enumeration<JarEntry> entries = jarFile.entries(); 340 while (entries.hasMoreElements()) { 341 JarEntry entry = entries.nextElement(); 342 if (entry.isDirectory() || entry.getName().equals(JarFile.MANIFEST_NAME)) { 343 continue; 344 } 345 resources.add(ResourceInfo.of(entry.getName(), classloader)); 346 } 347 } finally { 348 try { 349 jarFile.close(); 350 } catch (IOException ignored) {} 351 } 352 } 353 354 /** 355 * Returns the class path URIs specified by the {@code Class-Path} manifest attribute, according 356 * to <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html#Main%20Attributes"> 357 * JAR File Specification</a>. If {@code manifest} is null, it means the jar file has no 358 * manifest, and an empty set will be returned. 359 */ 360 @VisibleForTesting static ImmutableSet<URI> getClassPathFromManifest( 361 File jarFile, @Nullable Manifest manifest) { 362 if (manifest == null) { 363 return ImmutableSet.of(); 364 } 365 ImmutableSet.Builder<URI> builder = ImmutableSet.builder(); 366 String classpathAttribute = manifest.getMainAttributes() 367 .getValue(Attributes.Name.CLASS_PATH.toString()); 368 if (classpathAttribute != null) { 369 for (String path : CLASS_PATH_ATTRIBUTE_SEPARATOR.split(classpathAttribute)) { 370 URI uri; 371 try { 372 uri = getClassPathEntry(jarFile, path); 373 } catch (URISyntaxException e) { 374 // Ignore bad entry 375 logger.warning("Invalid Class-Path entry: " + path); 376 continue; 377 } 378 builder.add(uri); 379 } 380 } 381 return builder.build(); 382 } 383 384 /** 385 * Returns the absolute uri of the Class-Path entry value as specified in 386 * <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html#Main%20Attributes"> 387 * JAR File Specification</a>. Even though the specification only talks about relative urls, 388 * absolute urls are actually supported too (for example, in Maven surefire plugin). 389 */ 390 @VisibleForTesting static URI getClassPathEntry(File jarFile, String path) 391 throws URISyntaxException { 392 URI uri = new URI(path); 393 if (uri.isAbsolute()) { 394 return uri; 395 } else { 396 return new File(jarFile.getParentFile(), path.replace('/', File.separatorChar)).toURI(); 397 } 398 } 399 } 400 401 @VisibleForTesting static String getClassName(String filename) { 402 int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length(); 403 return filename.substring(0, classNameEnd).replace('/', '.'); 404 } 405}