< prev index next >

test/jdk/jdk/internal/jimage/ImageReaderTest.java

Print this page
*** 21,10 ***
--- 21,11 ---
   * questions.
   */
  
  import jdk.internal.jimage.ImageReader;
  import jdk.internal.jimage.ImageReader.Node;
+ import jdk.internal.jimage.PreviewMode;
  import jdk.test.lib.compiler.InMemoryJavaCompiler;
  import jdk.test.lib.util.JarBuilder;
  import jdk.tools.jlink.internal.LinkableRuntimeImage;
  import org.junit.jupiter.api.Assumptions;
  import org.junit.jupiter.api.Test;

*** 41,10 ***
--- 42,11 ---
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.stream.Collectors;
  
+ import static java.util.stream.Collectors.toSet;
  import static org.junit.jupiter.api.Assertions.assertEquals;
  import static org.junit.jupiter.api.Assertions.assertFalse;
  import static org.junit.jupiter.api.Assertions.assertNotNull;
  import static org.junit.jupiter.api.Assertions.assertNull;
  import static org.junit.jupiter.api.Assertions.assertSame;

*** 61,26 ***
   *          jdk.jlink/jdk.tools.jlink.internal
   *          jdk.jlink/jdk.tools.jimage
   * @library /test/jdk/tools/lib
   *          /test/lib
   * @build tests.*
