1 /*
   2  * Copyright (c) 2015, 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 package jdk.internal.jrtfs;
  26 
  27 import java.io.IOException;
  28 import java.nio.file.DirectoryStream;
  29 import java.nio.file.FileSystem;
  30 import java.nio.file.FileSystemException;
  31 import java.nio.file.FileSystems;
  32 import java.nio.file.Files;
  33 import java.nio.file.LinkOption;
  34 import java.nio.file.NoSuchFileException;
  35 import java.nio.file.NotDirectoryException;
  36 import java.nio.file.Path;
  37 import java.nio.file.attribute.BasicFileAttributes;
  38 import java.util.ArrayList;
  39 import java.util.Collections;
  40 import java.util.HashMap;
  41 import java.util.Iterator;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.function.Function;
  45 import static java.util.stream.Collectors.toList;
  46 import static jdk.internal.jrtfs.AbstractJrtFileSystem.getString;
  47 
  48 /**
  49  * A jrt file system built on $JAVA_HOME/modules directory ('exploded modules
  50  * build')
  51  */
  52 class JrtExplodedFileSystem extends AbstractJrtFileSystem {
  53 
  54     private static final String MODULES = "/modules/";
  55     private static final String PACKAGES = "/packages/";
  56     private static final int PACKAGES_LEN = PACKAGES.length();
  57 
  58     // root path
  59     private final JrtExplodedPath rootPath;
  60     private volatile boolean isOpen;
  61     private final FileSystem defaultFS;
  62     private final String separator;
  63     private final Map<String, Node> nodes = Collections.synchronizedMap(new HashMap<>());
  64     private final BasicFileAttributes modulesDirAttrs;
  65 
  66     JrtExplodedFileSystem(JrtFileSystemProvider provider,
  67             Map<String, ?> env)
  68             throws IOException {
  69 
  70         super(provider, env);
  71         checkExists(SystemImages.modulesDirPath);
  72         byte[] root = new byte[]{'/'};
  73         rootPath = new JrtExplodedPath(this, root);
  74         isOpen = true;
  75         defaultFS = FileSystems.getDefault();
  76         String str = defaultFS.getSeparator();
  77         separator = str.equals(getSeparator()) ? null : str;
  78         modulesDirAttrs = Files.readAttributes(SystemImages.modulesDirPath, BasicFileAttributes.class);
  79         initNodes();
  80     }
  81 
  82     @Override
  83     public void close() throws IOException {
  84         cleanup();
  85     }
  86 
  87     @Override
  88     public boolean isOpen() {
  89         return isOpen;
  90     }
  91 
  92     @Override
  93     protected void finalize() throws Throwable {
  94         cleanup();
  95         super.finalize();
  96     }
  97 
  98     private synchronized void cleanup() {
  99         isOpen = false;
 100         nodes.clear();
 101     }
 102 
 103     @Override
 104     JrtExplodedPath getRootPath() {
 105         return rootPath;
 106     }
 107 
 108     // Base class for Nodes of this file system
 109     abstract class Node {
 110 
 111         private final String name;
 112 
 113         Node(String name) {
 114             this.name = name;
 115         }
 116 
 117         final String getName() {
 118             return name;
 119         }
 120 
 121         final String getExtension() {
 122             if (isFile()) {
 123                 final int index = name.lastIndexOf(".");
 124                 if (index != -1) {
 125                     return name.substring(index + 1);
 126                 }
 127             }
 128 
 129             return null;
 130         }
 131 
 132         BasicFileAttributes getBasicAttrs() throws IOException {
 133             return modulesDirAttrs;
 134         }
 135 
 136         boolean isLink() {
 137             return false;
 138         }
 139 
 140         boolean isDirectory() {
 141             return false;
 142         }
 143 
 144         boolean isFile() {
 145             return false;
 146         }
 147 
 148         byte[] getContent() throws IOException {
 149             if (!isFile()) {
 150                 throw new FileSystemException(name + " is not file");
 151             }
 152 
 153             throw new AssertionError("ShouldNotReachHere");
 154         }
 155 
 156         List<Node> getChildren() throws IOException {
 157             if (!isDirectory()) {
 158                 throw new NotDirectoryException(name);
 159             }
 160 
 161             throw new AssertionError("ShouldNotReachHere");
 162         }
 163 
 164         final Node resolveLink() {
 165             return resolveLink(false);
 166         }
 167 
 168         Node resolveLink(boolean recursive) {
 169             return this;
 170         }
 171     }
 172 
 173     // A Node that is backed by actual default file system Path
 174     private final class PathNode extends Node {
 175 
 176         // Path in underlying default file system
 177         private final Path path;
 178         private final boolean file;
 179         // lazily initialized, don't read attributes unless required!
 180         private BasicFileAttributes attrs;
 181 
 182         PathNode(String name, Path path) {
 183             super(name);
 184             this.path = path;
 185             this.file = Files.isRegularFile(path);
 186         }
 187 
 188         @Override
 189         synchronized BasicFileAttributes getBasicAttrs() throws IOException {
 190             if (attrs == null) {
 191                 attrs = Files.readAttributes(path, BasicFileAttributes.class);
 192             }
 193             return attrs;
 194         }
 195 
 196         @Override
 197         boolean isDirectory() {
 198             return !file;
 199         }
 200 
 201         @Override
 202         boolean isFile() {
 203             return file;
 204         }
 205 
 206         @Override
 207         byte[] getContent() throws IOException {
 208             if (!isFile()) {
 209                 throw new FileSystemException(getName() + " is not file");
 210             }
 211 
 212             return Files.readAllBytes(path);
 213         }
 214 
 215         @Override
 216         List<Node> getChildren() throws IOException {
 217             if (!isDirectory()) {
 218                 throw new NotDirectoryException(getName());
 219             }
 220 
 221             List<Node> children = new ArrayList<>();
 222             try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
 223                 for (Path cp : stream) {
 224                     cp = SystemImages.modulesDirPath.relativize(cp);
 225                     String cpName = MODULES + nativeSlashToFrontSlash(cp.toString());
 226                     try {
 227                         children.add(findNode(cpName));
 228                     } catch (NoSuchFileException nsfe) {
 229                         // findNode may choose to hide certain files!
 230                     }
 231                 }
 232             }
 233 
 234             return children;
 235         }
 236     }
 237 
 238     // A Node that links to another Node
 239     private final class LinkNode extends Node {
 240 
 241         // underlying linked Node
 242         private final Node link;
 243 
 244         LinkNode(String name, Node link) {
 245             super(name);
 246             this.link = link;
 247         }
 248 
 249         @Override
 250         BasicFileAttributes getBasicAttrs() throws IOException {
 251             return link.getBasicAttrs();
 252         }
 253 
 254         @Override
 255         public boolean isLink() {
 256             return true;
 257         }
 258 
 259         @Override
 260         Node resolveLink(boolean recursive) {
 261             return recursive && (link instanceof LinkNode) ? ((LinkNode) link).resolveLink(true) : link;
 262         }
 263     }
 264 
 265     // A directory Node with it's children Nodes
 266     private final class DirNode extends Node {
 267 
 268         // children Nodes of this Node.
 269         private final List<Node> children;
 270 
 271         DirNode(String name, List<Node> children) {
 272             super(name);
 273             this.children = children;
 274         }
 275 
 276         @Override
 277         boolean isDirectory() {
 278             return true;
 279         }
 280 
 281         @Override
 282         List<Node> getChildren() throws IOException {
 283             return children;
 284         }
 285     }
 286 
 287     private JrtExplodedPath toJrtExplodedPath(String path) {
 288         return toJrtExplodedPath(getBytes(path));
 289     }
 290 
 291     private JrtExplodedPath toJrtExplodedPath(byte[] path) {
 292         return new JrtExplodedPath(this, path);
 293     }
 294 
 295     @Override
 296     boolean isSameFile(AbstractJrtPath p1, AbstractJrtPath p2) throws IOException {
 297         Node n1 = checkNode(p1.getName());
 298         Node n2 = checkNode(p2.getName());
 299         return n1 == n2;
 300     }
 301 
 302     @Override
 303     boolean isLink(AbstractJrtPath jrtPath) throws IOException {
 304         return checkNode(jrtPath.getName()).isLink();
 305     }
 306 
 307     @Override
 308     AbstractJrtPath resolveLink(AbstractJrtPath jrtPath) throws IOException {
 309         String name = checkNode(jrtPath.getName()).resolveLink().getName();
 310         return toJrtExplodedPath(name);
 311     }
 312 
 313     @Override
 314     AbstractJrtFileAttributes getFileAttributes(byte[] path, LinkOption... options) throws IOException {
 315         Node node = checkNode(path);
 316         if (node.isLink() && followLinks(options)) {
 317             node = node.resolveLink(true);
 318         }
 319         return new JrtExplodedFileAttributes(node);
 320     }
 321 
 322     @Override
 323     boolean exists(byte[] path) throws IOException {
 324         try {
 325             checkNode(path);
 326             return true;
 327         } catch (NoSuchFileException nsfe) {
 328             return false;
 329         }
 330     }
 331 
 332     @Override
 333     boolean isDirectory(byte[] path, boolean resolveLinks) throws IOException {
 334         Node node = checkNode(path);
 335         return resolveLinks && node.isLink()
 336                 ? node.resolveLink(true).isDirectory()
 337                 : node.isDirectory();
 338     }
 339 
 340     @Override
 341     Iterator<Path> iteratorOf(byte[] path, String childPrefix) throws IOException {
 342         Node node = checkNode(path).resolveLink(true);
 343         if (!node.isDirectory()) {
 344             throw new NotDirectoryException(getString(path));
 345         }
 346 
 347         Function<Node, Path> f = childPrefix == null
 348                 ? child -> toJrtExplodedPath(child.getName())
 349                 : child -> toJrtExplodedPath(childPrefix + child.getName().substring(1));
 350         return node.getChildren().stream().map(f).collect(toList()).iterator();
 351     }
 352 
 353     @Override
 354     byte[] getFileContent(byte[] path) throws IOException {
 355         return checkNode(path).getContent();
 356     }
 357 
 358     private Node checkNode(byte[] path) throws IOException {
 359         ensureOpen();
 360         return findNode(path);
 361     }
 362 
 363     synchronized Node findNode(byte[] path) throws IOException {
 364         return findNode(getString(path));
 365     }
 366 
 367     // find Node for the given Path
 368     synchronized Node findNode(String str) throws IOException {
 369         Node node = findModulesNode(str);
 370         if (node != null) {
 371             return node;
 372         }
 373 
 374         // lazily created for paths like /packages/<package>/<module>/xyz
 375         // For example /packages/java.lang/java.base/java/lang/
 376         if (str.startsWith(PACKAGES)) {
 377             // pkgEndIdx marks end of <package> part
 378             int pkgEndIdx = str.indexOf('/', PACKAGES_LEN);
 379             if (pkgEndIdx != -1) {
 380                 // modEndIdx marks end of <module> part
 381                 int modEndIdx = str.indexOf('/', pkgEndIdx + 1);
 382                 if (modEndIdx != -1) {
 383                     // make sure we have such module link!
 384                     // ie., /packages/<package>/<module> is valid
 385                     Node linkNode = nodes.get(str.substring(0, modEndIdx));
 386                     if (linkNode == null || !linkNode.isLink()) {
 387                         throw new NoSuchFileException(str);
 388                     }
 389 
 390                     // map to "/modules/zyz" path and return that node
 391                     // For example, "/modules/java.base/java/lang" for
 392                     // "/packages/java.lang/java.base/java/lang".
 393                     String mod = MODULES + str.substring(pkgEndIdx + 1);
 394                     return findNode(mod);
 395                 }
 396             }
 397         }
 398 
 399         throw new NoSuchFileException(str);
 400     }
 401 
 402     // find a Node for a path that starts like "/modules/..."
 403     synchronized Node findModulesNode(String str) throws IOException {
 404         Node node = nodes.get(str);
 405         if (node != null) {
 406             return node;
 407         }
 408 
 409         // lazily created "/modules/xyz/abc/" Node
 410         // This is mapped to default file system path "<JDK_MODULES_DIR>/xyz/abc"
 411         Path p = underlyingPath(str);
 412         if (p != null) {
 413             if (Files.isRegularFile(p)) {
 414                 Path file = p.getFileName();
 415                 if (file.toString().startsWith("_the.")) {
 416                     return null;
 417                 }
 418             }
 419             node = new PathNode(str, p);
 420             nodes.put(str, node);
 421             return node;
 422         }
 423 
 424         return null;
 425     }
 426 
 427     Path underlyingPath(String str) {
 428         if (str.startsWith(MODULES)) {
 429             str = frontSlashToNativeSlash(str.substring("/modules".length()));
 430             return defaultFS.getPath(SystemImages.modulesDirPath.toString(), str);
 431         }
 432         return null;
 433     }
 434 
 435     // convert "/" to platform path separator
 436     private String frontSlashToNativeSlash(String str) {
 437         return separator == null ? str : str.replace("/", separator);
 438     }
 439 
 440     // convert platform path separator to "/"
 441     private String nativeSlashToFrontSlash(String str) {
 442         return separator == null ? str : str.replace(separator, "/");
 443     }
 444 
 445     // convert "/"s to "."s
 446     private String slashesToDots(String str) {
 447         return str.replace(separator != null ? separator : "/", ".");
 448     }
 449 
 450     // initialize file system Nodes
 451     private void initNodes() throws IOException {
 452         // same package prefix may exist in mutliple modules. This Map
 453         // is filled by walking "jdk modules" directory recursively!
 454         Map<String, List<String>> packageToModules = new HashMap<>();
 455 
 456         try (DirectoryStream<Path> stream = Files.newDirectoryStream(SystemImages.modulesDirPath)) {
 457             for (Path module : stream) {
 458                 if (Files.isDirectory(module)) {
 459                     String moduleName = module.getFileName().toString();
 460                     // make sure "/modules/<moduleName>" is created
 461                     findModulesNode(MODULES + moduleName);
 462 
 463                     Files.walk(module).filter(Files::isDirectory).forEach((p) -> {
 464                         p = module.relativize(p);
 465                         String pkgName = slashesToDots(p.toString());
 466                         // skip META-INFO and empty strings
 467                         if (!pkgName.isEmpty() && !pkgName.startsWith("META-INF")) {
 468                             List<String> moduleNames = packageToModules.get(pkgName);
 469                             if (moduleNames == null) {
 470                                 moduleNames = new ArrayList<>();
 471                                 packageToModules.put(pkgName, moduleNames);
 472                             }
 473                             moduleNames.add(moduleName);
 474                         }
 475                     });
 476                 }
 477             }
 478         }
 479 
 480         // create "/modules" directory
 481         // "nodes" map contains only /modules/<foo> nodes only so far and so add all as children of /modules
 482         DirNode modulesDir = new DirNode("/modules", new ArrayList<>(nodes.values()));
 483         nodes.put(modulesDir.getName(), modulesDir);
 484 
 485         // create children under "/packages"
 486         List<Node> packagesChildren = new ArrayList<>(packageToModules.size());
 487         for (Map.Entry<String, List<String>> entry : packageToModules.entrySet()) {
 488             String pkgName = entry.getKey();
 489             List<String> moduleNameList = entry.getValue();
 490             List<Node> moduleLinkNodes = new ArrayList<>(moduleNameList.size());
 491             for (String moduleName : moduleNameList) {
 492                 Node moduleNode = findModulesNode(MODULES + moduleName);
 493                 LinkNode linkNode = new LinkNode(PACKAGES + pkgName + "/" + moduleName, moduleNode);
 494                 nodes.put(linkNode.getName(), linkNode);
 495                 moduleLinkNodes.add(linkNode);
 496             }
 497 
 498             DirNode pkgDir = new DirNode(PACKAGES + pkgName, moduleLinkNodes);
 499             nodes.put(pkgDir.getName(), pkgDir);
 500             packagesChildren.add(pkgDir);
 501         }
 502 
 503         // "/packages" dir
 504         DirNode packagesDir = new DirNode("/packages", packagesChildren);
 505         nodes.put(packagesDir.getName(), packagesDir);
 506 
 507         // finally "/" dir!
 508         List<Node> rootChildren = new ArrayList<>();
 509         rootChildren.add(modulesDir);
 510         rootChildren.add(packagesDir);
 511         DirNode root = new DirNode("/", rootChildren);
 512         nodes.put(root.getName(), root);
 513     }
 514 }