< prev index next >

src/java.base/share/classes/jdk/internal/jimage/ImageReader.java

Print this page
*** 22,30 ***
--- 22,42 ---
   * or visit www.oracle.com if you need additional information or have any
   * questions.
   */
  package jdk.internal.jimage;
  
+ import jdk.internal.jimage.ImageLocation.LocationType;
+ 
  import java.io.IOException;
  import java.nio.ByteBuffer;
  import java.nio.ByteOrder;
  import java.nio.IntBuffer;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.nio.file.attribute.BasicFileAttributes;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
+ import java.util.Comparator;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Set;
+ import java.util.TreeMap;
  import java.util.function.Function;
  import java.util.function.Supplier;
  import java.util.stream.Stream;
  
+ import static jdk.internal.jimage.ImageLocation.LocationType.MODULES_DIR;
+ import static jdk.internal.jimage.ImageLocation.LocationType.MODULES_ROOT;
+ import static jdk.internal.jimage.ImageLocation.LocationType.PACKAGES_DIR;
+ import static jdk.internal.jimage.ImageLocation.LocationType.RESOURCE;
+ import static jdk.internal.jimage.ImageLocation.MODULES_PREFIX;
+ import static jdk.internal.jimage.ImageLocation.PACKAGES_PREFIX;
+ import static jdk.internal.jimage.ImageLocation.PREVIEW_INFIX;
+ 
  /**
   * A view over the entries of a jimage file with a unified namespace suitable
   * for file system use. The jimage entries (resources, module and package
   * information) are mapped into a unified hierarchy of named nodes, which serve
   * as the underlying structure for {@code JrtFileSystem} and other utilities.

*** 84,26 ***
      private ImageReader(SharedImageReader reader) {
          this.reader = reader;
      }
  
      /**
!      * Opens an image reader for a jimage file at the specified path, using the
!      * given byte order.
       */
!     public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
!         Objects.requireNonNull(imagePath);
-         Objects.requireNonNull(byteOrder);
- 
-         return SharedImageReader.open(imagePath, byteOrder);
      }
  
      /**
!      * Opens an image reader for a jimage file at the specified path, using the
!      * platform native byte order.
       */
