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