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