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: **/*~, **/#*#, **/.#*, **/%*%, **/._*</li>
148 * <li>CVS: **/CVS, **/CVS/**, **/.cvsignore</li>
149 * <li>RCS: **/RCS, **/RCS/**</li>
150 * <li>SCCS: **/SCCS, **/SCCS/**</li>
151 * <li>VSSercer: **/vssver.scc</li>
152 * <li>MKS: **/project.pj</li>
153 * <li>SVN: **/.svn, **/.svn/**</li>
154 * <li>GNU: **/.arch-ids, **/.arch-ids/**</li>
155 * <li>Bazaar: **/.bzr, **/.bzr/**</li>
156 * <li>SurroundSCM: **/.MySCMServerInfo</li>
157 * <li>Mac: **/.DS_Store</li>
158 * <li>Serena Dimension: **/.metadata, **/.metadata/**</li>
159 * <li>Mercurial: **/.hg, **/.hg/**</li>
160 * <li>Git: **/.git, **/.git/**</li>
161 * <li>Bitkeeper: **/BitKeeper, **/BitKeeper/**, **/ChangeSet,
162 * **/ChangeSet/**</li>
163 * <li>Darcs: **/_darcs, **/_darcs/**, **/.darcsrepo,
164 * **/.darcsrepo/****/-darcs-backup*, **/.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 }