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.util.Streams;
49 import org.codehaus.plexus.archiver.zip.ConcurrentJarCreator;
50 import org.codehaus.plexus.util.IOUtil;
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69 @Named("mjar")
70 public class JarToolModularJarArchiver extends ModularJarArchiver {
71 private static final String MODULE_DESCRIPTOR_FILE_NAME = "module-info.class";
72
73 private static final Pattern MRJAR_VERSION_AREA = Pattern.compile("META-INF/versions/\\d+/");
74
75 private Object jarTool;
76
77 private boolean moduleDescriptorFound;
78
79 private boolean hasJarDateOption;
80
81 public JarToolModularJarArchiver() {
82 try {
83 Class<?> toolProviderClass = Class.forName("java.util.spi.ToolProvider");
84 Object jarToolOptional =
85 toolProviderClass.getMethod("findFirst", String.class).invoke(null, "jar");
86
87 jarTool = jarToolOptional.getClass().getMethod("get").invoke(jarToolOptional);
88 } catch (ReflectiveOperationException | SecurityException e) {
89
90
91 }
92 }
93
94 @Override
95 protected void zipFile(
96 InputStreamSupplier is,
97 ConcurrentJarCreator zOut,
98 String vPath,
99 long lastModified,
100 File fromArchive,
101 int mode,
102 String symlinkDestination,
103 boolean addInParallel)
104 throws IOException, ArchiverException {
105 if (jarTool != null && isModuleDescriptor(vPath)) {
106 getLogger().debug("Module descriptor found: " + vPath);
107
108 moduleDescriptorFound = true;
109 }
110
111 super.zipFile(is, zOut, vPath, lastModified, fromArchive, mode, symlinkDestination, addInParallel);
112 }
113
114 @Override
115 protected void postCreateArchive() throws ArchiverException {
116 if (!moduleDescriptorFound) {
117
118 return;
119 }
120
121 try {
122 getLogger().debug("Using the jar tool to " + "update the archive to modular JAR.");
123
124 final Method jarRun =
125 jarTool.getClass().getMethod("run", PrintStream.class, PrintStream.class, String[].class);
126
127 if (getLastModifiedTime() != null) {
128 hasJarDateOption = isJarDateOptionSupported(jarRun);
129 getLogger().debug("jar tool --date option is supported: " + hasJarDateOption);
130 }
131
132 Integer result = (Integer) jarRun.invoke(jarTool, System.out, System.err, getJarToolArguments());
133
134 if (result != null && result != 0) {
135 throw new ArchiverException(
136 "Could not create modular JAR file. " + "The JDK jar tool exited with " + result);
137 }
138
139 if (!hasJarDateOption && getLastModifiedTime() != null) {
140 getLogger().debug("Fix last modified time zip entries.");
141
142
143 fixLastModifiedTimeZipEntries();
144 }
145 } catch (IOException | ReflectiveOperationException | SecurityException e) {
146 throw new ArchiverException("Exception occurred " + "while creating modular JAR file", e);
147 }
148 }
149
150
151
152
153 private void fixLastModifiedTimeZipEntries() throws IOException {
154 long timeMillis = getLastModifiedTime().toMillis();
155 Path destFile = getDestFile().toPath();
156 PosixFileAttributes posixFileAttributes = Files.getFileAttributeView(
157 destFile, PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS)
158 .readAttributes();
159 FileAttribute<?>[] attributes;
160 if (posixFileAttributes != null) {
161 attributes = new FileAttribute<?>[1];
162 attributes[0] = PosixFilePermissions.asFileAttribute(posixFileAttributes.permissions());
163 } else {
164 attributes = new FileAttribute<?>[0];
165 }
166 Path tmpZip = Files.createTempFile(destFile.getParent(), null, null, attributes);
167 try {
168 try (ZipFile zipFile = new ZipFile(getDestFile());
169 ZipOutputStream out = new ZipOutputStream(Streams.fileOutputStream(tmpZip))) {
170 Enumeration<? extends ZipEntry> entries = zipFile.entries();
171 while (entries.hasMoreElements()) {
172 ZipEntry entry = entries.nextElement();
173
174
175 entry.setTime(timeMillis);
176 out.putNextEntry(entry);
177 if (!entry.isDirectory()) {
178 IOUtil.copy(zipFile.getInputStream(entry), out);
179 }
180 out.closeEntry();
181 }
182 }
183 Files.move(tmpZip, destFile, StandardCopyOption.REPLACE_EXISTING);
184 } catch (IOException e) {
185
186 try {
187 Files.delete(tmpZip);
188 } catch (IOException ioe) {
189 e.addSuppressed(ioe);
190 }
191 throw e;
192 }
193 }
194
195
196
197
198
199 private boolean isModuleDescriptor(String path) {
200 if (path.endsWith(MODULE_DESCRIPTOR_FILE_NAME)) {
201 String prefix = path.substring(0, path.lastIndexOf(MODULE_DESCRIPTOR_FILE_NAME));
202
203
204
205
206 return prefix.isEmpty() || MRJAR_VERSION_AREA.matcher(prefix).matches();
207 } else {
208 return false;
209 }
210 }
211
212
213
214
215
216
217 private String[] getJarToolArguments() throws IOException {
218
219
220
221
222
223
224
225
226
227 File tempEmptyDir = Files.createTempDirectory(null).toFile();
228 tempEmptyDir.deleteOnExit();
229
230 List<String> args = new ArrayList<>();
231
232 args.add("--update");
233 args.add("--file");
234 args.add(getDestFile().getAbsolutePath());
235
236 String mainClass = getModuleMainClass() != null ? getModuleMainClass() : getManifestMainClass();
237
238 if (mainClass != null) {
239 args.add("--main-class");
240 args.add(mainClass);
241 }
242
243 if (getModuleVersion() != null) {
244 args.add("--module-version");
245 args.add(getModuleVersion());
246 }
247
248 if (!isCompress()) {
249 args.add("--no-compress");
250 }
251
252 if (hasJarDateOption) {
253
254 FileTime localTime = revertToLocalTime(getLastModifiedTime());
255 args.add("--date");
256 args.add(localTime.toString());
257 }
258
259 args.add("-C");
260 args.add(tempEmptyDir.getAbsolutePath());
261 args.add(".");
262
263 return args.toArray(new String[0]);
264 }
265
266 private static FileTime revertToLocalTime(FileTime time) {
267 long restoreToLocalTime = time.toMillis();
268 Calendar cal = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
269 cal.setTimeInMillis(restoreToLocalTime);
270 restoreToLocalTime = restoreToLocalTime + (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET));
271 return FileTime.fromMillis(restoreToLocalTime);
272 }
273
274
275
276
277
278
279 private boolean isJarDateOptionSupported(Method runMethod) {
280 try {
281
282 String[] args = {"--date", "2099-12-31T23:59:59Z", "--version"};
283
284 PrintStream nullPrintStream = NullPrintStream.INSTANCE;
285 Integer result = (Integer) runMethod.invoke(jarTool, nullPrintStream, nullPrintStream, args);
286
287 return result != null && result.intValue() == 0;
288 } catch (ReflectiveOperationException | SecurityException e) {
289 return false;
290 }
291 }
292 }