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