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