--- old/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java 2016-12-06 10:38:30.491480508 +0000 +++ new/src/java.base/share/classes/jdk/internal/jmod/JmodFile.java 2016-12-06 10:38:30.315484194 +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. --- old/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodOutputStream.java 2016-12-06 10:38:31.411461243 +0000 +++ new/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodOutputStream.java 2016-12-06 10:38:31.207465514 +0000 @@ -25,6 +25,8 @@ package jdk.tools.jmod; +import jdk.internal.jmod.JmodFile; + import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; @@ -57,7 +59,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); } --- old/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java 2016-12-06 10:38:32.211444491 +0000 +++ new/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java 2016-12-06 10:38:32.019448511 +0000 @@ -47,6 +47,7 @@ import java.lang.module.ResolutionException; import java.lang.module.ResolvedModule; import java.net.URI; +import java.nio.file.CopyOption; import java.nio.file.FileSystems; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; @@ -153,6 +154,7 @@ enum Mode { CREATE, + EXTRACT, LIST, DESCRIBE, HASH @@ -202,6 +204,9 @@ case CREATE: ok = create(); break; + case EXTRACT: + ok = extract(); + break; case LIST: ok = list(); break; @@ -248,6 +253,31 @@ } } + private boolean extract() throws IOException { + 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 = Paths.get(name.substring(0, index)); + if (Files.notExists(p)) + Files.createDirectories(p); + } + + try (OutputStream os = Files.newOutputStream(Paths.get(name))) { + jf.getInputStream(e).transferTo(os); + } + } catch (IOException x) { + throw new UncheckedIOException(x); + } + }); + + return true; + } + } + private boolean hashModules() { return new Hasher(options.moduleFinder).run(); } @@ -1158,6 +1188,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"); --- old/src/jdk.jlink/share/classes/jdk/tools/jmod/resources/jmod.properties 2016-12-06 10:38:33.047426985 +0000 +++ new/src/jdk.jlink/share/classes/jdk/tools/jmod/resources/jmod.properties 2016-12-06 10:38:32.851431088 +0000 @@ -24,11 +24,11 @@ # main.usage.summary=\ -Usage: {0} (create|list|describe|hash) \n\ +Usage: {0} (create|extract|list|describe|hash) \n\ use --help for a list of possible options main.usage=\ -Usage: {0} (create|list|describe|hash) \n\ +Usage: {0} (create|extract|list|describe|hash) \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=\ @@ -75,8 +77,8 @@ 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.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} --- old/test/tools/jmod/JmodNegativeTest.java 2016-12-06 10:38:33.895409227 +0000 +++ new/test/tools/jmod/JmodNegativeTest.java 2016-12-06 10:38:33.687413583 +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") --- old/test/tools/jmod/JmodTest.java 2016-12-06 10:38:34.727391804 +0000 +++ new/test/tools/jmod/JmodTest.java 2016-12-06 10:38:34.539395741 +0000 @@ -135,6 +135,29 @@ } @Test + public void testExtract() throws IOException { + Path cp = EXPLODED_DIR.resolve("foo").resolve("classes"); + jmod("create", + "--class-path", cp.toString(), + MODS_DIR.resolve("fooExtract.jmod").toString()) + .assertSuccess(); + + jmod("extract", + MODS_DIR.resolve("fooExtract.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 testMainClass() throws IOException { Path jmod = MODS_DIR.resolve("fooMainClass.jmod"); FileUtils.deleteFileIfExistsWithRetry(jmod); @@ -532,6 +555,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);