View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.source;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collections;
26  import java.util.List;
27  
28  import org.apache.maven.archiver.MavenArchiveConfiguration;
29  import org.apache.maven.archiver.MavenArchiver;
30  import org.apache.maven.artifact.Artifact;
31  import org.apache.maven.artifact.DependencyResolutionRequiredException;
32  import org.apache.maven.execution.MavenSession;
33  import org.apache.maven.model.Resource;
34  import org.apache.maven.plugin.AbstractMojo;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugins.annotations.Component;
37  import org.apache.maven.plugins.annotations.Parameter;
38  import org.apache.maven.project.MavenProject;
39  import org.apache.maven.project.MavenProjectHelper;
40  import org.codehaus.plexus.archiver.Archiver;
41  import org.codehaus.plexus.archiver.ArchiverException;
42  import org.codehaus.plexus.archiver.jar.JarArchiver;
43  import org.codehaus.plexus.archiver.jar.ManifestException;
44  import org.codehaus.plexus.archiver.util.DefaultFileSet;
45  import org.codehaus.plexus.util.FileUtils;
46  
47  /**
48   * Base class for bundling sources into a jar archive.
49   *
50   * @since 2.0.3
51   */
52  public abstract class AbstractSourceJarMojo extends AbstractMojo {
53      private static final String[] DEFAULT_INCLUDES = new String[] {"**/**"};
54  
55      private static final String[] DEFAULT_EXCLUDES = new String[] {};
56  
57      /**
58       * List of files to include. Specified as fileset patterns which are relative to the input directory whose contents
59       * is being packaged into the JAR.
60       *
61       * @since 2.1
62       */
63      @Parameter
64      private String[] includes;
65  
66      /**
67       * List of files to exclude. Specified as fileset patterns which are relative to the input directory whose contents
68       * is being packaged into the JAR.
69       *
70       * @since 2.1
71       */
72      @Parameter
73      private String[] excludes;
74  
75      /**
76       * Exclude commonly excluded files such as SCM configuration. These are defined in the plexus
77       * FileUtils.getDefaultExcludes()
78       *
79       * @since 2.1
80       */
81      @Parameter(property = "maven.source.useDefaultExcludes", defaultValue = "true")
82      private boolean useDefaultExcludes;
83  
84      /**
85       * The Maven Project Object
86       */
87      @Parameter(defaultValue = "${project}", readonly = true, required = true)
88      private MavenProject project;
89  
90      /**
91       * The Jar archiver.
92       */
93      @Component(role = Archiver.class, hint = "jar")
94      private JarArchiver jarArchiver;
95  
96      /**
97       * The archive configuration to use. See <a href="http://maven.apache.org/shared/maven-archiver/index.html">Maven
98       * Archiver Reference</a>. <br/>
99       * <b>Note: Since 3.0.0 the resulting archives contain a maven descriptor. If you need to suppress the generation of
100      * the maven descriptor you can simply achieve this by using the
101      * <a href="http://maven.apache.org/shared/maven-archiver/index.html#archive">archiver configuration</a>.</b>.
102      *
103      * @since 2.1
104      */
105     @Parameter
106     private MavenArchiveConfiguration archive = new MavenArchiveConfiguration();
107 
108     /**
109      * Path to the default MANIFEST file to use. It will be used if <code>useDefaultManifestFile</code> is set to
110      * <code>true</code>.
111      *
112      * @since 2.1
113      */
114     // CHECKSTYLE_OFF: LineLength
115     @Parameter(
116             defaultValue = "${project.build.outputDirectory}/META-INF/MANIFEST.MF",
117             readonly = false,
118             required = true)
119     // CHECKSTYLE_ON: LineLength
120     private File defaultManifestFile;
121 
122     /**
123      * Set this to <code>true</code> to enable the use of the <code>defaultManifestFile</code>. <br/>
124      *
125      * @since 2.1
126      */
127     @Parameter(property = "maven.source.useDefaultManifestFile", defaultValue = "false")
128     private boolean useDefaultManifestFile;
129 
130     /**
131      * Specifies whether or not to attach the artifact to the project
132      */
133     @Parameter(property = "maven.source.attach", defaultValue = "true")
134     private boolean attach;
135 
136     /**
137      * Specifies whether or not to exclude resources from the sources-jar. This can be convenient if your project
138      * includes large resources, such as images, and you don't want to include them in the sources-jar.
139      *
140      * @since 2.0.4
141      */
142     @Parameter(property = "maven.source.excludeResources", defaultValue = "false")
143     protected boolean excludeResources;
144 
145     /**
146      * Specifies whether or not to include the POM file in the sources-jar.
147      *
148      * @since 2.1
149      */
150     @Parameter(property = "maven.source.includePom", defaultValue = "false")
151     protected boolean includePom;
152 
153     /**
154      * Used for attaching the source jar to the project.
155      */
156     @Component
157     private MavenProjectHelper projectHelper;
158 
159     /**
160      * The directory where the generated archive file will be put.
161      */
162     @Parameter(defaultValue = "${project.build.directory}")
163     protected File outputDirectory;
164 
165     /**
166      * The filename to be used for the generated archive file. For the source:jar goal, "-sources" is appended to this
167      * filename. For the source:test-jar goal, "-test-sources" is appended.
168      */
169     @Parameter(defaultValue = "${project.build.finalName}")
170     protected String finalName;
171 
172     /**
173      * Contains the full list of projects in the reactor.
174      */
175     @Parameter(defaultValue = "${reactorProjects}", readonly = true)
176     protected List<MavenProject> reactorProjects;
177 
178     /**
179      * Whether creating the archive should be forced. If set to true, the jar will always be created. If set to false,
180      * the jar will only be created when the sources are newer than the jar.
181      *
182      * @since 2.1
183      */
184     @Parameter(property = "maven.source.forceCreation", defaultValue = "false")
185     private boolean forceCreation;
186 
187     /**
188      * A flag used to disable the source procedure. This is primarily intended for usage from the command line to
189      * occasionally adjust the build.
190      *
191      * @since 2.2
192      */
193     @Parameter(property = "maven.source.skip", defaultValue = "false")
194     private boolean skipSource;
195 
196     /**
197      * The Maven session.
198      */
199     @Parameter(defaultValue = "${session}", readonly = true, required = true)
200     private MavenSession session;
201 
202     /**
203      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
204      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
205      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
206      *
207      * @since 3.2.0
208      */
209     @Parameter(defaultValue = "${project.build.outputTimestamp}")
210     private String outputTimestamp;
211 
212     // ----------------------------------------------------------------------
213     // Public methods
214     // ----------------------------------------------------------------------
215 
216     /**
217      * {@inheritDoc}
218      */
219     public void execute() throws MojoExecutionException {
220         if (skipSource) {
221             getLog().info("Skipping source per configuration.");
222             return;
223         }
224 
225         packageSources(project);
226     }
227 
228     // ----------------------------------------------------------------------
229     // Protected methods
230     // ----------------------------------------------------------------------
231 
232     /**
233      * @return the wanted classifier, ie <code>sources</code> or <code>test-sources</code>
234      */
235     protected abstract String getClassifier();
236 
237     /**
238      * @param p {@link MavenProject} not null
239      * @return the compile or test sources
240      * @throws MojoExecutionException in case of an error.
241      */
242     protected abstract List<String> getSources(MavenProject p) throws MojoExecutionException;
243 
244     /**
245      * @param p {@link MavenProject} not null
246      * @return the compile or test resources
247      * @throws MojoExecutionException in case of an error.
248      */
249     protected abstract List<Resource> getResources(MavenProject p) throws MojoExecutionException;
250 
251     /**
252      * @param p {@link MavenProject}
253      * @throws MojoExecutionException in case of an error.
254      */
255     protected void packageSources(MavenProject p) throws MojoExecutionException {
256         if (!"pom".equals(p.getPackaging())) {
257             packageSources(Collections.singletonList(p));
258         }
259     }
260 
261     /**
262      * @param theProjects {@link MavenProject}
263      * @throws MojoExecutionException in case of an error.
264      */
265     protected void packageSources(List<MavenProject> theProjects) throws MojoExecutionException {
266         if (project.getArtifact().getClassifier() != null) {
267             getLog().warn("NOT adding sources to artifacts with classifier as Maven only supports one classifier "
268                     + "per artifact. Current artifact [" + project.getArtifact().getId() + "] has a ["
269                     + project.getArtifact().getClassifier() + "] classifier.");
270 
271             return;
272         }
273 
274         MavenArchiver archiver = createArchiver();
275 
276         for (MavenProject pItem : theProjects) {
277             MavenProject subProject = getProject(pItem);
278 
279             if ("pom".equals(subProject.getPackaging())) {
280                 continue;
281             }
282 
283             archiveProjectContent(subProject, archiver.getArchiver());
284         }
285 
286         if (archiver.getArchiver().getResources().hasNext() || forceCreation) {
287 
288             if (useDefaultManifestFile && defaultManifestFile.exists() && archive.getManifestFile() == null) {
289                 getLog().info("Adding existing MANIFEST to archive. Found under: " + defaultManifestFile.getPath());
290                 archive.setManifestFile(defaultManifestFile);
291             }
292 
293             File outputFile = new File(outputDirectory, finalName + "-" + getClassifier() + getExtension());
294 
295             try {
296                 archiver.setOutputFile(outputFile);
297                 archive.setForced(forceCreation);
298 
299                 getLog().debug("create archive " + outputFile);
300                 archiver.createArchive(session, project, archive);
301             } catch (IOException | ArchiverException | DependencyResolutionRequiredException | ManifestException e) {
302                 throw new MojoExecutionException("Error creating source archive: " + e.getMessage(), e);
303             }
304 
305             if (attach) {
306                 for (Artifact attachedArtifact : project.getAttachedArtifacts()) {
307                     if (isAlreadyAttached(attachedArtifact, project, getClassifier())) {
308                         getLog().error("We have duplicated artifacts attached.");
309                         throw new MojoExecutionException("Presumably you have configured maven-source-plugin "
310                                 + "to execute twice in your build. You have to configure a classifier "
311                                 + "for at least one of them.");
312                     }
313                 }
314                 projectHelper.attachArtifact(project, getType(), getClassifier(), outputFile);
315             } else {
316                 getLog().info("NOT adding java-sources to attached artifacts list.");
317             }
318         } else {
319             getLog().info("No sources in project. Archive not created.");
320         }
321     }
322 
323     private boolean isAlreadyAttached(Artifact artifact, MavenProject checkProject, String classifier) {
324         return artifact.getType().equals(getType())
325                 && artifact.getGroupId().equals(checkProject.getGroupId())
326                 && artifact.getArtifactId().equals(checkProject.getArtifactId())
327                 && artifact.getVersion().equals(checkProject.getVersion())
328                 && (artifact.getClassifier() != null ? artifact.getClassifier().equals(classifier) : false);
329     }
330 
331     /**
332      * @param p {@link MavenProject}
333      * @param archiver {@link Archiver}
334      * @throws MojoExecutionException in case of an error.
335      */
336     protected void archiveProjectContent(MavenProject p, Archiver archiver) throws MojoExecutionException {
337         if (includePom) {
338             try {
339                 archiver.addFile(p.getFile(), p.getFile().getName());
340             } catch (ArchiverException e) {
341                 throw new MojoExecutionException("Error adding POM file to target jar file.", e);
342             }
343         }
344 
345         for (String s : getSources(p)) {
346 
347             File sourceDirectory = new File(s);
348 
349             if (sourceDirectory.exists()) {
350                 addDirectory(archiver, sourceDirectory, getCombinedIncludes(null), getCombinedExcludes(null));
351             }
352         }
353 
354         // MAPI: this should be taken from the resources plugin
355         for (Resource resource : getResources(p)) {
356 
357             File sourceDirectory = new File(resource.getDirectory());
358 
359             if (!sourceDirectory.exists()) {
360                 continue;
361             }
362 
363             List<String> resourceIncludes = resource.getIncludes();
364 
365             String[] combinedIncludes = getCombinedIncludes(resourceIncludes);
366 
367             List<String> resourceExcludes = resource.getExcludes();
368 
369             String[] combinedExcludes = getCombinedExcludes(resourceExcludes);
370 
371             String targetPath = resource.getTargetPath();
372             if (targetPath != null) {
373                 if (!targetPath.trim().endsWith("/")) {
374                     targetPath += "/";
375                 }
376                 addDirectory(archiver, sourceDirectory, targetPath, combinedIncludes, combinedExcludes);
377             } else {
378                 addDirectory(archiver, sourceDirectory, combinedIncludes, combinedExcludes);
379             }
380         }
381     }
382 
383     /**
384      * @return {@link MavenArchiver}
385      * @throws MojoExecutionException in case of an error.
386      */
387     protected MavenArchiver createArchiver() throws MojoExecutionException {
388         MavenArchiver archiver = new MavenArchiver();
389         archiver.setArchiver(jarArchiver);
390         archiver.setCreatedBy("Maven Source Plugin", "org.apache.maven.plugins", "maven-source-plugin");
391         archiver.setBuildJdkSpecDefaultEntry(false);
392 
393         // configure for Reproducible Builds based on outputTimestamp value
394         archiver.configureReproducibleBuild(outputTimestamp);
395 
396         if (project.getBuild() != null) {
397             List<Resource> resources = project.getBuild().getResources();
398 
399             for (Resource r : resources) {
400                 if (r.getDirectory().endsWith("maven-shared-archive-resources")) {
401                     addDirectory(
402                             archiver.getArchiver(),
403                             new File(r.getDirectory()),
404                             getCombinedIncludes(null),
405                             getCombinedExcludes(null));
406                 }
407             }
408         }
409 
410         return archiver;
411     }
412 
413     /**
414      * @param archiver {@link Archiver}
415      * @param sourceDirectory {@link File}
416      * @param pIncludes The list of includes.
417      * @param pExcludes The list of excludes.
418      * @throws MojoExecutionException in case of an error.
419      */
420     protected void addDirectory(Archiver archiver, File sourceDirectory, String[] pIncludes, String[] pExcludes)
421             throws MojoExecutionException {
422         try {
423             getLog().debug("add directory " + sourceDirectory + " to archiver");
424             archiver.addFileSet(DefaultFileSet.fileSet(sourceDirectory).includeExclude(pIncludes, pExcludes));
425         } catch (ArchiverException e) {
426             throw new MojoExecutionException("Error adding directory to source archive.", e);
427         }
428     }
429 
430     /**
431      * @param archiver {@link Archiver}
432      * @param sourceDirectory {@link File}
433      * @param prefix The prefix.
434      * @param pIncludes the includes.
435      * @param pExcludes the excludes.
436      * @throws MojoExecutionException in case of an error.
437      */
438     protected void addDirectory(
439             Archiver archiver, File sourceDirectory, String prefix, String[] pIncludes, String[] pExcludes)
440             throws MojoExecutionException {
441         try {
442             getLog().debug("add directory " + sourceDirectory + " to archiver with prefix " + prefix);
443             archiver.addFileSet(
444                     DefaultFileSet.fileSet(sourceDirectory).prefixed(prefix).includeExclude(pIncludes, pExcludes));
445         } catch (ArchiverException e) {
446             throw new MojoExecutionException("Error adding directory to source archive.", e);
447         }
448     }
449 
450     /**
451      * @return The extension {@code .jar}
452      */
453     protected String getExtension() {
454         return ".jar";
455     }
456 
457     /**
458      * @param p {@link MavenProject}
459      * @return The execution projet.
460      */
461     protected MavenProject getProject(MavenProject p) {
462         if (p.getExecutionProject() != null) {
463             return p.getExecutionProject();
464         }
465 
466         return p;
467     }
468 
469     /**
470      * @return The type {@code java-source}
471      */
472     protected String getType() {
473         return "java-source";
474     }
475 
476     /**
477      * Combines the includes parameter and additional includes. Defaults to {@link #DEFAULT_INCLUDES} If the
478      * additionalIncludes parameter is null, it is not added to the combined includes.
479      *
480      * @param additionalIncludes The includes specified in the pom resources section
481      * @return The combined array of includes.
482      */
483     private String[] getCombinedIncludes(List<String> additionalIncludes) {
484         List<String> combinedIncludes = new ArrayList<>();
485 
486         if (includes != null && includes.length > 0) {
487             combinedIncludes.addAll(Arrays.asList(includes));
488         }
489 
490         if (additionalIncludes != null && !additionalIncludes.isEmpty()) {
491             combinedIncludes.addAll(additionalIncludes);
492         }
493 
494         // If there are no other includes, use the default.
495         if (combinedIncludes.isEmpty()) {
496             combinedIncludes.addAll(Arrays.asList(DEFAULT_INCLUDES));
497         }
498 
499         return combinedIncludes.toArray(new String[0]);
500     }
501 
502     /**
503      * Combines the user parameter {@link #excludes}, the default excludes from plexus FileUtils, and the contents of
504      * the parameter addionalExcludes.
505      *
506      * @param additionalExcludes Additional excludes to add to the array
507      * @return The combined list of excludes.
508      */
509     private String[] getCombinedExcludes(List<String> additionalExcludes) {
510         List<String> combinedExcludes = new ArrayList<>();
511 
512         if (useDefaultExcludes) {
513             combinedExcludes.addAll(FileUtils.getDefaultExcludesAsList());
514         }
515 
516         if (excludes != null && excludes.length > 0) {
517             combinedExcludes.addAll(Arrays.asList(excludes));
518         }
519 
520         if (additionalExcludes != null && !additionalExcludes.isEmpty()) {
521             combinedExcludes.addAll(additionalExcludes);
522         }
523 
524         if (combinedExcludes.isEmpty()) {
525             combinedExcludes.addAll(Arrays.asList(DEFAULT_EXCLUDES));
526         }
527 
528         return combinedExcludes.toArray(new String[0]);
529     }
530 
531     /**
532      * @return The current project.
533      */
534     protected MavenProject getProject() {
535         return project;
536     }
537 
538     /**
539      * @param project {@link MavenProject}
540      */
541     protected void setProject(MavenProject project) {
542         this.project = project;
543     }
544 }