1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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
89
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
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
141
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
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
173
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
185 try {
186 Files.delete(tmpZip);
187 } catch (IOException ioe) {
188 e.addSuppressed(ioe);
189 }
190 throw e;
191 }
192 }
193
194
195
196
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
203
204
205 return prefix.isEmpty() || MRJAR_VERSION_AREA.matcher(prefix).matches();
206 } else {
207 return false;
208 }
209 }
210
211
212
213
214
215
216 private String[] getJarToolArguments() throws IOException {
217
218
219
220
221
222
223
224
225
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
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
275
276
277
278 private boolean isJarDateOptionSupported(Method runMethod) {
279 try {
280
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 }