1 /*
2  * Copyright (c) 2009, 2017, 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 
26 package com.sun.tools.javac.file;
27 
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.InputStreamReader;
31 import java.io.OutputStream;
32 import java.io.OutputStreamWriter;
33 import java.io.Reader;
34 import java.io.Writer;
35 import java.net.URI;
36 import java.net.URISyntaxException;
37 import java.nio.ByteBuffer;
38 import java.nio.CharBuffer;
39 import java.nio.charset.CharsetDecoder;
40 import java.nio.file.FileSystem;
41 import java.nio.file.FileSystems;
42 import java.nio.file.Files;
43 import java.nio.file.LinkOption;
44 import java.nio.file.Path;
45 import java.text.Normalizer;
46 import java.util.Objects;
47 
48 import javax.lang.model.element.Modifier;
49 import javax.lang.model.element.NestingKind;
50 import javax.tools.FileObject;
51 import javax.tools.JavaFileObject;
52 
53 import com.sun.tools.javac.file.RelativePath.RelativeFile;
54 import com.sun.tools.javac.util.DefinedBy;
55 import com.sun.tools.javac.util.DefinedBy.Api;
56 
57 
58 /**
59  *  Implementation of JavaFileObject using java.nio.file API.
60  *
61  *  <p>PathFileObjects are, for the most part, straightforward wrappers around
62  *  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  */
78 public 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      */
forDirectoryPath(BaseFileManager fileManager, Path path, Path userPackageRootDir, RelativePath relativePath)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 
DirectoryFileObject(BaseFileManager fileManager, Path path, Path userPackageRootDir, RelativePath relativePath)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)
getName()117         public String getName() {
118             return relativePath.resolveAgainst(userPackageRootDir).toString();
119         }
120 
121         @Override
inferBinaryName(Iterable<? extends Path> paths)122         public String inferBinaryName(Iterable<? extends Path> paths) {
123             return toBinaryName(relativePath);
124         }
125 
126         @Override
toString()127         public String toString() {
128             return "DirectoryFileObject[" + userPackageRootDir + ":" + relativePath.path + "]";
129         }
130 
131         @Override
getSibling(String baseName)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      * @return the file object
154      */
forJarPath(BaseFileManager fileManager, Path path, Path userJarPath)155     public static PathFileObject forJarPath(BaseFileManager fileManager,
156             Path path, Path userJarPath) {
157         return new JarFileObject(fileManager, path, userJarPath);
158     }
159 
160     private static class JarFileObject extends PathFileObject {
161         private final Path userJarPath;
162 
JarFileObject(BaseFileManager fileManager, Path path, Path userJarPath)163         private JarFileObject(BaseFileManager fileManager, Path path, Path userJarPath) {
164             super(fileManager, path);
165             this.userJarPath = userJarPath;
166         }
167 
168         @Override @DefinedBy(Api.COMPILER)
getName()169         public String getName() {
170             // The use of ( ) to delimit the entry name is not ideal
171             // but it does match earlier behavior
172             return userJarPath + "(" + path + ")";
173         }
174 
175         @Override
inferBinaryName(Iterable<? extends Path> paths)176         public String inferBinaryName(Iterable<? extends Path> paths) {
177             Path root = path.getFileSystem().getRootDirectories().iterator().next();
178             return toBinaryName(root.relativize(path));
179         }
180 
181         @Override @DefinedBy(Api.COMPILER)
toUri()182         public URI toUri() {
183             // Work around bug JDK-8134451:
184             // path.toUri() returns double-encoded URIs, that cannot be opened by URLConnection
185             return createJarUri(userJarPath, path.toString());
186         }
187 
188         @Override
toString()189         public String toString() {
190             return "JarFileObject[" + userJarPath + ":" + path + "]";
191         }
192 
193         @Override
getSibling(String baseName)194         PathFileObject getSibling(String baseName) {
195             return new JarFileObject(fileManager,
196                     path.resolveSibling(baseName),
197                     userJarPath
198             );
199         }
200 
createJarUri(Path jarFile, String entryName)201         private static URI createJarUri(Path jarFile, String entryName) {
202             URI jarURI = jarFile.toUri().normalize();
203             String separator = entryName.startsWith("/") ? "!" : "!/";
204             try {
205                 // The jar URI convention appears to be not to re-encode the jarURI
206                 return new URI("jar:" + jarURI + separator + entryName);
207             } catch (URISyntaxException e) {
208                 throw new CannotCreateUriError(jarURI + separator + entryName, e);
209             }
210         }
211     }
212 
213     /**
214      * Create a PathFileObject for a file in a modular file system, such as jrt:,
215      * such that the binary name can be inferred from its position within the
216      * filesystem.
217      *
218      * The binary name is derived from {@code path}, ignoring the first two
219      * elements of the name (which are "modules" and a module name).
220      * The name is derived from {@code path}.
221      *
222      * @param fileManager the file manager creating this file object
223      * @param path the path referred to by this file object
224      * @return the file object
225      */
forJRTPath(BaseFileManager fileManager, final Path path)226     public static PathFileObject forJRTPath(BaseFileManager fileManager,
227             final Path path) {
228         return new JRTFileObject(fileManager, path);
229     }
230 
231     private static class JRTFileObject extends PathFileObject {
232         // private final Path javaHome;
JRTFileObject(BaseFileManager fileManager, Path path)233         private JRTFileObject(BaseFileManager fileManager, Path path) {
234             super(fileManager, path);
235         }
236 
237         @Override @DefinedBy(Api.COMPILER)
getName()238         public String getName() {
239             return path.toString();
240         }
241 
242         @Override
inferBinaryName(Iterable<? extends Path> paths)243         public String inferBinaryName(Iterable<? extends Path> paths) {
244             // use subpath to ignore the leading /modules/MODULE-NAME
245             return toBinaryName(path.subpath(2, path.getNameCount()));
246         }
247 
248         @Override
toString()249         public String toString() {
250             return "JRTFileObject[" + path + "]";
251         }
252 
253         @Override
getSibling(String baseName)254         PathFileObject getSibling(String baseName) {
255             return new JRTFileObject(fileManager,
256                     path.resolveSibling(baseName)
257             );
258         }
259     }
260 
261     /**
262      * Create a PathFileObject for a file whose binary name must be inferred
263      * from its position on a search path.
264      *
265      * The binary name is inferred by finding an enclosing directory in
266      * the sequence of paths associated with the location given to
267      * {@link JavaFileManager#inferBinaryName).
268      * The name is derived from {@code userPath}.
269      *
270      * @param fileManager the file manager creating this file object
271      * @param path the path referred to by this file object
272      * @param userPath the "user-friendly" name for this path.
273      */
forSimplePath(BaseFileManager fileManager, Path path, Path userPath)274     static PathFileObject forSimplePath(BaseFileManager fileManager,
275             Path path, Path userPath) {
276         return new SimpleFileObject(fileManager, path, userPath);
277     }
278 
279     private static class SimpleFileObject extends PathFileObject {
280         private final Path userPath;
SimpleFileObject(BaseFileManager fileManager, Path path, Path userPath)281         private SimpleFileObject(BaseFileManager fileManager, Path path, Path userPath) {
282             super(fileManager, path);
283             this.userPath = userPath;
284         }
285 
286         @Override @DefinedBy(Api.COMPILER)
getName()287         public String getName() {
288             return userPath.toString();
289         }
290 
291         @Override @DefinedBy(Api.COMPILER)
getShortName()292         public String getShortName() {
293             return userPath.getFileName().toString();
294         }
295 
296         @Override
inferBinaryName(Iterable<? extends Path> paths)297         public String inferBinaryName(Iterable<? extends Path> paths) {
298             Path absPath = path.toAbsolutePath();
299             for (Path p: paths) {
300                 Path ap = p.toAbsolutePath();
301                 if (absPath.startsWith(ap)) {
302                     try {
303                         Path rp = ap.relativize(absPath);
304                         if (rp != null) // maybe null if absPath same as ap
305                             return toBinaryName(rp);
306                     } catch (IllegalArgumentException e) {
307                         // ignore this p if cannot relativize path to p
308                     }
309                 }
310             }
311             return null;
312         }
313 
314         @Override @DefinedBy(Api.COMPILER)
getKind()315         public Kind getKind() {
316             return BaseFileManager.getKind(userPath);
317         }
318 
319         @Override @DefinedBy(Api.COMPILER)
isNameCompatible(String simpleName, Kind kind)320         public boolean isNameCompatible(String simpleName, Kind kind) {
321             return isPathNameCompatible(userPath, simpleName, kind);
322         }
323 
324         @Override @DefinedBy(Api.COMPILER)
toUri()325         public URI toUri() {
326             return userPath.toUri().normalize();
327         }
328 
329         @Override
getSibling(String baseName)330         PathFileObject getSibling(String baseName) {
331             return new SimpleFileObject(fileManager,
332                     path.resolveSibling(baseName),
333                     userPath.resolveSibling(baseName)
334             );
335         }
336     }
337 
338     /**
339      * Create a PathFileObject, for a specified path, in the context of
340      * a given file manager.
341      *
342      * In general, this path should be an
343      * {@link Path#toAbsolutePath absolute path}, if not a
344      * {@link Path#toRealPath} real path.
345      * It will be used as the basis of {@code equals}, {@code hashCode}
346      * and {@code isSameFile} methods on this file object.
347      *
348      * A PathFileObject should also have a "friendly name" per the
349      * specification for {@link FileObject#getName}. The friendly name
350      * is provided by the various subtypes of {@code PathFileObject}.
351      *
352      * @param fileManager the file manager creating this file object
353      * @param path the path contained in this file object.
354      */
PathFileObject(BaseFileManager fileManager, Path path)355     protected PathFileObject(BaseFileManager fileManager, Path path) {
356         this.fileManager = Objects.requireNonNull(fileManager);
357         if (Files.isDirectory(path)) {
358             throw new IllegalArgumentException("directories not supported");
359         }
360         this.path = path;
361     }
362 
363     /**
364      * See {@link JavacFileManager#inferBinaryName}.
365      */
inferBinaryName(Iterable<? extends Path> paths)366     abstract String inferBinaryName(Iterable<? extends Path> paths);
367 
368     /**
369      * Return the file object for a sibling file with a given file name.
370      * See {@link JavacFileManager#getFileForOutput} and
371      * {@link JavacFileManager#getJavaFileForOutput}.
372      */
getSibling(String basename)373     abstract PathFileObject getSibling(String basename);
374 
375     /**
376      * Return the Path for this object.
377      * @return the Path for this object.
378      * @see StandardJavaFileManager#asPath
379      */
getPath()380     public Path getPath() {
381         return path;
382     }
383 
384     /**
385      * The short name is used when generating raw diagnostics.
386      * @return the last component of the path
387      */
getShortName()388     public String getShortName() {
389         return path.getFileName().toString();
390     }
391 
392     @Override @DefinedBy(Api.COMPILER)
getKind()393     public Kind getKind() {
394         return BaseFileManager.getKind(path);
395     }
396 
397     @Override @DefinedBy(Api.COMPILER)
isNameCompatible(String simpleName, Kind kind)398     public boolean isNameCompatible(String simpleName, Kind kind) {
399         return isPathNameCompatible(path, simpleName, kind);
400     }
401 
isPathNameCompatible(Path p, String simpleName, Kind kind)402     protected boolean isPathNameCompatible(Path p, String simpleName, Kind kind) {
403         Objects.requireNonNull(simpleName);
404         Objects.requireNonNull(kind);
405 
406         if (kind == Kind.OTHER && BaseFileManager.getKind(p) != kind) {
407             return false;
408         }
409 
410         String sn = simpleName + kind.extension;
411         String pn = p.getFileName().toString();
412         if (pn.equals(sn)) {
413             return true;
414         }
415 
416         if (p.getFileSystem() == defaultFileSystem) {
417             if (isMacOS) {
418                 if (Normalizer.isNormalized(pn, Normalizer.Form.NFD)
419                         && Normalizer.isNormalized(sn, Normalizer.Form.NFC)) {
420                     // On Mac OS X it is quite possible to have the file name and the
421                     // given simple name normalized in different ways.
422                     // In that case we have to normalize file name to the
423                     // Normal Form Composed (NFC).
424                     String normName = Normalizer.normalize(pn, Normalizer.Form.NFC);
425                     if (normName.equals(sn)) {
426                         return true;
427                     }
428                 }
429             }
430 
431             if (pn.equalsIgnoreCase(sn)) {
432                 try {
433                     // allow for Windows
434                     return p.toRealPath(LinkOption.NOFOLLOW_LINKS).getFileName().toString().equals(sn);
435                 } catch (IOException e) {
436                 }
437             }
438         }
439 
440         return false;
441     }
442 
443     @Override @DefinedBy(Api.COMPILER)
getNestingKind()444     public NestingKind getNestingKind() {
445         return null;
446     }
447 
448     @Override @DefinedBy(Api.COMPILER)
getAccessLevel()449     public Modifier getAccessLevel() {
450         return null;
451     }
452 
453     @Override @DefinedBy(Api.COMPILER)
toUri()454     public URI toUri() {
455         return path.toUri();
456     }
457 
458     @Override @DefinedBy(Api.COMPILER)
openInputStream()459     public InputStream openInputStream() throws IOException {
460         fileManager.updateLastUsedTime();
461         return Files.newInputStream(path);
462     }
463 
464     @Override @DefinedBy(Api.COMPILER)
openOutputStream()465     public OutputStream openOutputStream() throws IOException {
466         fileManager.updateLastUsedTime();
467         fileManager.flushCache(this);
468         ensureParentDirectoriesExist();
469         return Files.newOutputStream(path);
470     }
471 
472     @Override @DefinedBy(Api.COMPILER)
openReader(boolean ignoreEncodingErrors)473     public Reader openReader(boolean ignoreEncodingErrors) throws IOException {
474         CharsetDecoder decoder = fileManager.getDecoder(fileManager.getEncodingName(), ignoreEncodingErrors);
475         return new InputStreamReader(openInputStream(), decoder);
476     }
477 
478     @Override @DefinedBy(Api.COMPILER)
getCharContent(boolean ignoreEncodingErrors)479     public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
480         CharBuffer cb = fileManager.getCachedContent(this);
481         if (cb == null) {
482             try (InputStream in = openInputStream()) {
483                 ByteBuffer bb = fileManager.makeByteBuffer(in);
484                 JavaFileObject prev = fileManager.log.useSource(this);
485                 try {
486                     cb = fileManager.decode(bb, ignoreEncodingErrors);
487                 } finally {
488                     fileManager.log.useSource(prev);
489                 }
490                 fileManager.recycleByteBuffer(bb);
491                 if (!ignoreEncodingErrors) {
492                     fileManager.cache(this, cb);
493                 }
494             }
495         }
496         return cb;
497     }
498 
499     @Override @DefinedBy(Api.COMPILER)
openWriter()500     public Writer openWriter() throws IOException {
501         fileManager.updateLastUsedTime();
502         fileManager.flushCache(this);
503         ensureParentDirectoriesExist();
504         return new OutputStreamWriter(Files.newOutputStream(path), fileManager.getEncodingName());
505     }
506 
507     @Override @DefinedBy(Api.COMPILER)
getLastModified()508     public long getLastModified() {
509         try {
510             return Files.getLastModifiedTime(path).toMillis();
511         } catch (IOException e) {
512             return 0;
513         }
514     }
515 
516     @Override @DefinedBy(Api.COMPILER)
delete()517     public boolean delete() {
518         try {
519             Files.delete(path);
520             return true;
521         } catch (IOException e) {
522             return false;
523         }
524     }
525 
isSameFile(PathFileObject other)526     boolean isSameFile(PathFileObject other) {
527         // By construction, the "path" field should be canonical in all likely, supported scenarios.
528         // (Any exceptions would involve the use of symlinks within a package hierarchy.)
529         // Therefore, it is sufficient to check that the paths are .equals.
530         return path.equals(other.path);
531     }
532 
533     @Override
equals(Object other)534     public boolean equals(Object other) {
535         return (other instanceof PathFileObject && path.equals(((PathFileObject) other).path));
536     }
537 
538     @Override
hashCode()539     public int hashCode() {
540         return path.hashCode();
541     }
542 
543     @Override
toString()544     public String toString() {
545         return getClass().getSimpleName() + "[" + path + "]";
546     }
547 
ensureParentDirectoriesExist()548     private void ensureParentDirectoriesExist() throws IOException {
549         if (!hasParents) {
550             Path parent = path.getParent();
551             if (parent != null && !Files.isDirectory(parent)) {
552                 try {
553                     Files.createDirectories(parent);
554                 } catch (IOException e) {
555                     throw new IOException("could not create parent directories", e);
556                 }
557             }
558             hasParents = true;
559         }
560     }
561 
toBinaryName(RelativePath relativePath)562     protected static String toBinaryName(RelativePath relativePath) {
563         return toBinaryName(relativePath.path, "/");
564     }
565 
toBinaryName(Path relativePath)566     protected static String toBinaryName(Path relativePath) {
567         return toBinaryName(relativePath.toString(),
568                 relativePath.getFileSystem().getSeparator());
569     }
570 
toBinaryName(String relativePath, String sep)571     private static String toBinaryName(String relativePath, String sep) {
572         return removeExtension(relativePath).replace(sep, ".");
573     }
574 
removeExtension(String fileName)575     private static String removeExtension(String fileName) {
576         int lastDot = fileName.lastIndexOf(".");
577         return (lastDot == -1 ? fileName : fileName.substring(0, lastDot));
578     }
579 
580     /**
581      * Return the last component of a presumed hierarchical URI.
582      * From the scheme specific part of the URI, it returns the substring
583      * after the last "/" if any, or everything if no "/" is found.
584      * @param fo the file object
585      * @return the simple name of the file object
586      */
getSimpleName(FileObject fo)587     public static String getSimpleName(FileObject fo) {
588         URI uri = fo.toUri();
589         String s = uri.getSchemeSpecificPart();
590         return s.substring(s.lastIndexOf("/") + 1); // safe when / not found
591 
592     }
593 
594     /** Used when URLSyntaxException is thrown unexpectedly during
595      *  implementations of FileObject.toURI(). */
596     public static class CannotCreateUriError extends Error {
597         private static final long serialVersionUID = 9101708840997613546L;
CannotCreateUriError(String value, Throwable cause)598         public CannotCreateUriError(String value, Throwable cause) {
599             super(value, cause);
600         }
601     }
602 }
603