diff --git a/src/main/java/org/codehaus/plexus/archiver/jar/JarArchiver.java b/src/main/java/org/codehaus/plexus/archiver/jar/JarArchiver.java index 8243894ab..b6d25e5ff 100644 --- a/src/main/java/org/codehaus/plexus/archiver/jar/JarArchiver.java +++ b/src/main/java/org/codehaus/plexus/archiver/jar/JarArchiver.java @@ -825,4 +825,22 @@ else if ( !name.contains( "/" ) ) } } + /** + * Override the behavior of the Zip Archiver to match the output of the JAR tool. + * + * @param zipEntry to set the last modified time + * @param lastModified to set in the zip entry only if the {@link #getLastModifiedTime()} returns null. + */ + @Override + protected void setTime( ZipArchiveEntry zipEntry, long lastModified ) + { + if ( getLastModifiedTime() != null ) + { + lastModified = getLastModifiedTime().toMillis(); + } + + // The JAR tool does not round up, so we keep that behavior here (JDK-8277755). + zipEntry.setTime( lastModified ); + } + } diff --git a/src/main/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiver.java b/src/main/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiver.java index df6d0790f..ad09199fc 100644 --- a/src/main/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiver.java +++ b/src/main/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiver.java @@ -16,17 +16,30 @@ */ package org.codehaus.plexus.archiver.jar; -import org.apache.commons.compress.parallel.InputStreamSupplier; -import org.codehaus.plexus.archiver.ArchiverException; -import org.codehaus.plexus.archiver.zip.ConcurrentJarCreator; - import java.io.File; import java.io.IOException; import java.io.PrintStream; +import java.lang.reflect.Method; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; import java.util.ArrayList; +import java.util.Calendar; +import java.util.Enumeration; import java.util.List; +import java.util.Locale; +import java.util.TimeZone; import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import org.apache.commons.compress.parallel.InputStreamSupplier; +import org.apache.commons.io.output.NullOutputStream; +import org.codehaus.plexus.archiver.ArchiverException; +import org.codehaus.plexus.archiver.zip.ConcurrentJarCreator; +import org.codehaus.plexus.util.IOUtil; /** * A {@link ModularJarArchiver} implementation that uses @@ -58,6 +71,8 @@ public class JarToolModularJarArchiver private boolean moduleDescriptorFound; + private boolean hasJarDateOption; + public JarToolModularJarArchiver() { try @@ -111,18 +126,30 @@ protected void postCreateArchive() getLogger().debug( "Using the jar tool to " + "update the archive to modular JAR." ); - Integer result = (Integer) jarTool.getClass() - .getMethod( "run", - PrintStream.class, PrintStream.class, String[].class ) - .invoke( jarTool, - System.out, System.err, - getJarToolArguments() ); + final Method jarRun = jarTool.getClass() + .getMethod( "run", PrintStream.class, PrintStream.class, String[].class ); + + if ( getLastModifiedTime() != null ) + { + hasJarDateOption = isJarDateOptionSupported( jarRun ); + getLogger().debug( "jar tool --date option is supported: " + hasJarDateOption ); + } + + Integer result = (Integer) jarRun.invoke( jarTool, System.out, System.err, getJarToolArguments() ); if ( result != null && result != 0 ) { throw new ArchiverException( "Could not create modular JAR file. " + "The JDK jar tool exited with " + result ); } + + if ( !hasJarDateOption && getLastModifiedTime() != null ) + { + getLogger().debug( "Fix last modified time zip entries." ); + // --date option not supported, fallback to rewrite the JAR file + // https://github.com/codehaus-plexus/plexus-archiver/issues/164 + fixLastModifiedTimeZipEntries(); + } } catch ( IOException | ReflectiveOperationException | SecurityException e ) { @@ -131,6 +158,36 @@ protected void postCreateArchive() } } + /** + * Fallback to rewrite the JAR file with the correct timestamp if the {@code --date} option is not available. + */ + private void fixLastModifiedTimeZipEntries() + throws IOException + { + long timeMillis = getLastModifiedTime().toMillis(); + Path destFile = getDestFile().toPath(); + Path tmpZip = Files.createTempFile( destFile.getParent(), null, null ); + try ( ZipFile zipFile = new ZipFile( getDestFile() ); + ZipOutputStream out = new ZipOutputStream( Files.newOutputStream( tmpZip ) ) ) + { + Enumeration entries = zipFile.entries(); + while ( entries.hasMoreElements() ) + { + ZipEntry entry = entries.nextElement(); + // Not using setLastModifiedTime(FileTime) as it sets the extended timestamp + // which is not compatible with the jar tool output. + entry.setTime( timeMillis ); + out.putNextEntry( entry ); + if ( !entry.isDirectory() ) + { + IOUtil.copy( zipFile.getInputStream( entry ), out ); + } + out.closeEntry(); + } + } + Files.move( tmpZip, destFile, StandardCopyOption.REPLACE_EXISTING ); + } + /** * Returns {@code true} if {@code path} * is a module descriptor. @@ -201,6 +258,14 @@ private String[] getJarToolArguments() args.add( "--no-compress" ); } + if ( hasJarDateOption ) + { + // The --date option already normalize the time, so revert to the local time + FileTime localTime = revertToLocalTime( getLastModifiedTime() ); + args.add( "--date" ); + args.add( localTime.toString() ); + } + args.add( "-C" ); args.add( tempEmptyDir.getAbsolutePath() ); args.add( "." ); @@ -208,4 +273,36 @@ private String[] getJarToolArguments() return args.toArray( new String[0] ); } + private static FileTime revertToLocalTime( FileTime time ) + { + long restoreToLocalTime = time.toMillis(); + Calendar cal = Calendar.getInstance( TimeZone.getDefault(), Locale.ROOT ); + cal.setTimeInMillis( restoreToLocalTime ); + restoreToLocalTime = restoreToLocalTime + ( cal.get( Calendar.ZONE_OFFSET ) + cal.get( Calendar.DST_OFFSET ) ); + return FileTime.fromMillis( restoreToLocalTime ); + } + + /** + * Check support for {@code --date} option introduced since Java 17.0.3 (JDK-8279925). + * + * @return true if the JAR tool supports the {@code --date} option + */ + private boolean isJarDateOptionSupported( Method runMethod ) + { + try + { + // Test the output code validating the --date option. + String[] args = { "--date", "2099-12-31T23:59:59Z", "--version" }; + + PrintStream nullPrintStream = new PrintStream( NullOutputStream.NULL_OUTPUT_STREAM ); + Integer result = (Integer) runMethod.invoke( jarTool, nullPrintStream, nullPrintStream, args ); + + return result != null && result.intValue() == 0; + } + catch ( ReflectiveOperationException | SecurityException e ) + { + return false; + } + } + } diff --git a/src/main/java/org/codehaus/plexus/archiver/zip/AbstractZipArchiver.java b/src/main/java/org/codehaus/plexus/archiver/zip/AbstractZipArchiver.java index 136c29e43..6ddfd02f0 100755 --- a/src/main/java/org/codehaus/plexus/archiver/zip/AbstractZipArchiver.java +++ b/src/main/java/org/codehaus/plexus/archiver/zip/AbstractZipArchiver.java @@ -534,7 +534,7 @@ public InputStream get() } } - private void setTime( java.util.zip.ZipEntry zipEntry, long lastModified ) + protected void setTime( ZipArchiveEntry zipEntry, long lastModified ) { if ( getLastModifiedTime() != null ) { diff --git a/src/test/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiverTest.java b/src/test/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiverTest.java index 931e8dece..ee1748e66 100644 --- a/src/test/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiverTest.java +++ b/src/test/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiverTest.java @@ -16,9 +16,16 @@ */ package org.codehaus.plexus.archiver.jar; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + import java.io.File; import java.io.InputStream; import java.lang.reflect.Method; +import java.nio.file.attribute.FileTime; +import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Enumeration; import java.util.HashSet; @@ -30,10 +37,6 @@ import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.*; -import static org.junit.Assume.assumeFalse; -import static org.junit.Assume.assumeTrue; - public class JarToolModularJarArchiverTest extends BaseJarArchiverTest { @@ -252,25 +255,57 @@ public void testModularMultiReleaseJar() { assumeTrue( modulesAreSupported() ); + // Add two module-info.class, one on the root and one on the multi-release dir. + archiver.addFile( new File( "src/test/resources/java-module-descriptor/module-info.class" ), + "META-INF/versions/9/module-info.class" ); archiver.addFile( new File( "src/test/resources/java-module-descriptor/module-info.class" ), - "META-INF/versions/9/module-info.class" ); + "module-info.class" ); + Manifest manifest = new Manifest(); + manifest.addConfiguredAttribute( new Manifest.Attribute( "Main-Class", "com.example.app.Main2" ) ); manifest.addConfiguredAttribute( new Manifest.Attribute( "Multi-Release", "true" ) ); archiver.addConfiguredManifest( manifest ); + archiver.setModuleVersion( "1.0.0" ); + // This attribute overwrites the one from the manifest. archiver.setModuleMainClass( "com.example.app.Main" ); + SimpleDateFormat isoFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssXXX" ); + long dateTimeMillis = isoFormat.parse( "2020-02-29T23:59:59Z" ).getTime(); + FileTime lastModTime = FileTime.fromMillis( dateTimeMillis ); + + archiver.configureReproducibleBuild( lastModTime ); archiver.createArchive(); + // Round-down two seconds precision + long roundedDown = lastModTime.toMillis() - ( lastModTime.toMillis() % 2000 ); + // Normalize to UTC + long expectedLastModifiedTime = normalizeLastModifiedTime( roundedDown ); + // verify that the resulting modular jar has the proper version and main class set try ( ZipFile resultingArchive = new ZipFile( archiver.getDestFile() ) ) { - ZipEntry moduleDescriptorEntry = - resultingArchive.getEntry( "META-INF/versions/9/module-info.class" ); + ZipEntry moduleDescriptorEntry = resultingArchive.getEntry( "META-INF/versions/9/module-info.class" ); InputStream resultingModuleDescriptor = resultingArchive.getInputStream( moduleDescriptorEntry ); + assertModuleDescriptor( resultingModuleDescriptor, "1.0.0", "com.example.app.Main", "com.example.app", + "com.example.resources" ); - assertModuleDescriptor( resultingModuleDescriptor, - "1.0.0", "com.example.app.Main", "com.example.app", "com.example.resources" ); + ZipEntry rootModuleDescriptorEntry = resultingArchive.getEntry( "module-info.class" ); + InputStream rootResultingModuleDescriptor = resultingArchive.getInputStream( rootModuleDescriptorEntry ); + assertModuleDescriptor( rootResultingModuleDescriptor, "1.0.0", "com.example.app.Main", "com.example.app", + "com.example.resources" ); + + // verify every entry has the correct last modified time + Enumeration entries = resultingArchive.entries(); + while ( entries.hasMoreElements() ) + { + ZipEntry element = entries.nextElement(); + assertEquals( "Last Modified Time does not match with expected", expectedLastModifiedTime, + element.getTime() ); + FileTime expectedFileTime = FileTime.fromMillis( expectedLastModifiedTime ); + assertEquals( "Last Modified Time does not match with expected", expectedFileTime, + element.getLastModifiedTime() ); + } } }