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