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