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