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: **/*~, **/#*#, **/.#*, **/%*%, **/._*</li>
179 * <li>CVS: **/CVS, **/CVS/**, **/.cvsignore</li>
180 * <li>RCS: **/RCS, **/RCS/**</li>
181 * <li>SCCS: **/SCCS, **/SCCS/**</li>
182 * <li>VSSercer: **/vssver.scc</li>
183 * <li>MKS: **/project.pj</li>
184 * <li>SVN: **/.svn, **/.svn/**</li>
185 * <li>GNU: **/.arch-ids, **/.arch-ids/**</li>
186 * <li>Bazaar: **/.bzr, **/.bzr/**</li>
187 * <li>SurroundSCM: **/.MySCMServerInfo</li>
188 * <li>Mac: **/.DS_Store</li>
189 * <li>Serena Dimension: **/.metadata, **/.metadata/**</li>
190 * <li>Mercurial: **/.hg, **/.hg/**</li>
191 * <li>Git: **/.git, **/.git/**</li>
192 * <li>Bitkeeper: **/BitKeeper, **/BitKeeper/**, **/ChangeSet,
193 * **/ChangeSet/**</li>
194 * <li>Darcs: **/_darcs, **/_darcs/**, **/.darcsrepo,
195 * **/.darcsrepo/****/-darcs-backup*, **/.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 }