Skip to content

Commit

Permalink
#222 Use ZipFile API to set timestamps
Browse files Browse the repository at this point in the history
Fixes #222
  • Loading branch information
aalmiray committed Feb 11, 2024
1 parent be7d707 commit 5d63f7b
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 54 deletions.
120 changes: 66 additions & 54 deletions core/src/main/java/org/moditect/commands/AddModuleInfo.java
Expand Up @@ -15,35 +15,38 @@
*/
package org.moditect.commands;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Enumeration;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import org.moditect.internal.compiler.ModuleInfoCompiler;

import com.github.javaparser.ast.modules.ModuleDeclaration;

import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;

/**
* Creates a copy of a given JAR file, adding a module-info.class descriptor.
*
* @author Gunnar Morling
*/
public class AddModuleInfo {

private static final int DEFAULT_BUFFER_SIZE = 8192;
private static final String NO_JVM_VERSION = "base";
private static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
private static final String MODULE_INFO_CLASS = "module-info.class";

private final String moduleInfoSource;
private final String mainClass;
Expand Down Expand Up @@ -99,60 +102,65 @@ public void run() {
"File " + outputJar + " already exists; either set 'overwriteExistingFiles' to true or specify another output directory");
}

ModuleDeclaration module = ModuleInfoCompiler.parseModuleInfo(moduleInfoSource);
byte[] clazz = ModuleInfoCompiler.compileModuleInfo(module, mainClass, version);

try {
Files.copy(inputJar, outputJar, StandardCopyOption.REPLACE_EXISTING);
Files.createDirectories(outputJar.toAbsolutePath().getParent());
Files.createFile(outputJar.toAbsolutePath());
}
catch (IOException e) {
throw new RuntimeException("Couldn't copy JAR file", e);
}

ModuleDeclaration module = ModuleInfoCompiler.parseModuleInfo(moduleInfoSource);
byte[] clazz = ModuleInfoCompiler.compileModuleInfo(module, mainClass, version);

Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI uri = URI.create("jar:" + outputJar.toUri());

