001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.bcel.util;
020
021import java.io.Closeable;
022import java.io.DataInputStream;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.FilenameFilter;
026import java.io.IOException;
027import java.io.InputStream;
028import java.net.MalformedURLException;
029import java.net.URL;
030import java.nio.file.Files;
031import java.nio.file.Path;
032import java.nio.file.Paths;
033import java.util.ArrayList;
034import java.util.Arrays;
035import java.util.Collections;
036import java.util.Enumeration;
037import java.util.List;
038import java.util.Objects;
039import java.util.StringTokenizer;
040import java.util.stream.Collectors;
041import java.util.zip.ZipEntry;
042import java.util.zip.ZipFile;
043
044import org.apache.bcel.classfile.JavaClass;
045import org.apache.bcel.classfile.Utility;
046import org.apache.commons.io.IOUtils;
047import org.apache.commons.lang3.StringUtils;
048import org.apache.commons.lang3.SystemProperties;
049
050/**
051 * Loads class files from the CLASSPATH. Inspired by sun.tools.ClassPath.
052 */
053public class ClassPath implements Closeable {
054
055    private abstract static class AbstractPathEntry implements Closeable {
056
057        abstract ClassFile getClassFile(String name, String suffix);
058
059        abstract URL getResource(String name);
060
061        abstract InputStream getResourceAsStream(String name);
062    }
063
064    private abstract static class AbstractZip extends AbstractPathEntry {
065
066        private final ZipFile zipFile;
067
068        AbstractZip(final ZipFile zipFile) {
069            this.zipFile = Objects.requireNonNull(zipFile, "zipFile");
070        }
071
072        @Override
073        public void close() throws IOException {
074            IOUtils.close(zipFile);
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(String name, 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         * Gets the base path of found class.
150         *
151         * @return base path of found class, for example class is contained relative to that path, which may either denote a directory,
152         *         or ZIP file.
153         */
154        String getBase();
155
156        /**
157         * Gets the input stream for class file.
158         *
159         * @return input stream for class file.
160         * @throws IOException if an I/O error occurs.
161         */
162        InputStream getInputStream() throws IOException;
163
164        /**
165         * Gets the canonical path to class file.
166         *
167         * @return canonical path to class file.
168         */
169        String getPath();
170
171        /**
172         * Gets the size of class file.
173         *
174         * @return size of class file.
175         */
176        long getSize();
177
178        /**
179         * Gets the modification time of class file.
180         *
181         * @return modification time of class file.
182         */
183        long getTime();
184    }
185
186    private static final class Dir extends AbstractPathEntry {
187
188        private final String dir;
189
190        Dir(final String d) {
191            dir = d;
192        }
193
194        @Override
195        public void close() throws IOException {
196            // Nothing to do
197
198        }
199
200        @Override
201        ClassFile getClassFile(final String name, final String suffix) {
202            final File file = new File(dir + File.separatorChar + name.replace('.', File.separatorChar) + suffix);
203            return file.exists() ? new ClassFile() {
204
205                @Override
206                public String getBase() {
207                    return dir;
208                }
209
210                @Override
211                public InputStream getInputStream() throws IOException {
212                    return new FileInputStream(file);
213                }
214
215                @Override
216                public String getPath() {
217                    try {
218                        return file.getCanonicalPath();
219                    } catch (final IOException e) {
220                        return null;
221                    }
222                }
223
224                @Override
225                public long getSize() {
226                    return file.length();
227                }
228
229                @Override
230                public long getTime() {
231                    return file.lastModified();
232                }
233            } : null;
234        }
235
236        @Override
237        URL getResource(final String name) {
238            // Resource specification uses '/' whatever the platform
239            final File file = toFile(name);
240            try {
241                return file.exists() ? file.toURI().toURL() : null;
242            } catch (final MalformedURLException e) {
243                return null;
244            }
245        }
246
247        @Override
248        InputStream getResourceAsStream(final String name) {
249            // Resource specification uses '/' whatever the platform
250            final File file = toFile(name);
251            try {
252                return file.exists() ? new FileInputStream(file) : null;
253            } catch (final IOException e) {
254                return null;
255            }
256        }
257
258        private File toFile(final String name) {
259            return new File(dir + File.separatorChar + name.replace('/', File.separatorChar));
260        }
261
262        @Override
263        public String toString() {
264            return dir;
265        }
266    }
267
268    private static final class Jar extends AbstractZip {
269
270        Jar(final ZipFile zip) {
271            super(zip);
272        }
273
274        @Override
275        protected String toEntryName(final String name, final String suffix) {
276            return Utility.packageToPath(name) + suffix;
277        }
278
279    }
280
281    private static final class JrtModule extends AbstractPathEntry {
282
283        private final Path modulePath;
284
285        JrtModule(final Path modulePath) {
286            this.modulePath = Objects.requireNonNull(modulePath, "modulePath");
287        }
288
289        @Override
290        public void close() throws IOException {
291            // Nothing to do.
292
293        }
294
295        @Override
296        ClassFile getClassFile(final String name, final String suffix) {
297            final Path resolved = modulePath.resolve(Utility.packageToPath(name) + suffix);
298            if (Files.exists(resolved)) {
299                return new ClassFile() {
300
301                    @Override
302                    public String getBase() {
303                        return Objects.toString(resolved.getFileName(), null);
304                    }
305
306                    @Override
307                    public InputStream getInputStream() throws IOException {
308                        return Files.newInputStream(resolved);
309                    }
310
311                    @Override
312                    public String getPath() {
313                        return resolved.toString();
314                    }
315
316                    @Override
317                    public long getSize() {
318                        try {
319                            return Files.size(resolved);
320                        } catch (final IOException e) {
321                            return 0;
322                        }
323                    }
324
325                    @Override
326                    public long getTime() {
327                        try {
328                            return Files.getLastModifiedTime(resolved).toMillis();
329                        } catch (final IOException e) {
330                            return 0;
331                        }
332                    }
333                };
334            }
335            return null;
336        }
337
338        @Override
339        URL getResource(final String name) {
340            final Path resovled = modulePath.resolve(name);
341            try {
342                return Files.exists(resovled) ? new URL("jrt:" + modulePath + "/" + name) : null;
343            } catch (final MalformedURLException e) {
344                return null;
345            }
346        }
347
348        @Override
349        InputStream getResourceAsStream(final String name) {
350            try {
351                return Files.newInputStream(modulePath.resolve(name));
352            } catch (final IOException e) {
353                return null;
354            }
355        }
356
357        @Override
358        public String toString() {
359            return modulePath.toString();
360        }
361
362    }
363
364    private static final class JrtModules extends AbstractPathEntry {
365
366        private final ModularRuntimeImage modularRuntimeImage;
367        private final JrtModule[] modules;
368
369        JrtModules(final String path) throws IOException {
370            this.modularRuntimeImage = new ModularRuntimeImage();
371            this.modules = modularRuntimeImage.list(path).stream().map(JrtModule::new).toArray(JrtModule[]::new);
372        }
373
374        @Override
375        public void close() throws IOException {
376            if (modules != null) {
377                // don't use a for each loop to avoid creating an iterator for the GC to collect.
378                for (final JrtModule module : modules) {
379                    module.close();
380                }
381            }
382            if (modularRuntimeImage != null) {
383                modularRuntimeImage.close();
384            }
385        }
386
387        @Override
388        ClassFile getClassFile(final String name, final String suffix) {
389            // don't use a for each loop to avoid creating an iterator for the GC to collect.
390            for (final JrtModule module : modules) {
391                final ClassFile classFile = module.getClassFile(name, suffix);
392                if (classFile != null) {
393                    return classFile;
394                }
395            }
396            return null;
397        }
398
399        @Override
400        URL getResource(final String name) {
401            // don't use a for each loop to avoid creating an iterator for the GC to collect.
402            for (final JrtModule module : modules) {
403                final URL url = module.getResource(name);
404                if (url != null) {
405                    return url;
406                }
407            }
408            return null;
409        }
410
411        @Override
412        InputStream getResourceAsStream(final String name) {
413            // don't use a for each loop to avoid creating an iterator for the GC to collect.
414            for (final JrtModule module : modules) {
415                final InputStream inputStream = module.getResourceAsStream(name);
416                if (inputStream != null) {
417                    return inputStream;
418                }
419            }
420            return null;
421        }
422
423        @Override
424        public String toString() {
425            return Arrays.toString(modules);
426        }
427
428    }
429
430    private static final class Module extends AbstractZip {
431
432        Module(final ZipFile zip) {
433            super(zip);
434        }
435
436        @Override
437        protected String toEntryName(final String name, final String suffix) {
438            return "classes/" + Utility.packageToPath(name) + suffix;
439        }
440
441    }
442
443    /** Filter for archive files (.zip and .jar). */
444    private static final FilenameFilter ARCHIVE_FILTER = (dir, name) -> {
445        name = StringUtils.toRootLowerCase(name);
446        return name.endsWith(".zip") || name.endsWith(".jar");
447    };
448
449    /** Filter for module files. */
450    private static final FilenameFilter MODULES_FILTER = (dir, name) -> {
451        name = StringUtils.toRootLowerCase(name);
452        return name.endsWith(org.apache.bcel.classfile.Module.EXTENSION);
453    };
454
455    /** The system class path. */
456    public static final ClassPath SYSTEM_CLASS_PATH = new ClassPath(getClassPath());
457
458    private static void addJdkModules(final String javaHome, final List<String> list) {
459        String modulesPath = SystemProperties.getJdkModulePath();
460        if (modulesPath == null || modulesPath.trim().isEmpty()) {
461            // Default to looking in JAVA_HOME/jmods
462            modulesPath = javaHome + File.separator + "jmods";
463        }
464        final File modulesDir = new File(modulesPath);
465        if (modulesDir.exists()) {
466            final String[] modules = modulesDir.list(MODULES_FILTER);
467            if (modules != null) {
468                for (final String module : modules) {
469                    list.add(modulesDir.getPath() + File.separatorChar + module);
470                }
471            }
472        }
473    }
474
475    /**
476     * Checks for class path components in the following properties: "java.class.path", "sun.boot.class.path",
477     * "java.ext.dirs"
478     *
479     * @return class path as used by default by BCEL.
480     */
481    // @since 6.0 no longer final
482    public static String getClassPath() {
483        final String classPathProp = SystemProperties.getJavaClassPath();
484        final String bootClassPathProp = System.getProperty("sun.boot.class.path");
485        final String extDirs = SystemProperties.getJavaExtDirs();
486        // System.out.println("java.version = " + System.getProperty("java.version"));
487        // System.out.println("java.class.path = " + classPathProp);
488        // System.out.println("sun.boot.class.path=" + bootClassPathProp);
489        // System.out.println("java.ext.dirs=" + extDirs);
490        final String javaHome = SystemProperties.getJavaHome();
491        final List<String> list = new ArrayList<>();
492
493        // Starting in JRE 9, .class files are in the modules directory. Add them to the path.
494        final Path modulesPath = Paths.get(javaHome).resolve("lib/modules");
495        if (Files.exists(modulesPath) && Files.isRegularFile(modulesPath)) {
496            list.add(modulesPath.toAbsolutePath().toString());
497        }
498        // Starting in JDK 9, .class files are in the jmods directory. Add them to the path.
499        addJdkModules(javaHome, list);
500
501        getPathComponents(classPathProp, list);
502        getPathComponents(bootClassPathProp, list);
503        final List<String> dirs = new ArrayList<>();
504        getPathComponents(extDirs, dirs);
505        for (final String d : dirs) {
506            final File extDir = new File(d);
507            final String[] extensions = extDir.list(ARCHIVE_FILTER);
508            if (extensions != null) {
509                for (final String extension : extensions) {
510                    list.add(extDir.getPath() + File.separatorChar + extension);
511                }
512            }
513        }
514
515        return list.stream().collect(Collectors.joining(File.pathSeparator));
516    }
517
518    private static void getPathComponents(final String path, final List<String> list) {
519        if (path != null) {
520            final StringTokenizer tokenizer = new StringTokenizer(path, File.pathSeparator);
521            while (tokenizer.hasMoreTokens()) {
522                final String name = tokenizer.nextToken();
523                final File file = new File(name);
524                if (file.exists()) {
525                    list.add(name);
526                }
527            }
528        }
529    }
530
531    private final String classPathString;
532
533    private final ClassPath parent;
534
535    private final List<AbstractPathEntry> paths;
536
537    /**
538     * Search for classes in CLASSPATH.
539     *
540     * @deprecated Use SYSTEM_CLASS_PATH constant
541     */
542    @Deprecated
543    public ClassPath() {
544        this(getClassPath());
545    }
546
547    /**
548     * Constructs a ClassPath with a parent and class path string.
549     *
550     * @param parent the parent ClassPath.
551     * @param classPathString the class path string.
552     */
553    @SuppressWarnings("resource")
554    public ClassPath(final ClassPath parent, final String classPathString) {
555        this.parent = parent;
556        this.classPathString = Objects.requireNonNull(classPathString, "classPathString");
557        this.paths = new ArrayList<>();
558        for (final StringTokenizer tokenizer = new StringTokenizer(classPathString, File.pathSeparator); tokenizer.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                            paths.add(new Dir(path));
566                        } else if (path.endsWith(org.apache.bcel.classfile.Module.EXTENSION)) {
567                            paths.add(new Module(new ZipFile(file)));
568                        } else if (path.endsWith(ModularRuntimeImage.MODULES_PATH)) {
569                            paths.add(new JrtModules(ModularRuntimeImage.MODULES_PATH));
570                        } else {
571                            paths.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    }
582
583    /**
584     * Search for classes in given path.
585     *
586     * @param classPath the class path string.
587     */
588    public ClassPath(final String classPath) {
589        this(null, classPath);
590    }
591
592    @Override
593    public void close() throws IOException {
594        for (final AbstractPathEntry path : paths) {
595            path.close();
596        }
597    }
598
599    @Override
600    public boolean equals(final Object obj) {
601        if (this == obj) {
602            return true;
603        }
604        if (obj == null) {
605            return false;
606        }
607        if (getClass() != obj.getClass()) {
608            return false;
609        }
610        final ClassPath other = (ClassPath) obj;
611        return Objects.equals(classPathString, other.classPathString);
612    }
613
614    /**
615     * Gets byte array for the given class.
616     *
617     * @param name fully qualified file name, for example java/lang/String.
618     * @return byte array for class.
619     * @throws IOException if an I/O error occurs.
620     */
621    public byte[] getBytes(final String name) throws IOException {
622        return getBytes(name, JavaClass.EXTENSION);
623    }
624
625    /**
626     * Gets byte array for the given file.
627     *
628     * @param name fully qualified file name, for example java/lang/String.
629     * @param suffix file name ends with suffix, for example .java.
630     * @return byte array for file on class path.
631     * @throws IOException if an I/O error occurs.
632     */
633    public byte[] getBytes(final String name, final String suffix) throws IOException {
634        DataInputStream dis = null;
635        try (InputStream inputStream = getInputStream(name, suffix)) {
636            if (inputStream == null) {
637                throw new IOException("Couldn't find: " + name + suffix);
638            }
639            dis = new DataInputStream(inputStream);
640            final byte[] bytes = new byte[inputStream.available()];
641            dis.readFully(bytes);
642            return bytes;
643        } finally {
644            if (dis != null) {
645                dis.close();
646            }
647        }
648    }
649
650    /**
651     * Gets the input stream for the given class.
652     *
653     * @param name fully qualified class name, for example {@link String}.
654     * @return input stream for class.
655     * @throws IOException if an I/O error occurs.
656     */
657    public ClassFile getClassFile(final String name) throws IOException {
658        return getClassFile(name, JavaClass.EXTENSION);
659    }
660
661    /**
662     * Gets the class file for the given Java class.
663     *
664     * @param name fully qualified file name, for example java/lang/String.
665     * @param suffix file name ends with suffix, for example .java.
666     * @return class file for the Java class.
667     * @throws IOException if an I/O error occurs.
668     */
669    public ClassFile getClassFile(final String name, final String suffix) throws IOException {
670        ClassFile cf = null;
671
672        if (parent != null) {
673            cf = parent.getClassFileInternal(name, suffix);
674        }
675
676        if (cf == null) {
677            cf = getClassFileInternal(name, suffix);
678        }
679
680        if (cf != null) {
681            return cf;
682        }
683
684        throw new IOException("Couldn't find: " + name + suffix);
685    }
686
687    private ClassFile getClassFileInternal(final String name, final String suffix) {
688        for (final AbstractPathEntry path : paths) {
689            final ClassFile cf = path.getClassFile(name, suffix);
690            if (cf != null) {
691                return cf;
692            }
693        }
694        return null;
695    }
696
697    /**
698     * Gets an InputStream.
699     * <p>
700     * The caller is responsible for closing the InputStream.
701     * </p>
702     *
703     * @param name fully qualified class name, for example {@link String}.
704     * @return input stream for class.
705     * @throws IOException if an I/O error occurs.
706     */
707    public InputStream getInputStream(final String name) throws IOException {
708        return getInputStream(Utility.packageToPath(name), JavaClass.EXTENSION);
709    }
710
711    /**
712     * Gets an InputStream for a class or resource on the classpath.
713     * <p>
714     * The caller is responsible for closing the InputStream.
715     * </p>
716     *
717     * @param name   fully qualified file name, for example java/lang/String.
718     * @param suffix file name ends with suff, for example .java.
719     * @return input stream for file on class path.
720     * @throws IOException if an I/O error occurs.
721     */
722    public InputStream getInputStream(final String name, final String suffix) throws IOException {
723        try {
724            final java.lang.ClassLoader classLoader = getClass().getClassLoader();
725            @SuppressWarnings("resource") // closed by caller
726            final
727            InputStream inputStream = classLoader == null ? null : classLoader.getResourceAsStream(name + suffix);
728            if (inputStream != null) {
729                return inputStream;
730            }
731        } catch (final Exception ignored) {
732            // ignored
733        }
734        return getClassFile(name, suffix).getInputStream();
735    }
736
737    /**
738     * Gets the full canonical path for the given file.
739     *
740     * @param name name of file to search for, for example java/lang/String.java.
741     * @return full (canonical) path for file.
742     * @throws IOException if an I/O error occurs.
743     */
744    public String getPath(String name) throws IOException {
745        final int index = name.lastIndexOf('.');
746        String suffix = "";
747        if (index > 0) {
748            suffix = name.substring(index);
749            name = name.substring(0, index);
750        }
751        return getPath(name, suffix);
752    }
753
754    /**
755     * Gets the full canonical path for the given file.
756     *
757     * @param name name of file to search for, for example java/lang/String.
758     * @param suffix file name suffix, for example .java.
759     * @return full (canonical) path for file, if it exists.
760     * @throws IOException if an I/O error occurs.
761     */
762    public String getPath(final String name, final String suffix) throws IOException {
763        return getClassFile(name, suffix).getPath();
764    }
765
766    /**
767     * Gets the URL for the given resource.
768     *
769     * @param name fully qualified resource name, for example java/lang/String.class.
770     * @return URL supplying the resource, or null if no resource with that name.
771     * @since 6.0
772     */
773    public URL getResource(final String name) {
774        for (final AbstractPathEntry path : paths) {
775            final URL url;
776            if ((url = path.getResource(name)) != null) {
777                return url;
778            }
779        }
780        return null;
781    }
782
783    /**
784     * Gets the InputStream for the given resource.
785     *
786     * @param name fully qualified resource name, for example java/lang/String.class.
787     * @return InputStream supplying the resource, or null if no resource with that name.
788     * @since 6.0
789     */
790    public InputStream getResourceAsStream(final String name) {
791        for (final AbstractPathEntry path : paths) {
792            final InputStream is;
793            if ((is = path.getResourceAsStream(name)) != null) {
794                return is;
795            }
796        }
797        return null;
798    }
799
800    /**
801     * Gets an Enumeration of URLs for the given resource.
802     *
803     * @param name fully qualified resource name, for example java/lang/String.class.
804     * @return An Enumeration of URLs supplying the resource, or an empty Enumeration if no resource with that name.
805     * @since 6.0
806     */
807    public Enumeration<URL> getResources(final String name) {
808        final List<URL> list = new ArrayList<>();
809        for (final AbstractPathEntry path : paths) {
810            final URL url;
811            if ((url = path.getResource(name)) != null) {
812                list.add(url);
813            }
814        }
815        return Collections.enumeration(list);
816    }
817
818    @Override
819    public int hashCode() {
820        return classPathString.hashCode();
821    }
822
823    /**
824     * @return used class path string.
825     */
826    @Override
827    public String toString() {
828        if (parent != null) {
829            return parent + File.pathSeparator + classPathString;
830        }
831        return classPathString;
832    }
833}