    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$
    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}