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}