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 PosixFileAttributeView view =
157 Files.getFileAttributeView(destFile, PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
158
159 PosixFileAttributes posixFileAttributes = null;
160 if (view != null) {
161 posixFileAttributes = view.readAttributes();
162 }
163
164 FileAttribute<?>[] attributes;
165 if (posixFileAttributes != null) {
166 attributes = new FileAttribute<?>[1];
167 attributes[0] = PosixFilePermissions.asFileAttribute(posixFileAttributes.permissions());
168 } else {
169 attributes = new FileAttribute<?>[0];
170 }
171 Path tmpZip = Files.createTempFile(destFile.getParent(), null, null, attributes);
172 try {
173 try (ZipFile zipFile = new ZipFile(getDestFile());
174 ZipOutputStream out = new ZipOutputStream(Streams.fileOutputStream(tmpZip))) {
175 Enumeration<? extends ZipEntry> entries = zipFile.entries();
176 while (entries.hasMoreElements()) {
177 ZipEntry entry = entries.nextElement();
178
179
180 entry.setTime(timeMillis);
181 out.putNextEntry(entry);
182 if (!entry.isDirectory()) {
183 IOUtil.copy(zipFile.getInputStream(entry), out);
184 }
185 out.closeEntry();
186 }
187 }
188 Files.move(tmpZip, destFile, StandardCopyOption.REPLACE_EXISTING);
189 } catch (IOException e) {
190
191 try {
192 Files.delete(tmpZip);
193 } catch (IOException ioe) {
194 e.addSuppressed(ioe);
195 }
196 throw e;
197 }
198 }
199
200
201
202
203
204 private boolean isModuleDescriptor(String path) {
205 if (path.endsWith(MODULE_DESCRIPTOR_FILE_NAME)) {
206 String prefix = path.substring(0, path.lastIndexOf(MODULE_DESCRIPTOR_FILE_NAME));
207
208
209
210
211 return prefix.isEmpty() || MRJAR_VERSION_AREA.matcher(prefix).matches();
212 } else {
213 return false;
214 }
215 }
216
217
218
219
220
221
222 private String[] getJarToolArguments() throws IOException {
223
224
225
226
227
228
229
230
231
232 File tempEmptyDir = Files.createTempDirectory(null).toFile();
233 tempEmptyDir.deleteOnExit();
234
235 List<String> args = new ArrayList<>();
236
237 args.add("--update");
238 args.add("--file");
239 args.add(getDestFile().getAbsolutePath());
240
241 String mainClass = getModuleMainClass() != null ? getModuleMainClass() : getManifestMainClass();
242
243 if (mainClass != null) {
244 args.add("--main-class");
245 args.add(mainClass);
246 }
247
248 if (getModuleVersion() != null) {
249 args.add("--module-version");
250 args.add(getModuleVersion());
251 }
252
253 if (!isCompress()) {
254 args.add("--no-compress");
255 }
256
257 if (hasJarDateOption) {
258
259 FileTime localTime = revertToLocalTime(getLastModifiedTime());
260 args.add("--date");
261 args.add(localTime.toString());
262 }
263
264 args.add("-C");
265 args.add(tempEmptyDir.getAbsolutePath());
266 args.add(".");
267
268 return args.toArray(new String[0]);
269 }
270
271 private static FileTime revertToLocalTime(FileTime time) {
272 long restoreToLocalTime = time.toMillis();
273 Calendar cal = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
274 cal.setTimeInMillis(restoreToLocalTime);
275 restoreToLocalTime = restoreToLocalTime + (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET));
276 return FileTime.fromMillis(restoreToLocalTime);
277 }
278
279
280
281
282
283
284 private boolean isJarDateOptionSupported(Method runMethod) {
285 try {
286
287 String[] args = {"--date", "2099-12-31T23:59:59Z", "--version"};
288
289 PrintStream nullPrintStream = NullPrintStream.INSTANCE;
290 Integer result = (Integer) runMethod.invoke(jarTool, nullPrintStream, nullPrintStream, args);
291
292 return result != null && result.intValue() == 0;
293 } catch (ReflectiveOperationException | SecurityException e) {
294 return false;
295 }
296 }
297 }