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