!  * @run junit/othervm ImageReaderTest
   */
  
  /// Using PER_CLASS lifecycle means the (expensive) image file is only build once.
  /// There is no mutable test instance state to worry about.
  @TestInstance(PER_CLASS)
  public class ImageReaderTest {
! 
      private static final Map<String, List<String>> IMAGE_ENTRIES = Map.of(
              "modfoo", Arrays.asList(
!                     "com.foo.Alpha",
!                     "com.foo.Beta",
!                     "com.foo.bar.Gamma"),
              "modbar", Arrays.asList(
                      "com.bar.One",
!                     "com.bar.Two"));
      private final Path image = buildJImage(IMAGE_ENTRIES);
  
      @ParameterizedTest
      @ValueSource(strings = {
              "/",
--- 63,37 ---
   *          jdk.jlink/jdk.tools.jlink.internal
   *          jdk.jlink/jdk.tools.jimage
   * @library /test/jdk/tools/lib
   *          /test/lib
   * @build tests.*
!  * @run junit/othervm -esa -DDISABLE_PREVIEW_PATCHING=true ImageReaderTest
   */
  
  /// Using PER_CLASS lifecycle means the (expensive) image file is only build once.
  /// There is no mutable test instance state to worry about.
  @TestInstance(PER_CLASS)
  public class ImageReaderTest {
!     // The '@' prefix marks the entry as a preview entry which will be placed in
+     // the '/modules/<module>/META-INF/preview/...' namespace.
      private static final Map<String, List<String>> IMAGE_ENTRIES = Map.of(
              "modfoo", Arrays.asList(
!                     "com.foo.HasPreviewVersion",
!                     "com.foo.NormalFoo",
!                     "com.foo.bar.NormalBar",
+                     // Replaces original class in preview mode.
+                     "@com.foo.HasPreviewVersion",
+                     // New class in existing package in preview mode.
+                     "@com.foo.bar.IsPreviewOnly"),
              "modbar", Arrays.asList(
                      "com.bar.One",
!                     "com.bar.Two",
+                     // Two new packages in preview mode (new symbolic links).
+                     "@com.bar.preview.stuff.Foo",
+                     "@com.bar.preview.stuff.Bar"),
+             "modgus", Arrays.asList(
+                     // A second module with a preview-only empty package (preview).
+                     "@com.bar.preview.other.Gus"));
      private final Path image = buildJImage(IMAGE_ENTRIES);
  
      @ParameterizedTest
      @ValueSource(strings = {
              "/",

*** 89,11 ***
              "/modules/modbar",
              "/modules/modfoo/com",
              "/modules/modfoo/com/foo",
              "/modules/modfoo/com/foo/bar"})
      public void testModuleDirectories_expected(String name) throws IOException {
!         try (ImageReader reader = ImageReader.open(image)) {
              assertDir(reader, name);
          }
      }
  
      @ParameterizedTest
--- 102,11 ---
              "/modules/modbar",
              "/modules/modfoo/com",
              "/modules/modfoo/com/foo",
              "/modules/modfoo/com/foo/bar"})
      public void testModuleDirectories_expected(String name) throws IOException {
!         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
              assertDir(reader, name);
          }
      }
  
      @ParameterizedTest

*** 104,36 ***
              "/modules/unknown",
              "/modules/modbar/",
              "/modules/modfoo//com",
              "/modules/modfoo/com/"})
      public void testModuleNodes_absent(String name) throws IOException {
!         try (ImageReader reader = ImageReader.open(image)) {
              assertAbsent(reader, name);
          }
      }
  
      @Test
      public void testModuleResources() throws IOException {
!         try (ImageReader reader = ImageReader.open(image)) {
!             assertNode(reader, "/modules/modfoo/com/foo/Alpha.class");
              assertNode(reader, "/modules/modbar/com/bar/One.class");
  
              ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
!             assertEquals("Class: com.foo.Alpha", loader.loadAndGetToString("modfoo", "com.foo.Alpha"));
!             assertEquals("Class: com.foo.Beta", loader.loadAndGetToString("modfoo", "com.foo.Beta"));
!             assertEquals("Class: com.foo.bar.Gamma", loader.loadAndGetToString("modfoo", "com.foo.bar.Gamma"));
              assertEquals("Class: com.bar.One", loader.loadAndGetToString("modbar", "com.bar.One"));
          }
      }
  
      @ParameterizedTest
      @CsvSource(delimiter = ':', value = {
!             "modfoo:com/foo/Alpha.class",
              "modbar:com/bar/One.class",
      })
      public void testResource_present(String modName, String resPath) throws IOException {
!         try (ImageReader reader = ImageReader.open(image)) {
              assertNotNull(reader.findResourceNode(modName, resPath));
              assertTrue(reader.containsResource(modName, resPath));
  
              String canonicalNodeName = "/modules/" + modName + "/" + resPath;
              Node node = reader.findNode(canonicalNodeName);
--- 117,36 ---
              "/modules/unknown",
              "/modules/modbar/",
              "/modules/modfoo//com",
              "/modules/modfoo/com/"})
      public void testModuleNodes_absent(String name) throws IOException {
!         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
              assertAbsent(reader, name);
          }
      }
  
      @Test
      public void testModuleResources() throws IOException {
!         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
!             assertNode(reader, "/modules/modfoo/com/foo/HasPreviewVersion.class");
              assertNode(reader, "/modules/modbar/com/bar/One.class");
  
              ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
!             assertEquals("Class: com.foo.HasPreviewVersion", loader.loadAndGetToString("modfoo", "com.foo.HasPreviewVersion"));
!             assertEquals("Class: com.foo.NormalFoo", loader.loadAndGetToString("modfoo", "com.foo.NormalFoo"));
!             assertEquals("Class: com.foo.bar.NormalBar", loader.loadAndGetToString("modfoo", "com.foo.bar.NormalBar"));
              assertEquals("Class: com.bar.One", loader.loadAndGetToString("modbar", "com.bar.One"));
          }
      }
  
      @ParameterizedTest
      @CsvSource(delimiter = ':', value = {
!             "modfoo:com/foo/HasPreviewVersion.class",
              "modbar:com/bar/One.class",
      })
      public void testResource_present(String modName, String resPath) throws IOException {
!         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
              assertNotNull(reader.findResourceNode(modName, resPath));
              assertTrue(reader.containsResource(modName, resPath));
  
              String canonicalNodeName = "/modules/" + modName + "/" + resPath;
              Node node = reader.findNode(canonicalNodeName);

*** 145,22 ***
      @CsvSource(delimiter = ':', value = {
              // Absolute resource names are not allowed.
              "modfoo:/com/bar/One.class",
              // Resource in wrong module.
              "modfoo:com/bar/One.class",
!             "modbar:com/foo/Alpha.class",
              // Directories are not returned.
              "modfoo:com/foo",
              "modbar:com/bar",
              // JImage entries exist for these, but they are not resources.
!             "modules:modfoo/com/foo/Alpha.class",
              "packages:com.foo/modfoo",
              // Empty module names/paths do not find resources.
!             "'':modfoo/com/foo/Alpha.class",
              "modfoo:''"})
      public void testResource_absent(String modName, String resPath) throws IOException {
!         try (ImageReader reader = ImageReader.open(image)) {
              assertNull(reader.findResourceNode(modName, resPath));
              assertFalse(reader.containsResource(modName, resPath));
  
              // Non-existent resources names should either not be found,
              // or (in the case of directory nodes) not be resources.
--- 158,22 ---
      @CsvSource(delimiter = ':', value = {
              // Absolute resource names are not allowed.
              "modfoo:/com/bar/One.class",
              // Resource in wrong module.
              "modfoo:com/bar/One.class",
!             "modbar:com/foo/HasPreviewVersion.class",
              // Directories are not returned.
              "modfoo:com/foo",
              "modbar:com/bar",
              // JImage entries exist for these, but they are not resources.
!             "modules:modfoo/com/foo/HasPreviewVersion.class",
              "packages:com.foo/modfoo",
              // Empty module names/paths do not find resources.
!             "'':modfoo/com/foo/HasPreviewVersion.class",
              "modfoo:''"})
      public void testResource_absent(String modName, String resPath) throws IOException {
!         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
              assertNull(reader.findResourceNode(modName, resPath));
              assertFalse(reader.containsResource(modName, resPath));
  
              // Non-existent resources names should either not be found,
              // or (in the case of directory nodes) not be resources.

*** 173,24 ***
      @ParameterizedTest
      @CsvSource(delimiter = ':', value = {
              // Don't permit module names to contain paths.
              "modfoo/com/bar:One.class",
              "modfoo/com:bar/One.class",
!             "modules/modfoo/com:foo/Alpha.class",
      })
      public void testResource_invalid(String modName, String resPath) throws IOException {
!         try (ImageReader reader = ImageReader.open(image)) {
              assertThrows(IllegalArgumentException.class, () -> reader.containsResource(modName, resPath));
              assertThrows(IllegalArgumentException.class, () -> reader.findResourceNode(modName, resPath));
          }
      }
  
      @Test
      public void testPackageDirectories() throws IOException {
!         try (ImageReader reader = ImageReader.open(image)) {
              Node root = assertDir(reader, "/packages");
!             Set<String> pkgNames = root.getChildNames().collect(Collectors.toSet());
              assertTrue(pkgNames.contains("/packages/com"));
              assertTrue(pkgNames.contains("/packages/com.foo"));
              assertTrue(pkgNames.contains("/packages/com.bar"));
  
              // Even though no classes exist directly in the "com" package, it still
--- 186,24 ---
      @ParameterizedTest
      @CsvSource(delimiter = ':', value = {
              // Don't permit module names to contain paths.
              "modfoo/com/bar:One.class",
              "modfoo/com:bar/One.class",
!             "modules/modfoo/com:foo/HasPreviewVersion.class",
      })
      public void testResource_invalid(String modName, String resPath) throws IOException {
!         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
              assertThrows(IllegalArgumentException.class, () -> reader.containsResource(modName, resPath));
              assertThrows(IllegalArgumentException.class, () -> reader.findResourceNode(modName, resPath));
          }
      }
  
      @Test
      public void testPackageDirectories() throws IOException {
!         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
              Node root = assertDir(reader, "/packages");
!             Set<String> pkgNames = root.getChildNames().collect(toSet());
              assertTrue(pkgNames.contains("/packages/com"));
              assertTrue(pkgNames.contains("/packages/com.foo"));
              assertTrue(pkgNames.contains("/packages/com.bar"));
  
              // Even though no classes exist directly in the "com" package, it still

*** 201,18 ***
          }
      }
  
      @Test
      public void testPackageLinks() throws IOException {
!         try (ImageReader reader = ImageReader.open(image)) {
              Node moduleFoo = assertDir(reader, "/modules/modfoo");
              Node moduleBar = assertDir(reader, "/modules/modbar");
              assertSame(assertLink(reader, "/packages/com.foo/modfoo").resolveLink(), moduleFoo);
              assertSame(assertLink(reader, "/packages/com.bar/modbar").resolveLink(), moduleBar);
          }
      }
  
      private static ImageReader.Node assertNode(ImageReader reader, String name) throws IOException {
          ImageReader.Node node = reader.findNode(name);
          assertNotNull(node, "Could not find node: " + name);
          return node;
      }
--- 214,135 ---
          }
      }
  
      @Test
      public void testPackageLinks() throws IOException {
!         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
              Node moduleFoo = assertDir(reader, "/modules/modfoo");
              Node moduleBar = assertDir(reader, "/modules/modbar");
              assertSame(assertLink(reader, "/packages/com.foo/modfoo").resolveLink(), moduleFoo);
              assertSame(assertLink(reader, "/packages/com.bar/modbar").resolveLink(), moduleBar);
          }
      }
  
+     @Test
+     public void testPreviewResources_disabled() throws IOException {
+         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
+             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
+ 
+             // No preview classes visible.
+             assertEquals("Class: com.foo.HasPreviewVersion", loader.loadAndGetToString("modfoo", "com.foo.HasPreviewVersion"));
+             assertEquals("Class: com.foo.NormalFoo", loader.loadAndGetToString("modfoo", "com.foo.NormalFoo"));
+             assertEquals("Class: com.foo.bar.NormalBar", loader.loadAndGetToString("modfoo", "com.foo.bar.NormalBar"));
+ 
+             // NormalBar exists but IsPreviewOnly doesn't.
+             assertResource(reader, "modfoo", "com/foo/bar/NormalBar.class");
+             assertAbsent(reader, "/modules/modfoo/com/foo/bar/IsPreviewOnly.class");
+             assertDirContents(reader, "/modules/modfoo/com/foo", "HasPreviewVersion.class", "NormalFoo.class", "bar");
+             assertDirContents(reader, "/modules/modfoo/com/foo/bar", "NormalBar.class");
+         }
+     }
+ 
+     @Test
+     public void testPreviewResources_enabled() throws IOException {
+         try (ImageReader reader = ImageReader.open(image, PreviewMode.ENABLED)) {
+             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
+ 
+             // Preview version of classes either overwrite existing entries or are added to directories.
+             assertEquals("Preview: com.foo.HasPreviewVersion", loader.loadAndGetToString("modfoo", "com.foo.HasPreviewVersion"));
+             assertEquals("Class: com.foo.NormalFoo", loader.loadAndGetToString("modfoo", "com.foo.NormalFoo"));
+             assertEquals("Class: com.foo.bar.NormalBar", loader.loadAndGetToString("modfoo", "com.foo.bar.NormalBar"));
+             assertEquals("Preview: com.foo.bar.IsPreviewOnly", loader.loadAndGetToString("modfoo", "com.foo.bar.IsPreviewOnly"));
+ 
+             // Both NormalBar and IsPreviewOnly exist (direct lookup and as child nodes).
+             assertResource(reader, "modfoo", "com/foo/bar/NormalBar.class");
+             assertResource(reader, "modfoo", "com/foo/bar/IsPreviewOnly.class");
+             assertDirContents(reader, "/modules/modfoo/com/foo", "HasPreviewVersion.class", "NormalFoo.class", "bar");
+             assertDirContents(reader, "/modules/modfoo/com/foo/bar", "NormalBar.class", "IsPreviewOnly.class");
+         }
+     }
+ 
+     @Test
+     public void testPreviewOnlyPackages_disabled() throws IOException {
+         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
+             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
+ 
+             // No 'preview' package or anything inside it.
+             assertDirContents(reader, "/modules/modbar/com/bar", "One.class", "Two.class");
+             assertAbsent(reader, "/modules/modbar/com/bar/preview");
+             assertAbsent(reader, "/modules/modbar/com/bar/preview/stuff/Foo.class");
+ 
+             // And no package link.
+             assertAbsent(reader, "/packages/com.bar.preview");
+         }
+     }
+ 
+     @Test
+     public void testPreviewOnlyPackages_enabled() throws IOException {
+         try (ImageReader reader = ImageReader.open(image, PreviewMode.ENABLED)) {
+             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
+ 
+             // In preview mode 'preview' package exists with preview only content.
+             assertDirContents(reader, "/modules/modbar/com/bar", "One.class", "Two.class", "preview");
+             assertDirContents(reader, "/modules/modbar/com/bar/preview/stuff", "Foo.class", "Bar.class");
+             assertResource(reader, "modbar", "com/bar/preview/stuff/Foo.class");
+ 
+             // And package links exists.
+             assertDirContents(reader, "/packages/com.bar.preview", "modbar", "modgus");
+         }
+     }
+ 
+     @Test
+     public void testPreviewModeLinks_disabled() throws IOException {
+         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
+             assertDirContents(reader, "/packages/com.bar", "modbar");
+             // Missing symbolic link and directory when not in preview mode.
+             assertAbsent(reader, "/packages/com.bar.preview");
+             assertAbsent(reader, "/packages/com.bar.preview.stuff");
+             assertAbsent(reader, "/modules/modbar/com/bar/preview");
+             assertAbsent(reader, "/modules/modbar/com/bar/preview/stuff");
+         }
+     }
+ 
+     @Test
+     public void testPreviewModeLinks_enabled() throws IOException {
+         try (ImageReader reader = ImageReader.open(image, PreviewMode.ENABLED)) {
+             // In preview mode there is a new preview-only module visible.
+             assertDirContents(reader, "/packages/com.bar", "modbar", "modgus");
+             // And additional packages are present.
+             assertDirContents(reader, "/packages/com.bar.preview", "modbar", "modgus");
+             assertDirContents(reader, "/packages/com.bar.preview.stuff", "modbar");
+             assertDirContents(reader, "/packages/com.bar.preview.other", "modgus");
+             // And the preview-only content appears as we expect.
+             assertDirContents(reader, "/modules/modbar/com/bar", "One.class", "Two.class", "preview");
+             assertDirContents(reader, "/modules/modbar/com/bar/preview", "stuff");
+             assertDirContents(reader, "/modules/modbar/com/bar/preview/stuff", "Foo.class", "Bar.class");
+             // In both modules in which it was added.
+             assertDirContents(reader, "/modules/modgus/com/bar", "preview");
+             assertDirContents(reader, "/modules/modgus/com/bar/preview", "other");
+             assertDirContents(reader, "/modules/modgus/com/bar/preview/other", "Gus.class");
+         }
+     }
+ 
+     @ParameterizedTest
+     @ValueSource(booleans = {false, true})
+     public void testPreviewEntriesAlwaysHidden(boolean previewMode) throws IOException {
+         try (ImageReader reader = ImageReader.open(image, previewMode ? PreviewMode.ENABLED : PreviewMode.DISABLED)) {
+             // The META-INF directory exists, but does not contain the preview directory.
+             Node dir = assertDir(reader, "/modules/modfoo/META-INF");
+             assertEquals(0, dir.getChildNames().filter(n -> n.endsWith("/preview")).count());
+             // Neither the preview directory, nor anything in it, can be looked-up directly.
+             assertAbsent(reader, "/modules/modfoo/META-INF/preview");
+             assertAbsent(reader, "/modules/modfoo/META-INF/preview/com/foo");
+             // HasPreviewVersion.class is a preview class in the test data, and thus appears in
+             // two places in the jimage). Ensure the preview version is always hidden.
+             String alphaPath = "com/foo/HasPreviewVersion.class";
+             assertNode(reader, "/modules/modfoo/" + alphaPath);
+             assertAbsent(reader, "/modules/modfoo/META-INF/preview/" + alphaPath);
+         }
+     }
+ 
      private static ImageReader.Node assertNode(ImageReader reader, String name) throws IOException {
          ImageReader.Node node = reader.findNode(name);
          assertNotNull(node, "Could not find node: " + name);
          return node;
      }

