aboutsummaryrefslogtreecommitdiffstats
path: root/guava/src/com/google/common/reflect/ClassPath.java
blob: cfbc479090e6d462ebd9a84d51bd59448e9f65d1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
/*
 * Copyright (C) 2012 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.reflect;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Logger;

import javax.annotation.Nullable;

/**
 * Scans the source of a {@link ClassLoader} and finds all the classes loadable.
 *
 * @author Ben Yu
 * @since 14.0
 */
@Beta
public final class ClassPath {

  private static final Logger logger = Logger.getLogger(ClassPath.class.getName());

  /** Separator for the Class-Path manifest attribute value in jar files. */
  private static final Splitter CLASS_PATH_ATTRIBUTE_SEPARATOR =
      Splitter.on(" ").omitEmptyStrings();

  private static final String CLASS_FILE_NAME_EXTENSION = ".class";

  private final ImmutableSet<ResourceInfo> resources;

  private ClassPath(ImmutableSet<ResourceInfo> resources) {
    this.resources = resources;
  }

  /**
   * Returns a {@code ClassPath} representing all classes and resources loadable from {@code
   * classloader} and its parent class loaders.
   *
   * <p>Currently only {@link URLClassLoader} and only {@code file://} urls are supported.
   *
   * @throws IOException if the attempt to read class path resources (jar files or directories)
   *         failed.
   */
  public static ClassPath from(ClassLoader classloader) throws IOException {
    ImmutableSortedSet.Builder<ResourceInfo> resources =
        new ImmutableSortedSet.Builder<ResourceInfo>(Ordering.usingToString());
    for (Map.Entry<URI, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) {
      browse(entry.getKey(), entry.getValue(), resources);
    }
    return new ClassPath(resources.build());
  }

  /**
   * Returns all resources loadable from the current class path, including the class files of all
   * loadable classes.
   */
  public ImmutableSet<ResourceInfo> getResources() {
    return resources;
  }

  /** Returns all top level classes loadable from the current class path. */
  public ImmutableSet<ClassInfo> getTopLevelClasses() {
    ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
    for (ResourceInfo resource : resources) {
      if (resource instanceof ClassInfo) {
        builder.add((ClassInfo) resource);
      }
    }
    return builder.build();
  }

  /** Returns all top level classes whose package name is {@code packageName}. */
  public ImmutableSet<ClassInfo> getTopLevelClasses(String packageName) {
    checkNotNull(packageName);
    ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
    for (ClassInfo classInfo : getTopLevelClasses()) {
      if (classInfo.getPackageName().equals(packageName)) {
        builder.add(classInfo);
      }
    }
    return builder.build();
  }

  /**
   * Returns all top level classes whose package name is {@code packageName} or starts with
   * {@code packageName} followed by a '.'.
   */
  public ImmutableSet<ClassInfo> getTopLevelClassesRecursive(String packageName) {
    checkNotNull(packageName);
    String packagePrefix = packageName + '.';
    ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
    for (ClassInfo classInfo : getTopLevelClasses()) {
      if (classInfo.getName().startsWith(packagePrefix)) {
        builder.add(classInfo);
      }
    }
    return builder.build();
  }

  /**
   * Represents a class path resource that can be either a class file or any other resource file
   * loadable from the class path.
   *
   * @since 14.0
   */
  @Beta
  public static class ResourceInfo {
    private final String resourceName;
    final ClassLoader loader;

    static ResourceInfo of(String resourceName, ClassLoader loader) {
      if (resourceName.endsWith(CLASS_FILE_NAME_EXTENSION) && !resourceName.contains("$")) {
        return new ClassInfo(resourceName, loader);
      } else {
        return new ResourceInfo(resourceName, loader);
      }
    }
  
    ResourceInfo(String resourceName, ClassLoader loader) {
      this.resourceName = checkNotNull(resourceName);
      this.loader = checkNotNull(loader);
    }

    /** Returns the url identifying the resource. */
    public final URL url() {
      return checkNotNull(loader.getResource(resourceName),
          "Failed to load resource: %s", resourceName);
    }

    /** Returns the fully qualified name of the resource. Such as "com/mycomp/foo/bar.txt". */
    public final String getResourceName() {
      return resourceName;
    }

    @Override public int hashCode() {
      return resourceName.hashCode();
    }

    @Override public boolean equals(Object obj) {
      if (obj instanceof ResourceInfo) {
        ResourceInfo that = (ResourceInfo) obj;
        return resourceName.equals(that.resourceName)
            && loader == that.loader;
      }
      return false;
    }

    @Override public String toString() {
      return resourceName;
    }
  }

  /**
   * Represents a class that can be loaded through {@link #load}.
   *
   * @since 14.0
   */
  @Beta
  public static final class ClassInfo extends ResourceInfo {
    private final String className;

    ClassInfo(String resourceName, ClassLoader loader) {
      super(resourceName, loader);
      this.className = getClassName(resourceName);
    }

    /** Returns the package name of the class, without attempting to load the class. */
    public String getPackageName() {
      return Reflection.getPackageName(className);
    }

    /** Returns the simple name of the underlying class as given in the source code. */
    public String getSimpleName() {
      String packageName = getPackageName();
      if (packageName.isEmpty()) {
        return className;
      }
      // Since this is a top level class, its simple name is always the part after package name.
      return className.substring(packageName.length() + 1);
    }

    /** Returns the fully qualified name of the class. */
    public String getName() {
      return className;
    }

