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.ejb;
20  
21  import javax.inject.Inject;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.util.Arrays;
26  import java.util.Collections;
27  import java.util.List;
28  
29  import org.apache.commons.io.input.XmlStreamReader;
30  import org.apache.maven.archiver.MavenArchiveConfiguration;
31  import org.apache.maven.archiver.MavenArchiver;
32  import org.apache.maven.artifact.DependencyResolutionRequiredException;
33  import org.apache.maven.execution.MavenSession;
34  import org.apache.maven.plugin.AbstractMojo;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugins.annotations.LifecyclePhase;
37  import org.apache.maven.plugins.annotations.Mojo;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.apache.maven.plugins.annotations.ResolutionScope;
40  import org.apache.maven.project.MavenProject;
41  import org.apache.maven.project.MavenProjectHelper;
42  import org.apache.maven.shared.filtering.FilterWrapper;
43  import org.apache.maven.shared.filtering.MavenFileFilter;
44  import org.apache.maven.shared.filtering.MavenFilteringException;
45  import org.apache.maven.shared.filtering.MavenResourcesExecution;
46  import org.codehaus.plexus.archiver.ArchiverException;
47  import org.codehaus.plexus.archiver.jar.JarArchiver;
48  import org.codehaus.plexus.archiver.jar.ManifestException;
49  import org.codehaus.plexus.util.FileUtils;
50  
51  /**
52   * Build an EJB (and optional client) from the current project.
53   *
54   * @author <a href="evenisse@apache.org">Emmanuel Venisse</a>
55   * @version $Id$
56   */
57  @Mojo(
58          name = "ejb",
59          requiresDependencyResolution = ResolutionScope.RUNTIME,
60          threadSafe = true,
61          defaultPhase = LifecyclePhase.PACKAGE)
62  public class EjbMojo extends AbstractMojo {
63      private static final List<String> DEFAULT_INCLUDES_LIST = Collections.unmodifiableList(Arrays.asList("**/**"));
64  
65      private static final String EJB_TYPE = "ejb";
66  
67      private static final String EJB_CLIENT_TYPE = "ejb-client";
68  
69      // @formatter:off
70      private static final List<String> DEFAULT_CLIENT_EXCLUDES_LIST = Collections.unmodifiableList(
71              Arrays.asList("**/*Bean.class", "**/*CMP.class", "**/*Session.class", "**/package.html"));
72      // @formatter:on
73  
74      /**
75       * Default value for {@link #clientClassifier}
76       */
77      public static final String DEFAULT_CLIENT_CLASSIFIER = "client";
78  
79      /**
80       * Default value for {@link #ejbJar}.
81       */
82      public static final String DEFAULT_EJBJAR = "META-INF/ejb-jar.xml";
83  
84      /**
85       * The directory location for the generated EJB.
86       */
87      @Parameter(defaultValue = "${project.build.directory}", required = true, readonly = true)
88      private File outputDirectory;
89  
90      /**
91       * Directory that contains the resources which are packaged into the created archive {@code target/classes}.
92       */
93      @Parameter(defaultValue = "${project.build.outputDirectory}", required = true)
94      private File sourceDirectory;
95  
96      /**
97       * The name of the EJB file to generate.
98       */
99      @Parameter(defaultValue = "${project.build.finalName}", readonly = true)
100     private String jarName;
101 
102     /**
103      * Classifier to add to the artifact generated. If given, the artifact will be an attachment instead.
104      */
105     @Parameter
106     private String classifier;
107 
108     /**
109      * Classifier which is used for the client artifact.
110      *
111      * @since 3.0.0
112      */
113     @Parameter(defaultValue = DEFAULT_CLIENT_CLASSIFIER)
114     private String clientClassifier;
115 
116     /**
117      * You can define the location of <code>ejb-jar.xml</code> file.
118      */
119     @Parameter(defaultValue = DEFAULT_EJBJAR)
120     private String ejbJar;
121 
122     /**
123      * Whether the EJB client jar should be generated or not.
124      */
125     @Parameter(defaultValue = "false")
126     private boolean generateClient;
127 
128     /**
129      * The files and directories to exclude from the client jar. Usage:
130      * <p/>
131      *
132      * <pre>
133      * &lt;clientExcludes&gt;
134      * &nbsp;&nbsp;&lt;clientExclude&gt;**&#47;*Ejb.class&lt;&#47;clientExclude&gt;
135      * &nbsp;&nbsp;&lt;clientExclude&gt;**&#47;*Bean.class&lt;&#47;clientExclude&gt;
136      * &lt;&#47;clientExcludes&gt;
137      * </pre>
138      *
139      * <br/>
140      * Attribute is used only if client jar is generated. <br/>
141      * Default exclusions: **&#47;*Bean.class, **&#47;*CMP.class, **&#47;*Session.class, **&#47;package.html
142      */
143     @Parameter
144     private List<String> clientExcludes;
145 
146     /**
147      * The files and directories to include in the client jar. Usage:
148      * <p/>
149      *
150      * <pre>
151      * &lt;clientIncludes&gt;
152      * &nbsp;&nbsp;&lt;clientInclude&gt;**&#47;*&lt;&#47;clientInclude&gt;
153      * &lt;&#47;clientIncludes&gt;
154      * </pre>
155      *
156      * <br/>
157      * Attribute is used only if client jar is generated. <br/>
158      * Default value: **&#47;**
159      */
160     @Parameter
161     private List<String> clientIncludes;
162 
163     /**
164      * The files and directories to exclude from the main EJB jar. Usage:
165      * <p/>
166      *
167      * <pre>
168      * &lt;excludes&gt;
169      *   &lt;exclude&gt;**&#47;*Ejb.class&lt;&#47;exclude&gt;
170      *   &lt;exclude&gt;**&#47;*Bean.class&lt;&#47;exclude&gt;
171      * &lt;&#47;excludes&gt;
172      * </pre>
173      *
174      * <br/>
175      * Default exclusions: META-INF&#47;ejb-jar.xml, **&#47;package.html
176      */
177     @Parameter
178     private List<String> excludes;
179 
180     /**
181      * The Maven project.
182      */
183     @Parameter(defaultValue = "${project}", readonly = true, required = true)
184     private MavenProject project;
185 
186     /**
187      * What EJB version should the EJB Plugin generate? Valid values are "2.x", "3.x" or "4.x" (where x is a digit).
188      * When ejbVersion is "2.x", the <code>ejb-jar.xml</code> file is mandatory.
189      * <p/>
190      * Usage:
191      *
192      * <pre>
193      * &lt;ejbVersion&gt;3.0&lt;&#47;ejbVersion&gt;
194      * </pre>
195      *
196      * @since 2.1
197      */
198     @Parameter(defaultValue = "3.1")
199     private String ejbVersion;
200 
201     /**
202      * The archive configuration to use. See <a href="http://maven.apache.org/shared/maven-archiver/index.html">Maven
203      * Archiver Reference</a>.
204      */
205     @Parameter
206     private MavenArchiveConfiguration archive = new MavenArchiveConfiguration();
207 
208     /**
209      * To escape interpolated value with windows path. c:\foo\bar will be replaced with c:\\foo\\bar.
210      *
211      * @since 2.3
212      */
213     @Parameter(defaultValue = "false")
214     private boolean escapeBackslashesInFilePath;
215 
216     /**
217      * An expression preceded with this String won't be interpolated. \${foo} will be replaced with ${foo}.
218      *
219      * @since 2.3
220      */
221     @Parameter
222     protected String escapeString;
223 
224     /**
225      * To filter the deployment descriptor.
226      *
227      * @since 2.3
228      */
229     @Parameter(defaultValue = "false")
230     private boolean filterDeploymentDescriptor;
231 
232     /**
233      * Filters (properties files) to include during the interpolation of the deployment descriptor.
234      *
235      * @since 2.3
236      */
237     @Parameter
238     private List<String> filters;
239 
240     /**
241      * @since 2.3
242      */
243     @Parameter(defaultValue = "${session}", readonly = true, required = true)
244     private MavenSession session;
245 
246     /**
247      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
248      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
249      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
250      *
251      * @since 3.1.0
252      */
253     @Parameter(defaultValue = "${project.build.outputTimestamp}")
254     private String outputTimestamp;
255 
256     /**
257      * The Jar archiver.
258      */
259     private final JarArchiver jarArchiver = new JarArchiver();
260 
261     /**
262      * The client Jar archiver.
263      */
264     private JarArchiver clientJarArchiver = new JarArchiver();
265 
266     /**
267      * The Maven project's helper.
268      */
269     private final MavenProjectHelper projectHelper;
270 
271     /**
272      * @since 2.3
273      */
274     private final MavenFileFilter mavenFileFilter;
275 
276     @Inject
277     public EjbMojo(MavenProjectHelper projectHelper, MavenFileFilter mavenFileFilter) {
278         this.projectHelper = projectHelper;
279         this.mavenFileFilter = mavenFileFilter;
280     }
281 
282     /**
283      * Generates an EJB jar and optionally an ejb-client jar.
284      */
285     public void execute() throws MojoExecutionException {
286 
287         if (!sourceDirectory.exists()) {
288             getLog().warn("The created EJB jar will be empty cause the " + sourceDirectory.getPath()
289                     + " did not exist.");
290             sourceDirectory.mkdirs();
291         }
292 
293         File jarFile = generateEjb();
294 
295         if (hasClassifier()) {
296             if (!isClassifierValid()) {
297                 String message = "The given classifier '" + getClassifier() + "' is not valid.";
298                 getLog().error(message);
299                 throw new MojoExecutionException(message);
300             }
301 
302             // TODO: We should check the attached artifacts to be sure we don't attach
303             // the same file twice...
304             projectHelper.attachArtifact(project, EJB_TYPE, getClassifier(), jarFile);
305         } else {
306             if (projectHasAlreadySetAnArtifact()) {
307                 throw new MojoExecutionException("You have to use a classifier "
308                         + "to attach supplemental artifacts to the project instead of replacing them.");
309             }
310 
311             project.getArtifact().setFile(jarFile);
312         }
313 
314         if (generateClient) {
315             File clientJarFile = generateEjbClient();
316             if (hasClientClassifier()) {
317                 if (!isClientClassifierValid()) {
318                     String message = "The given client classifier '" + getClientClassifier() + "' is not valid.";
319                     getLog().error(message);
320                     throw new MojoExecutionException(message);
321                 }
322 
323                 projectHelper.attachArtifact(project, EJB_CLIENT_TYPE, getClientClassifier(), clientJarFile);
324             } else {
325                 // FIXME: This does not make sense, cause a classifier for the client should always exist otherwise
326                 // Failure!
327                 projectHelper.attachArtifact(project, "ejb-client", getClientClassifier(), clientJarFile);
328             }
329         }
330     }
331 
332     private boolean projectHasAlreadySetAnArtifact() {
333         if (getProject().getArtifact().getFile() != null) {
334             return getProject().getArtifact().getFile().isFile();
335         } else {
336             return false;
337         }
338     }
339 
340     private File generateEjb() throws MojoExecutionException {
341         File jarFile = EjbHelper.getJarFile(outputDirectory, jarName, getClassifier());
342 
343         getLog().info("Building EJB " + jarName + " with EJB version " + ejbVersion);
344 
345         MavenArchiver archiver = new MavenArchiver();
346 
347         archiver.setArchiver(jarArchiver);
348 
349         archiver.setCreatedBy("Maven EJB Plugin", "org.apache.maven.plugins", "maven-ejb-plugin");
350 
351         archiver.setOutputFile(jarFile);
352 
353         // configure for Reproducible Builds based on outputTimestamp value
354         archiver.configureReproducibleBuild(outputTimestamp);
355 
356         File deploymentDescriptor = new File(sourceDirectory, ejbJar);
357 
358         checkEJBVersionCompliance(deploymentDescriptor);
359 
360         try {
361             List<String> defaultExcludes = Arrays.asList(ejbJar, "**/package.html");
362 
363             IncludesExcludes ie =
364                     new IncludesExcludes(Collections.emptyList(), excludes, DEFAULT_INCLUDES_LIST, defaultExcludes);
365 
366             archiver.getArchiver().addDirectory(sourceDirectory, ie.resultingIncludes(), ie.resultingExcludes());
367 
368             // FIXME: We should be able to filter more than just the deployment descriptor?
369             if (deploymentDescriptor.exists()) {
370                 // EJB-34 Filter ejb-jar.xml
371                 if (filterDeploymentDescriptor) {
372                     filterDeploymentDescriptor(deploymentDescriptor);
373                 }
374                 archiver.getArchiver().addFile(deploymentDescriptor, ejbJar);
375             }
376 
377             // create archive
378             archiver.createArchive(session, project, archive);
379         } catch (ArchiverException | ManifestException | IOException | DependencyResolutionRequiredException e) {
380             throw new MojoExecutionException("There was a problem creating the EJB archive: " + e.getMessage(), e);
381         } catch (MavenFilteringException e) {
382             throw new MojoExecutionException(
383                     "There was a problem filtering the deployment descriptor: " + e.getMessage(), e);
384         }
385 
386         return jarFile;
387     }
388 
389     private File generateEjbClient() throws MojoExecutionException {
390         File clientJarFile = EjbHelper.getJarFile(outputDirectory, jarName, getClientClassifier());
391 
392         getLog().info("Building EJB client " + clientJarFile.getPath());
393 
394         MavenArchiver clientArchiver = new MavenArchiver();
395 
396         clientArchiver.setArchiver(clientJarArchiver);
397 
398         clientArchiver.setCreatedBy("Maven EJB Plugin", "org.apache.maven.plugins", "maven-ejb-plugin");
399 
400         clientArchiver.setOutputFile(clientJarFile);
401 
402         // configure for Reproducible Builds based on outputTimestamp value
403         clientArchiver.configureReproducibleBuild(outputTimestamp);
404 
405         try {
406 
407             IncludesExcludes ie = new IncludesExcludes(
408                     clientIncludes, clientExcludes, DEFAULT_INCLUDES_LIST, DEFAULT_CLIENT_EXCLUDES_LIST);
409 
410             clientArchiver.getArchiver().addDirectory(sourceDirectory, ie.resultingIncludes(), ie.resultingExcludes());
411 
412             clientArchiver.createArchive(session, project, archive);
413 
414         } catch (ArchiverException | ManifestException | IOException | DependencyResolutionRequiredException e) {
415             throw new MojoExecutionException(
416                     "There was a problem creating the EJB client archive: " + e.getMessage(), e);
417         }
418 
419         return clientJarFile;
420     }
421 
422     static void validateEjbVersion(String ejbVersion) throws MojoExecutionException {
423         if (!ejbVersion.matches("\\A[2-4]\\.[0-9]\\z")) {
424             throw new MojoExecutionException(
425                     "ejbVersion is not valid: " + ejbVersion + ". Must be 2.x, 3.x or 4.x (where x is a digit)");
426         }
427     }
428 
429     private void checkEJBVersionCompliance(File deploymentDescriptor) throws MojoExecutionException {
430         validateEjbVersion(ejbVersion);
431 
432         if (ejbVersion.matches("\\A2\\.[0-9]\\z") && !deploymentDescriptor.exists()) {
433             throw new MojoExecutionException("Error assembling EJB: " + ejbJar + " is required for ejbVersion 2.x");
434         }
435     }
436 
437     private void filterDeploymentDescriptor(File deploymentDescriptor) throws MavenFilteringException, IOException {
438         getLog().debug("Filtering deployment descriptor.");
439         MavenResourcesExecution mavenResourcesExecution = new MavenResourcesExecution();
440         mavenResourcesExecution.setEscapeString(escapeString);
441         List<FilterWrapper> filterWrappers = mavenFileFilter.getDefaultFilterWrappers(
442                 project, filters, escapeBackslashesInFilePath, this.session, mavenResourcesExecution);
443 
444         // Create a temporary file that we can copy-and-filter
445         File unfilteredDeploymentDescriptor = new File(sourceDirectory, ejbJar + ".unfiltered");
446         FileUtils.copyFile(deploymentDescriptor, unfilteredDeploymentDescriptor);
447         mavenFileFilter.copyFile(
448                 unfilteredDeploymentDescriptor,
449                 deploymentDescriptor,
450                 true,
451                 filterWrappers,
452                 getEncoding(unfilteredDeploymentDescriptor));
453         // Remove the temporary file
454         FileUtils.forceDelete(unfilteredDeploymentDescriptor);
455     }
456 
457     /**
458      * @return true in case where the classifier is not {@code null} and contains something else than white spaces.
459      */
460     private boolean hasClassifier() {
461         return EjbHelper.hasClassifier(getClassifier());
462     }
463 
464     /**
465      * @return true in case where the clientClassifier is not {@code null} and contains something else than white
466      *         spaces.
467      */
468     private boolean hasClientClassifier() {
469         return EjbHelper.hasClassifier(getClientClassifier());
470     }
471 
472     private boolean isClassifierValid() {
473         return EjbHelper.isClassifierValid(getClassifier());
474     }
475 
476     private boolean isClientClassifierValid() {
477         return EjbHelper.isClassifierValid(getClientClassifier());
478     }
479 
480     /**
481      * Get the encoding from an XML-file.
482      *
483      * @param xmlFile the XML file
484      * @return The encoding of the XML file, or UTF-8 if it's not specified in the file
485      * @throws IOException if an error occurred while reading the file
486      */
487     private String getEncoding(File xmlFile) throws IOException {
488         try (XmlStreamReader xmlReader =
489                 XmlStreamReader.builder().setFile(xmlFile).get()) {
490             return xmlReader.getEncoding();
491         }
492     }
493 
494     public String getClassifier() {
495         return classifier;
496     }
497 
498     public String getClientClassifier() {
499         return clientClassifier;
500     }
501 
502     public MavenProject getProject() {
503         return project;
504     }
505 }