*** 221,13 ***
          ImageReader.Node dir = assertNode(reader, name);
          assertTrue(dir.isDirectory(), "Node was not a directory: " + name);
          return dir;
      }
  
      private static ImageReader.Node assertLink(ImageReader reader, String name) throws IOException {
          ImageReader.Node link = assertNode(reader, name);
!         assertTrue(link.isLink(), "Node was not a symbolic link: " + name);
          return link;
      }
  
      private static void assertAbsent(ImageReader reader, String name) throws IOException {
          assertNull(reader.findNode(name), "Should not be able to find node: " + name);
--- 351,34 ---
          ImageReader.Node dir = assertNode(reader, name);
          assertTrue(dir.isDirectory(), "Node was not a directory: " + name);
          return dir;
      }
  
+     private static void assertDirContents(ImageReader reader, String name, String... expectedChildNames) throws IOException {
+         Node dir = assertDir(reader, name);
+         Set<String> localChildNames = dir.getChildNames()
+                 .peek(s -> assertTrue(s.startsWith(name + "/")))
+                 .map(s -> s.substring(name.length() + 1))
+                 .collect(toSet());
+         assertEquals(
+                 Set.of(expectedChildNames),
+                 localChildNames,
+                 String.format("Unexpected child names in directory '%s'", name));
+     }
+ 
+     private static void assertResource(ImageReader reader, String modName, String resPath) throws IOException {
+         assertTrue(reader.containsResource(modName, resPath), "Resource should exist: " + modName + "/" + resPath);
+         Node resNode = reader.findResourceNode(modName, resPath);
+         assertTrue(resNode.isResource(), "Node should be a resource: " + resNode.getName());
+         String nodeName = "/modules/" + modName + "/" + resPath;
+         assertEquals(nodeName, resNode.getName());
+         assertSame(resNode, reader.findNode(nodeName));
+     }
+ 
      private static ImageReader.Node assertLink(ImageReader reader, String name) throws IOException {
          ImageReader.Node link = assertNode(reader, name);
!         assertTrue(link.isLink(), "Node should be a symbolic link: " + link.getName());
          return link;
      }
  
      private static void assertAbsent(ImageReader reader, String name) throws IOException {
          assertNull(reader.findNode(name), "Should not be able to find node: " + name);

*** 248,24 ***
              JarBuilder jar = new JarBuilder(jarDir.resolve(module + ".jar").toString());
              String moduleInfo = "module " + module + " {}";
              jar.addEntry("module-info.class", InMemoryJavaCompiler.compile("module-info", moduleInfo));
  
              classes.forEach(fqn -> {
                  int lastDot = fqn.lastIndexOf('.');
                  String pkg = fqn.substring(0, lastDot);
                  String cls = fqn.substring(lastDot + 1);
- 
-                 String path = fqn.replace('.', '/') + ".class";
                  String source = String.format(
                          """
                          package %s;
                          public class %s {
                              public String toString() {
!                                 return "Class: %s";
                              }
                          }
!                         """, pkg, cls, fqn);
                  jar.addEntry(path, InMemoryJavaCompiler.compile(fqn, source));
              });
              try {
                  jar.build();
              } catch (IOException e) {
--- 399,27 ---
              JarBuilder jar = new JarBuilder(jarDir.resolve(module + ".jar").toString());
              String moduleInfo = "module " + module + " {}";
              jar.addEntry("module-info.class", InMemoryJavaCompiler.compile("module-info", moduleInfo));
  
              classes.forEach(fqn -> {
+                 boolean isPreviewEntry = fqn.startsWith("@");
+                 if (isPreviewEntry) {
+                     fqn = fqn.substring(1);
+                 }
                  int lastDot = fqn.lastIndexOf('.');
                  String pkg = fqn.substring(0, lastDot);
                  String cls = fqn.substring(lastDot + 1);
                  String source = String.format(
                          """
                          package %s;
                          public class %s {
                              public String toString() {
!                                 return "%s: %s";
                              }
                          }
!                         """, pkg, cls, isPreviewEntry ? "Preview" : "Class", fqn);
+                 String path = (isPreviewEntry ? "META-INF/preview/" : "") + fqn.replace('.', '/') + ".class";
                  jar.addEntry(path, InMemoryJavaCompiler.compile(fqn, source));
              });
              try {
                  jar.build();
              } catch (IOException e) {
< prev index next >