PathFileObject.java revision 3392:04fcbc7234a4
11638Srgrimes/*
21638Srgrimes * Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved.
31638Srgrimes * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
41638Srgrimes *
51638Srgrimes * This code is free software; you can redistribute it and/or modify it
61638Srgrimes * under the terms of the GNU General Public License version 2 only, as
71638Srgrimes * published by the Free Software Foundation.  Oracle designates this
81638Srgrimes * particular file as subject to the "Classpath" exception as provided
91638Srgrimes * by Oracle in the LICENSE file that accompanied this code.
101638Srgrimes *
111638Srgrimes * This code is distributed in the hope that it will be useful, but WITHOUT
12263142Seadler * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
131638Srgrimes * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
141638Srgrimes * version 2 for more details (a copy is included in the LICENSE file that
151638Srgrimes * accompanied this code).
161638Srgrimes *
171638Srgrimes * You should have received a copy of the GNU General Public License version
181638Srgrimes * 2 along with this work; if not, write to the Free Software Foundation,
191638Srgrimes * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
201638Srgrimes *
211638Srgrimes * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
221638Srgrimes * or visit www.oracle.com if you need additional information or have any
231638Srgrimes * questions.
241638Srgrimes */
251638Srgrimes
261638Srgrimespackage com.sun.tools.javac.file;
271638Srgrimes
281638Srgrimesimport java.io.IOException;
2950476Speterimport java.io.InputStream;
301638Srgrimesimport java.io.InputStreamReader;
311638Srgrimesimport java.io.OutputStream;
321638Srgrimesimport java.io.OutputStreamWriter;
3379538Sruimport java.io.Reader;
341638Srgrimesimport java.io.Writer;
351638Srgrimesimport java.net.URI;
361638Srgrimesimport java.net.URISyntaxException;
371638Srgrimesimport java.nio.ByteBuffer;
381638Srgrimesimport java.nio.CharBuffer;
3968962Sruimport java.nio.charset.CharsetDecoder;
401638Srgrimesimport java.nio.file.FileSystem;
411638Srgrimesimport java.nio.file.FileSystems;
421638Srgrimesimport java.nio.file.Files;
431638Srgrimesimport java.nio.file.LinkOption;
441638Srgrimesimport java.nio.file.Path;
451638Srgrimesimport java.text.Normalizer;
461638Srgrimesimport java.util.Objects;
471638Srgrimes
481638Srgrimesimport javax.lang.model.element.Modifier;
491638Srgrimesimport javax.lang.model.element.NestingKind;
501638Srgrimesimport javax.tools.FileObject;
511638Srgrimesimport javax.tools.JavaFileObject;
5268962Sru
531638Srgrimesimport com.sun.tools.javac.file.RelativePath.RelativeFile;
541638Srgrimesimport com.sun.tools.javac.util.DefinedBy;
551638Srgrimesimport com.sun.tools.javac.util.DefinedBy.Api;
561638Srgrimes
571638Srgrimes
581638Srgrimes/**
591638Srgrimes *  Implementation of JavaFileObject using java.nio.file API.
601638Srgrimes *
611638Srgrimes *  <p>PathFileObjects are, for the most part, straightforward wrappers around
621638Srgrimes *  immutable absolute Path objects. Different subtypes are used to provide
63 *  specialized implementations of "inferBinaryName" and "getName" that capture
64 *  additional information available at the time the object is created.
65 *
66 *  <p>In general, {@link JavaFileManager#isSameFile} should be used to
67 *  determine whether two file objects refer to the same file on disk.
68 *  PathFileObject also supports the standard {@code equals} and {@code hashCode}
69 *  methods, primarily for convenience when working with collections.
70 *  All of these operations delegate to the equivalent operations on the
71 *  underlying Path object.
72 *
73 *  <p><b>This is NOT part of any supported API.
74 *  If you write code that depends on this, you do so at your own risk.
75 *  This code and its internal interfaces are subject to change or
76 *  deletion without notice.</b>
77 */
78public abstract class PathFileObject implements JavaFileObject {
79    private static final FileSystem defaultFileSystem = FileSystems.getDefault();
80    private static final boolean isMacOS = System.getProperty("os.name", "").contains("OS X");
81
82    protected final BaseFileManager fileManager;
83    protected final Path path;
84    private boolean hasParents;
85
86    /**
87     * Create a PathFileObject for a file within a directory, such that the
88     * binary name can be inferred from the relationship to an enclosing directory.
89     *
90     * The binary name is derived from {@code relativePath}.
91     * The name is derived from the composition of {@code userPackageRootDir}
92     * and {@code relativePath}.
93     *
94     * @param fileManager the file manager creating this file object
95     * @param path the absolute path referred to by this file object
96     * @param userPackageRootDir the path of the directory containing the
97     *          root of the package hierarchy
98     * @param relativePath the path of this file relative to {@code userPackageRootDir}
99     */
100    static PathFileObject forDirectoryPath(BaseFileManager fileManager, Path path,
101            Path userPackageRootDir, RelativePath relativePath) {
102        return new DirectoryFileObject(fileManager, path, userPackageRootDir, relativePath);
103    }
104
105    private static class DirectoryFileObject extends PathFileObject {
106        private final Path userPackageRootDir;
107        private final RelativePath relativePath;
108
109        private DirectoryFileObject(BaseFileManager fileManager, Path path,
110                Path userPackageRootDir, RelativePath relativePath) {
111            super(fileManager, path);
112            this.userPackageRootDir = Objects.requireNonNull(userPackageRootDir);
113            this.relativePath = relativePath;
114        }
115
116        @Override @DefinedBy(Api.COMPILER)
117        public String getName() {
118            return relativePath.resolveAgainst(userPackageRootDir).toString();
119        }
120
121        @Override
122        public String inferBinaryName(Iterable<? extends Path> paths) {
123            return toBinaryName(relativePath);
124        }
125
126        @Override
127        public String toString() {
128            return "DirectoryFileObject[" + userPackageRootDir + ":" + relativePath.path + "]";
129        }
130
131        @Override
132        PathFileObject getSibling(String baseName) {
133            return new DirectoryFileObject(fileManager,
134                    path.resolveSibling(baseName),
135                    userPackageRootDir,
136                    new RelativeFile(relativePath.dirname(), baseName)
137            );
138        }
139    }
140
141    /**
142     * Create a PathFileObject for a file in a file system such as a jar file,
143     * such that the binary name can be inferred from its position within the
144     * file system.
145     *
146     * The binary name is derived from {@code path}.
147     * The name is derived from the composition of {@code userJarPath}
148     * and {@code path}.
149     *
150     * @param fileManager the file manager creating this file object
151     * @param path the path referred to by this file object
152     * @param userJarPath the path of the jar file containing the file system.
153     */
154    public static PathFileObject forJarPath(BaseFileManager fileManager,
155            Path path, Path userJarPath) {
156        return new JarFileObject(fileManager, path, userJarPath);
157    }
158
159    private static class JarFileObject extends PathFileObject {
160        private final Path userJarPath;
161
162        private JarFileObject(BaseFileManager fileManager, Path path, Path userJarPath) {
163            super(fileManager, path);
164            this.userJarPath = userJarPath;
165        }
166
167        @Override @DefinedBy(Api.COMPILER)
168        public String getName() {
169            // The use of ( ) to delimit the entry name is not ideal
170            // but it does match earlier behavior
171            return userJarPath + "(" + path + ")";
172        }
173
174        @Override
175        public String inferBinaryName(Iterable<? extends Path> paths) {
176            Path root = path.getFileSystem().getRootDirectories().iterator().next();
177            return toBinaryName(root.relativize(path));
178        }
179
180        @Override @DefinedBy(Api.COMPILER)
181        public URI toUri() {
182            // Work around bug JDK-8134451:
183            // path.toUri() returns double-encoded URIs, that cannot be opened by URLConnection
184            return createJarUri(userJarPath, path.toString());
185        }
186
187        @Override
188        public String toString() {
189            return "JarFileObject[" + userJarPath + ":" + path + "]";
190        }
191
192        @Override
193        PathFileObject getSibling(String baseName) {
194            return new JarFileObject(fileManager,
195                    path.resolveSibling(baseName),
196                    userJarPath
197            );
198        }
199
200        private static URI createJarUri(Path jarFile, String entryName) {
201            URI jarURI = jarFile.toUri().normalize();
202            String separator = entryName.startsWith("/") ? "!" : "!/";
203            try {
204                // The jar URI convention appears to be not to re-encode the jarURI
205                return new URI("jar:" + jarURI + separator + entryName);
206            } catch (URISyntaxException e) {
207                throw new CannotCreateUriError(jarURI + separator + entryName, e);
208            }
209        }
210    }
211
212    /**
213     * Create a PathFileObject for a file in a modular file system, such as jrt:,
214     * such that the binary name can be inferred from its position within the
215     * filesystem.
216     *
217     * The binary name is derived from {@code path}, ignoring the first two
218     * elements of the name (which are "modules" and a module name).
219     * The name is derived from {@code path}.
220     *
221     * @param fileManager the file manager creating this file object
222     * @param path the path referred to by this file object
223     */
224    public static PathFileObject forJRTPath(BaseFileManager fileManager,
225            final Path path) {
226        return new JRTFileObject(fileManager, path);
227    }
228
229    private static class JRTFileObject extends PathFileObject {
230        // private final Path javaHome;
231        private JRTFileObject(BaseFileManager fileManager, Path path) {
232            super(fileManager, path);
233        }
234
235        @Override @DefinedBy(Api.COMPILER)
236        public String getName() {
237            return path.toString();
238        }
239
240        @Override
241        public String inferBinaryName(Iterable<? extends Path> paths) {
242            // use subpath to ignore the leading /modules/MODULE-NAME
243            return toBinaryName(path.subpath(2, path.getNameCount()));
244        }
245
246        @Override
247        public String toString() {
248            return "JRTFileObject[" + path + "]";
249        }
250
251        @Override
252        PathFileObject getSibling(String baseName) {
253            return new JRTFileObject(fileManager,
254                    path.resolveSibling(baseName)
255            );
256        }
257    }
258
259    /**
260     * Create a PathFileObject for a file whose binary name must be inferred
261     * from its position on a search path.
262     *
263     * The binary name is inferred by finding an enclosing directory in
264     * the sequence of paths associated with the location given to
265     * {@link JavaFileManager#inferBinaryName).
266     * The name is derived from {@code userPath}.
267     *
268     * @param fileManager the file manager creating this file object
269     * @param path the path referred to by this file object
270     * @param userPath the "user-friendly" name for this path.
271     */
272    static PathFileObject forSimplePath(BaseFileManager fileManager,
273            Path path, Path userPath) {
274        return new SimpleFileObject(fileManager, path, userPath);
275    }
276
277    private static class SimpleFileObject extends PathFileObject {
278        private final Path userPath;
279        private SimpleFileObject(BaseFileManager fileManager, Path path, Path userPath) {
280            super(fileManager, path);
281            this.userPath = userPath;
282        }
283
284        @Override @DefinedBy(Api.COMPILER)
285        public String getName() {
286            return userPath.toString();
287        }
288
289        @Override
290        public String inferBinaryName(Iterable<? extends Path> paths) {
291            Path absPath = path.toAbsolutePath();
292            for (Path p: paths) {
293                Path ap = p.toAbsolutePath();
294                if (absPath.startsWith(ap)) {
295                    try {
296                        Path rp = ap.relativize(absPath);
297                        if (rp != null) // maybe null if absPath same as ap
298                            return toBinaryName(rp);
299                    } catch (IllegalArgumentException e) {
300                        // ignore this p if cannot relativize path to p
301                    }
302                }
303            }
304            return null;
305        }
306
307        @Override
308        PathFileObject getSibling(String baseName) {
309            return new SimpleFileObject(fileManager,
310                    path.resolveSibling(baseName),
311                    userPath.resolveSibling(baseName)
312            );
313        }
314    }
315
316    /**
317     * Create a PathFileObject, for a specified path, in the context of
318     * a given file manager.
319     *
320     * In general, this path should be an
321     * {@link Path#toAbsolutePath absolute path}, if not a
322     * {@link Path#toRealPath} real path.
323     * It will be used as the basis of {@code equals}, {@code hashCode}
324     * and {@code isSameFile} methods on this file object.
325     *
326     * A PathFileObject should also have a "friendly name" per the
327     * specification for {@link FileObject#getName}. The friendly name
328     * is provided by the various subtypes of {@code PathFileObject}.
329     *
330     * @param fileManager the file manager creating this file object
331     * @param path the path contained in this file object.
332     */
333    protected PathFileObject(BaseFileManager fileManager, Path path) {
334        this.fileManager = Objects.requireNonNull(fileManager);
335        if (Files.isDirectory(path)) {
336            throw new IllegalArgumentException("directories not supported");
337        }
338        this.path = path;
339    }
340
341    /**
342     * See {@link JavacFileManager#inferBinaryName}.
343     */
344    abstract String inferBinaryName(Iterable<? extends Path> paths);
345
346    /**
347     * Return the file object for a sibling file with a given file name.
348     * See {@link JavacFileManager#getFileForOutput} and
349     * {@link JavacFileManager#getJavaFileForOutput}.
350     */
351    abstract PathFileObject getSibling(String basename);
352
353    /**
354     * Return the Path for this object.
355     * @return the Path for this object.
356     * @see StandardJavaFileManager#asPath
357     */
358    public Path getPath() {
359        return path;
360    }
361
362    /**
363     * The short name is used when generating raw diagnostics.
364     * @return the last component of the path
365     */
366    public String getShortName() {
367        return path.getFileName().toString();
368    }
369
370    @Override @DefinedBy(Api.COMPILER)
371    public Kind getKind() {
372        return BaseFileManager.getKind(path.getFileName().toString());
373    }
374
375    @Override @DefinedBy(Api.COMPILER)
376    public boolean isNameCompatible(String simpleName, Kind kind) {
377        Objects.requireNonNull(simpleName);
378        Objects.requireNonNull(kind);
379
380        if (kind == Kind.OTHER && getKind() != kind) {
381            return false;
382        }
383
384        String sn = simpleName + kind.extension;
385        String pn = path.getFileName().toString();
386        if (pn.equals(sn)) {
387            return true;
388        }
389
390        if (path.getFileSystem() == defaultFileSystem) {
391            if (isMacOS) {
392                String name = path.getFileName().toString();
393                if (Normalizer.isNormalized(name, Normalizer.Form.NFD)
394                        && Normalizer.isNormalized(sn, Normalizer.Form.NFC)) {
395                    // On Mac OS X it is quite possible to have the file name and the
396                    // given simple name normalized in different ways.
397                    // In that case we have to normalize file name to the
398                    // Normal Form Composed (NFC).
399                    String normName = Normalizer.normalize(name, Normalizer.Form.NFC);
400                    if (normName.equals(sn)) {
401                        return true;
402                    }
403                }
404            }
405
406            if (pn.equalsIgnoreCase(sn)) {
407                try {
408                    // allow for Windows
409                    return path.toRealPath(LinkOption.NOFOLLOW_LINKS).getFileName().toString().equals(sn);
410                } catch (IOException e) {
411                }
412            }
413        }
414
415        return false;
416    }
417
418    @Override @DefinedBy(Api.COMPILER)
419    public NestingKind getNestingKind() {
420        return null;
421    }
422
423    @Override @DefinedBy(Api.COMPILER)
424    public Modifier getAccessLevel() {
425        return null;
426    }
427
428    @Override @DefinedBy(Api.COMPILER)
429    public URI toUri() {
430        return path.toUri();
431    }
432
433    @Override @DefinedBy(Api.COMPILER)
434    public InputStream openInputStream() throws IOException {
435        fileManager.updateLastUsedTime();
436        return Files.newInputStream(path);
437    }
438
439    @Override @DefinedBy(Api.COMPILER)
440    public OutputStream openOutputStream() throws IOException {
441        fileManager.updateLastUsedTime();
442        fileManager.flushCache(this);
443        ensureParentDirectoriesExist();
444        return Files.newOutputStream(path);
445    }
446
447    @Override @DefinedBy(Api.COMPILER)
448    public Reader openReader(boolean ignoreEncodingErrors) throws IOException {
449        CharsetDecoder decoder = fileManager.getDecoder(fileManager.getEncodingName(), ignoreEncodingErrors);
450        return new InputStreamReader(openInputStream(), decoder);
451    }
452
453    @Override @DefinedBy(Api.COMPILER)
454    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
455        CharBuffer cb = fileManager.getCachedContent(this);
456        if (cb == null) {
457            try (InputStream in = openInputStream()) {
458                ByteBuffer bb = fileManager.makeByteBuffer(in);
459                JavaFileObject prev = fileManager.log.useSource(this);
460                try {
461                    cb = fileManager.decode(bb, ignoreEncodingErrors);
462                } finally {
463                    fileManager.log.useSource(prev);
464                }
465                fileManager.recycleByteBuffer(bb);
466                if (!ignoreEncodingErrors) {
467                    fileManager.cache(this, cb);
468                }
469            }
470        }
471        return cb;
472    }
473
474    @Override @DefinedBy(Api.COMPILER)
475    public Writer openWriter() throws IOException {
476        fileManager.updateLastUsedTime();
477        fileManager.flushCache(this);
478        ensureParentDirectoriesExist();
479        return new OutputStreamWriter(Files.newOutputStream(path), fileManager.getEncodingName());
480    }
481
482    @Override @DefinedBy(Api.COMPILER)
483    public long getLastModified() {
484        try {
485            return Files.getLastModifiedTime(path).toMillis();
486        } catch (IOException e) {
487            return 0;
488        }
489    }
490
491    @Override @DefinedBy(Api.COMPILER)
492    public boolean delete() {
493        try {
494            Files.delete(path);
495            return true;
496        } catch (IOException e) {
497            return false;
498        }
499    }
500
501    boolean isSameFile(PathFileObject other) {
502        try {
503            return Files.isSameFile(path, other.path);
504        } catch (IOException e) {
505            return false;
506        }
507    }
508
509    @Override
510    public boolean equals(Object other) {
511        return (other instanceof PathFileObject && path.equals(((PathFileObject) other).path));
512    }
513
514    @Override
515    public int hashCode() {
516        return path.hashCode();
517    }
518
519    @Override
520    public String toString() {
521        return getClass().getSimpleName() + "[" + path + "]";
522    }
523
524    private void ensureParentDirectoriesExist() throws IOException {
525        if (!hasParents) {
526            Path parent = path.getParent();
527            if (parent != null && !Files.isDirectory(parent)) {
528                try {
529                    Files.createDirectories(parent);
530                } catch (IOException e) {
531                    throw new IOException("could not create parent directories", e);
532                }
533            }
534            hasParents = true;
535        }
536    }
537
538    protected static String toBinaryName(RelativePath relativePath) {
539        return toBinaryName(relativePath.path, "/");
540    }
541
542    protected static String toBinaryName(Path relativePath) {
543        return toBinaryName(relativePath.toString(),
544                relativePath.getFileSystem().getSeparator());
545    }
546
547    private static String toBinaryName(String relativePath, String sep) {
548        return removeExtension(relativePath).replace(sep, ".");
549    }
550
551    private static String removeExtension(String fileName) {
552        int lastDot = fileName.lastIndexOf(".");
553        return (lastDot == -1 ? fileName : fileName.substring(0, lastDot));
554    }
555
556    /** Return the last component of a presumed hierarchical URI.
557     *  From the scheme specific part of the URI, it returns the substring
558     *  after the last "/" if any, or everything if no "/" is found.
559     */
560    public static String getSimpleName(FileObject fo) {
561        URI uri = fo.toUri();
562        String s = uri.getSchemeSpecificPart();
563        return s.substring(s.lastIndexOf("/") + 1); // safe when / not found
564
565    }
566
567    /** Used when URLSyntaxException is thrown unexpectedly during
568     *  implementations of FileObject.toURI(). */
569    public static class CannotCreateUriError extends Error {
570        private static final long serialVersionUID = 9101708840997613546L;
571        public CannotCreateUriError(String value, Throwable cause) {
572            super(value, cause);
573        }
574    }
575}
576