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