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   */
18  package org.apache.bcel.util;
19  
20  import java.io.Closeable;
21  import java.io.DataInputStream;
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.Enumeration;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Objects;
38  import java.util.StringTokenizer;
39  import java.util.Vector;
40  import java.util.zip.ZipEntry;
41  import java.util.zip.ZipFile;
42  
43  /**
44   * Responsible for loading (class) files from the CLASSPATH. Inspired by sun.tools.ClassPath.
45   *
46   */
47  public class ClassPath implements Closeable {
48  
49      private abstract static class AbstractPathEntry implements Closeable {
50  
51          abstract ClassFile getClassFile(String name, String suffix) throws IOException;
52  
53          abstract URL getResource(String name);
54  
55          abstract InputStream getResourceAsStream(String name);
56      }
57  
58      private abstract static class AbstractZip extends AbstractPathEntry {
59  
60          private final ZipFile zipFile;
61  
62          AbstractZip(final ZipFile zipFile) {
63              this.zipFile = Objects.requireNonNull(zipFile, "zipFile");
64          }
65  
66          @Override
67          public void close() throws IOException {
68              if (zipFile != null) {
69                  zipFile.close();
70              }
71  
72          }
73  
74          @Override
75          ClassFile getClassFile(final String name, final String suffix) throws IOException {
76              final ZipEntry entry = zipFile.getEntry(toEntryName(name, suffix));
77  
78              if (entry == null) {
79                  return null;
80              }
81  
82              return new ClassFile() {
83  
84                  @Override
85                  public String getBase() {
86                      return zipFile.getName();
87                  }
88  
89                  @Override
90                  public InputStream getInputStream() throws IOException {
91                      return zipFile.getInputStream(entry);
92                  }
93  
94                  @Override
95                  public String getPath() {
96                      return entry.toString();
97                  }
98  
99                  @Override
100                 public long getSize() {
101                     return entry.getSize();
102                 }
103 
104                 @Override
105                 public long getTime() {
106                     return entry.getTime();
107                 }
108             };
109         }
110 
111         @Override
112         URL getResource(final String name) {
113             final ZipEntry entry = zipFile.getEntry(name);
114             try {
115                 return entry != null ? new URL("jar:file:" + zipFile.getName() + "!/" + name) : null;
116             } catch (final MalformedURLException e) {
117                 return null;
118             }
119         }
120 
121         @Override
122         InputStream getResourceAsStream(final String name) {
123             final ZipEntry entry = zipFile.getEntry(name);
124             try {
125                 return entry != null ? zipFile.getInputStream(entry) : null;
126             } catch (final IOException e) {
127                 return null;
128             }
129         }
130 
131         protected abstract String toEntryName(final String name, final String suffix);
132 
133         @Override
134         public String toString() {
135             return zipFile.getName();
136         }
137 
138     }
139 
140     /**
141      * Contains information about file/ZIP entry of the Java class.
142      */
143     public interface ClassFile {
144 
145         /**
146          * @return base path of found class, i.e. class is contained relative to that path, which may either denote a
147          *         directory, or zip file
148          */
149         String getBase();
150 
151         /**
152          * @return input stream for class file.
153          */
154         InputStream getInputStream() throws IOException;
155 
156         /**
157          * @return canonical path to class file.
158          */
159         String getPath();
160 
161         /**
162          * @return size of class file.
163          */
164         long getSize();
165 
166         /**
167          * @return modification time of class file.
168          */
169         long getTime();
170     }
171 
172     private static class Dir extends AbstractPathEntry {
173 
174         private final String dir;
175 
176         Dir(final String d) {
177             dir = d;
178         }
179 
180         @Override
181         public void close() throws IOException {
182             // Nothing to do
183 
184         }
185 
186         @Override
187         ClassFile getClassFile(final String name, final String suffix) throws IOException {
188             final File file = new File(dir + File.separatorChar + name.replace('.', File.separatorChar) + suffix);
189             return file.exists() ? new ClassFile() {
190 
191                 @Override
192                 public String getBase() {
193                     return dir;
194                 }
195 
196                 @Override
197                 public InputStream getInputStream() throws IOException {
198                     return new FileInputStream(file);
199                 }
200 
201                 @Override
202                 public String getPath() {
203                     try {
204                         return file.getCanonicalPath();
205                     } catch (final IOException e) {
206                         return null;
207                     }
208                 }
209 
210                 @Override
211                 public long getSize() {
212                     return file.length();
213                 }
214 
215                 @Override
216                 public long getTime() {
217                     return file.lastModified();
218                 }
219             } : null;
220         }
221 
222         @Override
223         URL getResource(final String name) {
224             // Resource specification uses '/' whatever the platform
225             final File file = toFile(name);
226             try {
227                 return file.exists() ? file.toURI().toURL() : null;
228             } catch (final MalformedURLException e) {
229                 return null;
230             }
231         }
232 
233         @Override
234         InputStream getResourceAsStream(final String name) {
235             // Resource specification uses '/' whatever the platform
236             final File file = toFile(name);
237             try {
238                 return file.exists() ? new FileInputStream(file) : null;
239             } catch (final IOException e) {
240                 return null;
241             }
242         }
243 
244         private File toFile(final String name) {
245             return new File(dir + File.separatorChar + name.replace('/', File.separatorChar));
246         }
247 
248         @Override
249         public String toString() {
250             return dir;
251         }
252     }
253 
254     private static class Jar extends AbstractZip {
255 
256         Jar(final ZipFile zip) {
257             super(zip);
258         }
259 
260         @Override
261         protected String toEntryName(final String name, final String suffix) {
262             return packageToFolder(name) + suffix;
263         }
264 
265     }
266 
267     private static class JrtModule extends AbstractPathEntry {
268 
269         private final Path modulePath;
270 
271         public JrtModule(final Path modulePath) {
272             this.modulePath = Objects.requireNonNull(modulePath, "modulePath");
273         }
274 
275         @Override
276         public void close() throws IOException {
277             // Nothing to do.
278 
279         }
280 
281         @Override
282         ClassFile getClassFile(final String name, final String suffix) throws IOException {
283             final Path resolved = modulePath.resolve(packageToFolder(name) + suffix);
284             if (Files.exists(resolved)) {
285                 return new ClassFile() {
286 
287                     @Override
288                     public String getBase() {
289                         return resolved.getFileName().toString();
290                     }
291 
292                     @Override
293                     public InputStream getInputStream() throws IOException {
294                         return Files.newInputStream(resolved);
295                     }
296 
297                     @Override
298                     public String getPath() {
299                         return resolved.toString();
300                     }
301 
302                     @Override
303                     public long getSize() {
304                         try {
305                             return Files.size(resolved);
306                         } catch (final IOException e) {
307                             return 0;
308                         }
309                     }
310 
311                     @Override
312                     public long getTime() {
313                         try {
314                             return Files.getLastModifiedTime(resolved).toMillis();
315                         } catch (final IOException e) {
316                             return 0;
317                         }
318                     }
319                 };
320             }
321             return null;
322         }
323 
324         @Override
325         URL getResource(final String name) {
326             final Path resovled = modulePath.resolve(name);
327             try {
328                 return Files.exists(resovled) ? new URL("jrt:" + modulePath + "/" + name) : null;
329             } catch (final MalformedURLException e) {
330                 return null;
331             }
332         }
333 
334         @Override
335         InputStream getResourceAsStream(final String name) {
336             try {
337                 return Files.newInputStream(modulePath.resolve(name));
338             } catch (final IOException e) {
339                 return null;
340             }
341         }
342 
343         @Override
344         public String toString() {
345             return modulePath.toString();
346         }
347 
348     }
349 
350     private static class JrtModules extends AbstractPathEntry {
351 
352         private final ModularRuntimeImage modularRuntimeImage;
353         private final JrtModule[] modules;
354 
355         public JrtModules(final String path) throws IOException {
356             this.modularRuntimeImage = new ModularRuntimeImage();
357             final List<Path> list = modularRuntimeImage.list(path);
358             this.modules = new JrtModule[list.size()];
359             for (int i = 0; i < modules.length; i++) {
360                 modules[i] = new JrtModule(list.get(i));
361             }
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 (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) throws IOException {
379             // don't use a for each loop to avoid creating an iterator for the GC to collect.
380             for (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 (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 (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 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/" + packageToFolder(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(".jmod");
441     };
442 
443     public static final ClassPathath">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             for (String module : modules) {
455                 list.add(modulesDir.getPath() + File.separatorChar + module);
456             }
457         }
458     }
459 
460     /**
461      * Checks for class path components in the following properties: "java.class.path", "sun.boot.class.path",
462      * "java.ext.dirs"
463      *
464      * @return class path as used by default by BCEL
465      */
466     // @since 6.0 no longer final
467     public static String getClassPath() {
468         final String classPathProp = System.getProperty("java.class.path");
469         final String bootClassPathProp = System.getProperty("sun.boot.class.path");
470         final String extDirs = System.getProperty("java.ext.dirs");
471         // System.out.println("java.version = " + System.getProperty("java.version"));
472         // System.out.println("java.class.path = " + classPathProp);
473         // System.out.println("sun.boot.class.path=" + bootClassPathProp);
474         // System.out.println("java.ext.dirs=" + extDirs);
475         final String javaHome = System.getProperty("java.home");
476         final List<String> list = new ArrayList<>();
477 
478         // Starting in JRE 9, .class files are in the modules directory. Add them to the path.
479         final Path modulesPath = Paths.get(javaHome).resolve("lib/modules");
480         if (Files.exists(modulesPath) && Files.isRegularFile(modulesPath)) {
481             list.add(modulesPath.toAbsolutePath().toString());
482         }
483         // Starting in JDK 9, .class files are in the jmods directory. Add them to the path.
484         addJdkModules(javaHome, list);
485 
486         getPathComponents(classPathProp, list);
487         getPathComponents(bootClassPathProp, list);
488         final List<String> dirs = new ArrayList<>();
489         getPathComponents(extDirs, dirs);
490         for (final String d : dirs) {
491             final File ext_dir = new File(d);
492             final String[] extensions = ext_dir.list(ARCHIVE_FILTER);
493             if (extensions != null) {
494                 for (final String extension : extensions) {
495                     list.add(ext_dir.getPath() + File.separatorChar + extension);
496                 }
497             }
498         }
499 
500         final StringBuilder buf = new StringBuilder();
501         String separator = "";
502         for (final String path : list) {
503             buf.append(separator);
504             separator = File.pathSeparator;
505             buf.append(path);
506         }
507         return buf.toString().intern();
508     }
509 
510     private static void getPathComponents(final String path, final List<String> list) {
511         if (path != null) {
512             final StringTokenizer tokenizer = new StringTokenizer(path, File.pathSeparator);
513             while (tokenizer.hasMoreTokens()) {
514                 final String name = tokenizer.nextToken();
515                 final File file = new File(name);
516                 if (file.exists()) {
517                     list.add(name);
518                 }
519             }
520         }
521     }
522 
523     static String packageToFolder(final String name) {
524         return name.replace('.', '/');
525     }
526 
527     private final String classPath;
528 
529     private ClassPath parent;
530 
531     private final AbstractPathEntry[] paths;
532 
533     /**
534      * Search for classes in CLASSPATH.
535      *
536      * @deprecated Use SYSTEM_CLASS_PATH constant
537      */
538     @Deprecated
539     public ClassPath() {
540         this(getClassPath());
541     }
542 
543     public ClassPathssPath.html#ClassPath">ClassPath(final ClassPath parent, final String classPath) {
544         this(classPath);
545         this.parent = parent;
546     }
547 
548     /**
549      * Search for classes in given path.
550      *
551      * @param classPath
552      */
553     @SuppressWarnings("resource")
554     public ClassPath(final String classPath) {
555         this.classPath = classPath;
556         final List<AbstractPathEntry> list = new ArrayList<>();
557         for (final StringTokenizer tokenizer = new StringTokenizer(classPath, File.pathSeparator); tokenizer
558                 .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                             list.add(new Dir(path));
566                         } else if (path.endsWith(".jmod")) {
567                             list.add(new Module(new ZipFile(file)));
568                         } else if (path.endsWith(ModularRuntimeImage.MODULES_PATH)) {
569                             list.add(new JrtModules(ModularRuntimeImage.MODULES_PATH));
570                         } else {
571                             list.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         paths = new AbstractPathEntry[list.size()];
582         list.toArray(paths);
583     }
584 
585     @Override
586     public void close() throws IOException {
587         if (paths != null) {
588             for (final AbstractPathEntry path : paths) {
589                 path.close();
590             }
591         }
592 
593     }
594 
595     @Override
596     public boolean equals(final Object o) {
597         if (o instanceof ClassPath) {
598             final ClassPath"../../../../org/apache/bcel/util/ClassPath.html#ClassPath">ClassPath cp = (ClassPath) o;
599             return classPath.equals(cp.toString());
600         }
601         return false;
602     }
603 
604     /**
605      * @return byte array for class
606      */
607     public byte[] getBytes(final String name) throws IOException {
608         return getBytes(name, ".class");
609     }
610 
611     /**
612      * @param name
613      *            fully qualified file name, e.g. java/lang/String
614      * @param suffix
615      *            file name ends with suffix, e.g. .java
616      * @return byte array for file on class path
617      */
618     public byte[] getBytes(final String name, final String suffix) throws IOException {
619         DataInputStream dis = null;
620         try (InputStream inputStream = getInputStream(name, suffix)) {
621             if (inputStream == null) {
622                 throw new IOException("Couldn't find: " + name + suffix);
623             }
624             dis = new DataInputStream(inputStream);
625             final byte[] bytes = new byte[inputStream.available()];
626             dis.readFully(bytes);
627             return bytes;
628         } finally {
629             if (dis != null) {
630                 dis.close();
631             }
632         }
633     }
634 
635     /**
636      * @param name
637      *            fully qualified class name, e.g. java.lang.String
638      * @return input stream for class
639      */
640     public ClassFile getClassFile(final String name) throws IOException {
641         return getClassFile(name, ".class");
642     }
643 
644     /**
645      * @param name
646      *            fully qualified file name, e.g. java/lang/String
647      * @param suffix
648      *            file name ends with suff, e.g. .java
649      * @return class file for the java class
650      */
651     public ClassFile getClassFile(final String name, final String suffix) throws IOException {
652         ClassFile cf = null;
653 
654         if (parent != null) {
655             cf = parent.getClassFileInternal(name, suffix);
656         }
657 
658         if (cf == null) {
659             cf = getClassFileInternal(name, suffix);
660         }
661 
662         if (cf != null) {
663             return cf;
664         }
665 
666         throw new IOException("Couldn't find: " + name + suffix);
667     }
668 
669     private ClassFile getClassFileInternal(final String name, final String suffix) throws IOException {
670 
671         for (final AbstractPathEntry path : paths) {
672             final ClassFile cf = path.getClassFile(name, suffix);
673 
674             if (cf != null) {
675                 return cf;
676             }
677         }
678 
679         return null;
680     }
681 
682     /**
683      * @param name
684      *            fully qualified class name, e.g. java.lang.String
685      * @return input stream for class
686      */
687     public InputStream getInputStream(final String name) throws IOException {
688         return getInputStream(packageToFolder(name), ".class");
689     }
690 
691     /**
692      * Return stream for class or resource on CLASSPATH.
693      *
694      * @param name
695      *            fully qualified file name, e.g. java/lang/String
696      * @param suffix
697      *            file name ends with suff, e.g. .java
698      * @return input stream for file on class path
699      */
700     public InputStream getInputStream(final String name, final String suffix) throws IOException {
701         InputStream inputStream = null;
702         try {
703             inputStream = getClass().getClassLoader().getResourceAsStream(name + suffix); // may return null
704         } catch (final Exception e) {
705             // ignored
706         }
707         if (inputStream != null) {
708             return inputStream;
709         }
710         return getClassFile(name, suffix).getInputStream();
711     }
712 
713     /**
714      * @param name
715      *            name of file to search for, e.g. java/lang/String.java
716      * @return full (canonical) path for file
717      */
718     public String getPath(String name) throws IOException {
719         final int index = name.lastIndexOf('.');
720         String suffix = "";
721         if (index > 0) {
722             suffix = name.substring(index);
723             name = name.substring(0, index);
724         }
725         return getPath(name, suffix);
726     }
727 
728     /**
729      * @param name
730      *            name of file to search for, e.g. java/lang/String
731      * @param suffix
732      *            file name suffix, e.g. .java
733      * @return full (canonical) path for file, if it exists
734      */
735     public String getPath(final String name, final String suffix) throws IOException {
736         return getClassFile(name, suffix).getPath();
737     }
738 
739     /**
740      * @param name
741      *            fully qualified resource name, e.g. java/lang/String.class
742      * @return URL supplying the resource, or null if no resource with that name.
743      * @since 6.0
744      */
745     public URL getResource(final String name) {
746         for (final AbstractPathEntry path : paths) {
747             URL url;
748             if ((url = path.getResource(name)) != null) {
749                 return url;
750             }
751         }
752         return null;
753     }
754 
755     /**
756      * @param name
757      *            fully qualified resource name, e.g. java/lang/String.class
758      * @return InputStream supplying the resource, or null if no resource with that name.
759      * @since 6.0
760      */
761     public InputStream getResourceAsStream(final String name) {
762         for (final AbstractPathEntry path : paths) {
763             InputStream is;
764             if ((is = path.getResourceAsStream(name)) != null) {
765                 return is;
766             }
767         }
768         return null;
769     }
770 
771     /**
772      * @param name
773      *            fully qualified resource name, e.g. java/lang/String.class
774      * @return An Enumeration of URLs supplying the resource, or an empty Enumeration if no resource with that name.
775      * @since 6.0
776      */
777     public Enumeration<URL> getResources(final String name) {
778         final Vector<URL> results = new Vector<>();
779         for (final AbstractPathEntry path : paths) {
780             URL url;
781             if ((url = path.getResource(name)) != null) {
782                 results.add(url);
783             }
784         }
785         return results.elements();
786     }
787 
788     @Override
789     public int hashCode() {
790         if (parent != null) {
791             return classPath.hashCode() + parent.hashCode();
792         }
793         return classPath.hashCode();
794     }
795 
796     /**
797      * @return used class path string
798      */
799     @Override
800     public String toString() {
801         if (parent != null) {
802             return parent + File.pathSeparator + classPath;
803         }
804         return classPath;
805     }
806 }