• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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