    /** Loads (but doesn't link or initialize) the class. */
    public Class<?> load() {
      try {
        return loader.loadClass(className);
      } catch (ClassNotFoundException e) {
        // Shouldn't happen, since the class name is read from the class path.
        throw new IllegalStateException(e);
      }
    }

    @Override public String toString() {
      return className;
    }
  }

  @VisibleForTesting static ImmutableMap<URI, ClassLoader> getClassPathEntries(
      ClassLoader classloader) {
    LinkedHashMap<URI, ClassLoader> entries = Maps.newLinkedHashMap();
    // Search parent first, since it's the order ClassLoader#loadClass() uses.
    ClassLoader parent = classloader.getParent();
    if (parent != null) {
      entries.putAll(getClassPathEntries(parent));
    }
    if (classloader instanceof URLClassLoader) {
      URLClassLoader urlClassLoader = (URLClassLoader) classloader;
      for (URL entry : urlClassLoader.getURLs()) {
        URI uri;
        try {
          uri = entry.toURI();
        } catch (URISyntaxException e) {
          throw new IllegalArgumentException(e);
        }
        if (!entries.containsKey(uri)) {
          entries.put(uri, classloader);
        }
      }
    }
    return ImmutableMap.copyOf(entries);
  }

  private static void browse(
      URI uri, ClassLoader classloader, ImmutableSet.Builder<ResourceInfo> resources)
      throws IOException {
    if (uri.getScheme().equals("file")) {
      browseFrom(new File(uri), classloader, resources);
    }
  }

  @VisibleForTesting static void browseFrom(
      File file, ClassLoader classloader, ImmutableSet.Builder<ResourceInfo> resources)
      throws IOException {
    if (!file.exists()) {
      return;
    }
    if (file.isDirectory()) {
      browseDirectory(file, classloader, resources);
    } else {
      browseJar(file, classloader, resources);
    }
  }

  private static void browseDirectory(
      File directory, ClassLoader classloader, ImmutableSet.Builder<ResourceInfo> resources) {
    browseDirectory(directory, classloader, "", resources);
  }

  private static void browseDirectory(
      File directory, ClassLoader classloader, String packagePrefix,
      ImmutableSet.Builder<ResourceInfo> resources) {
    for (File f : directory.listFiles()) {
      String name = f.getName();
      if (f.isDirectory()) {
        browseDirectory(f, classloader, packagePrefix + name + "/", resources);
      } else {
        String resourceName = packagePrefix + name;
        resources.add(ResourceInfo.of(resourceName, classloader));
      }
    }
  }

  private static void browseJar(
      File file, ClassLoader classloader, ImmutableSet.Builder<ResourceInfo> resources)
      throws IOException {
    JarFile jarFile;
    try {
      jarFile = new JarFile(file);
    } catch (IOException e) {
      // Not a jar file
      return;
    }
    try {
      for (URI uri : getClassPathFromManifest(file, jarFile.getManifest())) {
        browse(uri, classloader, resources);
      }
      Enumeration<JarEntry> entries = jarFile.entries();
      while (entries.hasMoreElements()) {
        JarEntry entry = entries.nextElement();
        if (entry.isDirectory() || entry.getName().startsWith("META-INF/")) {
          continue;
        }
        resources.add(ResourceInfo.of(entry.getName(), classloader));
      }
    } finally {
      try {
        jarFile.close();
      } catch (IOException ignored) {}
    }
  }

  /**
   * Returns the class path URIs specified by the {@code Class-Path} manifest attribute, according
   * to <a href="http://docs.oracle.com/javase/1.4.2/docs/guide/jar/jar.html#Main%20Attributes">
   * JAR File Specification</a>. If {@code manifest} is null, it means the jar file has no manifest,
   * and an empty set will be returned.
   */
  @VisibleForTesting static ImmutableSet<URI> getClassPathFromManifest(
      File jarFile, @Nullable Manifest manifest) {
    if (manifest == null) {
      return ImmutableSet.of();
    }
    ImmutableSet.Builder<URI> builder = ImmutableSet.builder();
    String classpathAttribute = manifest.getMainAttributes().getValue("Class-Path");
    if (classpathAttribute != null) {
      for (String path : CLASS_PATH_ATTRIBUTE_SEPARATOR.split(classpathAttribute)) {
        URI uri;
        try {
          uri = getClassPathEntry(jarFile, path);
        } catch (URISyntaxException e) {
          // Ignore bad entry
          logger.warning("Invalid Class-Path entry: " + path);
          continue;
        }
        builder.add(uri);
      }
    }
    return builder.build();
  }

  /**
   * Returns the absolute uri of the Class-Path entry value as specified in
   * <a href="http://docs.oracle.com/javase/1.4.2/docs/guide/jar/jar.html#Main%20Attributes">
   * JAR File Specification</a>. Even though the specification only talks about relative urls,
   * absolute urls are actually supported too (for example, in Maven surefire plugin).
   */
  @VisibleForTesting static URI getClassPathEntry(File jarFile, String path)
      throws URISyntaxException {
    URI uri = new URI(path);
    if (uri.isAbsolute()) {
      return uri;
    } else {
      return new File(jarFile.getParentFile(), path.replace('/', File.separatorChar)).toURI();
    }
  }

  @VisibleForTesting static String getClassName(String filename) {
    int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length();
    return filename.substring(0, classNameEnd).replace('/', '.');
  }
}