1 /* 2 * Copyright (C) 2013 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 17 package com.google.caliper.runner; 18 19 import com.google.common.annotations.VisibleForTesting; 20 import com.google.common.base.Splitter; 21 import com.google.common.collect.ImmutableMap; 22 import com.google.common.collect.ImmutableSet; 23 import com.google.common.collect.Lists; 24 import com.google.common.collect.Maps; 25 import com.google.common.collect.Sets; 26 import com.google.common.reflect.ClassPath; 27 28 import java.io.File; 29 import java.io.IOException; 30 import java.net.URI; 31 import java.net.URISyntaxException; 32 import java.net.URL; 33 import java.net.URLClassLoader; 34 import java.util.Map; 35 import java.util.Set; 36 import java.util.jar.Attributes; 37 import java.util.jar.JarFile; 38 import java.util.jar.Manifest; 39 import java.util.logging.Logger; 40 41 import javax.annotation.Nullable; 42 43 /** 44 * Scans the source of a {@link ClassLoader} and finds all jar files. This is a modified version 45 * of {@link ClassPath} that finds jars instead of resources. 46 */ 47 final class JarFinder { 48 private static final Logger logger = Logger.getLogger(JarFinder.class.getName()); 49 50 /** Separator for the Class-Path manifest attribute value in jar files. */ 51 private static final Splitter CLASS_PATH_ATTRIBUTE_SEPARATOR = 52 Splitter.on(' ').omitEmptyStrings(); 53 54 /** 55 * Returns a list of jar files reachable from the given class loaders. 56 * 57 * <p>Currently only {@link URLClassLoader} and only {@code file://} urls are supported. 58 * 59 * @throws IOException if the attempt to read class path resources (jar files or directories) 60 * failed. 61 */ findJarFiles(ClassLoader first, ClassLoader... rest)62 public static ImmutableSet<File> findJarFiles(ClassLoader first, ClassLoader... rest) 63 throws IOException { 64 Scanner scanner = new Scanner(); 65 Map<URI, ClassLoader> map = Maps.newLinkedHashMap(); 66 for (ClassLoader classLoader : Lists.asList(first, rest)) { 67 map.putAll(getClassPathEntries(classLoader)); 68 } 69 for (Map.Entry<URI, ClassLoader> entry : map.entrySet()) { 70 scanner.scan(entry.getKey(), entry.getValue()); 71 } 72 return scanner.jarFiles(); 73 } 74 getClassPathEntries( ClassLoader classloader)75 @VisibleForTesting static ImmutableMap<URI, ClassLoader> getClassPathEntries( 76 ClassLoader classloader) { 77 Map<URI, ClassLoader> entries = Maps.newLinkedHashMap(); 78 // Search parent first, since it's the order ClassLoader#loadClass() uses. 79 ClassLoader parent = classloader.getParent(); 80 if (parent != null) { 81 entries.putAll(getClassPathEntries(parent)); 82 } 83 if (classloader instanceof URLClassLoader) { 84 URLClassLoader urlClassLoader = (URLClassLoader) classloader; 85 for (URL entry : urlClassLoader.getURLs()) { 86 URI uri; 87 try { 88 uri = entry.toURI(); 89 } catch (URISyntaxException e) { 90 throw new IllegalArgumentException(e); 91 } 92 if (!entries.containsKey(uri)) { 93 entries.put(uri, classloader); 94 } 95 } 96 } 97 return ImmutableMap.copyOf(entries); 98 } 99 100 @VisibleForTesting static final class Scanner { 101 private final ImmutableSet.Builder<File> jarFiles = new ImmutableSet.Builder<File>(); 102 private final Set<URI> scannedUris = Sets.newHashSet(); 103 jarFiles()104 ImmutableSet<File> jarFiles() { 105 return jarFiles.build(); 106 } 107 scan(URI uri, ClassLoader classloader)108 void scan(URI uri, ClassLoader classloader) throws IOException { 109 if (uri.getScheme().equals("file") && scannedUris.add(uri)) { 110 scanFrom(new File(uri), classloader); 111 } 112 } 113 scanFrom(File file, ClassLoader classloader)114 @VisibleForTesting void scanFrom(File file, ClassLoader classloader) 115 throws IOException { 116 if (!file.exists()) { 117 return; 118 } 119 if (file.isDirectory()) { 120 scanDirectory(file, classloader); 121 } else { 122 scanJar(file, classloader); 123 } 124 } 125 scanDirectory(File directory, ClassLoader classloader)126 private void scanDirectory(File directory, ClassLoader classloader) { 127 scanDirectory(directory, classloader, ""); 128 } 129 scanDirectory( File directory, ClassLoader classloader, String packagePrefix)130 private void scanDirectory( 131 File directory, ClassLoader classloader, String packagePrefix) { 132 for (File file : directory.listFiles()) { 133 String name = file.getName(); 134 if (file.isDirectory()) { 135 scanDirectory(file, classloader, packagePrefix + name + "/"); 136 } 137 // do we need to look for jars here? 138 } 139 } 140 scanJar(File file, ClassLoader classloader)141 private void scanJar(File file, ClassLoader classloader) throws IOException { 142 JarFile jarFile; 143 try { 144 jarFile = new JarFile(file); 145 } catch (IOException e) { 146 // Not a jar file 147 return; 148 } 149 jarFiles.add(file); 150 try { 151 for (URI uri : getClassPathFromManifest(file, jarFile.getManifest())) { 152 scan(uri, classloader); 153 } 154 } finally { 155 try { 156 jarFile.close(); 157 } catch (IOException ignored) {} 158 } 159 } 160 161 /** 162 * Returns the class path URIs specified by the {@code Class-Path} manifest attribute, according 163 * to <a 164 * href="http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html#Main%20Attributes"> 165 * JAR File Specification</a>. If {@code manifest} is null, it means the jar file has no 166 * manifest, and an empty set will be returned. 167 */ getClassPathFromManifest( File jarFile, @Nullable Manifest manifest)168 @VisibleForTesting static ImmutableSet<URI> getClassPathFromManifest( 169 File jarFile, @Nullable Manifest manifest) { 170 if (manifest == null) { 171 return ImmutableSet.of(); 172 } 173 ImmutableSet.Builder<URI> builder = ImmutableSet.builder(); 174 String classpathAttribute = manifest.getMainAttributes() 175 .getValue(Attributes.Name.CLASS_PATH.toString()); 176 if (classpathAttribute != null) { 177 for (String path : CLASS_PATH_ATTRIBUTE_SEPARATOR.split(classpathAttribute)) { 178 URI uri; 179 try { 180 uri = getClassPathEntry(jarFile, path); 181 } catch (URISyntaxException e) { 182 // Ignore bad entry 183 logger.warning("Invalid Class-Path entry: " + path); 184 continue; 185 } 186 builder.add(uri); 187 } 188 } 189 return builder.build(); 190 } 191 192 /** 193 * Returns the absolute uri of the Class-Path entry value as specified in 194 * <a 195 * href="http://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html#Main%20Attributes"> 196 * JAR File Specification</a>. Even though the specification only talks about relative urls, 197 * absolute urls are actually supported too (for example, in Maven surefire plugin). 198 */ getClassPathEntry(File jarFile, String path)199 @VisibleForTesting static URI getClassPathEntry(File jarFile, String path) 200 throws URISyntaxException { 201 URI uri = new URI(path); 202 return uri.isAbsolute() 203 ? uri 204 : new File(jarFile.getParentFile(), path.replace('/', File.separatorChar)).toURI(); 205 } 206 } 207 } 208