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 org.truth0.Truth.ASSERT;
20
21import com.google.common.base.Charsets;
22import com.google.common.collect.ImmutableMap;
23import com.google.common.collect.Maps;
24import com.google.common.collect.Sets;
25import com.google.common.io.Closer;
26import com.google.common.io.Resources;
27import com.google.common.reflect.ClassPath.ClassInfo;
28import com.google.common.reflect.ClassPath.ResourceInfo;
29import com.google.common.reflect.subpackage.ClassInSubPackage;
30import com.google.common.testing.EqualsTester;
31import com.google.common.testing.NullPointerTester;
32
33import junit.framework.TestCase;
34import org.junit.Test;
35
36import java.io.ByteArrayInputStream;
37import java.io.File;
38import java.io.FileOutputStream;
39import java.io.IOException;
40import java.io.InputStream;
41import java.net.URI;
42import java.net.URISyntaxException;
43import java.net.URL;
44import java.net.URLClassLoader;
45import java.util.Map;
46import java.util.Set;
47import java.util.jar.Attributes;
48import java.util.jar.JarFile;
49import java.util.jar.JarOutputStream;
50import java.util.jar.Manifest;
51import java.util.zip.ZipEntry;
52
53/**
54 * Functional tests of {@link ClassPath}.
55 */
56public class ClassPathTest extends TestCase {
57
58  public void testGetResources() throws Exception {
59    Map<String, ResourceInfo> byName = Maps.newHashMap();
60    Map<String, ResourceInfo> byToString = Maps.newHashMap();
61    ClassPath classpath = ClassPath.from(getClass().getClassLoader());
62    for (ResourceInfo resource : classpath.getResources()) {
63      ASSERT.that(resource.getResourceName()).isNotEqualTo(JarFile.MANIFEST_NAME);
64      ASSERT.that(resource.toString()).isNotEqualTo(JarFile.MANIFEST_NAME);
65      byName.put(resource.getResourceName(), resource);
66      byToString.put(resource.toString(), resource);
67      // TODO: This will fail on maven resources in the classes directory on a mac.
68      // assertNotNull(resource.url());
69    }
70    String testResourceName = "com/google/common/reflect/test.txt";
71    ASSERT.that(byName.keySet()).has().allOf(
72        "com/google/common/reflect/ClassPath.class",
73        "com/google/common/reflect/ClassPathTest.class",
74        "com/google/common/reflect/ClassPathTest$Nested.class",
75        testResourceName);
76    ASSERT.that(byToString.keySet()).has().allOf(
77        "com.google.common.reflect.ClassPath",
78        "com.google.common.reflect.ClassPathTest",
79        "com.google.common.reflect.ClassPathTest$Nested",
80        testResourceName);
81    assertEquals(getClass().getClassLoader().getResource(testResourceName),
82        byName.get("com/google/common/reflect/test.txt").url());
83  }
84
85  public void testGetAllClasses() throws Exception {
86    Set<String> names = Sets.newHashSet();
87    Set<String> strings = Sets.newHashSet();
88    Set<Class<?>> classes = Sets.newHashSet();
89    Set<String> packageNames = Sets.newHashSet();
90    Set<String> simpleNames = Sets.newHashSet();
91    ClassPath classpath = ClassPath.from(getClass().getClassLoader());
92    for (ClassInfo classInfo : classpath.getAllClasses()) {
93      if (!classInfo.getPackageName().equals(ClassPathTest.class.getPackage().getName())) {
94        continue;
95      }
96      names.add(classInfo.getName());
97      strings.add(classInfo.toString());
98      classes.add(classInfo.load());
99      packageNames.add(classInfo.getPackageName());
100      simpleNames.add(classInfo.getSimpleName());
101    }
102    class LocalClass {}
103    Class<?> anonymousClass = new Object() {}.getClass();
104    ASSERT.that(names).has().allOf(anonymousClass.getName(), LocalClass.class.getName(),
105        ClassPath.class.getName(), ClassPathTest.class.getName());
106    ASSERT.that(strings).has().allOf(anonymousClass.getName(), LocalClass.class.getName(),
107        ClassPath.class.getName(), ClassPathTest.class.getName());
108    ASSERT.that(classes).has().allOf(anonymousClass, LocalClass.class, ClassPath.class,
109        ClassPathTest.class);
110    ASSERT.that(packageNames).has().exactly(ClassPath.class.getPackage().getName());
111    ASSERT.that(simpleNames).has().allOf("", "Local", "ClassPath", "ClassPathTest");
112  }
113
114  public void testGetTopLevelClasses() throws Exception {
115    Set<String> names = Sets.newHashSet();
116    Set<String> strings = Sets.newHashSet();
117    Set<Class<?>> classes = Sets.newHashSet();
118    Set<String> packageNames = Sets.newHashSet();
119    Set<String> simpleNames = Sets.newHashSet();
120    ClassPath classpath = ClassPath.from(getClass().getClassLoader());
121    for (ClassInfo classInfo
122        : classpath.getTopLevelClasses(ClassPathTest.class.getPackage().getName())) {
123      names.add(classInfo.getName());
124      strings.add(classInfo.toString());
125      classes.add(classInfo.load());
126      packageNames.add(classInfo.getPackageName());
127      simpleNames.add(classInfo.getSimpleName());
128    }
129    ASSERT.that(names).has().allOf(ClassPath.class.getName(), ClassPathTest.class.getName());
130    ASSERT.that(strings).has().allOf(ClassPath.class.getName(), ClassPathTest.class.getName());
131    ASSERT.that(classes).has().allOf(ClassPath.class, ClassPathTest.class);
132    ASSERT.that(packageNames).has().item(ClassPath.class.getPackage().getName());
133    ASSERT.that(simpleNames).has().allOf("ClassPath", "ClassPathTest");
134    assertFalse(classes.contains(ClassInSubPackage.class));
135  }
136
137  public void testGetTopLevelClassesRecursive() throws Exception {
138    Set<Class<?>> classes = Sets.newHashSet();
139    ClassPath classpath = ClassPath.from(ClassPathTest.class.getClassLoader());
140    for (ClassInfo classInfo
141        : classpath.getTopLevelClassesRecursive(ClassPathTest.class.getPackage().getName())) {
142      if (classInfo.getName().contains("ClassPathTest")) {
143        System.err.println("");
144      }
145      classes.add(classInfo.load());
146    }
147    ASSERT.that(classes).has().allOf(ClassPathTest.class, ClassInSubPackage.class);
148  }
149
150  public void testGetTopLevelClasses_diamond() throws Exception {
151    ClassLoader parent = ClassPathTest.class.getClassLoader();
152    ClassLoader sub1 = new ClassLoader(parent) {};
153    ClassLoader sub2 = new ClassLoader(parent) {};
154    assertEquals(findClass(ClassPath.from(sub1).getTopLevelClasses(), ClassPathTest.class),
155        findClass(ClassPath.from(sub2).getTopLevelClasses(), ClassPathTest.class));
156  }
157
158  public void testEquals() {
159    new EqualsTester()
160        .addEqualityGroup(classInfo(ClassPathTest.class), classInfo(ClassPathTest.class))
161        .addEqualityGroup(classInfo(Test.class), classInfo(Test.class, getClass().getClassLoader()))
162        .addEqualityGroup(
163            new ResourceInfo("a/b/c.txt", getClass().getClassLoader()),
164            new ResourceInfo("a/b/c.txt", getClass().getClassLoader()))
165        .addEqualityGroup(
166            new ResourceInfo("x.txt", getClass().getClassLoader()))
167        .testEquals();
168  }
169
170  public void testClassPathEntries_emptyURLClassLoader_noParent() {
171    ASSERT.that(ClassPath.getClassPathEntries(new URLClassLoader(new URL[0], null)).keySet())
172        .isEmpty();
173  }
174
175  public void testClassPathEntries_URLClassLoader_noParent() throws Exception {
176    URL url1 = new URL("file:/a");
177    URL url2 = new URL("file:/b");
178    URLClassLoader classloader = new URLClassLoader(new URL[] {url1, url2}, null);
179    assertEquals(
180        ImmutableMap.of(url1.toURI(), classloader, url2.toURI(), classloader),
181        ClassPath.getClassPathEntries(classloader));
182  }
183
184  public void testClassPathEntries_URLClassLoader_withParent() throws Exception {
185    URL url1 = new URL("file:/a");
186    URL url2 = new URL("file:/b");
187    URLClassLoader parent = new URLClassLoader(new URL[] {url1}, null);
188    URLClassLoader child = new URLClassLoader(new URL[] {url2}, parent) {};
189    ImmutableMap<URI, ClassLoader> classPathEntries = ClassPath.getClassPathEntries(child);
190    assertEquals(ImmutableMap.of(url1.toURI(), parent, url2.toURI(), child),  classPathEntries);
191    ASSERT.that(classPathEntries.keySet()).has().exactly(url1.toURI(), url2.toURI()).inOrder();
192  }
193
194  public void testClassPathEntries_duplicateUri_parentWins() throws Exception {
195    URL url = new URL("file:/a");
196    URLClassLoader parent = new URLClassLoader(new URL[] {url}, null);
197    URLClassLoader child = new URLClassLoader(new URL[] {url}, parent) {};
198    assertEquals(ImmutableMap.of(url.toURI(), parent), ClassPath.getClassPathEntries(child));
199  }
200
201  public void testClassPathEntries_notURLClassLoader_noParent() {
202    ASSERT.that(ClassPath.getClassPathEntries(new ClassLoader(null) {}).keySet()).isEmpty();
203  }
204
205  public void testClassPathEntries_notURLClassLoader_withParent() throws Exception {
206    URL url = new URL("file:/a");
207    URLClassLoader parent = new URLClassLoader(new URL[] {url}, null);
208    assertEquals(
209        ImmutableMap.of(url.toURI(), parent),
210        ClassPath.getClassPathEntries(new ClassLoader(parent) {}));
211  }
212
213  public void testClassPathEntries_notURLClassLoader_withParentAndGrandParent() throws Exception {
214    URL url1 = new URL("file:/a");
215    URL url2 = new URL("file:/b");
216    URLClassLoader grandParent = new URLClassLoader(new URL[] {url1}, null);
217    URLClassLoader parent = new URLClassLoader(new URL[] {url2}, grandParent);
218    assertEquals(
219        ImmutableMap.of(url1.toURI(), grandParent, url2.toURI(), parent),
220        ClassPath.getClassPathEntries(new ClassLoader(parent) {}));
221  }
222
223  public void testClassPathEntries_notURLClassLoader_withGrandParent() throws Exception {
224    URL url = new URL("file:/a");
225    URLClassLoader grandParent = new URLClassLoader(new URL[] {url}, null);
226    ClassLoader parent = new ClassLoader(grandParent) {};
227    assertEquals(
228        ImmutableMap.of(url.toURI(), grandParent),
229        ClassPath.getClassPathEntries(new ClassLoader(parent) {}));
230  }
231
232  public void testScan_classPathCycle() throws IOException {
233    File jarFile = File.createTempFile("with_circular_class_path", ".jar");
234    try {
235      writeSelfReferencingJarFile(jarFile, "test.txt");
236      ClassPath.Scanner scanner = new ClassPath.Scanner();
237      scanner.scan(jarFile.toURI(), ClassPathTest.class.getClassLoader());
238      assertEquals(1, scanner.getResources().size());
239    } finally {
240      jarFile.delete();
241    }
242  }
243
244  public void testScanFromFile_fileNotExists() throws IOException {
245    ClassLoader classLoader = ClassPathTest.class.getClassLoader();
246    ClassPath.Scanner scanner = new ClassPath.Scanner();
247    scanner.scanFrom(new File("no/such/file/anywhere"), classLoader);
248    ASSERT.that(scanner.getResources()).isEmpty();
249  }
250
251  public void testScanFromFile_notJarFile() throws IOException {
252    ClassLoader classLoader = ClassPathTest.class.getClassLoader();
253    File notJar = File.createTempFile("not_a_jar", "txt");
254    ClassPath.Scanner scanner = new ClassPath.Scanner();
255    try {
256      scanner.scanFrom(notJar, classLoader);
257    } finally {
258      notJar.delete();
259    }
260    ASSERT.that(scanner.getResources()).isEmpty();
261  }
262
263  public void testGetClassPathEntry() throws URISyntaxException {
264    assertEquals(URI.create("file:/usr/test/dep.jar"),
265        ClassPath.Scanner.getClassPathEntry(
266            new File("/home/build/outer.jar"), "file:/usr/test/dep.jar"));
267    assertEquals(URI.create("file:/home/build/a.jar"),
268        ClassPath.Scanner.getClassPathEntry(new File("/home/build/outer.jar"), "a.jar"));
269    assertEquals(URI.create("file:/home/build/x/y/z"),
270        ClassPath.Scanner.getClassPathEntry(new File("/home/build/outer.jar"), "x/y/z"));
271    assertEquals(URI.create("file:/home/build/x/y/z.jar"),
272        ClassPath.Scanner.getClassPathEntry(new File("/home/build/outer.jar"), "x/y/z.jar"));
273  }
274
275  public void testGetClassPathFromManifest_nullManifest() {
276    ASSERT.that(ClassPath.Scanner.getClassPathFromManifest(new File("some.jar"), null)).isEmpty();
277  }
278
279  public void testGetClassPathFromManifest_noClassPath() throws IOException {
280    File jarFile = new File("base.jar");
281    ASSERT.that(ClassPath.Scanner.getClassPathFromManifest(jarFile, manifest("")))
282        .isEmpty();
283  }
284
285  public void testGetClassPathFromManifest_emptyClassPath() throws IOException {
286    File jarFile = new File("base.jar");
287    ASSERT.that(ClassPath.Scanner.getClassPathFromManifest(jarFile, manifestClasspath("")))
288        .isEmpty();
289  }
290
291  public void testGetClassPathFromManifest_badClassPath() throws IOException {
292    File jarFile = new File("base.jar");
293    Manifest manifest = manifestClasspath("an_invalid^path");
294    ASSERT.that(ClassPath.Scanner.getClassPathFromManifest(jarFile, manifest))
295        .isEmpty();
296  }
297
298  public void testGetClassPathFromManifest_relativeDirectory() throws IOException {
299    File jarFile = new File("base/some.jar");
300    // with/relative/directory is the Class-Path value in the mf file.
301    Manifest manifest = manifestClasspath("with/relative/dir");
302    ASSERT.that(ClassPath.Scanner.getClassPathFromManifest(jarFile, manifest))
303        .has().exactly(new File("base/with/relative/dir").toURI()).inOrder();
304  }
305
306  public void testGetClassPathFromManifest_relativeJar() throws IOException {
307    File jarFile = new File("base/some.jar");
308    // with/relative/directory is the Class-Path value in the mf file.
309    Manifest manifest = manifestClasspath("with/relative.jar");
310    ASSERT.that(ClassPath.Scanner.getClassPathFromManifest(jarFile, manifest))
311        .has().exactly(new File("base/with/relative.jar").toURI()).inOrder();
312  }
313
314  public void testGetClassPathFromManifest_jarInCurrentDirectory() throws IOException {
315    File jarFile = new File("base/some.jar");
316    // with/relative/directory is the Class-Path value in the mf file.
317    Manifest manifest = manifestClasspath("current.jar");
318    ASSERT.that(ClassPath.Scanner.getClassPathFromManifest(jarFile, manifest))
319        .has().exactly(new File("base/current.jar").toURI()).inOrder();
320  }
321
322  public void testGetClassPathFromManifest_absoluteDirectory() throws IOException {
323    File jarFile = new File("base/some.jar");
324    Manifest manifest = manifestClasspath("file:/with/absolute/dir");
325    ASSERT.that(ClassPath.Scanner.getClassPathFromManifest(jarFile, manifest))
326        .has().exactly(new File("/with/absolute/dir").toURI()).inOrder();
327  }
328
329  public void testGetClassPathFromManifest_absoluteJar() throws IOException {
330    File jarFile = new File("base/some.jar");
331    Manifest manifest = manifestClasspath("file:/with/absolute.jar");
332    ASSERT.that(ClassPath.Scanner.getClassPathFromManifest(jarFile, manifest))
333        .has().exactly(new File("/with/absolute.jar").toURI()).inOrder();
334  }
335
336  public void testGetClassPathFromManifest_multiplePaths() throws IOException {
337    File jarFile = new File("base/some.jar");
338    Manifest manifest = manifestClasspath("file:/with/absolute.jar relative.jar  relative/dir");
339    ASSERT.that(ClassPath.Scanner.getClassPathFromManifest(jarFile, manifest))
340        .has().exactly(
341            new File("/with/absolute.jar").toURI(),
342            new File("base/relative.jar").toURI(),
343            new File("base/relative/dir").toURI())
344        .inOrder();
345  }
346
347  public void testGetClassPathFromManifest_leadingBlanks() throws IOException {
348    File jarFile = new File("base/some.jar");
349    Manifest manifest = manifestClasspath(" relative.jar");
350    ASSERT.that(ClassPath.Scanner.getClassPathFromManifest(jarFile, manifest))
351        .has().exactly(new File("base/relative.jar").toURI()).inOrder();
352  }
353
354  public void testGetClassPathFromManifest_trailingBlanks() throws IOException {
355    File jarFile = new File("base/some.jar");
356    Manifest manifest = manifestClasspath("relative.jar ");
357    ASSERT.that(ClassPath.Scanner.getClassPathFromManifest(jarFile, manifest))
358        .has().exactly(new File("base/relative.jar").toURI()).inOrder();
359  }
360
361  public void testGetClassName() {
362    assertEquals("abc.d.Abc", ClassPath.getClassName("abc/d/Abc.class"));
363  }
364
365  public void testResourceInfo_of() {
366    assertEquals(ClassInfo.class, resourceInfo(ClassPathTest.class).getClass());
367    assertEquals(ClassInfo.class, resourceInfo(ClassPath.class).getClass());
368    assertEquals(ClassInfo.class, resourceInfo(Nested.class).getClass());
369  }
370
371  public void testGetSimpleName() {
372    assertEquals("Foo",
373        new ClassInfo("Foo.class", getClass().getClassLoader()).getSimpleName());
374    assertEquals("Foo",
375        new ClassInfo("a/b/Foo.class", getClass().getClassLoader()).getSimpleName());
376    assertEquals("Foo",
377        new ClassInfo("a/b/Bar$Foo.class", getClass().getClassLoader()).getSimpleName());
378    assertEquals("",
379        new ClassInfo("a/b/Bar$1.class", getClass().getClassLoader()).getSimpleName());
380    assertEquals("Foo",
381        new ClassInfo("a/b/Bar$Foo.class", getClass().getClassLoader()).getSimpleName());
382    assertEquals("",
383        new ClassInfo("a/b/Bar$1.class", getClass().getClassLoader()).getSimpleName());
384    assertEquals("Local",
385        new ClassInfo("a/b/Bar$1Local.class", getClass().getClassLoader()).getSimpleName());
386
387  }
388
389  public void testGetPackageName() {
390    assertEquals("",
391        new ClassInfo("Foo.class", getClass().getClassLoader()).getPackageName());
392    assertEquals("a.b",
393        new ClassInfo("a/b/Foo.class", getClass().getClassLoader()).getPackageName());
394  }
395
396  private static class Nested {}
397
398  public void testNulls() throws IOException {
399    new NullPointerTester().testAllPublicStaticMethods(ClassPath.class);
400    new NullPointerTester()
401        .testAllPublicInstanceMethods(ClassPath.from(getClass().getClassLoader()));
402  }
403
404  private static ClassPath.ClassInfo findClass(
405      Iterable<ClassPath.ClassInfo> classes, Class<?> cls) {
406    for (ClassPath.ClassInfo classInfo : classes) {
407      if (classInfo.getName().equals(cls.getName())) {
408        return classInfo;
409      }
410    }
411    throw new AssertionError("failed to find " + cls);
412  }
413
414  private static ResourceInfo resourceInfo(Class<?> cls) {
415    return ResourceInfo.of(cls.getName().replace('.', '/') + ".class", cls.getClassLoader());
416  }
417
418  private static ClassInfo classInfo(Class<?> cls) {
419    return classInfo(cls, cls.getClassLoader());
420  }
421
422  private static ClassInfo classInfo(Class<?> cls, ClassLoader classLoader) {
423    return new ClassInfo(cls.getName().replace('.', '/') + ".class", classLoader);
424  }
425
426  private static Manifest manifestClasspath(String classpath) throws IOException {
427    return manifest("Class-Path: " + classpath + "\n");
428  }
429
430  private static void writeSelfReferencingJarFile(File jarFile, String... entries)
431      throws IOException {
432    Manifest manifest = new Manifest();
433    // Without version, the manifest is silently ignored. Ugh!
434    manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
435    manifest.getMainAttributes().put(Attributes.Name.CLASS_PATH, jarFile.getName());
436
437    Closer closer = Closer.create();
438    try {
439      FileOutputStream fileOut = closer.register(new FileOutputStream(jarFile));
440      JarOutputStream jarOut = closer.register(new JarOutputStream(fileOut));
441      for (String entry : entries) {
442        jarOut.putNextEntry(new ZipEntry(entry));
443        Resources.copy(ClassPathTest.class.getResource(entry), jarOut);
444        jarOut.closeEntry();
445      }
446    } catch (Throwable e) {
447      throw closer.rethrow(e);
448    } finally {
449      closer.close();
450    }
451  }
452
453  private static Manifest manifest(String content) throws IOException {
454    InputStream in = new ByteArrayInputStream(content.getBytes(Charsets.US_ASCII.name()));
455    Manifest manifest = new Manifest();
456    manifest.read(in);
457    return manifest;
458  }
459}
460