1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.shared.archiver;
20
21 import javax.lang.model.SourceVersion;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.nio.file.Files;
27 import java.nio.file.Path;
28 import java.nio.file.Paths;
29 import java.nio.file.attribute.FileTime;
30 import java.time.Instant;
31 import java.time.OffsetDateTime;
32 import java.time.ZoneOffset;
33 import java.time.format.DateTimeParseException;
34 import java.time.temporal.ChronoUnit;
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Optional;
40 import java.util.Properties;
41 import java.util.jar.Attributes;
42 import java.util.regex.Matcher;
43 import java.util.regex.Pattern;
44
45 import org.apache.maven.api.Dependency;
46 import org.apache.maven.api.PathScope;
47 import org.apache.maven.api.Project;
48 import org.apache.maven.api.Session;
49 import org.apache.maven.api.services.DependencyResolver;
50 import org.apache.maven.api.services.DependencyResolverResult;
51 import org.codehaus.plexus.archiver.jar.JarArchiver;
52 import org.codehaus.plexus.archiver.jar.Manifest;
53 import org.codehaus.plexus.archiver.jar.ManifestException;
54 import org.codehaus.plexus.interpolation.InterpolationException;
55 import org.codehaus.plexus.interpolation.Interpolator;
56 import org.codehaus.plexus.interpolation.PrefixAwareRecursionInterceptor;
57 import org.codehaus.plexus.interpolation.PrefixedObjectValueSource;
58 import org.codehaus.plexus.interpolation.PrefixedPropertiesValueSource;
59 import org.codehaus.plexus.interpolation.RecursionInterceptor;
60 import org.codehaus.plexus.interpolation.StringSearchInterpolator;
61 import org.codehaus.plexus.interpolation.ValueSource;
62
63 import static org.apache.maven.shared.archiver.ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_CUSTOM;
64 import static org.apache.maven.shared.archiver.ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_REPOSITORY;
65 import static org.apache.maven.shared.archiver.ManifestConfiguration.CLASSPATH_LAYOUT_TYPE_SIMPLE;
66
67
68
69
70 public class MavenArchiver {
71
72 private static final String CREATED_BY = "Maven Archiver";
73
74
75
76
77 public static final String SIMPLE_LAYOUT =
78 "${artifact.artifactId}-${artifact.version}${dashClassifier?}.${artifact.extension}";
79
80
81
82
83 public static final String REPOSITORY_LAYOUT =
84 "${artifact.groupIdPath}/${artifact.artifactId}/" + "${artifact.baseVersion}/${artifact.artifactId}-"
85 + "${artifact.version}${dashClassifier?}.${artifact.extension}";
86
87
88
89
90 public static final String SIMPLE_LAYOUT_NONUNIQUE =
91 "${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}";
92
93
94
95
96 public static final String REPOSITORY_LAYOUT_NONUNIQUE =
97 "${artifact.groupIdPath}/${artifact.artifactId}/" + "${artifact.baseVersion}/${artifact.artifactId}-"
98 + "${artifact.baseVersion}${dashClassifier?}.${artifact.extension}";
99
100 private static final Instant DATE_MIN = Instant.parse("1980-01-01T00:00:02Z");
101
102 private static final Instant DATE_MAX = Instant.parse("2099-12-31T23:59:59Z");
103
104 private static final List<String> ARTIFACT_EXPRESSION_PREFIXES;
105
106 static {
107 List<String> artifactExpressionPrefixes = new ArrayList<>();
108 artifactExpressionPrefixes.add("artifact.");
109
110 ARTIFACT_EXPRESSION_PREFIXES = artifactExpressionPrefixes;
111 }
112
113 static boolean isValidModuleName(String name) {
114 return SourceVersion.isName(name);
115 }
116
117 private JarArchiver archiver;
118
119 private File archiveFile;
120
121 private String createdBy;
122
123 private boolean buildJdkSpecDefaultEntry = true;
124
125
126
127
128
129
130
131
132
133
134 public Manifest getManifest(Session session, Project project, MavenArchiveConfiguration config)
135 throws MavenArchiverException {
136 boolean hasManifestEntries = !config.isManifestEntriesEmpty();
137 Map<String, String> entries = hasManifestEntries ? config.getManifestEntries() : Collections.emptyMap();
138
139 Manifest manifest = getManifest(session, project, config.getManifest(), entries);
140
141 try {
142
143 if (hasManifestEntries) {
144
145 for (Map.Entry<String, String> entry : entries.entrySet()) {
146 String key = entry.getKey();
147 String value = entry.getValue();
148 Manifest.Attribute attr = manifest.getMainSection().getAttribute(key);
149 if (key.equals(Attributes.Name.CLASS_PATH.toString()) && attr != null) {
150
151
152
153 attr.setValue(value + " " + attr.getValue());
154 } else {
155 addManifestAttribute(manifest, key, value);
156 }
157 }
158 }
159
160
161 if (!config.isManifestSectionsEmpty()) {
162 for (ManifestSection section : config.getManifestSections()) {
163 Manifest.Section theSection = new Manifest.Section();
164 theSection.setName(section.getName());
165
166 if (!section.isManifestEntriesEmpty()) {
167 Map<String, String> sectionEntries = section.getManifestEntries();
168
169 for (Map.Entry<String, String> entry : sectionEntries.entrySet()) {
170 String key = entry.getKey();
171 String value = entry.getValue();
172 Manifest.Attribute attr = new Manifest.Attribute(key, value);
173 theSection.addConfiguredAttribute(attr);
174 }
175 }
176
177 manifest.addConfiguredSection(theSection);
178 }
179 }
180 } catch (ManifestException e) {
181 throw new MavenArchiverException("Unable to create manifest", e);
182 }
183
184 return manifest;
185 }
186
187
188
189
190
191
192
193
194
195
196 public Manifest getManifest(Project project, ManifestConfiguration config) throws MavenArchiverException {
197 return getManifest(null, project, config, Collections.emptyMap());
198 }
199
200 public Manifest getManifest(Session session, Project project, ManifestConfiguration config)
201 throws MavenArchiverException {
202 return getManifest(session, project, config, Collections.emptyMap());
203 }
204
205 private void addManifestAttribute(Manifest manifest, Map<String, String> map, String key, String value)
206 throws ManifestException {
207 if (map.containsKey(key)) {
208 return;
209 }
210 addManifestAttribute(manifest, key, value);
211 }
212
213 private void addManifestAttribute(Manifest manifest, String key, String value) throws ManifestException {
214 if (!(value == null || value.isEmpty())) {
215 Manifest.Attribute attr = new Manifest.Attribute(key, value);
216 manifest.addConfiguredAttribute(attr);
217 } else {
218
219
220 Manifest.Attribute attr = new Manifest.Attribute(key, "");
221 manifest.addConfiguredAttribute(attr);
222 }
223 }
224
225
226
227
228
229
230
231
232
233
234
235 protected Manifest getManifest(
236 Session session, Project project, ManifestConfiguration config, Map<String, String> entries)
237 throws MavenArchiverException {
238 try {
239 return doGetManifest(session, project, config, entries);
240 } catch (ManifestException e) {
241 throw new MavenArchiverException("Unable to create manifest", e);
242 }
243 }
244
245 protected Manifest doGetManifest(
246 Session session, Project project, ManifestConfiguration config, Map<String, String> entries)
247 throws ManifestException {
248
249 Manifest m = new Manifest();
250
251 if (config.isAddDefaultEntries()) {
252 handleDefaultEntries(m, entries);
253 }
254
255 if (config.isAddBuildEnvironmentEntries()) {
256 handleBuildEnvironmentEntries(session, m, entries);
257 }
258
259 DependencyResolverResult result;
260 if (config.isAddClasspath() || config.isAddExtensions()) {
261 result = session.getService(DependencyResolver.class).resolve(session, project, PathScope.MAIN_RUNTIME);
262 } else {
263 result = null;
264 }
265
266 if (config.isAddClasspath()) {
267 StringBuilder classpath = new StringBuilder();
268
269 String classpathPrefix = config.getClasspathPrefix();
270 String layoutType = config.getClasspathLayoutType();
271 String layout = config.getCustomClasspathLayout();
272
273 Interpolator interpolator = new StringSearchInterpolator();
274
275 for (Map.Entry<Dependency, Path> entry : result.getDependencies().entrySet()) {
276 Path artifactFile = entry.getValue();
277 Dependency dependency = entry.getKey();
278 if (Files.isRegularFile(artifactFile.toAbsolutePath())) {
279 if (!classpath.isEmpty()) {
280 classpath.append(" ");
281 }
282 classpath.append(classpathPrefix);
283
284
285
286 if (dependency == null || layoutType == null) {
287 classpath.append(artifactFile.getFileName().toString());
288 } else {
289 List<ValueSource> valueSources = new ArrayList<>();
290
291 handleExtraExpression(dependency, valueSources);
292
293 for (ValueSource vs : valueSources) {
294 interpolator.addValueSource(vs);
295 }
296
297 RecursionInterceptor recursionInterceptor =
298 new PrefixAwareRecursionInterceptor(ARTIFACT_EXPRESSION_PREFIXES);
299
300 try {
301 switch (layoutType) {
302 case CLASSPATH_LAYOUT_TYPE_SIMPLE:
303 if (config.isUseUniqueVersions()) {
304 classpath.append(interpolator.interpolate(SIMPLE_LAYOUT, recursionInterceptor));
305 } else {
306 classpath.append(interpolator.interpolate(
307 SIMPLE_LAYOUT_NONUNIQUE, recursionInterceptor));
308 }
309 break;
310 case CLASSPATH_LAYOUT_TYPE_REPOSITORY:
311
312
313
314 if (config.isUseUniqueVersions()) {
315 classpath.append(
316 interpolator.interpolate(REPOSITORY_LAYOUT, recursionInterceptor));
317 } else {
318 classpath.append(interpolator.interpolate(
319 REPOSITORY_LAYOUT_NONUNIQUE, recursionInterceptor));
320 }
321 break;
322 case CLASSPATH_LAYOUT_TYPE_CUSTOM:
323 if (layout == null) {
324 throw new ManifestException(CLASSPATH_LAYOUT_TYPE_CUSTOM
325 + " layout type was declared, but custom layout expression was not"
326 + " specified. Check your <archive><manifest><customLayout/>"
327 + " element.");
328 }
329
330 classpath.append(interpolator.interpolate(layout, recursionInterceptor));
331 break;
332 default:
333 throw new ManifestException("Unknown classpath layout type: '" + layoutType
334 + "'. Check your <archive><manifest><layoutType/> element.");
335 }
336 } catch (InterpolationException e) {
337 ManifestException error = new ManifestException(
338 "Error interpolating artifact path for classpath entry: " + e.getMessage());
339
340 error.initCause(e);
341 throw error;
342 } finally {
343 for (ValueSource vs : valueSources) {
344 interpolator.removeValuesSource(vs);
345 }
346 }
347 }
348 }
349 }
350
351 if (!classpath.isEmpty()) {
352
353
354 addManifestAttribute(m, "Class-Path", classpath.toString());
355 }
356 }
357
358 if (config.isAddDefaultSpecificationEntries()) {
359 handleSpecificationEntries(project, entries, m);
360 }
361
362 if (config.isAddDefaultImplementationEntries()) {
363 handleImplementationEntries(project, entries, m);
364 }
365
366 String mainClass = config.getMainClass();
367 if (mainClass != null && !mainClass.isEmpty()) {
368 addManifestAttribute(m, entries, "Main-Class", mainClass);
369 }
370
371 addCustomEntries(m, entries, config);
372
373 return m;
374 }
375
376 private void handleExtraExpression(Dependency dependency, List<ValueSource> valueSources) {
377 valueSources.add(new PrefixedObjectValueSource(ARTIFACT_EXPRESSION_PREFIXES, dependency, true));
378 valueSources.add(new PrefixedObjectValueSource(ARTIFACT_EXPRESSION_PREFIXES, dependency.getType(), true));
379
380 Properties extraExpressions = new Properties();
381
382
383 if (!dependency.isSnapshot()) {
384 extraExpressions.setProperty("baseVersion", dependency.getVersion().toString());
385 }
386
387 extraExpressions.setProperty("groupIdPath", dependency.getGroupId().replace('.', '/'));
388 String classifier = dependency.getClassifier();
389 if (classifier != null && !classifier.isEmpty()) {
390 extraExpressions.setProperty("dashClassifier", "-" + classifier);
391 extraExpressions.setProperty("dashClassifier?", "-" + classifier);
392 } else {
393 extraExpressions.setProperty("dashClassifier", "");
394 extraExpressions.setProperty("dashClassifier?", "");
395 }
396 valueSources.add(new PrefixedPropertiesValueSource(ARTIFACT_EXPRESSION_PREFIXES, extraExpressions, true));
397 }
398
399 private void handleImplementationEntries(Project project, Map<String, String> entries, Manifest m)
400 throws ManifestException {
401 addManifestAttribute(
402 m, entries, "Implementation-Title", project.getModel().getName());
403 addManifestAttribute(m, entries, "Implementation-Version", project.getVersion());
404
405 if (project.getModel().getOrganization() != null) {
406 addManifestAttribute(
407 m,
408 entries,
409 "Implementation-Vendor",
410 project.getModel().getOrganization().getName());
411 }
412 }
413
414 private void handleSpecificationEntries(Project project, Map<String, String> entries, Manifest m)
415 throws ManifestException {
416 addManifestAttribute(
417 m, entries, "Specification-Title", project.getModel().getName());
418
419 String version = project.getPomArtifact().getVersion().toString();
420 Matcher matcher = Pattern.compile("([0-9]+\\.[0-9]+)(.*?)").matcher(version);
421 if (matcher.matches()) {
422 String specVersion = matcher.group(1);
423 addManifestAttribute(m, entries, "Specification-Version", specVersion);
424 }
425
426
427
428
429
430
431
432
433
434
435
436
437 if (project.getModel().getOrganization() != null) {
438 addManifestAttribute(
439 m,
440 entries,
441 "Specification-Vendor",
442 project.getModel().getOrganization().getName());
443 }
444 }
445
446 private void addCustomEntries(Manifest m, Map<String, String> entries, ManifestConfiguration config)
447 throws ManifestException {
448
449
450
451
452
453 if (config.getPackageName() != null) {
454 addManifestAttribute(m, entries, "Package", config.getPackageName());
455 }
456 }
457
458
459
460
461
462
463 public JarArchiver getArchiver() {
464 return archiver;
465 }
466
467
468
469
470
471
472 public void setArchiver(JarArchiver archiver) {
473 this.archiver = archiver;
474 }
475
476
477
478
479
480
481 public void setOutputFile(File outputFile) {
482 archiveFile = outputFile;
483 }
484
485
486
487
488
489
490
491
492
493 public void createArchive(Session session, Project project, MavenArchiveConfiguration archiveConfiguration)
494 throws MavenArchiverException {
495 try {
496 doCreateArchive(session, project, archiveConfiguration);
497 } catch (ManifestException | IOException e) {
498 throw new MavenArchiverException(e);
499 }
500 }
501
502 public void doCreateArchive(Session session, Project project, MavenArchiveConfiguration archiveConfiguration)
503 throws ManifestException, IOException {
504
505
506 boolean forced = archiveConfiguration.isForced();
507 if (archiveConfiguration.isAddMavenDescriptor()) {
508
509
510
511
512
513
514
515
516
517
518
519 String groupId = project.getGroupId();
520
521 String artifactId = project.getArtifactId();
522
523 String version;
524 if (project.getPomArtifact().isSnapshot()) {
525 version = project.getPomArtifact().getVersion().toString();
526 } else {
527 version = project.getVersion();
528 }
529
530 archiver.addFile(
531 project.getPomPath().toFile(), "META-INF/maven/" + groupId + "/" + artifactId + "/pom.xml");
532
533
534
535
536
537 Path customPomPropertiesFile = archiveConfiguration.getPomPropertiesFile();
538 Path dir = Paths.get(project.getBuild().getDirectory(), "maven-archiver");
539 Path pomPropertiesFile = dir.resolve("pom.properties");
540
541 new PomPropertiesUtil()
542 .createPomProperties(
543 groupId, artifactId, version, archiver, customPomPropertiesFile, pomPropertiesFile);
544 }
545
546
547
548
549
550 archiver.setMinimalDefaultManifest(true);
551 Path manifestFile = archiveConfiguration.getManifestFile();
552 if (manifestFile != null) {
553 archiver.setManifest(manifestFile.toFile());
554 }
555 Manifest manifest = getManifest(session, project, archiveConfiguration);
556
557 archiver.addConfiguredManifest(manifest);
558 archiver.setCompress(archiveConfiguration.isCompress());
559 archiver.setRecompressAddedZips(archiveConfiguration.isRecompressAddedZips());
560 archiver.setDestFile(archiveFile);
561 archiver.setForced(forced);
562 if (!archiveConfiguration.isForced() && archiver.isSupportingForced()) {
563
564
565
566 }
567 String automaticModuleName = manifest.getMainSection().getAttributeValue("Automatic-Module-Name");
568 if (automaticModuleName != null) {
569 if (!isValidModuleName(automaticModuleName)) {
570 throw new ManifestException("Invalid automatic module name: '" + automaticModuleName + "'");
571 }
572 }
573
574
575 archiver.createArchive();
576 }
577
578 private void handleDefaultEntries(Manifest m, Map<String, String> entries) throws ManifestException {
579 String createdBy = this.createdBy;
580 if (createdBy == null) {
581 createdBy = createdBy(CREATED_BY, "org.apache.maven", "maven-archiver");
582 }
583 addManifestAttribute(m, entries, "Created-By", createdBy);
584 if (buildJdkSpecDefaultEntry) {
585 addManifestAttribute(m, entries, "Build-Jdk-Spec", System.getProperty("java.specification.version"));
586 }
587 }
588
589 private void handleBuildEnvironmentEntries(Session session, Manifest m, Map<String, String> entries)
590 throws ManifestException {
591 addManifestAttribute(
592 m,
593 entries,
594 "Build-Tool",
595 session != null ? session.getSystemProperties().get("maven.build.version") : "Apache Maven");
596 addManifestAttribute(
597 m,
598 entries,
599 "Build-Jdk",
600 String.format("%s (%s)", System.getProperty("java.version"), System.getProperty("java.vendor")));
601 addManifestAttribute(
602 m,
603 entries,
604 "Build-Os",
605 String.format(
606 "%s (%s; %s)",
607 System.getProperty("os.name"),
608 System.getProperty("os.version"),
609 System.getProperty("os.arch")));
610 }
611
612 private static String getCreatedByVersion(String groupId, String artifactId) {
613 final Properties properties = loadOptionalProperties(MavenArchiver.class.getResourceAsStream(
614 "/META-INF/maven/" + groupId + "/" + artifactId + "/pom.properties"));
615
616 return properties.getProperty("version");
617 }
618
619 private static Properties loadOptionalProperties(final InputStream inputStream) {
620 Properties properties = new Properties();
621 if (inputStream != null) {
622 try (InputStream in = inputStream) {
623 properties.load(in);
624 } catch (IllegalArgumentException | IOException ex) {
625
626 }
627 }
628 return properties;
629 }
630
631
632
633
634
635
636
637
638
639 public void setCreatedBy(String description, String groupId, String artifactId) {
640 createdBy = createdBy(description, groupId, artifactId);
641 }
642
643 private String createdBy(String description, String groupId, String artifactId) {
644 String createdBy = description;
645 String version = getCreatedByVersion(groupId, artifactId);
646 if (version != null) {
647 createdBy += " " + version;
648 }
649 return createdBy;
650 }
651
652
653
654
655
656
657
658
659
660 public void setBuildJdkSpecDefaultEntry(boolean buildJdkSpecDefaultEntry) {
661 this.buildJdkSpecDefaultEntry = buildJdkSpecDefaultEntry;
662 }
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679 public static Optional<Instant> parseBuildOutputTimestamp(String outputTimestamp) {
680
681 if (outputTimestamp == null) {
682 return Optional.empty();
683 }
684
685
686 if (isNumeric(outputTimestamp)) {
687 final Instant date = Instant.ofEpochSecond(Long.parseLong(outputTimestamp));
688
689 if (date.isBefore(DATE_MIN) || date.isAfter(DATE_MAX)) {
690 throw new IllegalArgumentException(
691 "'" + date + "' is not within the valid range " + DATE_MIN + " to " + DATE_MAX);
692 }
693 return Optional.of(date);
694 }
695
696
697
698 if (outputTimestamp.length() < 2) {
699 return Optional.empty();
700 }
701
702 try {
703
704 final Instant date = OffsetDateTime.parse(outputTimestamp)
705 .withOffsetSameInstant(ZoneOffset.UTC)
706 .truncatedTo(ChronoUnit.SECONDS)
707 .toInstant();
708
709 if (date.isBefore(DATE_MIN) || date.isAfter(DATE_MAX)) {
710 throw new IllegalArgumentException(
711 "'" + date + "' is not within the valid range " + DATE_MIN + " to " + DATE_MAX);
712 }
713 return Optional.of(date);
714 } catch (DateTimeParseException pe) {
715 throw new IllegalArgumentException(
716 "Invalid project.build.outputTimestamp value '" + outputTimestamp + "'", pe);
717 }
718 }
719
720 private static boolean isNumeric(String str) {
721
722 if (str.isEmpty()) {
723 return false;
724 }
725
726 for (char c : str.toCharArray()) {
727 if (!Character.isDigit(c)) {
728 return false;
729 }
730 }
731
732 return true;
733 }
734
735
736
737
738
739
740
741
742 public void configureReproducibleBuild(String outputTimestamp) {
743 parseBuildOutputTimestamp(outputTimestamp).map(FileTime::from).ifPresent(modifiedTime -> getArchiver()
744 .configureReproducibleBuild(modifiedTime));
745 }
746 }