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