View Javadoc

1   package org.apache.maven.plugins.jarsigner;
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.FileInputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.text.MessageFormat;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Properties;
30  import java.util.ResourceBundle;
31  import java.util.zip.ZipEntry;
32  import java.util.zip.ZipInputStream;
33  
34  import org.apache.maven.artifact.Artifact;
35  import org.apache.maven.plugin.AbstractMojo;
36  import org.apache.maven.plugin.MojoExecutionException;
37  import org.apache.maven.project.MavenProject;
38  
39  import org.codehaus.plexus.util.FileUtils;
40  import org.codehaus.plexus.util.Os;
41  import org.codehaus.plexus.util.StringUtils;
42  import org.codehaus.plexus.util.cli.CommandLineException;
43  import org.codehaus.plexus.util.cli.CommandLineUtils;
44  import org.codehaus.plexus.util.cli.Commandline;
45  import org.codehaus.plexus.util.cli.StreamConsumer;
46  
47  /**
48   * Maven Jarsigner Plugin base class.
49   *
50   * @author <a href="cs@schulte.it">Christian Schulte</a>
51   * @version $Id: AbstractJarsignerMojo.java 802605 2009-08-09 21:10:34Z bentmann $
52   */
53  public abstract class AbstractJarsignerMojo
54      extends AbstractMojo
55  {
56  
57      /**
58       * See <a href="http://java.sun.com/javase/6/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
59       *
60       * @parameter expression="${jarsigner.verbose}" default-value="false"
61       */
62      private boolean verbose;
63  
64      /**
65       * The maximum memory available to the JAR signer, e.g. <code>256M</code>. See <a
66       * href="http://java.sun.com/javase/6/docs/technotes/tools/windows/java.html#Xms">-Xmx</a> for more details.
67       * 
68       * @parameter expression="${jarsigner.maxMemory}"
69       */
70      private String maxMemory;
71  
72      /**
73       * Archive to process. If set, neither the project artifact nor any attachments or archive sets are processed.
74       *
75       * @parameter expression="${jarsigner.archive}"
76       */
77      private File archive;
78  
79      /**
80       * The base directory to scan for JAR files using Ant-like inclusion/exclusion patterns.
81       * 
82       * @parameter expression="${jarsigner.archiveDirectory}"
83       * @since 1.1
84       */
85      private File archiveDirectory;
86  
87      /**
88       * The Ant-like inclusion patterns used to select JAR files to process. The patterns must be relative to the
89       * directory given by the parameter {@link #archiveDirectory}. By default, the pattern
90       * <code>&#42;&#42;/&#42;.?ar</code> is used.
91       * 
92       * @parameter
93       * @since 1.1
94       */
95      private String[] includes = { "**/*.?ar" };
96  
97      /**
98       * The Ant-like exclusion patterns used to exclude JAR files from processing. The patterns must be relative to the
99       * directory given by the parameter {@link #archiveDirectory}.
100      * 
101      * @parameter
102      * @since 1.1
103      */
104     private String[] excludes = {};
105 
106     /**
107      * List of additional arguments to append to the jarsigner command line.
108      *
109      * @parameter expression="${jarsigner.arguments}"
110      */
111     private String[] arguments;
112 
113     /**
114      * Set to {@code true} to disable the plugin.
115      *
116      * @parameter expression="${jarsigner.skip}" default-value="false"
117      */
118     private boolean skip;
119 
120     /**
121      * Controls processing of the main artifact produced by the project.
122      * 
123      * @parameter expression="${jarsigner.processMainArtifact}" default-value="true"
124      * @since 1.1
125      */
126     private boolean processMainArtifact;
127 
128     /**
129      * Controls processing of project attachments. If enabled, attached artifacts that are no JARs will be automatically
130      * excluded from processing.
131      * 
132      * @parameter expression="${jarsigner.processAttachedArtifacts}" default-value="true"
133      * @since 1.1
134      */
135     private boolean processAttachedArtifacts;
136 
137     /**
138      * Controls processing of project attachments.
139      * 
140      * @parameter expression="${jarsigner.attachments}"
141      * @deprecated As of version 1.1 in favor of the new parameter <code>processAttachedArtifacts</code>.
142      */
143     private Boolean attachments;
144 
145     /**
146      * The Maven project.
147      *
148      * @parameter default-value="${project}"
149      * @required
150      * @readonly
151      */
152     private MavenProject project;
153 
154     /**
155      * The path to the jarsigner we are going to use.
156      */
157     private String executable;
158 
159     public final void execute()
160         throws MojoExecutionException
161     {
162         if ( !this.skip )
163         {
164             this.executable = getExecutable();
165 
166             int processed = 0;
167 
168             if ( this.archive != null )
169             {
170                 processArchive( this.archive );
171                 processed++;
172             }
173             else
174             {
175                 if ( processMainArtifact )
176                 {
177                     processed += processArtifact( this.project.getArtifact() ) ? 1 : 0;
178                 }
179 
180                 if ( processAttachedArtifacts && !Boolean.FALSE.equals( attachments ) )
181                 {
182                     for ( Iterator it = this.project.getAttachedArtifacts().iterator(); it.hasNext(); )
183                     {
184                         final Artifact artifact = (Artifact) it.next();
185 
186                         processed += processArtifact( artifact ) ? 1 : 0;
187                     }
188                 }
189                 else
190                 {
191                     if ( verbose )
192                     {
193                         getLog().info( getMessage( "ignoringAttachments" ) );
194                     }
195                     else
196                     {
197                         getLog().debug( getMessage( "ignoringAttachments" ) );
198                     }
199                 }
200 
201                 if ( archiveDirectory != null )
202                 {
203                     String includeList = ( includes != null ) ? StringUtils.join( includes, "," ) : null;
204                     String excludeList = ( excludes != null ) ? StringUtils.join( excludes, "," ) : null;
205 
206                     List jarFiles;
207                     try
208                     {
209                         jarFiles = FileUtils.getFiles( archiveDirectory, includeList, excludeList );
210                     }
211                     catch ( IOException e )
212                     {
213                         throw new MojoExecutionException( "Failed to scan archive directory for JARs: "
214                             + e.getMessage(), e );
215                     }
216 
217                     for ( Iterator it = jarFiles.iterator(); it.hasNext(); )
218                     {
219                         File jarFile = (File) it.next();
220 
221                         processArchive( jarFile );
222                         processed++;
223                     }
224                 }
225             }
226 
227             getLog().info( getMessage( "processed", new Integer( processed ) ) );
228         }
229         else
230         {
231             getLog().info( getMessage( "disabled", null ) );
232         }
233     }
234 
235     /**
236      * Gets the {@code Commandline} to execute for a given Java archive taking a command line prepared for executing
237      * jarsigner.
238      *
239      * @param archive The Java archive to get a {@code Commandline} to execute for.
240      * @param commandLine A {@code Commandline} prepared for executing jarsigner without any arguments.
241      *
242      * @return A {@code Commandline} for executing jarsigner with {@code archive}.
243      *
244      * @throws NullPointerException if {@code archive} or {@code commandLine} is {@code null}.
245      */
246     protected abstract Commandline getCommandline( final File archive, final Commandline commandLine );
247 
248     /**
249      * Gets a string representation of a {@code Commandline}.
250      * <p>This method creates the string representation by calling {@code commandLine.toString()} by default.</p>
251      *
252      * @param commandLine The {@code Commandline} to get a string representation of.
253      *
254      * @return The string representation of {@code commandLine}.
255      *
256      * @throws NullPointerException if {@code commandLine} is {@code null}.
257      */
258     protected String getCommandlineInfo( final Commandline commandLine )
259     {
260         if ( commandLine == null )
261         {
262             throw new NullPointerException( "commandLine" );
263         }
264 
265         return commandLine.toString();
266     }
267 
268     /**
269      * Checks Java language capability of an artifact.
270      *
271      * @param artifact The artifact to check.
272      *
273      * @return {@code true} if {@code artifact} is Java language capable; {@code false} if not.
274      */
275     private boolean isJarFile( final Artifact artifact )
276     {
277         return artifact != null && artifact.getFile() != null && isJarFile( artifact.getFile() );
278     }
279 
280     /**
281      * Checks whether the specified file is a JAR file. For our purposes, a JAR file is a (non-empty) ZIP stream with a
282      * META-INF directory or some class files.
283      * 
284      * @param file The file to check, must not be <code>null</code>.
285      * @return <code>true</code> if the file looks like a JAR file, <code>false</code> otherwise.
286      */
287     private boolean isJarFile( final File file )
288     {
289         try
290         {
291             // NOTE: ZipFile.getEntry() might be shorter but is several factors slower on large files
292 
293             ZipInputStream zis = new ZipInputStream( new FileInputStream( file ) );
294             try
295             {
296                 for ( ZipEntry ze = zis.getNextEntry(); ze != null; ze = zis.getNextEntry() )
297                 {
298                     if ( ze.getName().startsWith( "META-INF/" ) || ze.getName().endsWith( ".class" ) )
299                     {
300                         return true;
301                     }
302                 }
303             }
304             finally
305             {
306                 zis.close();
307             }
308         }
309         catch ( Exception e )
310         {
311             // ignore, will fail below
312         }
313 
314         return false;
315     }
316 
317     /**
318      * Processes a given artifact.
319      *
320      * @param artifact The artifact to process.
321      * @return <code>true</code> if the artifact is a JAR and was processed, <code>false</code> otherwise.
322      *
323      * @throws NullPointerException if {@code artifact} is {@code null}.
324      * @throws MojoExecutionException if processing {@code artifact} fails.
325      */
326     private boolean processArtifact( final Artifact artifact )
327         throws MojoExecutionException
328     {
329         if ( artifact == null )
330         {
331             throw new NullPointerException( "artifact" );
332         }
333 
334         boolean processed = false;
335 
336         if ( isJarFile( artifact ) )
337         {
338             processArchive( artifact.getFile() );
339 
340             processed = true;
341         }
342         else
343         {
344             if ( this.verbose )
345             {
346                 getLog().info( getMessage( "unsupported", artifact ) );
347             }
348             else if ( getLog().isDebugEnabled() )
349             {
350                 getLog().debug( getMessage( "unsupported", artifact ) );
351             }
352         }
353 
354         return processed;
355     }
356 
357     /**
358      * Pre-processes a given archive.
359      * 
360      * @param archive The archive to process, must not be <code>null</code>.
361      * @throws MojoExecutionException If pre-processing failed.
362      */
363     protected void preProcessArchive( final File archive )
364         throws MojoExecutionException
365     {
366         // default does nothing
367     }
368 
369     /**
370      * Processes a given archive.
371      * 
372      * @param archive The archive to process.
373      * @throws NullPointerException if {@code archive} is {@code null}.
374      * @throws MojoExecutionException if processing {@code archive} fails.
375      */
376     private void processArchive( final File archive )
377         throws MojoExecutionException
378     {
379         if ( archive == null )
380         {
381             throw new NullPointerException( "archive" );
382         }
383 
384         preProcessArchive( archive );
385 
386         if ( this.verbose )
387         {
388             getLog().info( getMessage( "processing", archive ) );
389         }
390         else if ( getLog().isDebugEnabled() )
391         {
392             getLog().debug( getMessage( "processing", archive ) );
393         }
394 
395         Commandline commandLine = new Commandline();
396 
397         commandLine.setExecutable( this.executable );
398 
399         commandLine.setWorkingDirectory( this.project.getBasedir() );
400 
401         if ( this.verbose )
402         {
403             commandLine.createArg().setValue( "-verbose" );
404         }
405 
406         if ( StringUtils.isNotEmpty( maxMemory ) )
407         {
408             commandLine.createArg().setValue( "-J-Xmx" + maxMemory );
409         }
410 
411         if ( this.arguments != null )
412         {
413             commandLine.addArguments( this.arguments );
414         }
415 
416         commandLine = getCommandline( archive, commandLine );
417 
418         try
419         {
420             if ( getLog().isDebugEnabled() )
421             {
422                 getLog().debug( getMessage( "command", getCommandlineInfo( commandLine ) ) );
423             }
424 
425             final int result = CommandLineUtils.executeCommandLine( commandLine,
426                 new InputStream()
427             {
428 
429                 public int read()
430                 {
431                     return -1;
432                 }
433 
434             }, new StreamConsumer()
435             {
436 
437                 public void consumeLine( final String line )
438                 {
439                     if ( verbose )
440                     {
441                         getLog().info( line );
442                     }
443                     else
444                     {
445                         getLog().debug( line );
446                     }
447                 }
448 
449             }, new StreamConsumer()
450             {
451 
452                 public void consumeLine( final String line )
453                 {
454                     getLog().warn( line );
455                 }
456 
457             } );
458 
459             if ( result != 0 )
460             {
461                 throw new MojoExecutionException( getMessage( "failure", getCommandlineInfo( commandLine ),
462                                                               new Integer( result ) ) );
463             }
464         }
465         catch ( CommandLineException e )
466         {
467             throw new MojoExecutionException( getMessage( "commandLineException", getCommandlineInfo( commandLine ) ),
468                                               e );
469         }
470     }
471 
472     /**
473      * Locates the executable for the jarsigner tool.
474      * 
475      * @return The executable of the jarsigner tool, never <code>null<code>.
476      */
477     private String getExecutable()
478     {
479         String command = "jarsigner" + ( Os.isFamily( Os.FAMILY_WINDOWS ) ? ".exe" : "" );
480 
481         String executable =
482             findExecutable( command, System.getProperty( "java.home" ), new String[] { "../bin", "bin", "../sh" } );
483 
484         if ( executable == null )
485         {
486             try
487             {
488                 Properties env = CommandLineUtils.getSystemEnvVars();
489 
490                 String[] variables = { "JDK_HOME", "JAVA_HOME" };
491 
492                 for ( int i = 0; i < variables.length && executable == null; i++ )
493                 {
494                     executable =
495                         findExecutable( command, env.getProperty( variables[i] ), new String[] { "bin", "sh" } );
496                 }
497             }
498             catch ( IOException e )
499             {
500                 if ( getLog().isDebugEnabled() )
501                 {
502                     getLog().warn( "Failed to retrieve environment variables, cannot search for " + command, e );
503                 }
504                 else
505                 {
506                     getLog().warn( "Failed to retrieve environment variables, cannot search for " + command );
507                 }
508             }
509         }
510 
511         if ( executable == null )
512         {
513             executable = command;
514         }
515 
516         return executable;
517     }
518 
519     /**
520      * Finds the specified command in any of the given sub directories of the specified JDK/JRE home directory.
521      * 
522      * @param command The command to find, must not be <code>null</code>.
523      * @param homeDir The home directory to search in, may be <code>null</code>.
524      * @param subDirs The sub directories of the home directory to search in, must not be <code>null</code>.
525      * @return The (absolute) path to the command if found, <code>null</code> otherwise.
526      */
527     private String findExecutable( String command, String homeDir, String[] subDirs )
528     {
529         if ( StringUtils.isNotEmpty( homeDir ) )
530         {
531             for ( int i = 0; i < subDirs.length; i++ )
532             {
533                 File file = new File( new File( homeDir, subDirs[i] ), command );
534 
535                 if ( file.isFile() )
536                 {
537                     return file.getAbsolutePath();
538                 }
539             }
540         }
541 
542         return null;
543     }
544 
545     /**
546      * Gets a message for a given key from the resource bundle backing the implementation.
547      *
548      * @param key The key of the message to return.
549      * @param args Arguments to format the message with or {@code null}.
550      *
551      * @return The message with key {@code key} from the resource bundle backing the implementation.
552      *
553      * @throws NullPointerException if {@code key} is {@code null}.
554      * @throws java.util.MissingResourceException if there is no message available matching {@code key} or accessing
555      * the resource bundle fails.
556      */
557     private String getMessage( final String key, final Object[] args )
558     {
559         if ( key == null )
560         {
561             throw new NullPointerException( "key" );
562         }
563 
564         return new MessageFormat( ResourceBundle.getBundle( "jarsigner" ).getString( key ) ).format( args );
565     }
566 
567     private String getMessage( final String key )
568     {
569         return getMessage( key, null );
570     }
571 
572     private String getMessage( final String key, final Object arg )
573     {
574         return getMessage( key, new Object[] { arg } );
575     }
576 
577     private String getMessage( final String key, final Object arg1, final Object arg2 )
578     {
579         return getMessage( key, new Object[] { arg1, arg2 } );
580     }
581 
582 }