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