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.jar;
20  
21  import java.io.File;
22  import java.nio.file.FileSystems;
23  import java.util.Arrays;
24  import java.util.Map;
25  import java.util.Optional;
26  
27  import org.apache.maven.archiver.MavenArchiveConfiguration;
28  import org.apache.maven.archiver.MavenArchiver;
29  import org.apache.maven.execution.MavenSession;
30  import org.apache.maven.plugin.AbstractMojo;
31  import org.apache.maven.plugin.MojoExecutionException;
32  import org.apache.maven.plugins.annotations.Component;
33  import org.apache.maven.plugins.annotations.Parameter;
34  import org.apache.maven.project.MavenProject;
35  import org.apache.maven.project.MavenProjectHelper;
36  import org.apache.maven.shared.model.fileset.FileSet;
37  import org.apache.maven.shared.model.fileset.util.FileSetManager;
38  import org.apache.maven.toolchain.ToolchainManager;
39  import org.codehaus.plexus.archiver.Archiver;
40  import org.codehaus.plexus.archiver.jar.JarArchiver;
41  import org.codehaus.plexus.archiver.util.DefaultFileSet;
42  
43  /**
44   * Base class for creating a jar from project classes.
45   *
46   * @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
47   * @version $Id$
48   */
49  public abstract class AbstractJarMojo extends AbstractMojo {
50  
51      private static final String[] DEFAULT_EXCLUDES = new String[] {"**/package.html"};
52  
53      private static final String[] DEFAULT_INCLUDES = new String[] {"**/**"};
54  
55      private static final String MODULE_DESCRIPTOR_FILE_NAME = "module-info.class";
56  
57      private static final String SEPARATOR = FileSystems.getDefault().getSeparator();
58  
59      @Component
60      private ToolchainsJdkSpecification toolchainsJdkSpecification;
61  
62      @Component
63      private ToolchainManager toolchainManager;
64  
65      /**
66       * List of files to include. Specified as fileset patterns which are relative to the input directory whose contents
67       * is being packaged into the JAR.
68       */
69      @Parameter
70      private String[] includes;
71  
72      /**
73       * List of files to exclude. Specified as fileset patterns which are relative to the input directory whose contents
74       * is being packaged into the JAR.
75       */
76      @Parameter
77      private String[] excludes;
78  
79      /**
80       * Directory containing the generated JAR.
81       */
82      @Parameter(defaultValue = "${project.build.directory}", required = true)
83      private File outputDirectory;
84  
85      /**
86       * Name of the generated JAR.
87       */
88      @Parameter(defaultValue = "${project.build.finalName}", readonly = true)
89      private String finalName;
90  
91      /**
92       * The Jar archiver.
93       */
94      @Component
95      private Map<String, Archiver> archivers;
96  
97      /**
98       * The {@link MavenProject}.
99       */
100     @Parameter(defaultValue = "${project}", readonly = true, required = true)
101     private MavenProject project;
102 
103     /**
104      * The {@link MavenSession}.
105      */
106     @Parameter(defaultValue = "${session}", readonly = true, required = true)
107     private MavenSession session;
108 
109     /**
110      * The archive configuration to use. See <a href="http://maven.apache.org/shared/maven-archiver/index.html">Maven
111      * Archiver Reference</a>.
112      */
113     @Parameter
114     private MavenArchiveConfiguration archive = new MavenArchiveConfiguration();
115 
116     /**
117      * Using this property will fail your build cause it has been removed from the plugin configuration. See the
118      * <a href="https://maven.apache.org/plugins/maven-jar-plugin/">Major Version Upgrade to version 3.0.0</a> for the
119      * plugin.
120      *
121      * @deprecated For version 3.0.0 this parameter is only defined here to break the build if you use it!
122      */
123     @Parameter(property = "jar.useDefaultManifestFile", defaultValue = "false")
124     @Deprecated
125     private boolean useDefaultManifestFile;
126 
127     /**
128      *
129      */
130     @Component
131     private MavenProjectHelper projectHelper;
132 
133     /**
134      * Require the jar plugin to build a new JAR even if none of the contents appear to have changed. By default, this
135      * plugin looks to see if the output jar exists and inputs have not changed. If these conditions are true, the
136      * plugin skips creation of the jar. This does not work when other plugins, like the maven-shade-plugin, are
137      * configured to post-process the jar. This plugin can not detect the post-processing, and so leaves the
138      * post-processed jar in place. This can lead to failures when those plugins do not expect to find their own output
139      * as an input. Set this parameter to <tt>true</tt> to avoid these problems by forcing this plugin to recreate the
140      * jar every time.<br/>
141      * Starting with <b>3.0.0</b> the property has been renamed from <code>jar.forceCreation</code> to
142      * <code>maven.jar.forceCreation</code>.
143      */
144     @Parameter(property = "maven.jar.forceCreation", defaultValue = "false")
145     private boolean forceCreation;
146 
147     /**
148      * Skip creating empty archives.
149      */
150     @Parameter(defaultValue = "false")
151     private boolean skipIfEmpty;
152 
153     /**
154      * Timestamp for reproducible output archive entries, either formatted as ISO 8601 extended offset date-time
155      * (e.g. in UTC such as '2011-12-03T10:15:30Z' or with an offset '2019-10-05T20:37:42+06:00'),
156      * or as an int representing seconds since the epoch
157      * (like <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
158      *
159      * @since 3.2.0
160      */
161     @Parameter(defaultValue = "${project.build.outputTimestamp}")
162     private String outputTimestamp;
163 
164     /**
165      * If the JAR contains the {@code META-INF/versions} directory it will be detected as a multi-release JAR file
166      * ("MRJAR"), adding the {@code Multi-Release: true} attribute to the main section of the JAR MANIFEST.MF.
167      *
168      * @since 3.4.0
169      */
170     @Parameter(property = "maven.jar.detectMultiReleaseJar", defaultValue = "true")
171     private boolean detectMultiReleaseJar;
172 
173     /**
174      * If set to {@code false}, the files and directories that by default are excluded from the resulting archive,
175      * like {@code .gitignore}, {@code .cvsignore} etc. will be included.
176      * This means all files like the following will be included.
177      * <ul>
178      * <li>Misc: &#42;&#42;/&#42;~, &#42;&#42;/#&#42;#, &#42;&#42;/.#&#42;, &#42;&#42;/%&#42;%, &#42;&#42;/._&#42;</li>
179      * <li>CVS: &#42;&#42;/CVS, &#42;&#42;/CVS/&#42;&#42;, &#42;&#42;/.cvsignore</li>
180      * <li>RCS: &#42;&#42;/RCS, &#42;&#42;/RCS/&#42;&#42;</li>
181      * <li>SCCS: &#42;&#42;/SCCS, &#42;&#42;/SCCS/&#42;&#42;</li>
182      * <li>VSSercer: &#42;&#42;/vssver.scc</li>
183      * <li>MKS: &#42;&#42;/project.pj</li>
184      * <li>SVN: &#42;&#42;/.svn, &#42;&#42;/.svn/&#42;&#42;</li>
185      * <li>GNU: &#42;&#42;/.arch-ids, &#42;&#42;/.arch-ids/&#42;&#42;</li>
186      * <li>Bazaar: &#42;&#42;/.bzr, &#42;&#42;/.bzr/&#42;&#42;</li>
187      * <li>SurroundSCM: &#42;&#42;/.MySCMServerInfo</li>
188      * <li>Mac: &#42;&#42;/.DS_Store</li>
189      * <li>Serena Dimension: &#42;&#42;/.metadata, &#42;&#42;/.metadata/&#42;&#42;</li>
190      * <li>Mercurial: &#42;&#42;/.hg, &#42;&#42;/.hg/&#42;&#42;</li>
191      * <li>Git: &#42;&#42;/.git, &#42;&#42;/.git/&#42;&#42;</li>
192      * <li>Bitkeeper: &#42;&#42;/BitKeeper, &#42;&#42;/BitKeeper/&#42;&#42;, &#42;&#42;/ChangeSet,
193      * &#42;&#42;/ChangeSet/&#42;&#42;</li>
194      * <li>Darcs: &#42;&#42;/_darcs, &#42;&#42;/_darcs/&#42;&#42;, &#42;&#42;/.darcsrepo,
195      * &#42;&#42;/.darcsrepo/&#42;&#42;&#42;&#42;/-darcs-backup&#42;, &#42;&#42;/.darcs-temp-mail
196      * </ul>
197      *
198      * @see <a href="https://codehaus-plexus.github.io/plexus-utils/apidocs/org/codehaus/plexus/util/AbstractScanner.html#DEFAULTEXCLUDES">DEFAULTEXCLUDES</a>
199      *
200      * @since 3.4.0
201      */
202     @Parameter(defaultValue = "true")
203     private boolean addDefaultExcludes;
204 
205     /**
206      * Return the specific output directory to serve as the root for the archive.
207      * @return get classes directory.
208      */
209     protected abstract File getClassesDirectory();
210 
211     /**
212      * Return the {@link #project MavenProject}
213      *
214      * @return the MavenProject.
215      */
216     protected final MavenProject getProject() {
217         return project;
218     }
219 
220     /**
221      * Overload this to produce a jar with another classifier, for example a test-jar.
222      * @return get the classifier.
223      */
224     protected abstract String getClassifier();
225 
226     /**
227      * Overload this to produce a test-jar, for example.
228      * @return return the type.
229      */
230     protected abstract String getType();
231 
232     /**
233      * Returns the Jar file to generate, based on an optional classifier.
234      *
235      * @param basedir the output directory
236      * @param resultFinalName the name of the ear file
237      * @param classifier an optional classifier
238      * @return the file to generate
239      */
240     protected File getJarFile(File basedir, String resultFinalName, String classifier) {
241         if (basedir == null) {
242             throw new IllegalArgumentException("basedir is not allowed to be null");
243         }
244         if (resultFinalName == null) {
245             throw new IllegalArgumentException("finalName is not allowed to be null");
246         }
247 
248         String fileName = resultFinalName + (hasClassifier() ? "-" + classifier : "") + ".jar";
249 
250         return new File(basedir, fileName);
251     }
252 
253     /**
254      * Generates the JAR.
255      * @return The instance of File for the created archive file.
256      * @throws MojoExecutionException in case of an error.
257      */
258     public File createArchive() throws MojoExecutionException {
259         File jarFile = getJarFile(outputDirectory, finalName, getClassifier());
260 
261         FileSetManager fileSetManager = new FileSetManager();
262         FileSet jarContentFileSet = new FileSet();
263         jarContentFileSet.setDirectory(getClassesDirectory().getAbsolutePath());
264         jarContentFileSet.setIncludes(Arrays.asList(getIncludes()));
265         jarContentFileSet.setExcludes(Arrays.asList(getExcludes()));
266 
267         String[] includedFiles = fileSetManager.getIncludedFiles(jarContentFileSet);
268 
269         if (detectMultiReleaseJar
270                 && Arrays.stream(includedFiles)
271                         .anyMatch(p -> p.startsWith("META-INF" + SEPARATOR + "versions" + SEPARATOR))) {
272             getLog().debug("Adding 'Multi-Release: true' manifest entry.");
273             archive.addManifestEntry("Multi-Release", "true");
274         }
275 
276         // May give false positives if the files is named as module descriptor
277         // but is not in the root of the archive or in the versioned area
278         // (and hence not actually a module descriptor).
279         // That is fine since the modular Jar archiver will gracefully
280         // handle such case.
281         // And also such case is unlikely to happen as file ending
282         // with "module-info.class" is unlikely to be included in Jar file
283         // unless it is a module descriptor.
284         boolean containsModuleDescriptor =
285                 Arrays.stream(includedFiles).anyMatch(p -> p.endsWith(MODULE_DESCRIPTOR_FILE_NAME));
286 
287         String archiverName = containsModuleDescriptor ? "mjar" : "jar";
288 
289         MavenArchiver archiver = new MavenArchiver();
290         archiver.setCreatedBy("Maven JAR Plugin", "org.apache.maven.plugins", "maven-jar-plugin");
291         archiver.setArchiver((JarArchiver) archivers.get(archiverName));
292         archiver.setOutputFile(jarFile);
293 
294         Optional.ofNullable(toolchainManager.getToolchainFromBuildContext("jdk", session))
295                 .ifPresent(toolchain -> toolchainsJdkSpecification
296                         .getJDKSpecification(toolchain)
297                         .ifPresent(jdkSpec -> {
298                             archive.addManifestEntry("Build-Jdk-Spec", jdkSpec);
299                             archive.addManifestEntry(
300                                     "Build-Tool-Jdk-Spec", System.getProperty("java.specification.version"));
301                             archiver.setBuildJdkSpecDefaultEntry(false);
302                             getLog().info("Set Build-Jdk-Spec based on toolchain in maven-jar-plugin " + toolchain);
303                         }));
304 
305         // configure for Reproducible Builds based on outputTimestamp value
306         archiver.configureReproducibleBuild(outputTimestamp);
307 
308         archive.setForced(forceCreation);
309 
310         try {
311             File contentDirectory = getClassesDirectory();
312             if (!contentDirectory.exists()) {
313                 if (!forceCreation) {
314                     getLog().warn("JAR will be empty - no content was marked for inclusion!");
315                 }
316             } else {
317                 archiver.getArchiver().addFileSet(getFileSet(contentDirectory));
318             }
319 
320             archiver.createArchive(session, project, archive);
321 
322             return jarFile;
323         } catch (Exception e) {
324             // TODO: improve error handling
325             throw new MojoExecutionException("Error assembling JAR", e);
326         }
327     }
328 
329     /**
330      * Generates the JAR.
331      * @throws MojoExecutionException in case of an error.
332      */
333     @Override
334     public void execute() throws MojoExecutionException {
335         if (useDefaultManifestFile) {
336             throw new MojoExecutionException("You are using 'useDefaultManifestFile' which has been removed"
337                     + " from the maven-jar-plugin. "
338                     + "Please see the >>Major Version Upgrade to version 3.0.0<< on the plugin site.");
339         }
340 
341         if (skipIfEmpty
342                 && (!getClassesDirectory().exists() || getClassesDirectory().list().length < 1)) {
343             getLog().info("Skipping packaging of the " + getType());
344         } else {
345             File jarFile = createArchive();
346 
347             if (hasClassifier()) {
348                 projectHelper.attachArtifact(getProject(), getType(), getClassifier(), jarFile);
349             } else {
350                 if (projectHasAlreadySetAnArtifact()) {
351                     throw new MojoExecutionException("You have to use a classifier "
352                             + "to attach supplemental artifacts to the project instead of replacing them.");
353                 }
354                 getProject().getArtifact().setFile(jarFile);
355             }
356         }
357     }
358 
359     private boolean projectHasAlreadySetAnArtifact() {
360         if (getProject().getArtifact().getFile() == null) {
361             return false;
362         }
363 
364         return getProject().getArtifact().getFile().isFile();
365     }
366 
367     /**
368      * Return {@code true} in case where the classifier is not {@code null} and contains something else than white spaces.
369      *
370      * @return {@code true} if the classifier is set.
371      */
372     protected boolean hasClassifier() {
373         return getClassifier() != null && !getClassifier().trim().isEmpty();
374     }
375 
376     private String[] getIncludes() {
377         if (includes != null && includes.length > 0) {
378             return includes;
379         }
380         return DEFAULT_INCLUDES;
381     }
382 
383     private String[] getExcludes() {
384         if (excludes != null && excludes.length > 0) {
385             return excludes;
386         }
387         return DEFAULT_EXCLUDES;
388     }
389 
390     private org.codehaus.plexus.archiver.FileSet getFileSet(File contentDirectory) {
391         DefaultFileSet fileSet = DefaultFileSet.fileSet(contentDirectory)
392                 .prefixed("")
393                 .includeExclude(getIncludes(), getExcludes())
394                 .includeEmptyDirs(true);
395 
396         fileSet.setUsingDefaultExcludes(addDefaultExcludes);
397         return fileSet;
398     }
399 }