View Javadoc
1   /**
2    *
3    * Copyright 2018 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.File;
22  import java.io.IOException;
23  import java.io.PrintStream;
24  import java.lang.reflect.Method;
25  import java.nio.file.Files;
26  import java.nio.file.LinkOption;
27  import java.nio.file.Path;
28  import java.nio.file.StandardCopyOption;
29  import java.nio.file.attribute.FileAttribute;
30  import java.nio.file.attribute.FileTime;
31  import java.nio.file.attribute.PosixFileAttributeView;
32  import java.nio.file.attribute.PosixFileAttributes;
33  import java.nio.file.attribute.PosixFilePermissions;
34  import java.util.ArrayList;
35  import java.util.Calendar;
36  import java.util.Enumeration;
37  import java.util.List;
38  import java.util.Locale;
39  import java.util.TimeZone;
40  import java.util.regex.Pattern;
41  import java.util.zip.ZipEntry;
42  import java.util.zip.ZipFile;
43  import java.util.zip.ZipOutputStream;
44  
45  import org.apache.commons.compress.parallel.InputStreamSupplier;
46  import org.apache.commons.io.output.NullPrintStream;
47  import org.codehaus.plexus.archiver.ArchiverException;
48  import org.codehaus.plexus.archiver.zip.ConcurrentJarCreator;
49  import org.codehaus.plexus.util.IOUtil;
50  
51  /**
52   * A {@link ModularJarArchiver} implementation that uses
53   * the {@code jar} tool provided by
54   * {@code java.util.spi.ToolProvider} to create
55   * modular JAR files.
56   *
57   * <p>
58   * The basic JAR archive is created by {@link JarArchiver}
59   * and the {@code jar} tool is used to upgrade it to modular JAR.
60   *
61   * <p>
62   * If the JAR file does not contain module descriptor
63   * or the JDK does not provide the {@code jar} tool
64   * (for example JDK prior to Java 9), then the
65   * archive created by {@link JarArchiver}
66   * is left unchanged.
67   */
68  @Named("mjar")
69  public class JarToolModularJarArchiver extends ModularJarArchiver {
70      private static final String MODULE_DESCRIPTOR_FILE_NAME = "module-info.class";
71  
72      private static final Pattern MRJAR_VERSION_AREA = Pattern.compile("META-INF/versions/\\d+/");
73  
74      private Object jarTool;
75  
76      private boolean moduleDescriptorFound;
77  
78      private boolean hasJarDateOption;
79  
80      public JarToolModularJarArchiver() {
81          try {
82              Class<?> toolProviderClass = Class.forName("java.util.spi.ToolProvider");
83              Object jarToolOptional =
84                      toolProviderClass.getMethod("findFirst", String.class).invoke(null, "jar");
85  
86              jarTool = jarToolOptional.getClass().getMethod("get").invoke(jarToolOptional);
87          } catch (ReflectiveOperationException | SecurityException e) {
88              // Ignore. It is expected that the jar tool
89              // may not be available.
90          }
91      }
92  
93      @Override
94      protected void zipFile(
95              InputStreamSupplier is,
96              ConcurrentJarCreator zOut,
97              String vPath,
98              long lastModified,
99              File fromArchive,
100             int mode,
101             String symlinkDestination,
102             boolean addInParallel)
103             throws IOException, ArchiverException {
104         if (jarTool != null && isModuleDescriptor(vPath)) {
105             getLogger().debug("Module descriptor found: " + vPath);
106 
107             moduleDescriptorFound = true;
108         }
109 
110         super.zipFile(is, zOut, vPath, lastModified, fromArchive, mode, symlinkDestination, addInParallel);
111     }
112 
113     @Override
114     protected void postCreateArchive() throws ArchiverException {
115         if (!moduleDescriptorFound) {
116             // no need to update the JAR archive
117             return;
118         }
119 
120         try {
121             getLogger().debug("Using the jar tool to " + "update the archive to modular JAR.");
122 
123             final Method jarRun =
124                     jarTool.getClass().getMethod("run", PrintStream.class, PrintStream.class, String[].class);
125 
126             if (getLastModifiedTime() != null) {
127                 hasJarDateOption = isJarDateOptionSupported(jarRun);
128                 getLogger().debug("jar tool --date option is supported: " + hasJarDateOption);
129             }
130 
131             Integer result = (Integer) jarRun.invoke(jarTool, System.out, System.err, getJarToolArguments());
132 
133             if (result != null && result != 0) {
134                 throw new ArchiverException(
135                         "Could not create modular JAR file. " + "The JDK jar tool exited with " + result);
136             }
137 
138             if (!hasJarDateOption && getLastModifiedTime() != null) {
139                 getLogger().debug("Fix last modified time zip entries.");
140                 // --date option not supported, fallback to rewrite the JAR file
141                 // https://github.com/codehaus-plexus/plexus-archiver/issues/164
142                 fixLastModifiedTimeZipEntries();
143             }
144         } catch (IOException | ReflectiveOperationException | SecurityException e) {
145             throw new ArchiverException("Exception occurred " + "while creating modular JAR file", e);
146         }
147     }
148 
149     /**
150      * Fallback to rewrite the JAR file with the correct timestamp if the {@code --date} option is not available.
151      */
152     private void fixLastModifiedTimeZipEntries() throws IOException {
153         long timeMillis = getLastModifiedTime().toMillis();
154         Path destFile = getDestFile().toPath();
155         PosixFileAttributes posixFileAttributes = Files.getFileAttributeView(
156                         destFile, PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS)
157                 .readAttributes();
158         FileAttribute<?>[] attributes;
159         if (posixFileAttributes != null) {
160             attributes = new FileAttribute<?>[1];
161             attributes[0] = PosixFilePermissions.asFileAttribute(posixFileAttributes.permissions());
162         } else {
163             attributes = new FileAttribute<?>[0];
164         }
165         Path tmpZip = Files.createTempFile(destFile.getParent(), null, null, attributes);
166         try {
167             try (ZipFile zipFile = new ZipFile(getDestFile());
168                     ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(tmpZip))) {
169                 Enumeration<? extends ZipEntry> entries = zipFile.entries();
170                 while (entries.hasMoreElements()) {
171                     ZipEntry entry = entries.nextElement();
172                     // Not using setLastModifiedTime(FileTime) as it sets the extended timestamp
173                     // which is not compatible with the jar tool output.
174                     entry.setTime(timeMillis);
175                     out.putNextEntry(entry);
176                     if (!entry.isDirectory()) {
177                         IOUtil.copy(zipFile.getInputStream(entry), out);
178                     }
179                     out.closeEntry();
180                 }
181             }
182             Files.move(tmpZip, destFile, StandardCopyOption.REPLACE_EXISTING);
183         } catch (IOException e) {
184             // Clean up temporary file if an error occurs
185             try {
186                 Files.delete(tmpZip);
187             } catch (IOException ioe) {
188                 e.addSuppressed(ioe);
189             }
190             throw e;
191         }
192     }
193 
194     /**
195      * Returns {@code true} if {@code path}
196      * is a module descriptor.
197      */
198     private boolean isModuleDescriptor(String path) {
199         if (path.endsWith(MODULE_DESCRIPTOR_FILE_NAME)) {
200             String prefix = path.substring(0, path.lastIndexOf(MODULE_DESCRIPTOR_FILE_NAME));
201 
202             // the path is a module descriptor if it located
203             // into the root of the archive or into the
204             // version area of a multi-release JAR file
205             return prefix.isEmpty() || MRJAR_VERSION_AREA.matcher(prefix).matches();
206         } else {
207             return false;
208         }
209     }
210 
211     /**
212      * Prepares the arguments for the jar tool.
213      * It takes into account the module version,
214      * main class, etc.
215      */
216     private String[] getJarToolArguments() throws IOException {
217         // We add empty temporary directory to the JAR file.
218         // It may look strange at first, but to update a JAR file
219         // you need to add new files[1]. If we add empty directory
220         // it will be ignored (not added to the archive), but
221         // the module descriptor will be updated and validated.
222         //
223         // [1] There are some exceptions (such as when the main class
224         // is updated) but we need at least empty directory
225         // to ensure it will work in all cases.
226         File tempEmptyDir = Files.createTempDirectory(null).toFile();
227         tempEmptyDir.deleteOnExit();
228 
229         List<String> args = new ArrayList<>();
230 
231         args.add("--update");
232         args.add("--file");
233         args.add(getDestFile().getAbsolutePath());
234 
235         String mainClass = getModuleMainClass() != null ? getModuleMainClass() : getManifestMainClass();
236 
237         if (mainClass != null) {
238             args.add("--main-class");
239             args.add(mainClass);
240         }
241 
242         if (getModuleVersion() != null) {
243             args.add("--module-version");
244             args.add(getModuleVersion());
245         }
246 
247         if (!isCompress()) {
248             args.add("--no-compress");
249         }
250 
251         if (hasJarDateOption) {
252             // The --date option already normalize the time, so revert to the local time
253             FileTime localTime = revertToLocalTime(getLastModifiedTime());
254             args.add("--date");
255             args.add(localTime.toString());
256         }
257 
258         args.add("-C");
259         args.add(tempEmptyDir.getAbsolutePath());
260         args.add(".");
261 
262         return args.toArray(new String[0]);
263     }
264 
265     private static FileTime revertToLocalTime(FileTime time) {
266         long restoreToLocalTime = time.toMillis();
267         Calendar cal = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
268         cal.setTimeInMillis(restoreToLocalTime);
269         restoreToLocalTime = restoreToLocalTime + (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET));
270         return FileTime.fromMillis(restoreToLocalTime);
271     }
272 
273     /**
274      * Check support for {@code --date} option introduced since Java 17.0.3 (JDK-8279925).
275      *
276      * @return true if the JAR tool supports the {@code --date} option
277      */
278     private boolean isJarDateOptionSupported(Method runMethod) {
279         try {
280             // Test the output code validating the --date option.
281             String[] args = {"--date", "2099-12-31T23:59:59Z", "--version"};
282 
283             PrintStream nullPrintStream = NullPrintStream.NULL_PRINT_STREAM;
284             Integer result = (Integer) runMethod.invoke(jarTool, nullPrintStream, nullPrintStream, args);
285 
286             return result != null && result.intValue() == 0;
287         } catch (ReflectiveOperationException | SecurityException e) {
288             return false;
289         }
290     }
291 }