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