!     public static ImageReader open(Path imagePath) throws IOException {
!         return open(imagePath, ByteOrder.nativeOrder());
      }
  
      @Override
      public void close() throws IOException {
          if (closed) {
--- 96,31 ---
      private ImageReader(SharedImageReader reader) {
          this.reader = reader;
      }
  
      /**
!      * Opens an image reader for a jimage file at the specified path.
!      *
+      * @param imagePath file system path of the jimage file.
+      * @param mode whether to return preview resources.
       */
!     public static ImageReader open(Path imagePath, PreviewMode mode) throws IOException {
!         return open(imagePath, ByteOrder.nativeOrder(), mode);
      }
  
      /**
!      * Opens an image reader for a jimage file at the specified path.
!      *
+      * @param imagePath file system path of the jimage file.
+      * @param byteOrder the byte-order to be used when reading the jimage file.
+      * @param mode controls whether preview resources are visible.
       */
!     public static ImageReader open(Path imagePath, ByteOrder byteOrder, PreviewMode mode)
!             throws IOException {
+         Objects.requireNonNull(imagePath);
+         Objects.requireNonNull(byteOrder);
+         return SharedImageReader.open(imagePath, byteOrder, mode.isPreviewModeEnabled());
      }
  
      @Override
      public void close() throws IOException {
          if (closed) {

*** 211,78 ***
              throw new IllegalArgumentException("Not a resource node: " + node);
          }
          return reader.getResourceBuffer(node.getLocation());
      }
  
      private static final class SharedImageReader extends BasicImageReader {
-         private static final Map<Path, SharedImageReader> OPEN_FILES = new HashMap<>();
-         private static final String MODULES_ROOT = "/modules";
-         private static final String PACKAGES_ROOT = "/packages";
          // There are >30,000 nodes in a complete jimage tree, and even relatively
          // common tasks (e.g. starting up javac) load somewhere in the region of
          // 1000 classes. Thus, an initial capacity of 2000 is a reasonable guess.
          private static final int INITIAL_NODE_CACHE_CAPACITY = 2000;
  
          // List of openers for this shared image.
          private final Set<ImageReader> openers = new HashSet<>();
  
          // Attributes of the jimage file. The jimage file does not contain
          // attributes for the individual resources (yet). We use attributes
          // of the jimage file itself (creation, modification, access times).
          private final BasicFileAttributes imageFileAttributes;
  
          // Cache of all user visible nodes, guarded by synchronizing 'this' instance.
          private final Map<String, Node> nodes;
-         // Used to classify ImageLocation instances without string comparison.
-         private final int modulesStringOffset;
-         private final int packagesStringOffset;
  
!         private SharedImageReader(Path imagePath, ByteOrder byteOrder) throws IOException {
              super(imagePath, byteOrder);
              this.imageFileAttributes = Files.readAttributes(imagePath, BasicFileAttributes.class);
              this.nodes = new HashMap<>(INITIAL_NODE_CACHE_CAPACITY);
!             // Pick stable jimage names from which to extract string offsets (we cannot
-             // use "/modules" or "/packages", since those have a module offset of zero).
-             this.modulesStringOffset = getModuleOffset("/modules/java.base");
-             this.packagesStringOffset = getModuleOffset("/packages/java.lang");
  
              // Node creation is very lazy, so we can just make the top-level directories
              // now without the risk of triggering the building of lots of other nodes.
!             Directory packages = newDirectory(PACKAGES_ROOT);
!             nodes.put(packages.getName(), packages);
-             Directory modules = newDirectory(MODULES_ROOT);
-             nodes.put(modules.getName(), modules);
  
              Directory root = newDirectory("/");
              root.setChildren(Arrays.asList(packages, modules));
!             nodes.put(root.getName(), root);
          }
  
          /**
!          * Returns the offset of the string denoting the leading "module" segment in
!          * the given path (e.g. {@code <module>/<path>}). We can't just pass in the
!          * {@code /<module>} string here because that has a module offset of zero.
           */
!         private int getModuleOffset(String path) {
!             ImageLocation location = findLocation(path);
!             assert location != null : "Cannot find expected jimage location: " + path;
!             int offset = location.getModuleOffset();
!             assert offset != 0 : "Invalid module offset for jimage location: " + path;
!             return offset;
          }
  
!         private static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
              Objects.requireNonNull(imagePath);
              Objects.requireNonNull(byteOrder);
  
              synchronized (OPEN_FILES) {
!                 SharedImageReader reader = OPEN_FILES.get(imagePath);
  
                  if (reader == null) {
                      // Will fail with an IOException if wrong byteOrder.
!                     reader =  new SharedImageReader(imagePath, byteOrder);
!                     OPEN_FILES.put(imagePath, reader);
                  } else if (reader.getByteOrder() != byteOrder) {
                      throw new IOException("\"" + reader.getName() + "\" is not an image file");
                  }
  
                  ImageReader image = new ImageReader(reader);
--- 228,191 ---
              throw new IllegalArgumentException("Not a resource node: " + node);
          }
          return reader.getResourceBuffer(node.getLocation());
      }
  
+     // Package protected for use only by SystemImageReader.
+     ResourceEntries getResourceEntries() {
+         return reader.getResourceEntries();
+     }
+ 
      private static final class SharedImageReader extends BasicImageReader {
          // There are >30,000 nodes in a complete jimage tree, and even relatively
          // common tasks (e.g. starting up javac) load somewhere in the region of
          // 1000 classes. Thus, an initial capacity of 2000 is a reasonable guess.
          private static final int INITIAL_NODE_CACHE_CAPACITY = 2000;
  
+         static final class ReaderKey {
+             private final Path imagePath;
+             private final boolean previewMode;
+ 
+             public ReaderKey(Path imagePath, boolean previewMode) {
+                 this.imagePath = imagePath;
+                 this.previewMode = previewMode;
+             }
+ 
+             @Override
+             public boolean equals(Object obj) {
+                 // No pattern variables here (Java 8 compatible source).
+                 if (obj instanceof ReaderKey) {
+                     ReaderKey other = (ReaderKey) obj;
+                     return this.imagePath.equals(other.imagePath) && this.previewMode == other.previewMode;
+                 }
+                 return false;
+             }
+ 
+             @Override
+             public int hashCode() {
+                 return imagePath.hashCode() ^ Boolean.hashCode(previewMode);
+             }
+         }
+ 
+         private static final Map<ReaderKey, SharedImageReader> OPEN_FILES = new HashMap<>();
+ 
          // List of openers for this shared image.
          private final Set<ImageReader> openers = new HashSet<>();
  
          // Attributes of the jimage file. The jimage file does not contain
          // attributes for the individual resources (yet). We use attributes
          // of the jimage file itself (creation, modification, access times).
          private final BasicFileAttributes imageFileAttributes;
  
          // Cache of all user visible nodes, guarded by synchronizing 'this' instance.
          private final Map<String, Node> nodes;
  
!         // Preview mode support.
+         private final boolean previewMode;
+         // A relativized mapping from non-preview name to directories containing
+         // preview-only nodes. This is used to add preview-only content to
+         // directories as they are completed.
+         private final HashMap<String, Directory> previewDirectoriesToMerge;
+ 
+         private SharedImageReader(Path imagePath, ByteOrder byteOrder, boolean previewMode) throws IOException {
              super(imagePath, byteOrder);
              this.imageFileAttributes = Files.readAttributes(imagePath, BasicFileAttributes.class);
              this.nodes = new HashMap<>(INITIAL_NODE_CACHE_CAPACITY);
!             this.previewMode = previewMode;
  
              // Node creation is very lazy, so we can just make the top-level directories
              // now without the risk of triggering the building of lots of other nodes.
!             Directory packages = ensureCached(newDirectory(PACKAGES_PREFIX));
!             Directory modules = ensureCached(newDirectory(MODULES_PREFIX));
  
              Directory root = newDirectory("/");
              root.setChildren(Arrays.asList(packages, modules));
!             ensureCached(root);
+ 
+             // By scanning the /packages directory information early we can determine
+             // which module/package pairs have preview resources, and build the (small)
+             // set of preview nodes early. This also ensures that preview-only entries
+             // in the /packages directory are not present in non-preview mode.
+             this.previewDirectoriesToMerge = previewMode ? new HashMap<>() : null;
+             packages.setChildren(processPackagesDirectory(previewMode));
          }
  
          /**
!          * Process {@code "/packages/xxx"} entries to build the child nodes for the
!          * root {@code "/packages"} node. Preview-only entries will be skipped if
!          * {@code previewMode == false}.
+          *
+          * <p>If {@code previewMode == true}, this method also populates the {@link
+          * #previewDirectoriesToMerge} map with any preview-only nodes, to be merged
+          * into directories as they are completed. It also caches preview resources
+          * and preview-only directories for direct lookup.
           */
!         private ArrayList<Node> processPackagesDirectory(boolean previewMode) {
!             ImageLocation pkgRoot = findLocation(PACKAGES_PREFIX);
!             assert pkgRoot != null : "Invalid jimage file";
!             IntBuffer offsets = getOffsetBuffer(pkgRoot);
!             ArrayList<Node> pkgDirs = new ArrayList<>(offsets.capacity());
!             // Package path to module map, sorted in reverse order so that
+             // longer child paths get processed first.
+             Map<String, List<String>> previewPackagesToModules =
+                     new TreeMap<>(Comparator.reverseOrder());
+             for (int i = 0; i < offsets.capacity(); i++) {
+                 ImageLocation pkgDir = getLocation(offsets.get(i));
+                 int flags = pkgDir.getFlags();
+                 // A package subdirectory is "preview only" if all the modules
+                 // it references have that package marked as preview only.
+                 // Skipping these entries avoids empty package subdirectories.
+                 if (previewMode || !ImageLocation.isPreviewOnly(flags)) {
+                     pkgDirs.add(ensureCached(newDirectory(pkgDir.getFullName())));
+                 }
+                 if (previewMode && ImageLocation.hasPreviewVersion(flags)) {
+                     // Only do this in preview mode for the small set of packages with
+                     // preview versions (the number of preview entries should be small).
+                     List<String> moduleNames = new ArrayList<>();
+                     ModuleReference.readNameOffsets(getOffsetBuffer(pkgDir), /*normal*/ false, /*preview*/ true)
+                             .forEachRemaining(n -> moduleNames.add(getString(n)));
+                     previewPackagesToModules.put(pkgDir.getBase().replace('.', '/'), moduleNames);
+                 }
+             }
+             // Reverse sorted map means child directories are processed first.
+             previewPackagesToModules.forEach((pkgPath, modules) ->
+                     modules.forEach(modName -> processPreviewDir(MODULES_PREFIX + "/" + modName, pkgPath)));
+             // We might have skipped some preview-only package entries.
+             pkgDirs.trimToSize();
+             return pkgDirs;
+         }
+ 
+         void processPreviewDir(String namePrefix, String pkgPath) {
+             String previewDirName = namePrefix + PREVIEW_INFIX + "/" + pkgPath;
+             ImageLocation previewLoc = findLocation(previewDirName);
+             assert previewLoc != null : "Missing preview directory location: " + previewDirName;
+             String nonPreviewDirName = namePrefix + "/" + pkgPath;
+             List<Node> previewOnlyChildren = createChildNodes(previewLoc, 0, childLoc -> {
+                 String baseName = getBaseName(childLoc);
+                 String nonPreviewChildName = nonPreviewDirName + "/" + baseName;
+                 boolean isPreviewOnly = ImageLocation.isPreviewOnly(childLoc.getFlags());
+                 LocationType type = childLoc.getType();
+                 if (type == RESOURCE) {
+                     // Preview resources are cached to override non-preview versions.
+                     Node childNode = ensureCached(newResource(nonPreviewChildName, childLoc));
+                     return isPreviewOnly ? childNode : null;
+                 } else {
+                     // Child directories are not cached here (they are either cached
+                     // already or have been added to previewDirectoriesToMerge).
+                     assert type == MODULES_DIR : "Invalid location type: " + childLoc;
+                     Node childNode = nodes.get(nonPreviewChildName);
+                     assert isPreviewOnly == (childNode != null) :
+                             "Inconsistent child node: " + nonPreviewChildName;
+                     return childNode;
+                 }
+             });
+             Directory previewDir = newDirectory(nonPreviewDirName);
+             previewDir.setChildren(previewOnlyChildren);
+             if (ImageLocation.isPreviewOnly(previewLoc.getFlags())) {
+                 // If we are preview-only, our children are also preview-only, so
+                 // this directory is a complete hierarchy and should be cached.
+                 assert !previewOnlyChildren.isEmpty() : "Invalid empty preview-only directory: " + nonPreviewDirName;
+                 ensureCached(previewDir);
+             } else if (!previewOnlyChildren.isEmpty()) {
+                 // A partial directory containing extra preview-only nodes
+                 // to be merged when the non-preview directory is completed.
+                 previewDirectoriesToMerge.put(nonPreviewDirName, previewDir);
+             }
+         }
+ 
+         // Adds a node to the cache, ensuring that no matching entry already existed.
+         private <T extends Node> T ensureCached(T node) {
+             Node existingNode = nodes.put(node.getName(), node);
+             assert existingNode == null : "Unexpected node already cached for: " + node;
+             return node;
          }
  
!         private static ImageReader open(Path imagePath, ByteOrder byteOrder, boolean previewMode) throws IOException {
              Objects.requireNonNull(imagePath);
              Objects.requireNonNull(byteOrder);
  
              synchronized (OPEN_FILES) {
!                 ReaderKey key = new ReaderKey(imagePath, previewMode);
+                 SharedImageReader reader = OPEN_FILES.get(key);
  
                  if (reader == null) {
                      // Will fail with an IOException if wrong byteOrder.
!                     reader = new SharedImageReader(imagePath, byteOrder, previewMode);
!                     OPEN_FILES.put(key, reader);
                  } else if (reader.getByteOrder() != byteOrder) {
                      throw new IOException("\"" + reader.getName() + "\" is not an image file");
                  }
  
                  ImageReader image = new ImageReader(reader);

*** 302,11 ***
  
                  if (openers.isEmpty()) {
                      close();
                      nodes.clear();
  
!                     if (!OPEN_FILES.remove(this.getImagePath(), this)) {
                          throw new IOException("image file not found in open list");
                      }
                  }
              }
          }
--- 432,11 ---
  
                  if (openers.isEmpty()) {
                      close();
                      nodes.clear();
  
!                     if (!OPEN_FILES.remove(new ReaderKey(getImagePath(), previewMode), this)) {
                          throw new IOException("image file not found in open list");
                      }
                  }
              }
          }

*** 320,24 ***
           *
           * @param name an absolute, {@code /}-separated path string, prefixed with either
           *     "/modules" or "/packages".
           */
          synchronized Node findNode(String name) {
              Node node = nodes.get(name);
              if (node == null) {
!                 // We cannot get the root paths ("/modules" or "/packages") here
!                 // because those nodes are already in the nodes cache.
!                 if (name.startsWith(MODULES_ROOT + "/")) {
!                     // This may perform two lookups, one for a directory (in
-                     // "/modules/...") and one for a non-prefixed resource
-                     // (with "/modules" removed).
-                     node = buildModulesNode(name);
-                 } else if (name.startsWith(PACKAGES_ROOT + "/")) {
-                     node = buildPackagesNode(name);
-                 }
-                 if (node != null) {
-                     nodes.put(node.getName(), node);
                  }
              } else if (!node.isCompleted()) {
                  // Only directories can be incomplete.
                  assert node instanceof Directory : "Invalid incomplete node: " + node;
                  completeDirectory((Directory) node);
--- 450,18 ---
           *
           * @param name an absolute, {@code /}-separated path string, prefixed with either
           *     "/modules" or "/packages".
           */
          synchronized Node findNode(String name) {
+             // Root directories "/", "/modules" and "/packages", as well
+             // as all "/packages/xxx" subdirectories are already cached.
              Node node = nodes.get(name);
              if (node == null) {
!                 if (name.startsWith(MODULES_PREFIX + "/")) {
!                     node = buildAndCacheModulesNode(name);
!                 } else if (name.startsWith(PACKAGES_PREFIX + "/")) {
!                     node = buildAndCacheLinkNode(name);
                  }
              } else if (!node.isCompleted()) {
                  // Only directories can be incomplete.
                  assert node instanceof Directory : "Invalid incomplete node: " + node;
                  completeDirectory((Directory) node);

*** 357,17 ***
              // Unlike findNode(), this method makes only one lookup in the
              // underlying jimage, but can only reliably return resource nodes.
              if (moduleName.indexOf('/') >= 0) {
                  throw new IllegalArgumentException("invalid module name: " + moduleName);
              }
!             String nodeName = MODULES_ROOT + "/" + moduleName + "/" + resourcePath;
              // Synchronize as tightly as possible to reduce locking contention.
              synchronized (this) {
                  Node node = nodes.get(nodeName);
                  if (node == null) {
                      ImageLocation loc = findLocation(moduleName, resourcePath);
!                     if (loc != null && isResource(loc)) {
                          node = newResource(nodeName, loc);
                          nodes.put(node.getName(), node);
                      }
                      return node;
                  } else {
--- 481,17 ---
              // Unlike findNode(), this method makes only one lookup in the
              // underlying jimage, but can only reliably return resource nodes.
              if (moduleName.indexOf('/') >= 0) {
                  throw new IllegalArgumentException("invalid module name: " + moduleName);
              }
!             String nodeName = MODULES_PREFIX + "/" + moduleName + "/" + resourcePath;
              // Synchronize as tightly as possible to reduce locking contention.
              synchronized (this) {
                  Node node = nodes.get(nodeName);
                  if (node == null) {
                      ImageLocation loc = findLocation(moduleName, resourcePath);
!                     if (loc != null && loc.getType() == RESOURCE) {
                          node = newResource(nodeName, loc);
                          nodes.put(node.getName(), node);
                      }
                      return node;
                  } else {

*** 379,79 ***
          /**
           * Returns whether a resource exists in the given module.
           *
           * <p>This method is expected to be called frequently for resources
           * which do not exist in the given module (e.g. as part of classpath
!          * search). As such, it skips checking the nodes cache and only checks
!          * for an entry in the jimage file, as this is faster if the resource
!          * is not present. This also means it doesn't need synchronization.
           */
          boolean containsResource(String moduleName, String resourcePath) {
              if (moduleName.indexOf('/') >= 0) {
                  throw new IllegalArgumentException("invalid module name: " + moduleName);
              }
!             // If the given module name is 'modules', then 'isResource()'
!             // returns false to prevent false positives.
              ImageLocation loc = findLocation(moduleName, resourcePath);
!             return loc != null && isResource(loc);
          }
  
          /**
           * Builds a node in the "/modules/..." namespace.
           *
           * <p>Called by {@link #findNode(String)} if a {@code /modules/...} node
           * is not present in the cache.
           */
!         private Node buildModulesNode(String name) {
!             assert name.startsWith(MODULES_ROOT + "/") : "Invalid module node name: " + name;
              // Returns null for non-directory resources, since the jimage name does not
              // start with "/modules" (e.g. "/java.base/java/lang/Object.class").
              ImageLocation loc = findLocation(name);
              if (loc != null) {
                  assert name.equals(loc.getFullName()) : "Mismatched location for directory: " + name;
!                 assert isModulesSubdirectory(loc) : "Invalid modules directory: " + name;
!                 return completeModuleDirectory(newDirectory(name), loc);
              }
              // Now try the non-prefixed resource name, but be careful to avoid false
              // positives for names like "/modules/modules/xxx" which could return a
              // location of a directory entry.
!             loc = findLocation(name.substring(MODULES_ROOT.length()));
!             return loc != null && isResource(loc) ? newResource(name, loc) : null;
          }
  
          /**
!          * Builds a node in the "/packages/..." namespace.
           *
!          * <p>Called by {@link #findNode(String)} if a {@code /packages/...} node
!          * is not present in the cache.
           */
!         private Node buildPackagesNode(String name) {
!             // There are only locations for the root "/packages" or "/packages/xxx"
!             // directories, but not the symbolic links below them (the links can be
!             // entirely derived from the name information in the parent directory).
!             // However, unlike resources this means that we do not have a constant
-             // time lookup for link nodes when creating them.
-             int packageStart = PACKAGES_ROOT.length() + 1;
              int packageEnd = name.indexOf('/', packageStart);
!             if (packageEnd == -1) {
!                 ImageLocation loc = findLocation(name);
!                 return loc != null ? completePackageDirectory(newDirectory(name), loc) : null;
-             } else {
-                 // We cannot assume that the parent directory exists for a link node, since
-                 // the given name is untrusted and could reference a non-existent link.
-                 // However, if the parent directory is present, we can conclude that the
-                 // given name was not a valid link (or else it would already be cached).
                  String dirName = name.substring(0, packageEnd);
!                 if (!nodes.containsKey(dirName)) {
!                     ImageLocation loc = findLocation(dirName);
!                     // If the parent location doesn't exist, the link node cannot exist.
!                     if (loc != null) {
!                         nodes.put(dirName, completePackageDirectory(newDirectory(dirName), loc));
!                         // When the parent is created its child nodes are created and cached,
-                         // but this can still return null if given name wasn't a valid link.
-                         return nodes.get(name);
                      }
                  }
              }
              return null;
          }
  
--- 503,117 ---
          /**
           * Returns whether a resource exists in the given module.
           *
           * <p>This method is expected to be called frequently for resources
           * which do not exist in the given module (e.g. as part of classpath
!          * search). As such, it skips checking the nodes cache if possible, and
!          * only checks for an entry in the jimage file, as this is faster if the
!          * resource is not present. This also means it doesn't need
+          * synchronization most of the time.
           */
          boolean containsResource(String moduleName, String resourcePath) {
              if (moduleName.indexOf('/') >= 0) {
                  throw new IllegalArgumentException("invalid module name: " + moduleName);
              }
!             // In preview mode, preview-only resources are eagerly added to the
!             // cache, so we must check that first.
+             if (previewMode) {
+                 String nodeName = MODULES_PREFIX + "/" + moduleName + "/" + resourcePath;
+                 // Synchronize as tightly as possible to reduce locking contention.
+                 synchronized (this) {
+                     Node node = nodes.get(nodeName);
+                     if (node != null) {
+                         return node.isResource();
+                     }
+                 }
+             }
              ImageLocation loc = findLocation(moduleName, resourcePath);
!             return loc != null && loc.getType() == RESOURCE;
          }
  
          /**
           * Builds a node in the "/modules/..." namespace.
           *
           * <p>Called by {@link #findNode(String)} if a {@code /modules/...} node
           * is not present in the cache.
           */
!         private Node buildAndCacheModulesNode(String name) {
!             assert name.startsWith(MODULES_PREFIX + "/") : "Invalid module node name: " + name;
+             if (isPreviewName(name)) {
+                 return null;
+             }
              // Returns null for non-directory resources, since the jimage name does not
              // start with "/modules" (e.g. "/java.base/java/lang/Object.class").
              ImageLocation loc = findLocation(name);
              if (loc != null) {
                  assert name.equals(loc.getFullName()) : "Mismatched location for directory: " + name;
!                 assert loc.getType() == MODULES_DIR : "Invalid modules directory: " + name;
!                 return ensureCached(completeModuleDirectory(newDirectory(name), loc));
              }
              // Now try the non-prefixed resource name, but be careful to avoid false
              // positives for names like "/modules/modules/xxx" which could return a
              // location of a directory entry.
!             loc = findLocation(name.substring(MODULES_PREFIX.length()));
!             return loc != null && loc.getType() == RESOURCE
+                     ? ensureCached(newResource(name, loc))
+                     : null;
          }
  
          /**
!          * Returns whether a directory name in the "/modules/" directory could be referencing
+          * the "META-INF" directory".
+          */
+         private boolean isMetaInf(Directory dir) {
+             String name = dir.getName();
+             int pathStart = name.indexOf('/', MODULES_PREFIX.length() + 1);
+             return name.length() == pathStart + "/META-INF".length()
+                     && name.endsWith("/META-INF");
+         }
+ 
+         /**
+          * Returns whether a node name in the "/modules/" directory could be referencing
+          * a preview resource or directory under "META-INF/preview".
+          */
+         private boolean isPreviewName(String name) {
+             int pathStart = name.indexOf('/', MODULES_PREFIX.length() + 1);
+             int previewEnd = pathStart + PREVIEW_INFIX.length();
+             return pathStart > 0
+                     && name.regionMatches(pathStart, PREVIEW_INFIX, 0, PREVIEW_INFIX.length())
+                     && (name.length() == previewEnd || name.charAt(previewEnd) == '/');
+         }
+ 
+         private String getBaseName(ImageLocation loc) {
+             // Matches logic in ImageLocation#getFullName() regarding extensions.
+             String trailingParts = loc.getBase()
+                     + ((loc.getExtensionOffset() != 0) ? "." + loc.getExtension() : "");
+             return trailingParts.substring(trailingParts.lastIndexOf('/') + 1);
+         }
+ 
+         /**
+          * Builds a link node of the form "/packages/xxx/yyy".
           *
!          * <p>Called by {@link #findNode(String)} if a {@code /packages/...}
!          * node is not present in the cache (the name is not trusted).
           */
!         private Node buildAndCacheLinkNode(String name) {
!             // There are only locations for "/packages" or "/packages/xxx"
!             // directories, but not the symbolic links below them (links are
!             // derived from the name information in the parent directory).
!             int packageStart = PACKAGES_PREFIX.length() + 1;
              int packageEnd = name.indexOf('/', packageStart);
!             // We already built the 2-level "/packages/xxx" directories,
!             // so if this is a 2-level name, it cannot reference a node.
!             if (packageEnd >= 0) {
                  String dirName = name.substring(0, packageEnd);
!                 // If no parent exists here, the name cannot be valid.
!                 Directory parent = (Directory) nodes.get(dirName);
!                 if (parent != null) {
!                     if (!parent.isCompleted()) {
!                         // This caches all child links of the parent directory.
!                         completePackageSubdirectory(parent, findLocation(dirName));
                      }
+                     return nodes.get(name);
                  }
              }
              return null;
          }
  

*** 459,131 ***
          private void completeDirectory(Directory dir) {
              String name = dir.getName();
              // Since the node exists, we can assert that its name starts with
              // either "/modules" or "/packages", making differentiation easy.
              // It also means that the name is valid, so it must yield a location.
!             assert name.startsWith(MODULES_ROOT) || name.startsWith(PACKAGES_ROOT);
              ImageLocation loc = findLocation(name);
              assert loc != null && name.equals(loc.getFullName()) : "Invalid location for name: " + name;
!             // We cannot use 'isXxxSubdirectory()' methods here since we could
!             // be given a top-level directory (for which that test doesn't work).
-             // The string MUST start "/modules" or "/packages" here.
-             if (name.charAt(1) == 'm') {
                  completeModuleDirectory(dir, loc);
              } else {
!                 completePackageDirectory(dir, loc);
              }
              assert dir.isCompleted() : "Directory must be complete by now: " + dir;
          }
  
!         /**
-          * Completes a modules directory by setting the list of child nodes.
-          *
-          * <p>The given directory can be the top level {@code /modules} directory,
-          * so it is NOT safe to use {@code isModulesSubdirectory(loc)} here.
-          */
          private Directory completeModuleDirectory(Directory dir, ImageLocation loc) {
              assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
!             List<Node> children = createChildNodes(loc, childLoc -> {
!                 if (isModulesSubdirectory(childLoc)) {
!                     return nodes.computeIfAbsent(childLoc.getFullName(), this::newDirectory);
                  } else {
                      // Add "/modules" prefix to image location paths to get node names.
                      String resourceName = childLoc.getFullName(true);
                      return nodes.computeIfAbsent(resourceName, n -> newResource(n, childLoc));
                  }
              });
              dir.setChildren(children);
              return dir;
          }
  
          /**
!          * Completes a package directory by setting the list of child nodes.
           *
!          * <p>The given directory can be the top level {@code /packages} directory,
!          * so it is NOT safe to use {@code isPackagesSubdirectory(loc)} here.
           */
!         private Directory completePackageDirectory(Directory dir, ImageLocation loc) {
!             assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
!             // The only directories in the "/packages" namespace are "/packages" or
!             // "/packages/<package>". However, unlike "/modules" directories, the
!             // location offsets mean different things.
-             List<Node> children;
-             if (dir.getName().equals(PACKAGES_ROOT)) {
-                 // Top-level directory just contains a list of subdirectories.
-                 children = createChildNodes(loc, c -> nodes.computeIfAbsent(c.getFullName(), this::newDirectory));
-             } else {
-                 // A package directory's content is array of offset PAIRS in the
-                 // Strings table, but we only need the 2nd value of each pair.
-                 IntBuffer intBuffer = getOffsetBuffer(loc);
-                 int offsetCount = intBuffer.capacity();
-                 assert (offsetCount & 0x1) == 0 : "Offset count must be even: " + offsetCount;
-                 children = new ArrayList<>(offsetCount / 2);
-                 // Iterate the 2nd offset in each pair (odd indices).
-                 for (int i = 1; i < offsetCount; i += 2) {
-                     String moduleName = getString(intBuffer.get(i));
-                     children.add(nodes.computeIfAbsent(
-                             dir.getName() + "/" + moduleName,
-                             n -> newLinkNode(n, MODULES_ROOT + "/" + moduleName)));
                  }
              }
!             // This only happens once and "completes" the directory.
-             dir.setChildren(children);
-             return dir;
          }
  
          /**
!          * Creates the list of child nodes for a {@code Directory} based on a given
           *
!          * <p>Note: This cannot be used for package subdirectories as they have
!          * child offsets stored differently to other directories.
           */
!         private List<Node> createChildNodes(ImageLocation loc, Function<ImageLocation, Node> newChildFn) {
              IntBuffer offsets = getOffsetBuffer(loc);
              int childCount = offsets.capacity();
!             List<Node> children = new ArrayList<>(childCount);
              for (int i = 0; i < childCount; i++) {
!                 children.add(newChildFn.apply(getLocation(offsets.get(i))));
              }
              return children;
          }
  
          /** Helper to extract the integer offset buffer from a directory location. */
          private IntBuffer getOffsetBuffer(ImageLocation dir) {
!             assert !isResource(dir) : "Not a directory: " + dir.getFullName();
              byte[] offsets = getResource(dir);
              ByteBuffer buffer = ByteBuffer.wrap(offsets);
              buffer.order(getByteOrder());
              return buffer.asIntBuffer();
          }
  
-         /**
-          * Efficiently determines if an image location is a resource.
-          *
-          * <p>A resource must have a valid module associated with it, so its
-          * module offset must be non-zero, and not equal to the offsets for
-          * "/modules/..." or "/packages/..." entries.
-          */
-         private boolean isResource(ImageLocation loc) {
-             int moduleOffset = loc.getModuleOffset();
-             return moduleOffset != 0
-                     && moduleOffset != modulesStringOffset
-                     && moduleOffset != packagesStringOffset;
-         }
- 
-         /**
-          * Determines if an image location is a directory in the {@code /modules}
-          * namespace (if so, the location name is the node name).
-          *
-          * <p>In jimage, every {@code ImageLocation} under {@code /modules/} is a
-          * directory and has the same value for {@code getModule()}, and {@code
-          * getModuleOffset()}.
-          */
-         private boolean isModulesSubdirectory(ImageLocation loc) {
-             return loc.getModuleOffset() == modulesStringOffset;
-         }
- 
          /**
           * Creates an "incomplete" directory node with no child nodes set.
           * Directories need to be "completed" before they are returned by
           * {@link #findNode(String)}.
           */
--- 621,129 ---
          private void completeDirectory(Directory dir) {
              String name = dir.getName();
              // Since the node exists, we can assert that its name starts with
              // either "/modules" or "/packages", making differentiation easy.
              // It also means that the name is valid, so it must yield a location.
!             assert name.startsWith(MODULES_PREFIX) || name.startsWith(PACKAGES_PREFIX);
              ImageLocation loc = findLocation(name);
              assert loc != null && name.equals(loc.getFullName()) : "Invalid location for name: " + name;
!             LocationType type = loc.getType();
!             if (type == MODULES_DIR || type == MODULES_ROOT) {
                  completeModuleDirectory(dir, loc);
              } else {
!                 assert type == PACKAGES_DIR : "Invalid location type: " + loc;
+                 completePackageSubdirectory(dir, loc);
              }
              assert dir.isCompleted() : "Directory must be complete by now: " + dir;
          }
  
!         /** Completes a modules directory by setting the list of child nodes. */
          private Directory completeModuleDirectory(Directory dir, ImageLocation loc) {
              assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
!             List<Node> previewOnlyNodes = getPreviewNodesToMerge(dir);
!             // We hide preview names from direct lookup, but must also prevent
!             // the preview directory from appearing in any META-INF directories.
+             boolean parentIsMetaInfDir = isMetaInf(dir);
+             List<Node> children = createChildNodes(loc, previewOnlyNodes.size(), childLoc -> {
+                 LocationType type = childLoc.getType();
+                 if (type == MODULES_DIR) {
+                     String name = childLoc.getFullName();
+                     return parentIsMetaInfDir && name.endsWith("/preview")
+                             ? null
+                             : nodes.computeIfAbsent(name, this::newDirectory);
                  } else {
+                     assert type == RESOURCE : "Invalid location type: " + loc;
                      // Add "/modules" prefix to image location paths to get node names.
                      String resourceName = childLoc.getFullName(true);
                      return nodes.computeIfAbsent(resourceName, n -> newResource(n, childLoc));
                  }
              });
+             children.addAll(previewOnlyNodes);
              dir.setChildren(children);
              return dir;
          }
  
+         /** Completes a package directory by setting the list of child nodes. */
+         private void completePackageSubdirectory(Directory dir, ImageLocation loc) {
+             assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
+             assert !dir.isCompleted() : "Directory already completed: " + dir;
+             assert loc.getType() == PACKAGES_DIR : "Invalid location type: " + loc.getType();
+ 
+             // In non-preview mode we might skip a very small number of preview-only
+             // entries, but it's not worth "right-sizing" the array for that.
+             IntBuffer offsets = getOffsetBuffer(loc);
+             List<Node> children = new ArrayList<>(offsets.capacity() / 2);
+             ModuleReference.readNameOffsets(offsets, /*normal*/ true, previewMode)
+                     .forEachRemaining(n -> {
+                         String modName = getString(n);
+                         Node link = newLinkNode(dir.getName() + "/" + modName, MODULES_PREFIX + "/" + modName);
+                         children.add(ensureCached(link));
+                     });
+             // If the parent directory exists, there must be at least one child node.
+             assert !children.isEmpty() : "Invalid empty package directory: " + dir;
+             dir.setChildren(children);
+         }
+ 
          /**
!          * Returns the list of child preview nodes to be merged into the given directory.
           *
!          * <p>Because this is only called once per-directory (since the result is cached
!          * indefinitely) we can remove any entries we find from the cache. If ever the
+          * node cache allowed entries to expire, this would have to be changed so that
+          * directories could be completed more than once.
           */
!         List<Node> getPreviewNodesToMerge(Directory dir) {
!             if (previewDirectoriesToMerge != null) {
!                 Directory mergeDir = previewDirectoriesToMerge.remove(dir.getName());
!                 if (mergeDir != null) {
!                     return mergeDir.children;
                  }
              }
!             return Collections.emptyList();
          }
  
          /**
!          * Creates the list of child nodes for a modules {@code Directory} from
+          * its parent location.
+          *
+          * <p>The {@code getChildFn} may return existing cached nodes rather
+          * than creating them, and if newly created nodes are to be cached,
+          * it is the job of {@code getChildFn}, or the caller of this method,
+          * to do that.
           *
!          * @param loc a location relating to a "/modules" directory.
!          * @param extraNodesCount a known number of preview-only child nodes
+          *     which will be merged onto the end of the returned list later.
+          * @param getChildFn a function to return a node for each child location
+          *     (or null to skip putting anything in the list).
+          * @return the list of the non-null child nodes, returned by
+          *     {@code getChildFn}, in the order of the locations entries.
           */
!         private List<Node> createChildNodes(ImageLocation loc, int extraNodesCount, Function<ImageLocation, Node> getChildFn) {
+             LocationType type = loc.getType();
+             assert type == MODULES_DIR || type == MODULES_ROOT : "Invalid location type: " + loc;
              IntBuffer offsets = getOffsetBuffer(loc);
              int childCount = offsets.capacity();
!             List<Node> children = new ArrayList<>(childCount + extraNodesCount);
              for (int i = 0; i < childCount; i++) {
!                 Node childNode = getChildFn.apply(getLocation(offsets.get(i)));
+                 if (childNode != null) {
+                     children.add(childNode);
+                 }
              }
              return children;
          }
  
          /** Helper to extract the integer offset buffer from a directory location. */
          private IntBuffer getOffsetBuffer(ImageLocation dir) {
!             assert dir.getType() != RESOURCE : "Not a directory: " + dir.getFullName();
              byte[] offsets = getResource(dir);
              ByteBuffer buffer = ByteBuffer.wrap(offsets);
              buffer.order(getByteOrder());
              return buffer.asIntBuffer();
          }
  
          /**
           * Creates an "incomplete" directory node with no child nodes set.
           * Directories need to be "completed" before they are returned by
           * {@link #findNode(String)}.
           */

*** 595,11 ***
           * Creates a new resource from an image location. This is the only case
           * where the image location name does not match the requested node name.
           * In image files, resource locations are NOT prefixed by {@code /modules}.
           */
          private Resource newResource(String name, ImageLocation loc) {
-             assert name.equals(loc.getFullName(true)) : "Mismatched location for resource: " + name;
              return new Resource(name, loc, imageFileAttributes);
          }
  
          /**
           * Creates a new link node pointing at the given target name.
--- 755,10 ---

*** 827,15 ***
                  return children.stream().map(Node::getName);
              }
              throw new IllegalStateException("Cannot get child nodes of an incomplete directory: " + getName());
          }
  
!         private void setChildren(List<Node> children) {
              assert this.children == null : this + ": Cannot set child nodes twice!";
              this.children = Collections.unmodifiableList(children);
          }
      }
      /**
       * Resource node (e.g. a ".class" entry, or any other data resource).
       *
       * <p>Resources are leaf nodes referencing an underlying image location. They
       * are lightweight, and do not cache their contents.
--- 986,16 ---
                  return children.stream().map(Node::getName);
              }
              throw new IllegalStateException("Cannot get child nodes of an incomplete directory: " + getName());
          }
  
!         private void setChildren(List<? extends Node> children) {
              assert this.children == null : this + ": Cannot set child nodes twice!";
              this.children = Collections.unmodifiableList(children);
          }
      }
+ 
      /**
       * Resource node (e.g. a ".class" entry, or any other data resource).
       *
       * <p>Resources are leaf nodes referencing an underlying image location. They
       * are lightweight, and do not cache their contents.
< prev index next >