changeset 42468:7a9555a7e080

8166568: Add jmod extract subcommand 8169492: jdk.internal.jmod.JmodFile.JMOD_MAGIC_NUMBER is a mutable array Reviewed-by: alanb, anazarov, dfuchs, mchung
author chegar
date Sat, 10 Dec 2016 14:19:53 +0000
parents 46018567e1a6
children d1c0a9123f87
files jdk/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodOutputStream.java jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/resources/jmod.properties jdk/test/tools/jmod/JmodNegativeTest.java jdk/test/tools/jmod/JmodTest.java
diffstat 6 files changed, 166 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
--- a/jdk/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java	Fri Dec 09 16:38:34 2016 -0800
+++ b/jdk/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java	Sat Dec 10 14:19:53 2016 +0000
@@ -28,8 +28,10 @@
 import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Iterator;
 import java.util.stream.Stream;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
@@ -39,9 +41,9 @@
  */
 public class JmodFile implements AutoCloseable {
     // jmod magic number and version number
-    public static final int JMOD_MAJOR_VERSION = 0x01;
-    public static final int JMOD_MINOR_VERSION = 0x00;
-    public static final byte[] JMOD_MAGIC_NUMBER = {
+    private static final int JMOD_MAJOR_VERSION = 0x01;
+    private static final int JMOD_MINOR_VERSION = 0x00;
+    private static final byte[] JMOD_MAGIC_NUMBER = {
         0x4A, 0x4D, /* JM */
         JMOD_MAJOR_VERSION, JMOD_MINOR_VERSION, /* version 1.0 */
     };
@@ -175,6 +177,10 @@
         this.zipfile = new ZipFile(file.toFile());
     }
 
+    public static void writeMagicNumber(OutputStream os) throws IOException {
+        os.write(JMOD_MAGIC_NUMBER);
+    }
+
     /**
      * Returns the {@code Entry} for a resource in a JMOD file section
      * or {@code null} if not found.
--- a/jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodOutputStream.java	Fri Dec 09 16:38:34 2016 -0800
+++ b/jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodOutputStream.java	Sat Dec 10 14:19:53 2016 +0000
@@ -36,6 +36,7 @@
 import java.nio.file.Paths;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
+import jdk.internal.jmod.JmodFile;
 
 import static jdk.internal.jmod.JmodFile.*;
 
@@ -57,7 +58,7 @@
     private JmodOutputStream(OutputStream out) {
         this.zos = new ZipOutputStream(out);
         try {
-            out.write(JMOD_MAGIC_NUMBER);
+            JmodFile.writeMagicNumber(out);
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
--- a/jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java	Fri Dec 09 16:38:34 2016 -0800
+++ b/jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java	Sat Dec 10 14:19:53 2016 +0000
@@ -138,6 +138,8 @@
     private static final String PROGNAME = "jmod";
     private static final String MODULE_INFO = "module-info.class";
 
+    private static final Path CWD = Paths.get("");
+
     private Options options;
     private PrintWriter out = new PrintWriter(System.out, true);
     void setLog(PrintWriter out, PrintWriter err) {
@@ -153,6 +155,7 @@
 
     enum Mode {
         CREATE,
+        EXTRACT,
         LIST,
         DESCRIBE,
         HASH
@@ -178,6 +181,7 @@
         Pattern modulesToHash;
         boolean dryrun;
         List<PathMatcher> excludes;
+        Path extractDir;
     }
 
     public int run(String[] args) {
@@ -202,6 +206,9 @@
                 case CREATE:
                     ok = create();
                     break;
+                case EXTRACT:
+                    ok = extract();
+                    break;
                 case LIST:
                     ok = list();
                     break;
@@ -248,6 +255,32 @@
         }
     }
 
+    private boolean extract() throws IOException {
+        Path dir = options.extractDir != null ? options.extractDir : CWD;
+        try (JmodFile jf = new JmodFile(options.jmodFile)) {
+            jf.stream().forEach(e -> {
+                try {
+                    ZipEntry entry = e.zipEntry();
+                    String name = entry.getName();
+                    int index = name.lastIndexOf("/");
+                    if (index != -1) {
+                        Path p = dir.resolve(name.substring(0, index));
+                        if (Files.notExists(p))
+                            Files.createDirectories(p);
+                    }
+
+                    try (OutputStream os = Files.newOutputStream(dir.resolve(name))) {
+                        jf.getInputStream(e).transferTo(os);
+                    }
+                } catch (IOException x) {
+                    throw new UncheckedIOException(x);
+                }
+            });
+
+            return true;
+        }
+    }
+
     private boolean hashModules() {
         return new Hasher(options.moduleFinder).run();
     }
@@ -1019,8 +1052,6 @@
     static class ClassPathConverter implements ValueConverter<Path> {
         static final ValueConverter<Path> INSTANCE = new ClassPathConverter();
 
-        private static final Path CWD = Paths.get("");
-
         @Override
         public Path convert(String value) {
             try {
@@ -1044,8 +1075,6 @@
     static class DirPathConverter implements ValueConverter<Path> {
         static final ValueConverter<Path> INSTANCE = new DirPathConverter();
 
-        private static final Path CWD = Paths.get("");
-
         @Override
         public Path convert(String value) {
             try {
@@ -1065,6 +1094,33 @@
         @Override  public String valuePattern() { return "path"; }
     }
 
+    static class ExtractDirPathConverter implements ValueConverter<Path> {
+
+        @Override
+        public Path convert(String value) {
+            try {
+                Path path = CWD.resolve(value);
+                if (Files.exists(path)) {
+                    if (!Files.isDirectory(path))
+                        throw new CommandException("err.cannot.create.dir", path);
+                } else {
+                    try {
+                        Files.createDirectories(path);
+                    } catch (IOException ioe) {
+                        throw new CommandException("err.cannot.create.dir", path);
+                    }
+                }
+                return path;
+            } catch (InvalidPathException x) {
+                throw new CommandException("err.path.not.valid", value);
+            }
+        }
+
+        @Override  public Class<Path> valueType() { return Path.class; }
+
+        @Override  public String valuePattern() { return "path"; }
+    }
+
     static class ModuleVersionConverter implements ValueConverter<Version> {
         @Override
         public Version convert(String value) {
@@ -1158,6 +1214,7 @@
 
             builder.append(getMessage("main.opt.mode")).append("\n  ");
             builder.append(getMessage("main.opt.mode.create")).append("\n  ");
+            builder.append(getMessage("main.opt.mode.extract")).append("\n  ");
             builder.append(getMessage("main.opt.mode.list")).append("\n  ");
             builder.append(getMessage("main.opt.mode.describe")).append("\n  ");
             builder.append(getMessage("main.opt.mode.hash")).append("\n\n");
@@ -1203,6 +1260,11 @@
                         .withValuesSeparatedBy(File.pathSeparatorChar)
                         .withValuesConvertedBy(DirPathConverter.INSTANCE);
 
+        OptionSpec<Path> dir
+                = parser.accepts("dir", getMessage("main.opt.extractDir"))
+                        .withRequiredArg()
+                        .withValuesConvertedBy(new ExtractDirPathConverter());
+
         OptionSpec<Void> dryrun
             = parser.accepts("dry-run", getMessage("main.opt.dry-run"));
 
@@ -1303,6 +1365,8 @@
                 options.cmds = opts.valuesOf(cmds);
             if (opts.has(config))
                 options.configs = opts.valuesOf(config);
+            if (opts.has(dir))
+                options.extractDir = opts.valueOf(dir);
             if (opts.has(dryrun))
                 options.dryrun = true;
             if (opts.has(excludes))
@@ -1347,7 +1411,8 @@
                 if (options.mode.equals(Mode.CREATE) && Files.exists(path))
                     throw new CommandException("err.file.already.exists", path);
                 else if ((options.mode.equals(Mode.LIST) ||
-                            options.mode.equals(Mode.DESCRIBE))
+                            options.mode.equals(Mode.DESCRIBE) ||
+                            options.mode.equals((Mode.EXTRACT)))
                          && Files.notExists(path))
                     throw new CommandException("err.jmod.not.found", path);
 
--- a/jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/resources/jmod.properties	Fri Dec 09 16:38:34 2016 -0800
+++ b/jdk/src/jdk.jlink/share/classes/jdk/tools/jmod/resources/jmod.properties	Sat Dec 10 14:19:53 2016 +0000
@@ -24,11 +24,11 @@
 #
 
 main.usage.summary=\
-Usage: {0} (create|list|describe|hash) <OPTIONS> <jmod-file>\n\
+Usage: {0} (create|extract|list|describe|hash) <OPTIONS> <jmod-file>\n\
 use --help for a list of possible options
 
 main.usage=\
-Usage: {0} (create|list|describe|hash) <OPTIONS> <jmod-file>\n\
+Usage: {0} (create|extract|list|describe|hash) <OPTIONS> <jmod-file>\n\
 
 error.prefix=Error:
 warn.prefix=Warning:
@@ -37,6 +37,8 @@
 \Main operation modes:
 main.opt.mode.create=\
 \create    - Creates a new jmod archive
+main.opt.mode.extract=\
+\extract   - Extracts all the files from the archive
 main.opt.mode.list=\
 \list      - Prints the names of all the entries
 main.opt.mode.describe=\
@@ -50,6 +52,7 @@
 main.opt.libs=Location of native libraries
 main.opt.cmds=Location of native commands
 main.opt.config=Location of user-editable config files
+main.opt.extractDir=Target directory for extract
 main.opt.dry-run=Dry run of hash mode
 main.opt.exclude=Exclude files matching the supplied comma separated pattern\
 \ list, each element using one the following forms: <glob-pattern>,\
@@ -75,8 +78,9 @@
 
 module.hashes.recorded=Hashes are recorded in module {0}
 
-err.missing.mode=one of create, list, describe, or hash must be specified
-err.invalid.mode=mode must be one of create, list, describe, or hash: {0}
+err.missing.mode=one of create, extract, list, describe, or hash must be specified
+err.invalid.mode=mode must be one of create, extract, list, describe, or hash: {0}
+err.cannot.create.dir=cannot create directory {0}
 err.classpath.must.be.specified=--class-path must be specified
 err.jmod.must.be.specified=jmod-file must be specified
 err.invalid.version=invalid module version {0}
--- a/jdk/test/tools/jmod/JmodNegativeTest.java	Fri Dec 09 16:38:34 2016 -0800
+++ b/jdk/test/tools/jmod/JmodNegativeTest.java	Sat Dec 10 14:19:53 2016 +0000
@@ -82,7 +82,7 @@
         jmod()
             .assertFailure()
             .resultChecker(r ->
-                assertContains(r.output, "Error: one of create, list, describe, or hash must be specified")
+                assertContains(r.output, "Error: one of create, extract, list, describe, or hash must be specified")
             );
     }
 
@@ -91,7 +91,7 @@
         jmod("badAction")
             .assertFailure()
             .resultChecker(r ->
-                assertContains(r.output, "Error: mode must be one of create, list, describe, or hash")
+                assertContains(r.output, "Error: mode must be one of create, extract, list, describe, or hash")
             );
 
         jmod("--badOption")
--- a/jdk/test/tools/jmod/JmodTest.java	Fri Dec 09 16:38:34 2016 -0800
+++ b/jdk/test/tools/jmod/JmodTest.java	Sat Dec 10 14:19:53 2016 +0000
@@ -23,7 +23,7 @@
 
 /*
  * @test
- * @bug 8142968
+ * @bug 8142968 8166568
  * @summary Basic test for jmod
  * @library /lib/testlibrary
  * @modules jdk.compiler
@@ -135,6 +135,70 @@
     }
 
     @Test
+    public void testExtractCWD() throws IOException {
+        Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
+        jmod("create",
+             "--class-path", cp.toString(),
+             MODS_DIR.resolve("fooExtractCWD.jmod").toString())
+            .assertSuccess();
+
+        jmod("extract",
+             MODS_DIR.resolve("fooExtractCWD.jmod").toString())
+            .assertSuccess()
+            .resultChecker(r -> {
+                // module-info should exist, but jmod will have added its Packages attr.
+                assertTrue(Files.exists(Paths.get("classes/module-info.class")));
+                assertSameContent(cp.resolve("jdk/test/foo/Foo.class"),
+                                  Paths.get("classes/jdk/test/foo/Foo.class"));
+                assertSameContent(cp.resolve("jdk/test/foo/internal/Message.class"),
+                                  Paths.get("classes/jdk/test/foo/internal/Message.class"));
+                assertSameContent(cp.resolve("jdk/test/foo/resources/foo.properties"),
+                                  Paths.get("classes/jdk/test/foo/resources/foo.properties"));
+            });
+    }
+
+    @Test
+    public void testExtractDir() throws IOException {
+        if (Files.exists(Paths.get("extractTestDir")))
+            FileUtils.deleteFileTreeWithRetry(Paths.get("extractTestDir"));
+        Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
+        Path bp = EXPLODED_DIR.resolve("foo").resolve("bin");
+        Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
+        Path cf = EXPLODED_DIR.resolve("foo").resolve("conf");
+
+        jmod("create",
+             "--conf", cf.toString(),
+             "--cmds", bp.toString(),
+             "--libs", lp.toString(),
+             "--class-path", cp.toString(),
+             MODS_DIR.resolve("fooExtractDir.jmod").toString())
+            .assertSuccess();
+
+        jmod("extract",
+             "--dir", "extractTestDir",
+             MODS_DIR.resolve("fooExtractDir.jmod").toString())
+            .assertSuccess();
+
+        jmod("extract",
+             "--dir", "extractTestDir",
+             MODS_DIR.resolve("fooExtractDir.jmod").toString())
+            .assertSuccess()
+            .resultChecker(r -> {
+                // check a sample of the extracted files
+                Path p = Paths.get("extractTestDir");
+                assertTrue(Files.exists(p.resolve("classes/module-info.class")));
+                assertSameContent(cp.resolve("jdk/test/foo/Foo.class"),
+                                  p.resolve("classes/jdk/test/foo/Foo.class"));
+                assertSameContent(bp.resolve("first"),
+                                  p.resolve(CMDS_PREFIX).resolve("first"));
+                assertSameContent(lp.resolve("first.so"),
+                                  p.resolve(LIBS_PREFIX).resolve("second.so"));
+                assertSameContent(cf.resolve("second.cfg"),
+                                  p.resolve(CONFIGS_PREFIX).resolve("second.cfg"));
+            });
+    }
+
+    @Test
     public void testMainClass() throws IOException {
         Path jmod = MODS_DIR.resolve("fooMainClass.jmod");
         FileUtils.deleteFileIfExistsWithRetry(jmod);
@@ -532,6 +596,16 @@
         }
     }
 
+    static void assertSameContent(Path p1, Path p2) {
+        try {
+            byte[] ba1 = Files.readAllBytes(p1);
+            byte[] ba2 = Files.readAllBytes(p2);
+            assertEquals(ba1, ba2);
+        } catch (IOException x) {
+            throw new UncheckedIOException(x);
+        }
+    }
+
     static JmodResult jmod(String... args) {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         PrintStream ps = new PrintStream(baos);