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