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.war;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.net.MalformedURLException;
24  import java.net.URL;
25  import java.net.URLClassLoader;
26  import java.util.Arrays;
27  import java.util.List;
28  
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.plugin.MojoExecutionException;
33  import org.apache.maven.plugin.MojoFailureException;
34  import org.apache.maven.plugins.annotations.Component;
35  import org.apache.maven.plugins.annotations.LifecyclePhase;
36  import org.apache.maven.plugins.annotations.Mojo;
37  import org.apache.maven.plugins.annotations.Parameter;
38  import org.apache.maven.plugins.annotations.ResolutionScope;
39  import org.apache.maven.plugins.war.util.ClassesPackager;
40  import org.apache.maven.project.MavenProjectHelper;
41  import org.codehaus.plexus.archiver.Archiver;
42  import org.codehaus.plexus.archiver.ArchiverException;
43  import org.codehaus.plexus.archiver.jar.ManifestException;
44  import org.codehaus.plexus.archiver.war.WarArchiver;
45  import org.codehaus.plexus.util.FileUtils;
46  import org.codehaus.plexus.util.StringUtils;
47  
48  /**
49   * Build a WAR file.
50   *
51   * @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
52   */
53  @Mojo(
54          name = "war",
55          defaultPhase = LifecyclePhase.PACKAGE,
56          threadSafe = true,
57          requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
58  public class WarMojo extends AbstractWarMojo {
59      /**
60       * The directory for the generated WAR.
61       */
62      @Parameter(defaultValue = "${project.build.directory}", required = true)
63      private String outputDirectory;
64  
65      /**
66       * The name of the generated WAR.
67       */
68      @Parameter(defaultValue = "${project.build.finalName}", required = true, readonly = true)
69      private String warName;
70  
71      /**
72       * Classifier to add to the generated WAR. If given, the artifact will be an attachment instead. The classifier will
73       * not be applied to the JAR file of the project - only to the WAR file.
74       */
75      @Parameter
76      private String classifier;
77  
78      /**
79       * The comma separated list of tokens to exclude from the WAR before packaging. This option may be used to implement
80       * the skinny WAR use case. Note that you can use the Java Regular Expressions engine to include and exclude
81       * specific pattern using the expression %regex[]. Hint: read the about (?!Pattern).
82       *
83       * @since 2.1-alpha-2
84       */
85      @Parameter
86      private String packagingExcludes;
87  
88      /**
89       * The comma separated list of tokens to include in the WAR before packaging. By default everything is included.
90       * This option may be used to implement the skinny WAR use case. Note that you can use the Java Regular Expressions
91       * engine to include and exclude specific pattern using the expression %regex[].
92       *
93       * @since 2.1-beta-1
94       */
95      @Parameter
96      private String packagingIncludes;
97  
98      /**
99       * The WAR archiver.
100      */
101     @Component(role = Archiver.class, hint = "war")
102     private WarArchiver warArchiver;
103 
104     /**
105      */
106     @Component
107     private MavenProjectHelper projectHelper;
108 
109     /**
110      * Whether this is the main artifact being built. Set to <code>false</code> if you don't want to install or deploy
111      * it to the local repository instead of the default one in an execution.
112      */
113     @Parameter(defaultValue = "true")
114     private boolean primaryArtifact;
115 
116     /**
117      * Whether classes (that is the content of the WEB-INF/classes directory) should be attached to the project as an
118      * additional artifact.
119      * <p>
120      * By default the classifier for the additional artifact is 'classes'. You can change it with the
121      * <code><![CDATA[<classesClassifier>someclassifier</classesClassifier>]]></code> parameter.
122      * </p>
123      * <p>
124      * If this parameter true, another project can depend on the classes by writing something like:
125      *
126      * <pre>
127      * <![CDATA[<dependency>
128      *   <groupId>myGroup</groupId>
129      *   <artifactId>myArtifact</artifactId>
130      *   <version>myVersion</myVersion>
131      *   <classifier>classes</classifier>
132      * </dependency>]]>
133      * </pre>
134      * </p>
135      *
136      * @since 2.1-alpha-2
137      */
138     @Parameter(defaultValue = "false")
139     private boolean attachClasses;
140 
141     /**
142      * The classifier to use for the attached classes artifact.
143      *
144      * @since 2.1-alpha-2
145      */
146     @Parameter(defaultValue = "classes")
147     private String classesClassifier;
148 
149     /**
150      * You can skip the execution of the plugin if you need to. Its use is NOT RECOMMENDED, but quite convenient on
151      * occasion.
152      *
153      * @since 3.0.0
154      */
155     @Parameter(property = "maven.war.skip", defaultValue = "false")
156     private boolean skip;
157 
158     // ----------------------------------------------------------------------
159     // Implementation
160     // ----------------------------------------------------------------------
161 
162     /**
163      * Executes the WarMojo on the current project.
164      *
165      * @throws MojoExecutionException if an error occurred while building the webapp
166      * @throws MojoFailureException if an error.
167      */
168     @Override
169     public void execute() throws MojoExecutionException, MojoFailureException {
170 
171         if (isSkip()) {
172             getLog().info("Skipping the execution.");
173             return;
174         }
175 
176         File warFile = getTargetWarFile();
177 
178         try {
179             performPackaging(warFile);
180         } catch (DependencyResolutionRequiredException | ArchiverException e) {
181             throw new MojoExecutionException("Error assembling WAR: " + e.getMessage(), e);
182         } catch (ManifestException | IOException e) {
183             throw new MojoExecutionException("Error assembling WAR", e);
184         }
185     }
186 
187     /**
188      * Generates the webapp according to the {@code mode} attribute.
189      *
190      * @param warFile the target WAR file
191      * @throws IOException if an error occurred while copying files
192      * @throws ArchiverException if the archive could not be created
193      * @throws ManifestException if the manifest could not be created
194      * @throws DependencyResolutionRequiredException if an error occurred while resolving the dependencies
195      * @throws MojoExecutionException if the execution failed
196      * @throws MojoFailureException if a fatal exception occurred
197      */
198     private void performPackaging(File warFile)
199             throws IOException, ManifestException, DependencyResolutionRequiredException, MojoExecutionException,
200                     MojoFailureException {
201         getLog().info("Packaging webapp");
202 
203         buildExplodedWebapp(getWebappDirectory());
204 
205         MavenArchiver archiver = new MavenArchiver();
206 
207         archiver.setArchiver(warArchiver);
208 
209         archiver.setCreatedBy("Maven WAR Plugin", "org.apache.maven.plugins", "maven-war-plugin");
210 
211         archiver.setOutputFile(warFile);
212 
213         // configure for Reproducible Builds based on outputTimestamp value
214         archiver.configureReproducible(outputTimestamp);
215 
216         getLog().debug("Excluding " + Arrays.asList(getPackagingExcludes()) + " from the generated webapp archive.");
217         getLog().debug("Including " + Arrays.asList(getPackagingIncludes()) + " in the generated webapp archive.");
218 
219         warArchiver.addDirectory(getWebappDirectory(), getPackagingIncludes(), getPackagingExcludes());
220 
221         final File webXmlFile = new File(getWebappDirectory(), "WEB-INF/web.xml");
222         if (webXmlFile.exists()) {
223             warArchiver.setWebxml(webXmlFile);
224         }
225 
226         warArchiver.setRecompressAddedZips(isRecompressZippedFiles());
227 
228         warArchiver.setIncludeEmptyDirs(isIncludeEmptyDirectories());
229 
230         if (Boolean.FALSE.equals(failOnMissingWebXml)
231                 || (failOnMissingWebXml == null && isProjectUsingAtLeastServlet30())) {
232             getLog().debug("Build won't fail if web.xml file is missing.");
233             warArchiver.setExpectWebXml(false);
234         }
235 
236         // create archive
237         archiver.createArchive(getSession(), getProject(), getArchive());
238 
239         // create the classes to be attached if necessary
240         if (isAttachClasses()) {
241             if (isArchiveClasses() && getJarArchiver().getDestFile() != null) {
242                 // special handling in case of archived classes: MWAR-240
243                 File targetClassesFile = getTargetClassesFile();
244                 FileUtils.copyFile(getJarArchiver().getDestFile(), targetClassesFile);
245                 projectHelper.attachArtifact(getProject(), "jar", getClassesClassifier(), targetClassesFile);
246             } else {
247                 ClassesPackager packager = new ClassesPackager();
248                 final File classesDirectory = packager.getClassesDirectory(getWebappDirectory());
249                 if (classesDirectory.exists()) {
250                     getLog().info("Packaging classes");
251                     packager.packageClasses(
252                             classesDirectory,
253                             getTargetClassesFile(),
254                             getJarArchiver(),
255                             getSession(),
256                             getProject(),
257                             getArchive(),
258                             outputTimestamp);
259                     projectHelper.attachArtifact(getProject(), "jar", getClassesClassifier(), getTargetClassesFile());
260                 }
261             }
262         }
263 
264         if (this.classifier != null) {
265             projectHelper.attachArtifact(getProject(), "war", this.classifier, warFile);
266         } else {
267             Artifact artifact = getProject().getArtifact();
268             if (primaryArtifact) {
269                 artifact.setFile(warFile);
270             } else if (artifact.getFile() == null || artifact.getFile().isDirectory()) {
271                 artifact.setFile(warFile);
272             }
273         }
274     }
275 
276     /**
277      * Determines if the current Maven project being built uses the Servlet 3.0 API (JSR 315)
278      * or Jakarta Servlet API.
279      * If it does then the <code>web.xml</code> file can be omitted.
280      * <p>
281      * This is done by checking if the interface <code>javax.servlet.annotation.WebServlet</code>
282      * or <code>jakarta.servlet.annotation.WebServlet</code> is in the compile-time
283      * dependencies (which includes provided dependencies) of the Maven project.
284      *
285      * @return <code>true</code> if the project being built depends on Servlet 3.0 API or Jakarta Servlet API,
286      *         <code>false</code> otherwise.
287      * @throws DependencyResolutionRequiredException if the compile elements can't be resolved.
288      * @throws MalformedURLException if the path to a dependency file can't be transformed to a URL.
289      */
290     private boolean isProjectUsingAtLeastServlet30()
291             throws DependencyResolutionRequiredException, MalformedURLException {
292         List<String> classpathElements = getProject().getCompileClasspathElements();
293         URL[] urls = new URL[classpathElements.size()];
294         for (int i = 0; i < urls.length; i++) {
295             urls[i] = new File(classpathElements.get(i)).toURI().toURL();
296         }
297         URLClassLoader loader = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader());
298         try {
299             return hasWebServletAnnotationClassInClasspath(loader);
300         } finally {
301             try {
302                 loader.close();
303             } catch (IOException ex) {
304                 // ignore
305             }
306         }
307     }
308 
309     private static boolean hasWebServletAnnotationClassInClasspath(ClassLoader loader) {
310         return hasClassInClasspath(loader, "javax.servlet.annotation.WebServlet")
311                 || hasClassInClasspath(loader, "jakarta.servlet.annotation.WebServlet");
312     }
313 
314     private static boolean hasClassInClasspath(ClassLoader loader, String clazz) {
315         try {
316             Class.forName(clazz, false, loader);
317             return true;
318         } catch (ClassNotFoundException e) {
319             return false;
320         }
321     }
322 
323     /**
324      * @param basedir The basedir
325      * @param finalName The finalName
326      * @param classifier The classifier.
327      * @param type The type.
328      * @return {@link File}
329      */
330     protected static File getTargetFile(File basedir, String finalName, String classifier, String type) {
331         if (classifier == null) {
332             classifier = "";
333         } else if (classifier.trim().length() > 0 && !classifier.startsWith("-")) {
334             classifier = "-" + classifier;
335         }
336 
337         return new File(basedir, finalName + classifier + "." + type);
338     }
339 
340     /**
341      * @return The war {@link File}
342      */
343     protected File getTargetWarFile() {
344         return getTargetFile(new File(getOutputDirectory()), getWarName(), getClassifier(), "war");
345     }
346 
347     /**
348      * @return The target class {@link File}
349      */
350     protected File getTargetClassesFile() {
351         return getTargetFile(new File(getOutputDirectory()), getWarName(), getClassesClassifier(), "jar");
352     }
353 
354     // Getters and Setters
355 
356     /**
357      * @return {@link #classifier}
358      */
359     public String getClassifier() {
360         return classifier;
361     }
362 
363     /**
364      * @param classifier {@link #classifier}
365      */
366     public void setClassifier(String classifier) {
367         this.classifier = classifier;
368     }
369 
370     /**
371      * @return The package excludes.
372      */
373     public String[] getPackagingExcludes() {
374         if (packagingExcludes == null || packagingExcludes.isEmpty()) {
375             return new String[0];
376         } else {
377             return StringUtils.split(packagingExcludes, ",");
378         }
379     }
380 
381     /**
382      * @param packagingExcludes {@link #packagingExcludes}
383      */
384     public void setPackagingExcludes(String packagingExcludes) {
385         this.packagingExcludes = packagingExcludes;
386     }
387 
388     /**
389      * @return The packaging includes.
390      */
391     public String[] getPackagingIncludes() {
392         if (packagingIncludes == null || packagingIncludes.isEmpty()) {
393             return new String[] {"**"};
394         } else {
395             return StringUtils.split(packagingIncludes, ",");
396         }
397     }
398 
399     /**
400      * @param packagingIncludes {@link #packagingIncludes}
401      */
402     public void setPackagingIncludes(String packagingIncludes) {
403         this.packagingIncludes = packagingIncludes;
404     }
405 
406     /**
407      * @return {@link #outputDirectory}
408      */
409     public String getOutputDirectory() {
410         return outputDirectory;
411     }
412 
413     /**
414      * @param outputDirectory {@link #outputDirectory}
415      */
416     public void setOutputDirectory(String outputDirectory) {
417         this.outputDirectory = outputDirectory;
418     }
419 
420     /**
421      * @return {@link #warName}
422      */
423     public String getWarName() {
424         return warName;
425     }
426 
427     /**
428      * @param warName {@link #warName}
429      */
430     public void setWarName(String warName) {
431         this.warName = warName;
432     }
433 
434     /**
435      * @return {@link #warArchiver}
436      */
437     public WarArchiver getWarArchiver() {
438         return warArchiver;
439     }
440 
441     /**
442      * @param warArchiver {@link #warArchiver}
443      */
444     public void setWarArchiver(WarArchiver warArchiver) {
445         this.warArchiver = warArchiver;
446     }
447 
448     /**
449      * @return {@link #projectHelper}
450      */
451     public MavenProjectHelper getProjectHelper() {
452         return projectHelper;
453     }
454 
455     /**
456      * @param projectHelper {@link #projectHelper}
457      */
458     public void setProjectHelper(MavenProjectHelper projectHelper) {
459         this.projectHelper = projectHelper;
460     }
461 
462     /**
463      * @return {@link #primaryArtifact}
464      */
465     public boolean isPrimaryArtifact() {
466         return primaryArtifact;
467     }
468 
469     /**
470      * @param primaryArtifact {@link #primaryArtifact}
471      */
472     public void setPrimaryArtifact(boolean primaryArtifact) {
473         this.primaryArtifact = primaryArtifact;
474     }
475 
476     /**
477      * @return {@link #attachClasses}
478      */
479     public boolean isAttachClasses() {
480         return attachClasses;
481     }
482 
483     /**
484      * @param attachClasses {@link #attachClasses}
485      */
486     public void setAttachClasses(boolean attachClasses) {
487         this.attachClasses = attachClasses;
488     }
489 
490     /**
491      * @return {@link #classesClassifier}
492      */
493     public String getClassesClassifier() {
494         return classesClassifier;
495     }
496 
497     /**
498      * @param classesClassifier {@link #classesClassifier}
499      */
500     public void setClassesClassifier(String classesClassifier) {
501         this.classesClassifier = classesClassifier;
502     }
503 
504     /**
505      * @return {@link #failOnMissingWebXml}
506      */
507     public boolean isFailOnMissingWebXml() {
508         return failOnMissingWebXml;
509     }
510 
511     /**
512      * @param failOnMissingWebXml {@link #failOnMissingWebXml}
513      */
514     public void setFailOnMissingWebXml(boolean failOnMissingWebXml) {
515         this.failOnMissingWebXml = failOnMissingWebXml;
516     }
517 
518     /**
519      * Skip the mojo run
520      * @return {@link #skip}
521      */
522     public boolean isSkip() {
523         return skip;
524     }
525 }