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