View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  package org.apache.bcel.util;
18  
19  import java.io.Closeable;
20  import java.io.DataInputStream;
21  import java.io.File;
22  import java.io.FileInputStream;
23  import java.io.FilenameFilter;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.net.MalformedURLException;
27  import java.net.URL;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.nio.file.Paths;
31  import java.util.ArrayList;
32  import java.util.Arrays;
33  import java.util.Enumeration;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Objects;
37  import java.util.StringTokenizer;
38  import java.util.Vector;
39  import java.util.stream.Collectors;
40  import java.util.zip.ZipEntry;
41  import java.util.zip.ZipFile;
42  
43  import org.apache.bcel.classfile.JavaClass;
44  import org.apache.bcel.classfile.Utility;
45  
46  /**
47   * Loads class files from the CLASSPATH. Inspired by sun.tools.ClassPath.
48   */
49  public class ClassPath implements Closeable {
50  
51      private abstract static class AbstractPathEntry implements Closeable {
52  
53          abstract ClassFile getClassFile(String name, String suffix);
54  
55          abstract URL getResource(String name);
56  
57          abstract InputStream getResourceAsStream(String name);
58      }
59  
60      private abstract static class AbstractZip extends AbstractPathEntry {
61  
62          private final ZipFile zipFile;
63  
64          AbstractZip(final ZipFile zipFile) {
65              this.zipFile = Objects.requireNonNull(zipFile, "zipFile");
66          }
67  
68          @Override
69          public void close() throws IOException {
70              if (zipFile != null) {
71                  zipFile.close();
72              }
73  
74          }
75  
76          @Override
77          ClassFile getClassFile(final String name, final String suffix) {
78              final ZipEntry entry = zipFile.getEntry(toEntryName(name, suffix));
79  
80              if (entry == null) {
81                  return null;
82              }
83  
84              return new ClassFile() {
85  
86                  @Override
87                  public String getBase() {
88                      return zipFile.getName();
89                  }
90  
91                  @Override
92                  public InputStream getInputStream() throws IOException {
93                      return zipFile.getInputStream(entry);
94                  }
95  
96                  @Override
97                  public String getPath() {
98                      return entry.toString();
99                  }
100 
101                 @Override
102                 public long getSize() {
103                     return entry.getSize();
104                 }
105 
106                 @Override
107                 public long getTime() {
108                     return entry.getTime();
109                 }
110             };
111         }
112 
113         @Override
114         URL getResource(final String name) {
115             final ZipEntry entry = zipFile.getEntry(name);
116             try {
117                 return entry != null ? new URL("jar:file:" + zipFile.getName() + "!/" + name) : null;
118             } catch (final MalformedURLException e) {
119                 return null;
120             }
121         }
122 
123         @Override
124         InputStream getResourceAsStream(final String name) {
125             final ZipEntry entry = zipFile.getEntry(name);
126             try {
127                 return entry != null ? zipFile.getInputStream(entry) : null;
128             } catch (final IOException e) {
129                 return null;
130             }
131         }
132 
133         protected abstract String toEntryName(final String name, final String suffix);
134 
135         @Override
136         public String toString() {
137             return zipFile.getName();
138         }
139 
140     }
141 
142     /**
143      * Contains information about file/ZIP entry of the Java class.
144      */
145     public interface ClassFile {
146 
147         /**
148          * @return base path of found class, i.e. class is contained relative to that path, which may either denote a directory,
149          *         or ZIP file
150          */
151         String getBase();
152 
153         /**
154          * @return input stream for class file.
155          * @throws IOException if an I/O error occurs.
156          */
157         InputStream getInputStream() throws IOException;
158 
159         /**
160          * @return canonical path to class file.
161          */
162         String getPath();
163 
164         /**
165          * @return size of class file.
166          */
167         long getSize();
168 
169         /**
170          * @return modification time of class file.
171          */
172         long getTime();
173     }
174 
175     private static final class Dir extends AbstractPathEntry {
176 
177         private final String dir;
178 
179         Dir(final String d) {
180             dir = d;
181         }
182 
183         @Override
184         public void close() throws IOException {
185             // Nothing to do
186 
187         }
188 
189         @Override
190         ClassFile getClassFile(final String name, final String suffix) {
191             final File file = new File(dir + File.separatorChar + name.replace('.', File.separatorChar) + suffix);
192             return file.exists() ? new ClassFile() {
193 
194                 @Override
195                 public String getBase() {
196                     return dir;
197                 }
198 
199                 @Override
200                 public InputStream getInputStream() throws IOException {
201                     return new FileInputStream(file);
202                 }
203 
204                 @Override
205                 public String getPath() {
206                     try {
207                         return file.getCanonicalPath();
208                     } catch (final IOException e) {
209                         return null;
210                     }
211                 }
212 
213                 @Override
214                 public long getSize() {
215                     return file.length();
216                 }
217 
218                 @Override
219                 public long getTime() {
220                     return file.lastModified();
221                 }
222             } : null;
223         }
224 
225         @Override
226         URL getResource(final String name) {
227             // Resource specification uses '/' whatever the platform
228             final File file = toFile(name);
229             try {
230                 return file.exists() ? file.toURI().toURL() : null;
231             } catch (final MalformedURLException e) {
232                 return null;
233             }
234         }
235 
236         @Override
237         InputStream getResourceAsStream(final String name) {
238             // Resource specification uses '/' whatever the platform
239             final File file = toFile(name);
240             try {
241                 return file.exists() ? new FileInputStream(file) : null;
242             } catch (final IOException e) {
243                 return null;
244             }
245         }
246 
247         private File toFile(final String name) {
248             return new File(dir + File.separatorChar + name.replace('/', File.separatorChar));
249         }
250 
251         @Override
252         public String toString() {
253             return dir;
254         }
255     }
256 
257     private static final class Jar extends AbstractZip {
258 
259         Jar(final ZipFile zip) {
260             super(zip);
261         }
262 
263         @Override
264         protected String toEntryName(final String name, final String suffix) {
265             return Utility.packageToPath(name) + suffix;
266         }
267 
268     }
269 
270     private static final class JrtModule extends AbstractPathEntry {
271 
272         private final Path modulePath;
273 
274         public JrtModule(final Path modulePath) {
275             this.modulePath = Objects.requireNonNull(modulePath, "modulePath");
276         }
277 
278         @Override
279         public void close() throws IOException {
280             // Nothing to do.
281 
282         }
283 
284         @Override
285         ClassFile getClassFile(final String name, final String suffix) {
286             final Path resolved = modulePath.resolve(Utility.packageToPath(name) + suffix);
287             if (Files.exists(resolved)) {
288                 return new ClassFile() {
289 
290                     @Override
291                     public String getBase() {
292                         return Objects.toString(resolved.getFileName(), null);
293                     }
294 
295                     @Override
296                     public InputStream getInputStream() throws IOException {
297                         return Files.newInputStream(resolved);
298                     }
299 
300                     @Override
301                     public String getPath() {
302                         return resolved.toString();
303                     }
304 
305                     @Override
306                     public long getSize() {
307                         try {
308                             return Files.size(resolved);
309                         } catch (final IOException e) {
310                             return 0;
311                         }
312                     }
313 
314                     @Override
315                     public long getTime() {
316                         try {
317                             return Files.getLastModifiedTime(resolved).toMillis();
318                         } catch (final IOException e) {
319                             return 0;
320                         }
321                     }
322                 };
323             }
324             return null;
325         }
326 
327         @Override
328         URL getResource(final String name) {
329             final Path resovled = modulePath.resolve(name);
330             try {
331                 return Files.exists(resovled) ? new URL("jrt:" + modulePath + "/" + name) : null;
332             } catch (final MalformedURLException e) {
333                 return null;
334             }
335         }
336 
337         @Override
338         InputStream getResourceAsStream(final String name) {
339             try {
340                 return Files.newInputStream(modulePath.resolve(name));
341             } catch (final IOException e) {
342                 return null;
343             }
344         }
345 
346         @Override
347         public String toString() {
348             return modulePath.toString();
349         }
350 
351     }
352 
353     private static final class JrtModules extends AbstractPathEntry {
354 
355         private final ModularRuntimeImage modularRuntimeImage;
356         private final JrtModule[] modules;
357 
358         public JrtModules(final String path) throws IOException {
359             this.modularRuntimeImage = new ModularRuntimeImage();
360             this.modules = modularRuntimeImage.list(path).stream().map(JrtModule::new).toArray(JrtModule[]::new);
361         }
362 
363         @Override
364         public void close() throws IOException {
365             if (modules != null) {
366                 // don't use a for each loop to avoid creating an iterator for the GC to collect.
367                 for (final JrtModule module : modules) {
368                     module.close();
369                 }
370             }
371             if (modularRuntimeImage != null) {
372                 modularRuntimeImage.close();
373             }
374         }
375 
376         @Override
377         ClassFile getClassFile(final String name, final String suffix) {
378             // don't use a for each loop to avoid creating an iterator for the GC to collect.
379             for (final JrtModule module : modules) {
380                 final ClassFile classFile = module.getClassFile(name, suffix);
381                 if (classFile != null) {
382                     return classFile;
383                 }
384             }
385             return null;
386         }
387 
388         @Override
389         URL getResource(final String name) {
390             // don't use a for each loop to avoid creating an iterator for the GC to collect.
391             for (final JrtModule module : modules) {
392                 final URL url = module.getResource(name);
393                 if (url != null) {
394                     return url;
395                 }
396             }
397             return null;
398         }
399 
400         @Override
401         InputStream getResourceAsStream(final String name) {
402             // don't use a for each loop to avoid creating an iterator for the GC to collect.
403             for (final JrtModule module : modules) {
404                 final InputStream inputStream = module.getResourceAsStream(name);
405                 if (inputStream != null) {
406                     return inputStream;
407                 }
408             }
409             return null;
410         }
411 
412         @Override
413         public String toString() {
414             return Arrays.toString(modules);
415         }
416 
417     }
418 
419     private static final class Module extends AbstractZip {
420 
421         Module(final ZipFile zip) {
422             super(zip);
423         }
424 
425         @Override
426         protected String toEntryName(final String name, final String suffix) {
427             return "classes/" + Utility.packageToPath(name) + suffix;
428         }
429 
430     }
431 
432     private static final FilenameFilter ARCHIVE_FILTER = (dir, name) -> {
433         name = name.toLowerCase(Locale.ENGLISH);
434         return name.endsWith(".zip") || name.endsWith(".jar");
435     };
436 
437     private static final FilenameFilter MODULES_FILTER = (dir, name) -> {
438         name = name.toLowerCase(Locale.ENGLISH);
439         return name.endsWith(org.apache.bcel.classfile.Module.EXTENSION);
440     };
441 
442     public static final ClassPath SYSTEM_CLASS_PATH = new ClassPath(getClassPath());
443 
444     private static void addJdkModules(final String javaHome, final List<String> list) {
445         String modulesPath = System.getProperty("java.modules.path");
446         if (modulesPath == null || modulesPath.trim().isEmpty()) {
447             // Default to looking in JAVA_HOME/jmods
448             modulesPath = javaHome + File.separator + "jmods";
449         }
450         final File modulesDir = new File(modulesPath);
451         if (modulesDir.exists()) {
452             final String[] modules = modulesDir.list(MODULES_FILTER);
453             if (modules != null) {
454                 for (final String module : modules) {
455                     list.add(modulesDir.getPath() + File.separatorChar + module);
456                 }
457             }
458         }
459     }
460 
461     /**
462      * Checks for class path components in the following properties: "java.class.path", "sun.boot.class.path",
463      * "java.ext.dirs"
464      *
465      * @return class path as used by default by BCEL
466      */
467     // @since 6.0 no longer final
468     public static String getClassPath() {
469         final String classPathProp = System.getProperty("java.class.path");
470         final String bootClassPathProp = System.getProperty("sun.boot.class.path");
471         final String extDirs = System.getProperty("java.ext.dirs");
472         // System.out.println("java.version = " + System.getProperty("java.version"));
473         // System.out.println("java.class.path = " + classPathProp);
474         // System.out.println("sun.boot.class.path=" + bootClassPathProp);
475         // System.out.println("java.ext.dirs=" + extDirs);
476         final String javaHome = System.getProperty("java.home");
477         final List<String> list = new ArrayList<>();
478 
479         // Starting in JRE 9, .class files are in the modules directory. Add them to the path.
480         final Path modulesPath = Paths.get(javaHome).resolve("lib/modules");
481         if (Files.exists(modulesPath) && Files.isRegularFile(modulesPath)) {
482             list.add(modulesPath.toAbsolutePath().toString());
483         }
484         // Starting in JDK 9, .class files are in the jmods directory. Add them to the path.
485         addJdkModules(javaHome, list);
486 
487         getPathComponents(classPathProp, list);
488         getPathComponents(bootClassPathProp, list);
489         final List<String> dirs = new ArrayList<>();
490         getPathComponents(extDirs, dirs);
491         for (final String d : dirs) {
492             final File extDir = new File(d);
493             final String[] extensions = extDir.list(ARCHIVE_FILTER);
494             if (extensions != null) {
495                 for (final String extension : extensions) {
496                     list.add(extDir.getPath() + File.separatorChar + extension);
497                 }
498             }
499         }
500 
501         return list.stream().collect(Collectors.joining(File.pathSeparator));
502     }
503 
504     private static void getPathComponents(final String path, final List<String> list) {
505         if (path != null) {
506             final StringTokenizer tokenizer = new StringTokenizer(path, File.pathSeparator);
507             while (tokenizer.hasMoreTokens()) {
508                 final String name = tokenizer.nextToken();
509                 final File file = new File(name);
510                 if (file.exists()) {
511                     list.add(name);
512                 }
513             }
514         }
515     }
516 
517     private final String classPathString;
518 
519     private final ClassPath parent;
520 
521     private final List<AbstractPathEntry> paths;
522 
523     /**
524      * Search for classes in CLASSPATH.
525      *
526      * @deprecated Use SYSTEM_CLASS_PATH constant
527      */
528     @Deprecated
529     public ClassPath() {
530         this(getClassPath());
531     }
532 
533     @SuppressWarnings("resource")
534     public ClassPath(final ClassPath parent, final String classPathString) {
535         this.parent = parent;
536         this.classPathString = Objects.requireNonNull(classPathString, "classPathString");
537         this.paths = new ArrayList<>();
538         for (final StringTokenizer tokenizer = new StringTokenizer(classPathString, File.pathSeparator); tokenizer.hasMoreTokens();) {
539             final String path = tokenizer.nextToken();
540             if (!path.isEmpty()) {
541                 final File file = new File(path);
542                 try {
543                     if (file.exists()) {
544                         if (file.isDirectory()) {
545                             paths.add(new Dir(path));
546                         } else if (path.endsWith(org.apache.bcel.classfile.Module.EXTENSION)) {
547                             paths.add(new Module(new ZipFile(file)));
548                         } else if (path.endsWith(ModularRuntimeImage.MODULES_PATH)) {
549                             paths.add(new JrtModules(ModularRuntimeImage.MODULES_PATH));
550                         } else {
551                             paths.add(new Jar(new ZipFile(file)));
552                         }
553                     }
554                 } catch (final IOException e) {
555                     if (path.endsWith(".zip") || path.endsWith(".jar")) {
556                         System.err.println("CLASSPATH component " + file + ": " + e);
557                     }
558                 }
559             }
560         }
561     }
562 
563     /**
564      * Search for classes in given path.
565      *
566      * @param classPath
567      */
568     public ClassPath(final String classPath) {
569         this(null, classPath);
570     }
571 
572     @Override
573     public void close() throws IOException {
574         for (final AbstractPathEntry path : paths) {
575             path.close();
576         }
577     }
578 
579     @Override
580     public boolean equals(final Object obj) {
581         if (this == obj) {
582             return true;
583         }
584         if (obj == null) {
585             return false;
586         }
587         if (getClass() != obj.getClass()) {
588             return false;
589         }
590         final ClassPath other = (ClassPath) obj;
591         return Objects.equals(classPathString, other.classPathString);
592     }
593 
594     /**
595      * @param name fully qualified file name, e.g. java/lang/String
596      * @return byte array for class
597      * @throws IOException if an I/O error occurs.
598      */
599     public byte[] getBytes(final String name) throws IOException {
600         return getBytes(name, JavaClass.EXTENSION);
601     }
602 
603     /**
604      * @param name fully qualified file name, e.g. java/lang/String
605      * @param suffix file name ends with suffix, e.g. .java
606      * @return byte array for file on class path
607      * @throws IOException if an I/O error occurs.
608      */
609     public byte[] getBytes(final String name, final String suffix) throws IOException {
610         DataInputStream dis = null;
611         try (InputStream inputStream = getInputStream(name, suffix)) {
612             if (inputStream == null) {
613                 throw new IOException("Couldn't find: " + name + suffix);
614             }
615             dis = new DataInputStream(inputStream);
616             final byte[] bytes = new byte[inputStream.available()];
617             dis.readFully(bytes);
618             return bytes;
619         } finally {
620             if (dis != null) {
621                 dis.close();
622             }
623         }
624     }
625 
626     /**
627      * @param name fully qualified class name, e.g. java.lang.String
628      * @return input stream for class
629      * @throws IOException if an I/O error occurs.
630      */
631     public ClassFile getClassFile(final String name) throws IOException {
632         return getClassFile(name, JavaClass.EXTENSION);
633     }
634 
635     /**
636      * @param name fully qualified file name, e.g. java/lang/String
637      * @param suffix file name ends with suff, e.g. .java
638      * @return class file for the Java class
639      * @throws IOException if an I/O error occurs.
640      */
641     public ClassFile getClassFile(final String name, final String suffix) throws IOException {
642         ClassFile cf = null;
643 
644         if (parent != null) {
645             cf = parent.getClassFileInternal(name, suffix);
646         }
647 
648         if (cf == null) {
649             cf = getClassFileInternal(name, suffix);
650         }
651 
652         if (cf != null) {
653             return cf;
654         }
655 
656         throw new IOException("Couldn't find: " + name + suffix);
657     }
658 
659     private ClassFile getClassFileInternal(final String name, final String suffix) {
660         for (final AbstractPathEntry path : paths) {
661             final ClassFile cf = path.getClassFile(name, suffix);
662             if (cf != null) {
663                 return cf;
664             }
665         }
666         return null;
667     }
668 
669     /**
670      * Gets an InputStream.
671      * <p>
672      * The caller is responsible for closing the InputStream.
673      * </p>
674      * @param name fully qualified class name, e.g. java.lang.String
675      * @return input stream for class
676      * @throws IOException if an I/O error occurs.
677      */
678     public InputStream getInputStream(final String name) throws IOException {
679         return getInputStream(Utility.packageToPath(name), JavaClass.EXTENSION);
680     }
681 
682     /**
683      * Gets an InputStream for a class or resource on the classpath.
684      * <p>
685      * The caller is responsible for closing the InputStream.
686      * </p>
687      *
688      * @param name   fully qualified file name, e.g. java/lang/String
689      * @param suffix file name ends with suff, e.g. .java
690      * @return input stream for file on class path
691      * @throws IOException if an I/O error occurs.
692      */
693     public InputStream getInputStream(final String name, final String suffix) throws IOException {
694         try {
695             final java.lang.ClassLoader classLoader = getClass().getClassLoader();
696             @SuppressWarnings("resource") // closed by caller
697             final
698             InputStream inputStream = classLoader == null ? null : classLoader.getResourceAsStream(name + suffix);
699             if (inputStream != null) {
700                 return inputStream;
701             }
702         } catch (final Exception ignored) {
703             // ignored
704         }
705         return getClassFile(name, suffix).getInputStream();
706     }
707 
708     /**
709      * @param name name of file to search for, e.g. java/lang/String.java
710      * @return full (canonical) path for file
711      * @throws IOException if an I/O error occurs.
712      */
713     public String getPath(String name) throws IOException {
714         final int index = name.lastIndexOf('.');
715         String suffix = "";
716         if (index > 0) {
717             suffix = name.substring(index);
718             name = name.substring(0, index);
719         }
720         return getPath(name, suffix);
721     }
722 
723     /**
724      * @param name name of file to search for, e.g. java/lang/String
725      * @param suffix file name suffix, e.g. .java
726      * @return full (canonical) path for file, if it exists
727      * @throws IOException if an I/O error occurs.
728      */
729     public String getPath(final String name, final String suffix) throws IOException {
730         return getClassFile(name, suffix).getPath();
731     }
732 
733     /**
734      * @param name fully qualified resource name, e.g. java/lang/String.class
735      * @return URL supplying the resource, or null if no resource with that name.
736      * @since 6.0
737      */
738     public URL getResource(final String name) {
739         for (final AbstractPathEntry path : paths) {
740             URL url;
741             if ((url = path.getResource(name)) != null) {
742                 return url;
743             }
744         }
745         return null;
746     }
747 
748     /**
749      * @param name fully qualified resource name, e.g. java/lang/String.class
750      * @return InputStream supplying the resource, or null if no resource with that name.
751      * @since 6.0
752      */
753     public InputStream getResourceAsStream(final String name) {
754         for (final AbstractPathEntry path : paths) {
755             InputStream is;
756             if ((is = path.getResourceAsStream(name)) != null) {
757                 return is;
758             }
759         }
760         return null;
761     }
762 
763     /**
764      * @param name fully qualified resource name, e.g. java/lang/String.class
765      * @return An Enumeration of URLs supplying the resource, or an empty Enumeration if no resource with that name.
766      * @since 6.0
767      */
768     public Enumeration<URL> getResources(final String name) {
769         final Vector<URL> results = new Vector<>();
770         for (final AbstractPathEntry path : paths) {
771             URL url;
772             if ((url = path.getResource(name)) != null) {
773                 results.add(url);
774             }
775         }
776         return results.elements();
777     }
778 
779     @Override
780     public int hashCode() {
781         return classPathString.hashCode();
782     }
783 
784     /**
785      * @return used class path string
786      */
787     @Override
788     public String toString() {
789         if (parent != null) {
790             return parent + File.pathSeparator + classPathString;
791         }
792         return classPathString;
793     }
794 }