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