    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 * Loads class files from the CLASSPATH. Inspired by sun.tools.ClassPath.
    045 */
    046public class ClassPath implements Closeable {
    047
    048    private abstract static class AbstractPathEntry implements Closeable {
    049
    050        abstract ClassFile getClassFile(String name, String suffix);
    051
    052        abstract URL getResource(String name);
    053
    054        abstract InputStream getResourceAsStream(String name);
    055    }
    056
    057    private abstract static class AbstractZip extends AbstractPathEntry {
    058
    059        private final ZipFile zipFile;
    060
    061        AbstractZip(final ZipFile zipFile) {
    062            this.zipFile = Objects.requireNonNull(zipFile, "zipFile");
    063        }
    064
    065        @Override
    066        public void close() throws IOException {
    067            if (zipFile != null) {
    068                zipFile.close();
    069            }
    070
    071        }
    072
    073        @Override
    074        ClassFile getClassFile(final String name, final String suffix) {
    075            final ZipEntry entry = zipFile.getEntry(toEntryName(name, suffix));
    076
    077            if (entry == null) {
    078                return null;
    079            }
    080
    081            return new ClassFile() {
    082
    083                @Override
    084                public String getBase() {
    085                    return zipFile.getName();
    086                }
    087
    088                @Override
    089                public InputStream getInputStream() throws IOException {
    090                    return zipFile.getInputStream(entry);
    091                }
    092
    093                @Override
    094                public String getPath() {
    095                    return entry.toString();
    096                }
    097
    098                @Override
    099                public long getSize() {
    100                    return entry.getSize();
    101                }
    102
    103                @Override
    104                public long getTime() {
    105                    return entry.getTime();
    106                }
    107            };
    108        }
    109
    110        @Override
    111        URL getResource(final String name) {
    112            final ZipEntry entry = zipFile.getEntry(name);
    113            try {
    114                return entry != null ? new URL("jar:file:" + zipFile.getName() + "!/" + name) : null;
    115            } catch (final MalformedURLException e) {
    116                return null;
    117            }
    118        }
    119
    120        @Override
    121        InputStream getResourceAsStream(final String name) {
    122            final ZipEntry entry = zipFile.getEntry(name);
    123            try {
    124                return entry != null ? zipFile.getInputStream(entry) : null;
    125            } catch (final IOException e) {
    126                return null;
    127            }
    128        }
    129
    130        protected abstract String toEntryName(final String name, final String suffix);
    131
    132        @Override
    133        public String toString() {
    134            return zipFile.getName();
    135        }
    136
    137    }
    138
    139    /**
    140     * Contains information about file/ZIP entry of the Java class.
    141     */
    142    public interface ClassFile {
    143
    144        /**
    145         * @return base path of found class, i.e. class is contained relative to that path, which may either denote a directory,
    146         *         or zip file
    147         */
    148        String getBase();
    149
    150        /**
    151         * @return input stream for class file.
    152         * @throws IOException if an I/O error occurs.
    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) {
    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) {
    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 Objects.toString(resolved.getFileName(), null);
    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            this.modules = modularRuntimeImage.list(path).stream().map(JrtModule::new).toArray(JrtModule[]::new);
    358        }
    359
    360        @Override
    361        public void close() throws IOException {
    362            if (modules != null) {
    363                // don't use a for each loop to avoid creating an iterator for the GC to collect.
    364                for (final JrtModule module : modules) {
    365                    module.close();
    366                }
    367            }
    368            if (modularRuntimeImage != null) {
    369                modularRuntimeImage.close();
    370            }
    371        }
    372
    373        @Override
    374        ClassFile getClassFile(final String name, final String suffix) {
    375            // don't use a for each loop to avoid creating an iterator for the GC to collect.
    376            for (final JrtModule module : modules) {
    377                final ClassFile classFile = module.getClassFile(name, suffix);
    378                if (classFile != null) {
    379                    return classFile;
    380                }
    381            }
    382            return null;
    383        }
    384
    385        @Override
    386        URL getResource(final String name) {
    387            // don't use a for each loop to avoid creating an iterator for the GC to collect.
    388            for (final JrtModule module : modules) {
    389                final URL url = module.getResource(name);
    390                if (url != null) {
    391                    return url;
    392                }
    393            }
    394            return null;
    395        }
    396
    397        @Override
    398        InputStream getResourceAsStream(final String name) {
    399            // don't use a for each loop to avoid creating an iterator for the GC to collect.
    400            for (final JrtModule module : modules) {
    401                final InputStream inputStream = module.getResourceAsStream(name);
    402                if (inputStream != null) {
    403                    return inputStream;
    404                }
    405            }
    406            return null;
    407        }
    408
    409        @Override
    410        public String toString() {
    411            return Arrays.toString(modules);
    412        }
    413
    414    }
    415
    416    private static class Module extends AbstractZip {
    417
    418        Module(final ZipFile zip) {
    419            super(zip);
    420        }
    421
    422        @Override
    423        protected String toEntryName(final String name, final String suffix) {
    424            return "classes/" + packageToFolder(name) + suffix;
    425        }
    426
    427    }
    428
    429    private static final FilenameFilter ARCHIVE_FILTER = (dir, name) -> {
    430        name = name.toLowerCase(Locale.ENGLISH);
    431        return name.endsWith(".zip") || name.endsWith(".jar");
    432    };
    433
    434    private static final FilenameFilter MODULES_FILTER = (dir, name) -> {
    435        name = name.toLowerCase(Locale.ENGLISH);
    436        return name.endsWith(".jmod");
    437    };
    438
    439    public static final ClassPath SYSTEM_CLASS_PATH = new ClassPath(getClassPath());
    440
    441    private static void addJdkModules(final String javaHome, final List<String> list) {
    442        String modulesPath = System.getProperty("java.modules.path");
    443        if (modulesPath == null || modulesPath.trim().isEmpty()) {
    444            // Default to looking in JAVA_HOME/jmods
    445            modulesPath = javaHome + File.separator + "jmods";
    446        }
    447        final File modulesDir = new File(modulesPath);
    448        if (modulesDir.exists()) {
    449            final String[] modules = modulesDir.list(MODULES_FILTER);
    450            if (modules != null) {
    451                for (final String module : modules) {
    452                    list.add(modulesDir.getPath() + File.separatorChar + module);
    453                }
    454            }
    455        }
    456    }
    457
    458    /**
    459     * Checks for class path components in the following properties: "java.class.path", "sun.boot.class.path",
    460     * "java.ext.dirs"
    461     *
    462     * @return class path as used by default by BCEL
    463     */
    464    // @since 6.0 no longer final
    465    public static String getClassPath() {
    466        final String classPathProp = System.getProperty("java.class.path");
    467        final String bootClassPathProp = System.getProperty("sun.boot.class.path");
    468        final String extDirs = System.getProperty("java.ext.dirs");
    469        // System.out.println("java.version = " + System.getProperty("java.version"));
    470        // System.out.println("java.class.path = " + classPathProp);
    471        // System.out.println("sun.boot.class.path=" + bootClassPathProp);
    472        // System.out.println("java.ext.dirs=" + extDirs);
    473        final String javaHome = System.getProperty("java.home");
    474        final List<String> list = new ArrayList<>();
    475
    476        // Starting in JRE 9, .class files are in the modules directory. Add them to the path.
    477        final Path modulesPath = Paths.get(javaHome).resolve("lib/modules");
    478        if (Files.exists(modulesPath) && Files.isRegularFile(modulesPath)) {
    479            list.add(modulesPath.toAbsolutePath().toString());
    480        }
    481        // Starting in JDK 9, .class files are in the jmods directory. Add them to the path.
    482        addJdkModules(javaHome, list);
    483
    484        getPathComponents(classPathProp, list);
    485        getPathComponents(bootClassPathProp, list);
    486        final List<String> dirs = new ArrayList<>();
    487        getPathComponents(extDirs, dirs);
    488        for (final String d : dirs) {
    489            final File ext_dir = new File(d);
    490            final String[] extensions = ext_dir.list(ARCHIVE_FILTER);
    491            if (extensions != null) {
    492                for (final String extension : extensions) {
    493                    list.add(ext_dir.getPath() + File.separatorChar + extension);
    494                }
    495            }
    496        }
    497
    498        final StringBuilder buf = new StringBuilder();
    499        String separator = "";
    500        for (final String path : list) {
    501            buf.append(separator);
    502            separator = File.pathSeparator;
    503            buf.append(path);
    504        }
    505        return buf.toString().intern();
    506    }
    507
    508    private static void getPathComponents(final String path, final List<String> list) {
    509        if (path != null) {
    510            final StringTokenizer tokenizer = new StringTokenizer(path, File.pathSeparator);
    511            while (tokenizer.hasMoreTokens()) {
    512                final String name = tokenizer.nextToken();
    513                final File file = new File(name);
    514                if (file.exists()) {
    515                    list.add(name);
    516                }
    517            }
    518        }
    519    }
    520
    521    static String packageToFolder(final String name) {
    522        return name.replace('.', '/');
    523    }
    524
    525    private final String classPath;
    526
    527    private ClassPath parent;
    528
    529    private final AbstractPathEntry[] paths;
    530
    531    /**
    532     * Search for classes in CLASSPATH.
    533     *
    534     * @deprecated Use SYSTEM_CLASS_PATH constant
    535     */
    536    @Deprecated
    537    public ClassPath() {
    538        this(getClassPath());
    539    }
    540
    541    public ClassPath(final ClassPath parent, final String classPath) {
    542        this(classPath);
    543        this.parent = parent;
    544    }
    545
    546    /**
    547     * Search for classes in given path.
    548     *
    549     * @param classPath
    550     */
    551    @SuppressWarnings("resource")
    552    public ClassPath(final String classPath) {
    553        this.classPath = classPath;
    554        final List<AbstractPathEntry> list = new ArrayList<>();
    555        for (final StringTokenizer tokenizer = new StringTokenizer(classPath, File.pathSeparator); tokenizer.hasMoreTokens();) {
    556            final String path = tokenizer.nextToken();
    557            if (!path.isEmpty()) {
    558                final File file = new File(path);
    559                try {
    560                    if (file.exists()) {
    561                        if (file.isDirectory()) {
    562                            list.add(new Dir(path));
    563                        } else if (path.endsWith(".jmod")) {
    564                            list.add(new Module(new ZipFile(file)));
    565                        } else if (path.endsWith(ModularRuntimeImage.MODULES_PATH)) {
    566                            list.add(new JrtModules(ModularRuntimeImage.MODULES_PATH));
    567                        } else {
    568                            list.add(new Jar(new ZipFile(file)));
    569                        }
    570                    }
    571                } catch (final IOException e) {
    572                    if (path.endsWith(".zip") || path.endsWith(".jar")) {
    573                        System.err.println("CLASSPATH component " + file + ": " + e);
    574                    }
    575                }
    576            }
    577        }
    578        paths = new AbstractPathEntry[list.size()];
    579        list.toArray(paths);
    580    }
    581
    582    @Override
    583    public void close() throws IOException {
    584        if (paths != null) {
    585            for (final AbstractPathEntry path : paths) {
    586                path.close();
    587            }
    588        }
    589
    590    }
    591
    592    @Override
    593    public boolean equals(final Object o) {
    594        if (o instanceof ClassPath) {
    595            final ClassPath cp = (ClassPath) o;
    596            return classPath.equals(cp.toString());
    597        }
    598        return false;
    599    }
    600
    601    /**
    602     * @param name fully qualified file name, e.g. java/lang/String
    603     * @return byte array for class
    604     * @throws IOException if an I/O error occurs.
    605     */
    606    public byte[] getBytes(final String name) throws IOException {
    607        return getBytes(name, ".class");
    608    }
    609
    610    /**
    611     * @param name fully qualified file name, e.g. java/lang/String
    612     * @param suffix file name ends with suffix, e.g. .java
    613     * @return byte array for file on class path
    614     * @throws IOException if an I/O error occurs.
    615     */
    616    public byte[] getBytes(final String name, final String suffix) throws IOException {
    617        DataInputStream dis = null;
    618        try (InputStream inputStream = getInputStream(name, suffix)) {
    619            if (inputStream == null) {
    620                throw new IOException("Couldn't find: " + name + suffix);
    621            }
    622            dis = new DataInputStream(inputStream);
    623            final byte[] bytes = new byte[inputStream.available()];
    624            dis.readFully(bytes);
    625            return bytes;
    626        } finally {
    627            if (dis != null) {
    628                dis.close();
    629            }
    630        }
    631    }
    632
    633    /**
    634     * @param name fully qualified class name, e.g. java.lang.String
    635     * @return input stream for class
    636     * @throws IOException if an I/O error occurs.
    637     */
    638    public ClassFile getClassFile(final String name) throws IOException {
    639        return getClassFile(name, ".class");
    640    }
    641
    642    /**
    643     * @param name fully qualified file name, e.g. java/lang/String
    644     * @param suffix file name ends with suff, e.g. .java
    645     * @return class file for the java class
    646     * @throws IOException if an I/O error occurs.
    647     */
    648    public ClassFile getClassFile(final String name, final String suffix) throws IOException {
    649        ClassFile cf = null;
    650
    651        if (parent != null) {
    652            cf = parent.getClassFileInternal(name, suffix);
    653        }
    654
    655        if (cf == null) {
    656            cf = getClassFileInternal(name, suffix);
    657        }
    658
    659        if (cf != null) {
    660            return cf;
    661        }
    662
    663        throw new IOException("Couldn't find: " + name + suffix);
    664    }
    665
    666    private ClassFile getClassFileInternal(final String name, final String suffix) {
    667        for (final AbstractPathEntry path : paths) {
    668            final ClassFile cf = path.getClassFile(name, suffix);
    669            if (cf != null) {
    670                return cf;
    671            }
    672        }
    673        return null;
    674    }
    675
    676    /**
    677     * @param name fully qualified class name, e.g. java.lang.String
    678     * @return input stream for class
    679     * @throws IOException if an I/O error occurs.
    680     */
    681    public InputStream getInputStream(final String name) throws IOException {
    682        return getInputStream(packageToFolder(name), ".class");
    683    }
    684
    685    /**
    686     * Return stream for class or resource on CLASSPATH.
    687     *
    688     * @param name fully qualified file name, e.g. java/lang/String
    689     * @param suffix file name ends with suff, e.g. .java
    690     * @return input stream for file on class path
    691     * @throws IOException if an I/O error occurs.
    692     */
    693    public InputStream getInputStream(final String name, final String suffix) throws IOException {
    694        InputStream inputStream = null;
    695        try {
    696            inputStream = getClass().getClassLoader().getResourceAsStream(name + suffix); // may return null
    697        } catch (final Exception ignored) {
    698            // ignored
    699        }
    700        if (inputStream != null) {
    701            return inputStream;
    702        }
    703        return getClassFile(name, suffix).getInputStream();
    704    }
    705
    706    /**
    707     * @param name name of file to search for, e.g. java/lang/String.java
    708     * @return full (canonical) path for file
    709     * @throws IOException if an I/O error occurs.
    710     */
    711    public String getPath(String name) throws IOException {
    712        final int index = name.lastIndexOf('.');
    713        String suffix = "";
    714        if (index > 0) {
    715            suffix = name.substring(index);
    716            name = name.substring(0, index);
    717        }
    718        return getPath(name, suffix);
    719    }
    720
    721    /**
    722     * @param name name of file to search for, e.g. java/lang/String
    723     * @param suffix file name suffix, e.g. .java
    724     * @return full (canonical) path for file, if it exists
    725     * @throws IOException if an I/O error occurs.
    726     */
    727    public String getPath(final String name, final String suffix) throws IOException {
    728        return getClassFile(name, suffix).getPath();
    729    }
    730
    731    /**
    732     * @param name fully qualified resource name, e.g. java/lang/String.class
    733     * @return URL supplying the resource, or null if no resource with that name.
    734     * @since 6.0
    735     */
    736    public URL getResource(final String name) {
    737        for (final AbstractPathEntry path : paths) {
    738            URL url;
    739            if ((url = path.getResource(name)) != null) {
    740                return url;
    741            }
    742        }
    743        return null;
    744    }
    745
    746    /**
    747     * @param name fully qualified resource name, e.g. java/lang/String.class
    748     * @return InputStream supplying the resource, or null if no resource with that name.
    749     * @since 6.0
    750     */
    751    public InputStream getResourceAsStream(final String name) {
    752        for (final AbstractPathEntry path : paths) {
    753            InputStream is;
    754            if ((is = path.getResourceAsStream(name)) != null) {
    755                return is;
    756            }
    757        }
    758        return null;
    759    }
    760
    761    /**
    762     * @param name fully qualified resource name, e.g. java/lang/String.class
    763     * @return An Enumeration of URLs supplying the resource, or an empty Enumeration if no resource with that name.
    764     * @since 6.0
    765     */
    766    public Enumeration<URL> getResources(final String name) {
    767        final Vector<URL> results = new Vector<>();
    768        for (final AbstractPathEntry path : paths) {
    769            URL url;
    770            if ((url = path.getResource(name)) != null) {
    771                results.add(url);
    772            }
    773        }
    774        return results.elements();
    775    }
    776
    777    @Override
    778    public int hashCode() {
    779        if (parent != null) {
    780            return classPath.hashCode() + parent.hashCode();
    781        }
    782        return classPath.hashCode();
    783    }
    784
    785    /**
    786     * @return used class path string
    787     */
    788    @Override
    789    public String toString() {
    790        if (parent != null) {
    791            return parent + File.pathSeparator + classPath;
    792        }
    793        return classPath;
    794    }
    795}