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