ClassFileReader.java revision 3827:44bdefe64114
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 FileIterator::new;
151    }
152
153    protected ClassFile readClassFile(Path p) throws IOException {
154        InputStream is = null;
155        try {
156            is = Files.newInputStream(p);
157            return ClassFile.read(is);
158        } catch (ConstantPoolException e) {
159            throw new ClassFileError(e);
160        } finally {
161            if (is != null) {
162                is.close();
163            }
164        }
165    }
166
167    protected Set<String> scan() {
168        try {
169            ClassFile cf = ClassFile.read(path);
170            String name = cf.access_flags.is(AccessFlags.ACC_MODULE)
171                ? "module-info" : cf.getName();
172            return Collections.singleton(name);
173        } catch (ConstantPoolException|IOException e) {
174            throw new ClassFileError(e);
175        }
176    }
177
178    static boolean isClass(Path file) {
179        String fn = file.getFileName().toString();
180        return fn.endsWith(".class");
181    }
182
183    @Override
184    public void close() throws IOException {
185    }
186
187    class FileIterator implements Iterator<ClassFile> {
188        int count;
189        FileIterator() {
190            this.count = 0;
191        }
192        public boolean hasNext() {
193            return count == 0 && baseFileName.endsWith(".class");
194        }
195
196        public ClassFile next() {
197            if (!hasNext()) {
198                throw new NoSuchElementException();
199            }
200            try {
201                ClassFile cf = readClassFile(path);
202                count++;
203                return cf;
204            } catch (IOException e) {
205                throw new ClassFileError(e);
206            }
207        }
208
209        public void remove() {
210            throw new UnsupportedOperationException("Not supported yet.");
211        }
212    }
213
214    public String toString() {
215        return path.toString();
216    }
217
218    private static class DirectoryReader extends ClassFileReader {
219        protected final String fsSep;
220        DirectoryReader(Path path) throws IOException {
221            this(FileSystems.getDefault(), path);
222        }
223        DirectoryReader(FileSystem fs, Path path) throws IOException {
224            super(path);
225            this.fsSep = fs.getSeparator();
226        }
227
228        protected Set<String> scan() {
229            try (Stream<Path> stream = Files.walk(path, Integer.MAX_VALUE)) {
230                return stream.filter(ClassFileReader::isClass)
231                             .map(path::relativize)
232                             .map(Path::toString)
233                             .map(p -> p.replace(File.separatorChar, '/'))
234                             .collect(Collectors.toSet());
235            } catch (IOException e) {
236                throw new UncheckedIOException(e);
237            }
238        }
239
240        public ClassFile getClassFile(String name) throws IOException {
241            if (name.indexOf('.') > 0) {
242                int i = name.lastIndexOf('.');
243                String pathname = name.replace(".", fsSep) + ".class";
244                Path p = path.resolve(pathname);
245                if (Files.notExists(p)) {
246                    p = path.resolve(pathname.substring(0, i) + "$" +
247                            pathname.substring(i+1, pathname.length()));
248                }
249                if (Files.exists(p)) {
250                    return readClassFile(p);
251                }
252            } else {
253                Path p = path.resolve(name + ".class");
254                if (Files.exists(p)) {
255                    return readClassFile(p);
256                }
257            }
258            return null;
259        }
260
261        public Iterable<ClassFile> getClassFiles() throws IOException {
262            final Iterator<ClassFile> iter = new DirectoryIterator();
263            return () -> iter;
264        }
265
266        class DirectoryIterator implements Iterator<ClassFile> {
267            private final List<Path> entries;
268            private int index = 0;
269            DirectoryIterator() throws IOException {
270                List<Path> paths = null;
271                try (Stream<Path> stream = Files.walk(path, Integer.MAX_VALUE)) {
272                    paths = stream.filter(ClassFileReader::isClass)
273                                  .collect(Collectors.toList());
274                }
275                this.entries = paths;
276                this.index = 0;
277            }
278
279            public boolean hasNext() {
280                return index != entries.size();
281            }
282
283            public ClassFile next() {
284                if (!hasNext()) {
285                    throw new NoSuchElementException();
286                }
287                Path path = entries.get(index++);
288                try {
289                    return readClassFile(path);
290                } catch (IOException e) {
291                    throw new ClassFileError(e);
292                }
293            }
294
295            public void remove() {
296                throw new UnsupportedOperationException("Not supported yet.");
297            }
298        }
299    }
300
301    static class JarFileReader extends ClassFileReader {
302        private final JarFile jarfile;
303        private final Runtime.Version version;
304
305        JarFileReader(Path path, Runtime.Version version) throws IOException {
306            this(path, openJarFile(path.toFile(), version), version);
307        }
308
309        JarFileReader(Path path, JarFile jf, Runtime.Version version) throws IOException {
310            super(path);
311            this.jarfile = jf;
312            this.version = version;
313        }
314
315        @Override
316        public void close() throws IOException {
317            jarfile.close();
318        }
319
320        private static JarFile openJarFile(File f, Runtime.Version version)
321                throws IOException {
322            JarFile jf;
323            if (version == null) {
324                jf = new JarFile(f, false);
325                if (jf.isMultiRelease()) {
326                    throw new MultiReleaseException("err.multirelease.option.notfound", f.getName());
327                }
328            } else {
329                jf = new JarFile(f, false, ZipFile.OPEN_READ, version);
330                if (!jf.isMultiRelease()) {
331                    throw new MultiReleaseException("err.multirelease.option.exists", f.getName());
332                }
333            }
334            return jf;
335        }
336
337        protected Set<String> scan() {
338            try (JarFile jf = openJarFile(path.toFile(), version)) {
339                return VersionedStream.stream(jf).map(JarEntry::getName)
340                         .filter(n -> n.endsWith(".class"))
341                         .collect(Collectors.toSet());
342            } catch (IOException e) {
343                throw new UncheckedIOException(e);
344            }
345        }
346
347        public ClassFile getClassFile(String name) throws IOException {
348            if (name.indexOf('.') > 0) {
349                int i = name.lastIndexOf('.');
350                String entryName = name.replace('.', '/') + ".class";
351                JarEntry e = jarfile.getJarEntry(entryName);
352                if (e == null) {
353                    e = jarfile.getJarEntry(entryName.substring(0, i) + "$"
354                            + entryName.substring(i + 1, entryName.length()));
355                }
356                if (e != null) {
357                    return readClassFile(jarfile, e);
358                }
359            } else {
360                JarEntry e = jarfile.getJarEntry(name + ".class");
361                if (e != null) {
362                    return readClassFile(jarfile, e);
363                }
364            }
365            return null;
366        }
367
368        protected ClassFile readClassFile(JarFile jarfile, JarEntry e) throws IOException {
369            try (InputStream is = jarfile.getInputStream(e)) {
370                ClassFile cf = ClassFile.read(is);
371                if (jarfile.isMultiRelease()) {
372                    VersionHelper.add(jarfile, e, cf);
373                }
374                return cf;
375            } catch (ConstantPoolException ex) {
376                throw new ClassFileError(ex);
377            }
378        }
379
380        public Iterable<ClassFile> getClassFiles() throws IOException {
381            final Iterator<ClassFile> iter = new JarFileIterator(this, jarfile);
382            return () -> iter;
383        }
384    }
385
386    Enumeration<JarEntry> versionedEntries(JarFile jf) {
387        Iterator<JarEntry> it = VersionedStream.stream(jf).iterator();
388        return new Enumeration<>() {
389            @Override
390            public boolean hasMoreElements() {
391                return it.hasNext();
392            }
393
394            @Override
395            public JarEntry nextElement() {
396                return it.next();
397            }
398        };
399    }
400
401    class JarFileIterator implements Iterator<ClassFile> {
402        protected final JarFileReader reader;
403        protected Enumeration<JarEntry> entries;
404        protected JarFile jf;
405        protected JarEntry nextEntry;
406        protected ClassFile cf;
407        JarFileIterator(JarFileReader reader) {
408            this(reader, null);
409        }
410        JarFileIterator(JarFileReader reader, JarFile jarfile) {
411            this.reader = reader;
412            setJarFile(jarfile);
413        }
414
415        void setJarFile(JarFile jarfile) {
416            if (jarfile == null) return;
417
418            this.jf = jarfile;
419            this.entries = versionedEntries(jf);
420            this.nextEntry = nextEntry();
421        }
422
423        public boolean hasNext() {
424            if (nextEntry != null && cf != null) {
425                return true;
426            }
427            while (nextEntry != null) {
428                try {
429                    cf = reader.readClassFile(jf, nextEntry);
430                    return true;
431                } catch (ClassFileError | IOException ex) {
432                    skippedEntries.add(String.format("%s: %s (%s)",
433                                                     ex.getMessage(),
434                                                     nextEntry.getName(),
435                                                     jf.getName()));
436                }
437                nextEntry = nextEntry();
438            }
439            return false;
440        }
441
442        public ClassFile next() {
443            if (!hasNext()) {
444                throw new NoSuchElementException();
445            }
446            ClassFile classFile = cf;
447            cf = null;
448            nextEntry = nextEntry();
449            return classFile;
450        }
451
452        protected JarEntry nextEntry() {
453            while (entries.hasMoreElements()) {
454                JarEntry e = entries.nextElement();
455                String name = e.getName();
456                if (name.endsWith(".class")) {
457                    return e;
458                }
459            }
460            return null;
461        }
462
463        public void remove() {
464            throw new UnsupportedOperationException("Not supported yet.");
465        }
466    }
467    private static final String MODULE_INFO = "module-info.class";
468}
469