try (FileSystem zipfs = FileSystems.newFileSystem(uri, env)) {
if (jvmVersion == null) {
Path path = zipfs.getPath("module-info.class");
Files.write(path, clazz,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING);
setTimes(path, toFileTime(timestamp));
}
else {
Path path = zipfs.getPath("META-INF/versions", jvmVersion.toString(), "module-info.class");
Files.createDirectories(path.getParent());
Files.write(path, clazz,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING);
FileTime lastModifiedTime = toFileTime(timestamp);
// module-info.class
setTimes(path, lastModifiedTime);
// jvmVersion
setTimes(path.getParent(), lastModifiedTime);
// versions
setTimes(path.getParent().getParent(), lastModifiedTime);

Path manifestPath = zipfs.getPath("META-INF/MANIFEST.MF");
Manifest manifest;
if (Files.exists(manifestPath)) {
manifest = new Manifest(Files.newInputStream(manifestPath));
boolean versionedModuleInfo = jvmVersion != null;
String versionedModuleInfoClass = "META-INF/versions/" + jvmVersion + "/" + MODULE_INFO_CLASS;
long lastModifiedTime = toFileTime(timestamp).toMillis();

// brute force copy all entries
try (JarFile jarFile = new JarFile(inputJar.toAbsolutePath().toFile());
JarOutputStream jarout = new JarOutputStream(Files.newOutputStream(outputJar.toAbsolutePath(), TRUNCATE_EXISTING))) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry inputEntry = entries.nextElement();

// manifest requires extra care due to MRJARs
if (MANIFEST_ENTRY_NAME.equals(inputEntry.getName()) && versionedModuleInfo) {
Manifest manifest = jarFile.getManifest();
if (null == manifest) {
manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
}
manifest.getMainAttributes().put(new Attributes.Name("Multi-Release"), "true");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
manifest.write(baos);

JarEntry outputEntry = new JarEntry(inputEntry.getName());
outputEntry.setTime(lastModifiedTime);
jarout.putNextEntry(outputEntry);
jarout.write(baos.toByteArray(), 0, baos.size());
jarout.closeEntry();
}
else {
manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
else if ((MODULE_INFO_CLASS.equals(inputEntry.getName()) && !versionedModuleInfo) ||
(versionedModuleInfoClass.equals(inputEntry.getName()) && versionedModuleInfo)) {
// skip this entry as we'll overwrite it
}

manifest.getMainAttributes().put(new Attributes.Name("Multi-Release"), "true");
try (OutputStream manifestOs = Files.newOutputStream(manifestPath, StandardOpenOption.TRUNCATE_EXISTING)) {
manifest.write(manifestOs);
else {
// copy entry as is, set timestamp
JarEntry outputEntry = new JarEntry(inputEntry.getName());
outputEntry.setTime(lastModifiedTime);
jarout.putNextEntry(outputEntry);
copy(jarFile.getInputStream(inputEntry), jarout);
jarout.closeEntry();
}
setTimes(manifestPath, lastModifiedTime);
}

// copy module descriptor
JarEntry outputEntry = versionedModuleInfo ? new JarEntry(versionedModuleInfoClass) : new JarEntry(MODULE_INFO_CLASS);
outputEntry.setTime(lastModifiedTime);
jarout.putNextEntry(outputEntry);
jarout.write(clazz, 0, clazz.length);
jarout.closeEntry();
}
catch (IOException e) {
throw new RuntimeException("Couldn't add module-info.class to JAR", e);
Expand All @@ -163,7 +171,11 @@ private FileTime toFileTime(Instant timestamp) {
return FileTime.from(timestamp != null ? timestamp : Instant.now());
}

private void setTimes(Path path, FileTime time) throws IOException {
Files.getFileAttributeView(path, BasicFileAttributeView.class).setTimes(time, time, time);
private void copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int read;
while ((read = in.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
out.write(buffer, 0, read);
}
}
}
120 changes: 120 additions & 0 deletions core/src/test/java/org/moditect/test/AddModuleInfoTest.java
Expand Up @@ -26,6 +26,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Optional;
import java.util.jar.Attributes;
Expand Down Expand Up @@ -117,6 +118,67 @@ public void addJvmVersionModuleInfoAndRunModular() throws Exception {
}
}

@Test
public void addJvmVersionModuleInfoTwiceAndRunModular() throws Exception {
prepareTestJar();

String javaHome = System.getProperty("java.home");
String javaBin = javaHome +
File.separator + "bin" +
File.separator + "java";

ProcessBuilder builder = new ProcessBuilder(
javaBin, "--module-path", GENERATED_TEST_RESOURCES + File.separator + "example.jar", "--module", "com.example")
.redirectOutput(Redirect.INHERIT);

Process process = builder.start();
process.waitFor();

if (process.exitValue() == 0) {
throw new AssertionError();
}

new AddModuleInfo(
"module com.example {}",
"com.example.HelloWorld",
"1.42.3",
Paths.get("target", "generated-test-resources", "example.jar"),
Paths.get("target", "generated-test-modules"),
"9",
false,
null)
.run();

Files.copy(
Paths.get("target", "generated-test-modules", "example.jar"),
Paths.get("target", "generated-test-resources", "example2.jar"),
StandardCopyOption.REPLACE_EXISTING);

new AddModuleInfo(
"module com.example {}",
"com.example.HelloWorld",
"1.42.3",
Paths.get("target", "generated-test-resources", "example2.jar"),
Paths.get("target", "generated-test-modules"),
"9",
false,
null)
.run();

builder = new ProcessBuilder(
javaBin, "--module-path", GENERATED_TEST_MODULES + File.separator + "example2.jar", "--module", "com.example");

process = builder.start();
process.waitFor();

if (process.exitValue() != 0) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
process.getInputStream().transferTo(baos);
process.getErrorStream().transferTo(baos);
throw new AssertionError(baos.toString());
}
}

@Test
public void addModuleInfoAndRunModular() throws Exception {
prepareTestJar();
Expand Down Expand Up @@ -159,6 +221,64 @@ public void addModuleInfoAndRunModular() throws Exception {
}
}

@Test
public void addModuleInfoTwiceAndRunModular() throws Exception {
prepareTestJar();

String javaHome = System.getProperty("java.home");
String javaBin = javaHome +
File.separator + "bin" +
File.separator + "java";

ProcessBuilder builder = new ProcessBuilder(
javaBin, "--module-path", GENERATED_TEST_RESOURCES + File.separator + "example.jar", "--module", "com.example")
.redirectOutput(Redirect.INHERIT);

Process process = builder.start();
process.waitFor();

if (process.exitValue() == 0) {
throw new AssertionError();
}

new AddModuleInfo(
"module com.example {}",
"com.example.HelloWorld",
"1.42.3",
Paths.get("target", "generated-test-resources", "example.jar"),
Paths.get("target", "generated-test-modules"),
null,
false,
null)
.run();

Files.copy(
Paths.get("target", "generated-test-modules", "example.jar"),
Paths.get("target", "generated-test-resources", "example2.jar"),
StandardCopyOption.REPLACE_EXISTING);

new AddModuleInfo(
"module com.example {}",
"com.example.HelloWorld",
"1.42.3",
Paths.get("target", "generated-test-resources", "example2.jar"),
Paths.get("target", "generated-test-modules"),
null,
false,
null)
.run();

builder = new ProcessBuilder(
javaBin, "--module-path", GENERATED_TEST_MODULES + File.separator + "example2.jar", "--module", "com.example");

process = builder.start();
process.waitFor();

if (process.exitValue() != 0) {
throw new AssertionError();
}
}

private void prepareTestJar() throws Exception {
Compilation compilation = Compiler.javac()
.compile(
Expand Down

0 comments on commit 5d63f7b

Please sign in to comment.