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