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 }