View Javadoc
1   /**
2    *
3    * Copyright 2004 The Apache Software Foundation
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.codehaus.plexus.archiver.jar;
18  
19  import javax.inject.Named;
20  
21  import java.io.ByteArrayInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStreamWriter;
27  import java.io.PrintWriter;
28  import java.nio.charset.StandardCharsets;
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.Comparator;
32  import java.util.Enumeration;
33  import java.util.HashSet;
34  import java.util.List;
35  import java.util.Set;
36  import java.util.SortedMap;
37  import java.util.StringTokenizer;
38  import java.util.TreeMap;
39  
40  import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
41  import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
42  import org.apache.commons.compress.archivers.zip.ZipFile;
43  import org.apache.commons.compress.parallel.InputStreamSupplier;
44  import org.codehaus.plexus.archiver.ArchiverException;
45  import org.codehaus.plexus.archiver.zip.ConcurrentJarCreator;
46  import org.codehaus.plexus.archiver.zip.ZipArchiver;
47  
48  import static org.codehaus.plexus.archiver.util.Streams.bufferedOutputStream;
49  import static org.codehaus.plexus.archiver.util.Streams.fileInputStream;
50  import static org.codehaus.plexus.archiver.util.Streams.fileOutputStream;
51  
52  /**
53   * Base class for tasks that build archives in JAR file format.
54   */
55  @Named("jar")
56  public class JarArchiver extends ZipArchiver {
57  
58      /**
59       * the name of the meta-inf dir
60       */
61      private static final String META_INF_NAME = "META-INF";
62  
63      /**
64       * The index file name.
65       *
66       * @deprecated See <a href="https://bugs.openjdk.org/browse/JDK-8302819">JDK-8302819</a>
67       */
68      @Deprecated
69      private static final String INDEX_NAME = "META-INF/INDEX.LIST";
70  
71      /**
72       * The manifest file name.
73       */
74      private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
75  
76      /**
77       * merged manifests added through addConfiguredManifest
78       */
79      private Manifest configuredManifest;
80  
81      /**
82       * shadow of the above if upToDate check alters the value
83       */
84      private Manifest savedConfiguredManifest;
85  
86      /**
87       * merged manifests added through filesets
88       */
89      private Manifest filesetManifest;
90  
91      /**
92       * Manifest of original archive, will be set to null if not in
93       * update mode.
94       */
95      private Manifest originalManifest;
96  
97      /**
98       * whether to merge fileset manifests;
99       * value is true if filesetmanifest is 'merge' or 'mergewithoutmain'
100      */
101     private FilesetManifestConfig filesetManifestConfig;
102 
103     /**
104      * whether to merge the main section of fileset manifests;
105      * value is true if filesetmanifest is 'merge'
106      */
107     private boolean mergeManifestsMain = true;
108 
109     /**
110      * the manifest specified by the 'manifest' attribute *
111      */
112     private Manifest manifest;
113 
114     /**
115      * The file found from the 'manifest' attribute. This can be
116      * either the location of a manifest, or the name of a jar added
117      * through a fileset. If its the name of an added jar, the
118      * manifest is looked for in META-INF/MANIFEST.MF
119      */
120     private File manifestFile;
121 
122     /**
123      * jar index is JDK 1.3+ only
124      *
125      * @deprecated See <a href="https://bugs.openjdk.org/browse/JDK-8302819">JDK-8302819</a>
126      */
127     @Deprecated
128     private boolean index = false;
129 
130     /**
131      * whether to really create the archive in createEmptyZip, will
132      * get set in getResourcesToAdd.
133      */
134     private boolean createEmpty = false;
135 
136     /**
137      * Stores all files that are in the root of the archive (i.e. that
138      * have a name that doesn't contain a slash) so they can get
139      * listed in the index.
140      * <p/>
141      * Will not be filled unless the user has asked for an index.
142      */
143     private final List<String> rootEntries;
144 
145     /**
146      * Path containing jars that shall be indexed in addition to this archive.
147      *
148      * @deprecated See <a href="https://bugs.openjdk.org/browse/JDK-8302819">JDK-8302819</a>
149      */
150     @Deprecated
151     private List<String> indexJars;
152 
153     /**
154      * Creates a minimal default manifest with {@code Manifest-Version: 1.0} only.
155      */
156     private boolean minimalDefaultManifest = false;
157 
158     /**
159      * constructor
160      */
161     public JarArchiver() {
162         super();
163         archiveType = "jar";
164         setEncoding("UTF8");
165         rootEntries = new ArrayList<>();
166     }
167 
168     /**
169      * Set whether or not to create an index list for classes.
170      * This may speed up classloading in some cases.
171      *
172      * @param flag true to create an index
173      * @deprecated See <a href="https://bugs.openjdk.org/browse/JDK-8302819">JDK-8302819</a>
174      */
175     @Deprecated
176     public void setIndex(boolean flag) {
177         index = flag;
178     }
179 
180     /**
181      * Set whether the default manifest is minimal, thus having only {@code Manifest-Version: 1.0} in it.
182      *
183      * @param minimalDefaultManifest true to create minimal default manifest
184      */
185     public void setMinimalDefaultManifest(boolean minimalDefaultManifest) {
186         this.minimalDefaultManifest = minimalDefaultManifest;
187     }
188 
189     @SuppressWarnings({"JavaDoc", "UnusedDeclaration"})
190     @Deprecated // Useless method. Manifests should be UTF-8 by convention. Calling this setter does nothing
191     public void setManifestEncoding(String manifestEncoding) {}
192 
193     /**
194      * Allows the manifest for the archive file to be provided inline
195      * in the build file rather than in an external file.
196      *
197      * @param newManifest The new manifest
198      *
199      * @throws ManifestException
200      */
201     public void addConfiguredManifest(Manifest newManifest) throws ManifestException {
202         if (configuredManifest == null) {
203             configuredManifest = newManifest;
204         } else {
205             JdkManifestFactory.merge(configuredManifest, newManifest, false);
206         }
207         savedConfiguredManifest = configuredManifest;
208     }
209 
210     /**
211      * The manifest file to use. This can be either the location of a manifest, or the name of a jar added through a
212      * fileset. If its the name of an added jar, the task expects the manifest to be in the jar at META-INF/MANIFEST.MF.
213      *
214      * @param manifestFile the manifest file to use.
215      *
216      * @throws org.codehaus.plexus.archiver.ArchiverException
217      */
218     @SuppressWarnings({"UnusedDeclaration"})
219     public void setManifest(File manifestFile) throws ArchiverException {
220         if (!manifestFile.exists()) {
221             throw new ArchiverException("Manifest file: " + manifestFile + " does not exist.");
222         }
223 
224         this.manifestFile = manifestFile;
225     }
226 
227     private Manifest getManifest(File manifestFile) throws ArchiverException {
228         try (InputStream in = fileInputStream(manifestFile)) {
229             return getManifest(in);
230         } catch (IOException e) {
231             throw new ArchiverException(
232                     "Unable to read manifest file: " + manifestFile + " (" + e.getMessage() + ")", e);
233         }
234     }
235 
236     private Manifest getManifest(InputStream is) throws ArchiverException {
237         try {
238             return new Manifest(is);
239         } catch (IOException e) {
240             throw new ArchiverException("Unable to read manifest file" + " (" + e.getMessage() + ")", e);
241         }
242     }
243 
244     /**
245      * Behavior when a Manifest is found in a zipfileset or zipgroupfileset file.
246      * Valid values are "skip", "merge", and "mergewithoutmain".
247      * "merge" will merge all of manifests together, and merge this into any
248      * other specified manifests.
249      * "mergewithoutmain" merges everything but the Main section of the manifests.
250      * Default value is "skip".
251      * <p>
252      * Note: if this attribute's value is not "skip", the created jar will not
253      * be readable by using java.util.jar.JarInputStream</p>
254      *
255      * @param config setting for found manifest behavior.
256      */
257     @SuppressWarnings({"UnusedDeclaration"})
258     public void setFilesetmanifest(FilesetManifestConfig config) {
259         filesetManifestConfig = config;
260         mergeManifestsMain = FilesetManifestConfig.merge == config;
261 
262         if ((filesetManifestConfig != null) && filesetManifestConfig != FilesetManifestConfig.skip) {
263 
264             doubleFilePass = true;
265         }
266     }
267 
268     /**
269      * @param indexJar The indexjar
270      * @deprecated See <a href="https://bugs.openjdk.org/browse/JDK-8302819">JDK-8302819</a>
271      */
272     @Deprecated
273     public void addConfiguredIndexJars(File indexJar) {
274         if (indexJars == null) {
275             indexJars = new ArrayList<>();
276         }
277         indexJars.add(indexJar.getAbsolutePath());
278     }
279 
280     @Override
281     protected void initZipOutputStream(ConcurrentJarCreator zOut) throws ArchiverException, IOException {
282         if (!skipWriting) {
283             Manifest jarManifest = createManifest();
284             writeManifest(zOut, jarManifest);
285         }
286     }
287 
288     @Override
289     protected boolean hasVirtualFiles() {
290         getLogger().debug("\n\n\nChecking for jar manifest virtual files...\n\n\n");
291         System.out.flush();
292 
293         return (configuredManifest != null) || (manifest != null) || (manifestFile != null) || super.hasVirtualFiles();
294     }
295 
296     /**
297      * Creates the manifest to be added to the JAR archive.
298      * Sub-classes may choose to override this method
299      * in order to inspect or modify the JAR manifest file.
300      *
301      * @return the manifest for the JAR archive.
302      *
303      * @throws ArchiverException
304      */
305     protected Manifest createManifest() throws ArchiverException {
306         Manifest finalManifest = Manifest.getDefaultManifest(minimalDefaultManifest);
307 
308         if ((manifest == null) && (manifestFile != null)) {
309             // if we haven't got the manifest yet, attempt to
310             // get it now and have manifest be the final merge
311             manifest = getManifest(manifestFile);
312         }
313 
314         /*
315          * Precedence: manifestFile wins over inline manifest,
316          * over manifests read from the filesets over the original
317          * manifest.
318          *
319          * merge with null argument is a no-op
320          */
321         if (isInUpdateMode()) {
322             JdkManifestFactory.merge(finalManifest, originalManifest, false);
323         }
324         JdkManifestFactory.merge(finalManifest, filesetManifest, false);
325         JdkManifestFactory.merge(finalManifest, configuredManifest, false);
326         JdkManifestFactory.merge(finalManifest, manifest, !mergeManifestsMain);
327 
328         return finalManifest;
329     }
330 
331     private void writeManifest(ConcurrentJarCreator zOut, Manifest manifest) throws IOException, ArchiverException {
332         for (Enumeration<String> e = manifest.getWarnings(); e.hasMoreElements(); ) {
333             getLogger().warn("Manifest warning: " + e.nextElement());
334         }
335 
336         zipDir(null, zOut, "META-INF/", DEFAULT_DIR_MODE, getEncoding());
337 
338         // time to write the manifest
339         ByteArrayOutputStream baos = new ByteArrayOutputStream(128);
340         manifest.write(baos);
341         InputStreamSupplier in = () -> new ByteArrayInputStream(baos.toByteArray());
342 
343         super.zipFile(in, zOut, MANIFEST_NAME, System.currentTimeMillis(), null, DEFAULT_FILE_MODE, null, false);
344         super.initZipOutputStream(zOut);
345     }
346 
347     @Override
348     protected void finalizeZipOutputStream(ConcurrentJarCreator zOut) throws IOException, ArchiverException {
349         if (index) {
350             createIndexList(zOut);
351         }
352     }
353 
354     /**
355      * Create the index list to speed up classloading.
356      * This is a JDK 1.3+ specific feature and is enabled by default. See
357      * <a href="http://java.sun.com/j2se/1.3/docs/guide/jar/jar.html#JAR%20Index">
358      * the JAR index specification</a> for more details.
359      *
360      * @param zOut the zip stream representing the jar being built.
361      *
362      * @throws IOException thrown if there is an error while creating the
363      * index and adding it to the zip stream.
364      * @throws org.codehaus.plexus.archiver.ArchiverException
365      * @deprecated See <a href="https://bugs.openjdk.org/browse/JDK-8302819">JDK-8302819</a>
366      */
367     @Deprecated
368     private void createIndexList(ConcurrentJarCreator zOut) throws IOException, ArchiverException {
369         ByteArrayOutputStream baos = new ByteArrayOutputStream(128);
370         // encoding must be UTF8 as specified in the specs.
371         PrintWriter writer = new PrintWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8));
372 
373         // version-info blankline
374         writer.println("JarIndex-Version: 1.0");
375         writer.println();
376 
377         // header newline
378         writer.println(getDestFile().getName());
379 
380         // filter out META-INF if it doesn't contain anything other than the index and manifest.
381         // this is what sun.misc.JarIndex does, guess we ought to be consistent.
382         Set<String> filteredDirs = addedDirs.allAddedDirs();
383         // our added dirs always have a trailing slash
384         if (filteredDirs.contains(META_INF_NAME + '/')) {
385             boolean add = false;
386             for (String entry : entries.keySet()) {
387                 if (entry.startsWith(META_INF_NAME + '/')
388                         && !entry.equals(INDEX_NAME)
389                         && !entry.equals(MANIFEST_NAME)) {
390                     add = true;
391                     break;
392                 }
393             }
394             if (!add) {
395                 filteredDirs.remove(META_INF_NAME + '/');
396             }
397         }
398         writeIndexLikeList(new ArrayList<>(filteredDirs), rootEntries, writer);
399         writer.println();
400 
401         if (indexJars != null) {
402             java.util.jar.Manifest mf = createManifest();
403             String classpath = mf.getMainAttributes().getValue(ManifestConstants.ATTRIBUTE_CLASSPATH);
404             String[] cpEntries = null;
405             if (classpath != null) {
406                 StringTokenizer tok = new StringTokenizer(classpath, " ");
407                 cpEntries = new String[tok.countTokens()];
408                 int c = 0;
409                 while (tok.hasMoreTokens()) {
410                     cpEntries[c++] = tok.nextToken();
411                 }
412             }
413 
414             for (String indexJar : indexJars) {
415                 String name = findJarName(indexJar, cpEntries);
416                 if (name != null) {
417                     List<String> dirs = new ArrayList<>();
418                     List<String> files = new ArrayList<>();
419                     grabFilesAndDirs(indexJar, dirs, files);
420                     if (dirs.size() + files.size() > 0) {
421                         writer.println(name);
422                         writeIndexLikeList(dirs, files, writer);
423                         writer.println();
424                     }
425                 }
426             }
427         }
428 
429         writer.flush();
430 
431         InputStreamSupplier in = () -> new ByteArrayInputStream(baos.toByteArray());
432 
433         super.zipFile(in, zOut, INDEX_NAME, System.currentTimeMillis(), null, DEFAULT_FILE_MODE, null, true);
434     }
435 
436     /**
437      * Overridden from Zip class to deal with manifests and index lists.
438      */
439     @Override
440     protected void zipFile(
441             InputStreamSupplier is,
442             ConcurrentJarCreator zOut,
443             String vPath,
444             long lastModified,
445             File fromArchive,
446             int mode,
447             String symlinkDestination,
448             boolean addInParallel)
449             throws IOException, ArchiverException {
450         if (MANIFEST_NAME.equalsIgnoreCase(vPath)) {
451             if (!doubleFilePass || skipWriting) {
452                 try (InputStream manifestInputStream = is.get()) {
453                     filesetManifest(fromArchive, manifestInputStream);
454                 }
455             }
456         } else if (INDEX_NAME.equalsIgnoreCase(vPath) && index) {
457             getLogger()
458                     .warn("Warning: selected " + archiveType + " files include a META-INF/INDEX.LIST which will"
459                             + " be replaced by a newly generated one.");
460         } else {
461             if (index && !vPath.contains("/")) {
462                 rootEntries.add(vPath);
463             }
464             super.zipFile(is, zOut, vPath, lastModified, fromArchive, mode, symlinkDestination, addInParallel);
465         }
466     }
467 
468     private void filesetManifest(File file, InputStream is) throws ArchiverException {
469         if ((manifestFile != null) && manifestFile.equals(file)) {
470             // If this is the same name specified in 'manifest', this
471             // is the manifest to use
472             getLogger().debug("Found manifest " + file);
473             if (is != null) {
474                 manifest = getManifest(is);
475             } else {
476                 manifest = getManifest(file);
477             }
478         } else if ((filesetManifestConfig != null) && filesetManifestConfig != FilesetManifestConfig.skip) {
479             // we add this to our group of fileset manifests
480             getLogger().debug("Found manifest to merge in file " + file);
481 
482             Manifest newManifest;
483             if (is != null) {
484                 newManifest = getManifest(is);
485             } else {
486                 newManifest = getManifest(file);
487             }
488 
489             if (filesetManifest == null) {
490                 filesetManifest = newManifest;
491             } else {
492                 JdkManifestFactory.merge(filesetManifest, newManifest, false);
493             }
494         }
495     }
496 
497     @Override
498     protected boolean createEmptyZip(File zipFile) throws ArchiverException {
499         if (!createEmpty) {
500             return true;
501         }
502 
503         try {
504             getLogger().debug("Building MANIFEST-only jar: " + getDestFile().getAbsolutePath());
505             zipArchiveOutputStream =
506                     new ZipArchiveOutputStream(bufferedOutputStream(fileOutputStream(getDestFile(), "jar")));
507 
508             zipArchiveOutputStream.setEncoding(getEncoding());
509             if (isCompress()) {
510                 zipArchiveOutputStream.setMethod(ZipArchiveOutputStream.DEFLATED);
511             } else {
512                 zipArchiveOutputStream.setMethod(ZipArchiveOutputStream.STORED);
513             }
514             ConcurrentJarCreator ps = new ConcurrentJarCreator(
515                     isRecompressAddedZips(), Runtime.getRuntime().availableProcessors());
516             initZipOutputStream(ps);
517             finalizeZipOutputStream(ps);
518         } catch (IOException ioe) {
519             throw new ArchiverException("Could not create almost empty JAR archive (" + ioe.getMessage() + ")", ioe);
520         } finally {
521             // Close the output stream.
522             // IOUtil.close( zOut );
523             createEmpty = false;
524         }
525         return true;
526     }
527 
528     /**
529      * Make sure we don't think we already have a MANIFEST next time this task
530      * gets executed.
531      *
532      * @see ZipArchiver#cleanUp
533      */
534     @Override
535     protected void cleanUp() throws IOException {
536         super.cleanUp();
537 
538         // we want to save this info if we are going to make another pass
539         if (!doubleFilePass || !skipWriting) {
540             manifest = null;
541             configuredManifest = savedConfiguredManifest;
542             filesetManifest = null;
543             originalManifest = null;
544         }
545         rootEntries.clear();
546     }
547 
548     /**
549      * reset to default values.
550      *
551      * @see ZipArchiver#reset
552      */
553     @Override
554     public void reset() {
555         super.reset();
556         configuredManifest = null;
557         filesetManifestConfig = null;
558         mergeManifestsMain = false;
559         manifestFile = null;
560         index = false;
561     }
562 
563     public enum FilesetManifestConfig {
564         skip,
565         merge,
566         mergewithoutmain
567     }
568 
569     /**
570      * Writes the directory entries from the first and the filenames
571      * from the second list to the given writer, one entry per line.
572      *
573      * @param dirs The directories
574      * @param files The files
575      * @param writer The printwriter ;)
576      * @deprecated See <a href="https://bugs.openjdk.org/browse/JDK-8302819">JDK-8302819</a>
577      */
578     @Deprecated
579     protected final void writeIndexLikeList(List<String> dirs, List<String> files, PrintWriter writer) {
580         // JarIndex is sorting the directories by ascending order.
581         // it has no value but cosmetic since it will be read into a
582         // hashtable by the classloader, but we'll do so anyway.
583         Collections.sort(dirs);
584         Collections.sort(files);
585         for (String dir : dirs) {
586             // try to be smart, not to be fooled by a weird directory name
587             dir = dir.replace('\\', '/');
588             if (dir.startsWith("./")) {
589                 dir = dir.substring(2);
590             }
591             while (dir.startsWith("/")) {
592                 dir = dir.substring(1);
593             }
594             int pos = dir.lastIndexOf('/');
595             if (pos != -1) {
596                 dir = dir.substring(0, pos);
597             }
598 
599             // name newline
600             writer.println(dir);
601         }
602 
603         for (String file : files) {
604             writer.println(file);
605         }
606     }
607 
608     /**
609      * try to guess the name of the given file.
610      * <p>
611      * If this jar has a classpath attribute in its manifest, we
612      * can assume that it will only require an index of jars listed
613      * there. try to find which classpath entry is most likely the
614      * one the given file name points to.</p>
615      * <p>
616      * In the absence of a classpath attribute, assume the other
617      * files will be placed inside the same directory as this jar and
618      * use their basename.</p>
619      * <p>
620      * if there is a classpath and the given file doesn't match any
621      * of its entries, return null.</p>
622      *
623      * @param fileName .
624      * @param classpath .
625      *
626      * @return The guessed name
627      */
628     protected static String findJarName(String fileName, String[] classpath) {
629         if (classpath == null) {
630             return new File(fileName).getName();
631         }
632         fileName = fileName.replace(File.separatorChar, '/');
633 
634         // longest match comes first
635         SortedMap<String, String> matches =
636                 new TreeMap<>(Comparator.comparingInt(String::length).reversed());
637 
638         for (String aClasspath : classpath) {
639             if (fileName.endsWith(aClasspath)) {
640                 matches.put(aClasspath, aClasspath);
641             } else {
642                 int slash = aClasspath.indexOf("/");
643                 String candidate = aClasspath;
644                 while (slash > -1) {
645                     candidate = candidate.substring(slash + 1);
646                     if (fileName.endsWith(candidate)) {
647                         matches.put(candidate, aClasspath);
648                         break;
649                     }
650                     slash = candidate.indexOf("/");
651                 }
652             }
653         }
654 
655         return matches.size() == 0 ? null : matches.get(matches.firstKey());
656     }
657 
658     /**
659      * Grab lists of all root-level files and all directories
660      * contained in the given archive.
661      *
662      * @param file .
663      * @param files .
664      * @param dirs .
665      *
666      * @throws java.io.IOException
667      */
668     private void grabFilesAndDirs(String file, List<String> dirs, List<String> files) throws IOException {
669         File zipFile = new File(file);
670         if (!zipFile.exists()) {
671             getLogger().error("JarArchive skipping non-existing file: " + zipFile.getAbsolutePath());
672         } else if (zipFile.isDirectory()) {
673             getLogger().info("JarArchiver skipping indexJar " + zipFile + " because it is not a jar");
674         } else {
675             try (ZipFile zf = new ZipFile(file, "utf-8")) {
676                 Enumeration<ZipArchiveEntry> entries = zf.getEntries();
677                 HashSet<String> dirSet = new HashSet<>();
678                 while (entries.hasMoreElements()) {
679                     ZipArchiveEntry ze = entries.nextElement();
680                     String name = ze.getName();
681                     // avoid index for manifest-only jars.
682                     if (!name.equals(META_INF_NAME)
683                             && !name.equals(META_INF_NAME + '/')
684                             && !name.equals(INDEX_NAME)
685                             && !name.equals(MANIFEST_NAME)) {
686                         if (ze.isDirectory()) {
687                             dirSet.add(name);
688                         } else if (!name.contains("/")) {
689                             files.add(name);
690                         } else {
691                             // a file, not in the root
692                             // since the jar may be one without directory
693                             // entries, add the parent dir of this file as
694                             // well.
695                             dirSet.add(name.substring(0, name.lastIndexOf("/") + 1));
696                         }
697                     }
698                 }
699                 dirs.addAll(dirSet);
700             }
701         }
702     }
703 
704     /**
705      * Override the behavior of the Zip Archiver to match the output of the JAR tool.
706      *
707      * @param zipEntry to set the last modified time
708      * @param lastModifiedTime to set in the zip entry only if {@link #getLastModifiedTime()} returns null
709      */
710     @Override
711     protected void setZipEntryTime(ZipArchiveEntry zipEntry, long lastModifiedTime) {
712         if (getLastModifiedTime() != null) {
713             lastModifiedTime = getLastModifiedTime().toMillis();
714         }
715 
716         // The JAR tool does not round up, so we keep that behavior here (JDK-8277755).
717         zipEntry.setTime(lastModifiedTime);
718     }
719 }