ClassFileReader.java revision 3792:d516975e8110
1/*
2 * Copyright (c) 2012, 2014, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package com.sun.tools.jdeps;
27
28import com.sun.tools.classfile.AccessFlags;
29import com.sun.tools.classfile.ClassFile;
30import com.sun.tools.classfile.ConstantPoolException;
31import com.sun.tools.classfile.Dependencies.ClassFileError;
32
33import jdk.internal.util.jar.VersionedStream;
34
35import java.io.Closeable;
36import java.io.File;
37import java.io.FileNotFoundException;
38import java.io.IOException;
39import java.io.InputStream;
40import java.io.UncheckedIOException;
41import java.nio.file.FileSystem;
42import java.nio.file.FileSystems;
43import java.nio.file.Files;
44import java.nio.file.Path;
45import java.util.ArrayList;
46import java.util.Collections;
47import java.util.Enumeration;
48import java.util.Iterator;
49import java.util.List;
50import java.util.NoSuchElementException;
51import java.util.Set;
52import java.util.jar.JarEntry;
53import java.util.jar.JarFile;
54import java.util.stream.Collectors;
55import java.util.stream.Stream;
56import java.util.zip.ZipFile;
57
58/**
59 * ClassFileReader reads ClassFile(s) of a given path that can be
60 * a .class file, a directory, or a JAR file.
61 */
62public class ClassFileReader implements Closeable {
63    /**
64     * Returns a ClassFileReader instance of a given path.
65     */
66    public static ClassFileReader newInstance(Path path) throws IOException {
67        return newInstance(path, null);
68    }
69
70    /**
71     * Returns a ClassFileReader instance of a given path.
72     */
73    public static ClassFileReader newInstance(Path path, Runtime.Version version) throws IOException {
74        if (Files.notExists(path)) {
75            throw new FileNotFoundException(path.toString());
76        }
77
78        if (Files.isDirectory(path)) {
79            return new DirectoryReader(path);
80        } else if (path.getFileName().toString().endsWith(".jar")) {
81            return new JarFileReader(path, version);
82        } else {
83            return new ClassFileReader(path);
84        }
85    }
86
87    /**
88     * Returns a ClassFileReader instance of a given FileSystem and path.
89     *
90     * This method is used for reading classes from jrtfs.
91     */
92    public static ClassFileReader newInstance(FileSystem fs, Path path) throws IOException {
93        return new DirectoryReader(fs, path);
94    }
95
96    protected final Path path;
97    protected final String baseFileName;
98    protected Set<String> entries; // binary names
99
100    protected final List<String> skippedEntries = new ArrayList<>();
101    protected ClassFileReader(Path path) {
102        this.path = path;
103        this.baseFileName = path.getFileName() != null
104                                ? path.getFileName().toString()
105                                : path.toString();
106    }
107
108    public String getFileName() {
109        return baseFileName;
110    }
111
112    public List<String> skippedEntries() {
113        return skippedEntries;
114    }
115
116    /**
117     * Returns all entries in this archive.
118     */
119    public Set<String> entries() {
120        Set<String> es = this.entries;
121        if (es == null) {
122            // lazily scan the entries
123            this.entries = scan();
124        }
125        return this.entries;
126    }
127
128    /**
129     * Returns the ClassFile matching the given binary name
130     * or a fully-qualified class name.
131     */
132    public ClassFile getClassFile(String name) throws IOException {
133        if (name.indexOf('.') > 0) {
134            int i = name.lastIndexOf('.');
135            String pathname = name.replace('.', File.separatorChar) + ".class";
136            if (baseFileName.equals(pathname) ||
137                    baseFileName.equals(pathname.substring(0, i) + "$" +
138                                        pathname.substring(i+1, pathname.length()))) {
139                return readClassFile(path);
140            }
141        } else {
142            if (baseFileName.equals(name.replace('/', File.separatorChar) + ".class")) {
143                return readClassFile(path);
144            }
145        }
146        return null;
147    }
148
149    public Iterable<ClassFile> getClassFiles() throws IOException {
150        return new Iterable<ClassFile>() {
151            public Iterator<ClassFile> iterator() {
152                return new FileIterator();
153            }
154        };
155    }
156
157    protected ClassFile readClassFile(Path p) throws IOException {
158        InputStream is = null;
159        try {
160            is = Files.newInputStream(p);
161            return ClassFile.read(is);
162        } catch (ConstantPoolException e) {
163            throw new ClassFileError(e);
164        } finally {
165            if (is != null) {
166                is.close();
167            }
168        }
169    }
170
171    protected Set<String> scan() {
172        try {
173            ClassFile cf = ClassFile.read(path);
174            String name = cf.access_flags.is(AccessFlags.ACC_MODULE)
175                ? "module-info" : cf.getName();
176            return Collections.singleton(name);
177        } catch (ConstantPoolException|IOException e) {
178            throw new ClassFileError(e);
179        }
180    }
181
182    static boolean isClass(Path file) {
183        String fn = file.getFileName().toString();
184        return fn.endsWith(".class");
185    }
186
187    @Override
188    public void close() throws IOException {
189    }
190
191    class FileIterator implements Iterator<ClassFile> {
192        int count;
193        FileIterator() {
194            this.count = 0;
195        }
196        public boolean hasNext() {
197            return count == 0 && baseFileName.endsWith(".class");
198        }
199
200        public ClassFile next() {
201            if (!hasNext()) {
202                throw new NoSuchElementException();
203            }
204            try {
205                ClassFile cf = readClassFile(path);
206                count++;
207                return cf;
208            } catch (IOException e) {
209                throw new ClassFileError(e);
210            }
211        }
212
213        public void remove() {
214            throw new UnsupportedOperationException("Not supported yet.");
215        }
216    }
217
218    public String toString() {
219        return path.toString();
220    }
221
222    private static class DirectoryReader extends ClassFileReader {
223        protected final String fsSep;
224        DirectoryReader(Path path) throws IOException {
225            this(FileSystems.getDefault(), path);
226        }
227        DirectoryReader(FileSystem fs, Path path) throws IOException {
228            super(path);
229            this.fsSep = fs.getSeparator();
230        }
231
232        protected Set<String> scan() {
233            try (Stream<Path> stream = Files.walk(path, Integer.MAX_VALUE)) {
234                return stream.filter(ClassFileReader::isClass)
235                             .map(f -> path.relativize(f))
236                             .map(Path::toString)
237                             .map(p -> p.replace(File.separatorChar, '/'))
238                             .collect(Collectors.toSet());
239            } catch (IOException e) {
240                throw new UncheckedIOException(e);
241            }
242        }
243
244        public ClassFile getClassFile(String name) throws IOException {
245            if (name.indexOf('.') > 0) {
246                int i = name.lastIndexOf('.');
247                String pathname = name.replace(".", fsSep) + ".class";
248                Path p = path.resolve(pathname);
249                if (Files.notExists(p)) {
250                    p = path.resolve(pathname.substring(0, i) + "$" +
251                            pathname.substring(i+1, pathname.length()));
252                }
253                if (Files.exists(p)) {
254                    return readClassFile(p);
255                }
256            } else {
257                Path p = path.resolve(name + ".class");
258                if (Files.exists(p)) {
259                    return readClassFile(p);
260                }
261            }
262            return null;
263        }
264
265        public Iterable<ClassFile> getClassFiles() throws IOException {
266            final Iterator<ClassFile> iter = new DirectoryIterator();
267            return new Iterable<ClassFile>() {
268                public Iterator<ClassFile> iterator() {
269                    return iter;
270                }
271            };
272        }
273
274        class DirectoryIterator implements Iterator<ClassFile> {
275            private final List<Path> entries;
276            private int index = 0;
277            DirectoryIterator() throws IOException {
278                List<Path> paths = null;
279                try (Stream<Path> stream = Files.walk(path, Integer.MAX_VALUE)) {
280                    paths = stream.filter(ClassFileReader::isClass)
281                                  .collect(Collectors.toList());
282                }
283                this.entries = paths;
284                this.index = 0;
285            }
286
287            public boolean hasNext() {
288                return index != entries.size();
289            }
290
291            public ClassFile next() {
292                if (!hasNext()) {
293                    throw new NoSuchElementException();
294                }
295                Path path = entries.get(index++);
296                try {
297                    return readClassFile(path);
298                } catch (IOException e) {
299                    throw new ClassFileError(e);
300                }
301            }
302
303            public void remove() {
304                throw new UnsupportedOperationException("Not supported yet.");
305            }
306        }
307    }
308
309    static class JarFileReader extends ClassFileReader {
310        private final JarFile jarfile;
311        private final Runtime.Version version;
312
313        JarFileReader(Path path, Runtime.Version version) throws IOException {
314            this(path, openJarFile(path.toFile(), version), version);
315        }
316
317        JarFileReader(Path path, JarFile jf, Runtime.Version version) throws IOException {
318            super(path);
319            this.jarfile = jf;
320            this.version = version;
321        }
322
323        @Override
324        public void close() throws IOException {
325            jarfile.close();
326        }
327
328        private static JarFile openJarFile(File f, Runtime.Version version)
329                throws IOException {
330            JarFile jf;
331            if (version == null) {
332                jf = new JarFile(f, false);
333                if (jf.isMultiRelease()) {
334                    throw new MultiReleaseException("err.multirelease.option.notfound", f.getName());
335                }
336            } else {
337                jf = new JarFile(f, false, ZipFile.OPEN_READ, version);
338                if (!jf.isMultiRelease()) {
339                    throw new MultiReleaseException("err.multirelease.option.exists", f.getName());
340                }
341            }
342            return jf;
343        }
344
345        protected Set<String> scan() {
346            try (JarFile jf = openJarFile(path.toFile(), version)) {
347                return VersionedStream.stream(jf).map(JarEntry::getName)
348                         .filter(n -> n.endsWith(".class"))
349                         .collect(Collectors.toSet());
350            } catch (IOException e) {
351                throw new UncheckedIOException(e);
352            }
353        }
354
355        public ClassFile getClassFile(String name) throws IOException {
356            if (name.indexOf('.') > 0) {
357                int i = name.lastIndexOf('.');
358                String entryName = name.replace('.', '/') + ".class";
359                JarEntry e = jarfile.getJarEntry(entryName);
360                if (e == null) {
361                    e = jarfile.getJarEntry(entryName.substring(0, i) + "$"
362                            + entryName.substring(i + 1, entryName.length()));
363                }
364                if (e != null) {
365                    return readClassFile(jarfile, e);
366                }
367            } else {
368                JarEntry e = jarfile.getJarEntry(name + ".class");
369                if (e != null) {
370                    return readClassFile(jarfile, e);
371                }
372            }
373            return null;
374        }
375
376        protected ClassFile readClassFile(JarFile jarfile, JarEntry e) throws IOException {
377            try (InputStream is = jarfile.getInputStream(e)) {
378                ClassFile cf = ClassFile.read(is);
379                if (jarfile.isMultiRelease()) {
380                    VersionHelper.add(jarfile, e, cf);
381                }
382                return cf;
383            } catch (ConstantPoolException ex) {
384                throw new ClassFileError(ex);
385            }
386        }
387
388        public Iterable<ClassFile> getClassFiles() throws IOException {
389            final Iterator<ClassFile> iter = new JarFileIterator(this, jarfile);
390            return new Iterable<ClassFile>() {
391                public Iterator<ClassFile> iterator() {
392                    return iter;
393                }
394            };
395        }
396    }
397
398    Enumeration<JarEntry> versionedEntries(JarFile jf) {
399        Iterator<JarEntry> it = VersionedStream.stream(jf).iterator();
400        return new Enumeration<>() {
401            @Override
402            public boolean hasMoreElements() {
403                return it.hasNext();
404            }
405
406            @Override
407            public JarEntry nextElement() {
408                return it.next();
409            }
410        };
411    }
412
413    class JarFileIterator implements Iterator<ClassFile> {
414        protected final JarFileReader reader;
415        protected Enumeration<JarEntry> entries;
416        protected JarFile jf;
417        protected JarEntry nextEntry;
418        protected ClassFile cf;
419        JarFileIterator(JarFileReader reader) {
420            this(reader, null);
421        }
422        JarFileIterator(JarFileReader reader, JarFile jarfile) {
423            this.reader = reader;
424            setJarFile(jarfile);
425        }
426
427        void setJarFile(JarFile jarfile) {
428            if (jarfile == null) return;
429
430            this.jf = jarfile;
431            this.entries = versionedEntries(jf);
432            this.nextEntry = nextEntry();
433        }
434
435        public boolean hasNext() {
436            if (nextEntry != null && cf != null) {
437                return true;
438            }
439            while (nextEntry != null) {
440                try {
441                    cf = reader.readClassFile(jf, nextEntry);
442                    return true;
443                } catch (ClassFileError | IOException ex) {
444                    skippedEntries.add(String.format("%s: %s (%s)",
445                                                     ex.getMessage(),
446                                                     nextEntry.getName(),
447                                                     jf.getName()));
448                }
449                nextEntry = nextEntry();
450            }
451            return false;
452        }
453
454        public ClassFile next() {
455            if (!hasNext()) {
456                throw new NoSuchElementException();
457            }
458            ClassFile classFile = cf;
459            cf = null;
460            nextEntry = nextEntry();
461            return classFile;
462        }
463
464        protected JarEntry nextEntry() {
465            while (entries.hasMoreElements()) {
466                JarEntry e = entries.nextElement();
467                String name = e.getName();
468                if (name.endsWith(".class")) {
469                    return e;
470                }
471            }
472            return null;
473        }
474
475        public void remove() {
476            throw new UnsupportedOperationException("Not supported yet.");
477        }
478    }
479    private static final String MODULE_INFO = "module-info.class";
480}
481