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 org.apache.maven.artifact.Artifact;
23  import org.apache.maven.execution.MavenSession;
24  import org.apache.maven.plugin.AbstractMojo;
25  import org.apache.maven.plugin.MojoExecutionException;
26  import org.apache.maven.plugins.annotations.Component;
27  import org.apache.maven.plugins.annotations.Parameter;
28  import org.apache.maven.project.MavenProject;
29  import org.apache.maven.shared.jarsigner.JarSigner;
30  import org.apache.maven.shared.jarsigner.JarSignerRequest;
31  import org.apache.maven.shared.jarsigner.JarSignerUtil;
32  import org.apache.maven.shared.utils.StringUtils;
33  import org.apache.maven.shared.utils.cli.Commandline;
34  import org.apache.maven.shared.utils.cli.javatool.JavaToolException;
35  import org.apache.maven.shared.utils.cli.javatool.JavaToolResult;
36  import org.apache.maven.shared.utils.io.FileUtils;
37  import org.apache.maven.toolchain.Toolchain;
38  import org.apache.maven.toolchain.ToolchainManager;
39  import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher;
40  import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException;
41  
42  import java.io.File;
43  import java.io.IOException;
44  import java.text.MessageFormat;
45  import java.util.Arrays;
46  import java.util.Collection;
47  import java.util.HashSet;
48  import java.util.List;
49  import java.util.ResourceBundle;
50  
51  /**
52   * Maven Jarsigner Plugin base class.
53   *
54   * @author <a href="cs@schulte.it">Christian Schulte</a>
55   * @version $Id: AbstractJarsignerMojo.java 1640243 2014-11-17 22:12:18Z khmarbaise $
56   */
57  public abstract class AbstractJarsignerMojo
58      extends AbstractMojo
59  {
60  
61      /**
62       * See <a href="http://java.sun.com/javase/6/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
63       */
64      @Parameter( property = "jarsigner.verbose", defaultValue = "false" )
65      private boolean verbose;
66  
67      /**
68       * See <a href="http://java.sun.com/javase/6/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
69       */
70      @Parameter( property = "jarsigner.keystore" )
71      private String keystore;
72  
73      /**
74       * See <a href="http://java.sun.com/javase/6/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
75       */
76      @Parameter( property = "jarsigner.storetype" )
77      private String storetype;
78  
79      /**
80       * See <a href="http://java.sun.com/javase/6/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
81       */
82      @Parameter( property = "jarsigner.storepass" )
83      private String storepass;
84  
85      /**
86       * See <a href="http://java.sun.com/javase/6/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
87       */
88      @Parameter( property = "jarsigner.providerName" )
89      private String providerName;
90  
91      /**
92       * See <a href="http://java.sun.com/javase/6/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
93       */
94      @Parameter( property = "jarsigner.providerClass" )
95      private String providerClass;
96  
97      /**
98       * See <a href="http://java.sun.com/javase/6/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
99       */
100     @Parameter( property = "jarsigner.providerArg" )
101     private String providerArg;
102 
103     /**
104      * See <a href="http://java.sun.com/javase/6/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
105      */
106     @Parameter( property = "jarsigner.alias" )
107     private String alias;
108 
109     /**
110      * The maximum memory available to the JAR signer, e.g. <code>256M</code>. See <a
111      * href="http://java.sun.com/javase/6/docs/technotes/tools/windows/java.html#Xms">-Xmx</a> for more details.
112      */
113     @Parameter( property = "jarsigner.maxMemory" )
114     private String maxMemory;
115 
116     /**
117      * Archive to process. If set, neither the project artifact nor any attachments or archive sets are processed.
118      */
119     @Parameter( property = "jarsigner.archive" )
120     private File archive;
121 
122     /**
123      * The base directory to scan for JAR files using Ant-like inclusion/exclusion patterns.
124      *
125      * @since 1.1
126      */
127     @Parameter( property = "jarsigner.archiveDirectory" )
128     private File archiveDirectory;
129 
130     /**
131      * The Ant-like inclusion patterns used to select JAR files to process. The patterns must be relative to the
132      * directory given by the parameter {@link #archiveDirectory}. By default, the pattern
133      * <code>&#42;&#42;/&#42;.?ar</code> is used.
134      *
135      * @since 1.1
136      */
137     @Parameter
138     private String[] includes = { "**/*.?ar" };
139 
140     /**
141      * The Ant-like exclusion patterns used to exclude JAR files from processing. The patterns must be relative to the
142      * directory given by the parameter {@link #archiveDirectory}.
143      *
144      * @since 1.1
145      */
146     @Parameter
147     private String[] excludes = {};
148 
149     /**
150      * List of additional arguments to append to the jarsigner command line.
151      */
152     @Parameter( property = "jarsigner.arguments" )
153     private String[] arguments;
154 
155     /**
156      * Set to {@code true} to disable the plugin.
157      */
158     @Parameter( property = "jarsigner.skip", defaultValue = "false" )
159     private boolean skip;
160 
161     /**
162      * Controls processing of the main artifact produced by the project.
163      *
164      * @since 1.1
165      */
166     @Parameter( property = "jarsigner.processMainArtifact", defaultValue = "true" )
167     private boolean processMainArtifact;
168 
169     /**
170      * Controls processing of project attachments. If enabled, attached artifacts that are no JAR/ZIP files will be
171      * automatically excluded from processing.
172      *
173      * @since 1.1
174      */
175     @Parameter( property = "jarsigner.processAttachedArtifacts", defaultValue = "true" )
176     private boolean processAttachedArtifacts;
177 
178     /**
179      * Must be set to true if the password must be given via a protected
180      * authentication path such as a dedicated PIN reader.
181      *
182      * @since 1.3
183      */
184     @Parameter( property = "jarsigner.protectedAuthenticationPath", defaultValue = "false" )
185     private boolean protectedAuthenticationPath;
186 
187     /**
188      * Controls processing of project attachments.
189      *
190      * @deprecated As of version 1.1 in favor of the new parameter <code>processAttachedArtifacts</code>.
191      */
192     @Parameter( property = "jarsigner.attachments" )
193     private Boolean attachments;
194 
195     /**
196      * A set of artifact classifiers describing the project attachments that should be processed. This parameter is only
197      * relevant if {@link #processAttachedArtifacts} is <code>true</code>. If empty, all attachments are included.
198      *
199      * @since 1.2
200      */
201     @Parameter
202     private String[] includeClassifiers;
203 
204     /**
205      * A set of artifact classifiers describing the project attachments that should not be processed. This parameter is
206      * only relevant if {@link #processAttachedArtifacts} is <code>true</code>. If empty, no attachments are excluded.
207      *
208      * @since 1.2
209      */
210     @Parameter
211     private String[] excludeClassifiers;
212 
213     /**
214      * The Maven project.
215      */
216     @Parameter( defaultValue = "${project}", readonly = true, required = true )
217     private MavenProject project;
218 
219     /**
220      * Location of the working directory.
221      *
222      * @since 1.3
223      */
224     @Parameter( defaultValue = "${project.basedir}" )
225     private File workingDirectory;
226 
227     /**
228      */
229     @Component
230     private JarSigner jarSigner;
231 
232     /**
233      * The current build session instance. This is used for
234      * toolchain manager API calls.
235      *
236      * @since 1.3
237      */
238     @Parameter( defaultValue = "${session}", readonly = true, required = true )
239     private MavenSession session;
240 
241     /**
242      * To obtain a toolchain if possible.
243      *
244      * @since 1.3
245      */
246     @Component
247     private ToolchainManager toolchainManager;
248 
249     /**
250      * @since 1.3.2
251      */
252     @Component( hint = "mng-4384" )
253     private SecDispatcher securityDispatcher;
254 
255     public final void execute()
256         throws MojoExecutionException
257     {
258         if ( !this.skip )
259         {
260             Toolchain toolchain = getToolchain();
261 
262             if ( toolchain != null )
263             {
264                 getLog().info( "Toolchain in maven-jarsigner-plugin: " + toolchain );
265                 jarSigner.setToolchain( toolchain );
266             }
267 
268             int processed = 0;
269 
270             if ( this.archive != null )
271             {
272                 processArchive( this.archive );
273                 processed++;
274             }
275             else
276             {
277                 if ( processMainArtifact )
278                 {
279                     processed += processArtifact( this.project.getArtifact() ) ? 1 : 0;
280                 }
281 
282                 if ( processAttachedArtifacts && !Boolean.FALSE.equals( attachments ) )
283                 {
284                     Collection<String> includes = new HashSet<String>();
285                     if ( includeClassifiers != null )
286                     {
287                         includes.addAll( Arrays.asList( includeClassifiers ) );
288                     }
289 
290                     Collection<String> excludes = new HashSet<String>();
291                     if ( excludeClassifiers != null )
292                     {
293                         excludes.addAll( Arrays.asList( excludeClassifiers ) );
294                     }
295 
296                     for ( Object o : this.project.getAttachedArtifacts() )
297                     {
298                         final Artifact artifact = (Artifact) o;
299 
300                         if ( !includes.isEmpty() && !includes.contains( artifact.getClassifier() ) )
301                         {
302                             continue;
303                         }
304 
305                         if ( excludes.contains( artifact.getClassifier() ) )
306                         {
307                             continue;
308                         }
309 
310                         processed += processArtifact( artifact ) ? 1 : 0;
311                     }
312                 }
313                 else
314                 {
315                     if ( verbose )
316                     {
317                         getLog().info( getMessage( "ignoringAttachments" ) );
318                     }
319                     else
320                     {
321                         getLog().debug( getMessage( "ignoringAttachments" ) );
322                     }
323                 }
324 
325                 if ( archiveDirectory != null )
326                 {
327                     String includeList = ( includes != null ) ? StringUtils.join( includes, "," ) : null;
328                     String excludeList = ( excludes != null ) ? StringUtils.join( excludes, "," ) : null;
329 
330                     List<File> jarFiles;
331                     try
332                     {
333                         jarFiles = FileUtils.getFiles( archiveDirectory, includeList, excludeList );
334                     }
335                     catch ( IOException e )
336                     {
337                         throw new MojoExecutionException( "Failed to scan archive directory for JARs: "
338                             + e.getMessage(), e );
339                     }
340 
341                     for ( File jarFile : jarFiles )
342                     {
343                         processArchive( jarFile );
344                         processed++;
345                     }
346                 }
347             }
348 
349             getLog().info( getMessage( "processed", processed ) );
350         }
351         else
352         {
353             getLog().info( getMessage( "disabled", null ) );
354         }
355     }
356 
357     /**
358      * Creates the jar signer request to be executed.
359      *
360      * @param archive the archive file to treat by jarsigner
361      * @return the request
362      * @since 1.3
363      */
364     protected abstract JarSignerRequest createRequest( File archive )
365         throws MojoExecutionException;
366 
367     /**
368      * Gets a string representation of a {@code Commandline}.
369      * <p>
370      * This method creates the string representation by calling {@code commandLine.toString()} by default.
371      * </p>
372      *
373      * @param commandLine The {@code Commandline} to get a string representation of.
374      * @return The string representation of {@code commandLine}.
375      * @throws NullPointerException if {@code commandLine} is {@code null}.
376      */
377     protected String getCommandlineInfo( final Commandline commandLine )
378     {
379         if ( commandLine == null )
380         {
381             throw new NullPointerException( "commandLine" );
382         }
383 
384         String commandLineInfo = commandLine.toString();
385         commandLineInfo = StringUtils.replace( commandLineInfo, this.storepass, "'*****'" );
386         return commandLineInfo;
387     }
388 
389     public String getStoretype()
390     {
391         return storetype;
392     }
393 
394     public String getStorepass()
395     {
396         return storepass;
397     }
398 
399     /**
400      * Checks whether the specified artifact is a ZIP file.
401      *
402      * @param artifact The artifact to check, may be <code>null</code>.
403      * @return <code>true</code> if the artifact looks like a ZIP file, <code>false</code> otherwise.
404      */
405     private boolean isZipFile( final Artifact artifact )
406     {
407         return artifact != null && artifact.getFile() != null && JarSignerUtil.isZipFile( artifact.getFile() );
408     }
409 
410     /**
411      * Processes a given artifact.
412      *
413      * @param artifact The artifact to process.
414      * @return <code>true</code> if the artifact is a JAR and was processed, <code>false</code> otherwise.
415      * @throws NullPointerException if {@code artifact} is {@code null}.
416      * @throws MojoExecutionException if processing {@code artifact} fails.
417      */
418     private boolean processArtifact( final Artifact artifact )
419         throws MojoExecutionException
420     {
421         if ( artifact == null )
422         {
423             throw new NullPointerException( "artifact" );
424         }
425 
426         boolean processed = false;
427 
428         if ( isZipFile( artifact ) )
429         {
430             processArchive( artifact.getFile() );
431 
432             processed = true;
433         }
434         else
435         {
436             if ( this.verbose )
437             {
438                 getLog().info( getMessage( "unsupported", artifact ) );
439             }
440             else if ( getLog().isDebugEnabled() )
441             {
442                 getLog().debug( getMessage( "unsupported", artifact ) );
443             }
444         }
445 
446         return processed;
447     }
448 
449     /**
450      * Pre-processes a given archive.
451      *
452      * @param archive The archive to process, must not be <code>null</code>.
453      * @throws MojoExecutionException If pre-processing failed.
454      */
455     protected void preProcessArchive( final File archive )
456         throws MojoExecutionException
457     {
458         // default does nothing
459     }
460 
461     /**
462      * Processes a given archive.
463      *
464      * @param archive The archive to process.
465      * @throws NullPointerException if {@code archive} is {@code null}.
466      * @throws MojoExecutionException if processing {@code archive} fails.
467      */
468     private void processArchive( final File archive )
469         throws MojoExecutionException
470     {
471         if ( archive == null )
472         {
473             throw new NullPointerException( "archive" );
474         }
475 
476         preProcessArchive( archive );
477 
478         if ( this.verbose )
479         {
480             getLog().info( getMessage( "processing", archive ) );
481         }
482         else if ( getLog().isDebugEnabled() )
483         {
484             getLog().debug( getMessage( "processing", archive ) );
485         }
486 
487         JarSignerRequest request = createRequest( archive );
488         request.setVerbose( verbose );
489         request.setAlias( alias );
490         request.setArchive( archive );
491         request.setKeystore( keystore );
492         request.setStoretype( storetype );
493         request.setProviderArg( providerArg );
494         request.setProviderClass( providerClass );
495         request.setProviderName( providerName );
496         request.setWorkingDirectory( workingDirectory );
497         request.setMaxMemory( maxMemory );
498         request.setArguments( arguments );
499         request.setProtectedAuthenticationPath( protectedAuthenticationPath );
500 
501         // Special handling for passwords through the Maven Security Dispatcher
502         request.setStorepass( decrypt( storepass ) );
503 
504         try
505         {
506             JavaToolResult result = jarSigner.execute( request );
507 
508             Commandline commandLine = result.getCommandline();
509 
510             int resultCode = result.getExitCode();
511 
512             if ( resultCode != 0 )
513             {
514                 // CHECKSTYLE_OFF: LineLength
515                 throw new MojoExecutionException( getMessage( "failure", getCommandlineInfo( commandLine ), resultCode ) );
516                 // CHECKSTYLE_ON: LineLength
517             }
518 
519         }
520         catch ( JavaToolException e )
521         {
522             throw new MojoExecutionException( getMessage( "commandLineException", e.getMessage() ), e );
523         }
524     }
525 
526     protected String decrypt( String encoded )
527         throws MojoExecutionException
528     {
529         try
530         {
531             return securityDispatcher.decrypt( encoded );
532         }
533         catch ( SecDispatcherException e )
534         {
535             getLog().error( "error using security dispatcher: " + e.getMessage(), e );
536             throw new MojoExecutionException( "error using security dispatcher: " + e.getMessage(), e );
537         }
538     }
539 
540     /**
541      * Gets a message for a given key from the resource bundle backing the implementation.
542      *
543      * @param key The key of the message to return.
544      * @param args Arguments to format the message with or {@code null}.
545      * @return The message with key {@code key} from the resource bundle backing the implementation.
546      * @throws NullPointerException if {@code key} is {@code null}.
547      * @throws java.util.MissingResourceException
548      *             if there is no message available matching {@code key} or accessing
549      *             the resource bundle fails.
550      */
551     private String getMessage( final String key, final Object[] args )
552     {
553         if ( key == null )
554         {
555             throw new NullPointerException( "key" );
556         }
557 
558         return new MessageFormat( ResourceBundle.getBundle( "jarsigner" ).getString( key ) ).format( args );
559     }
560 
561     private String getMessage( final String key )
562     {
563         return getMessage( key, null );
564     }
565 
566     String getMessage( final String key, final Object arg )
567     {
568         return getMessage( key, new Object[] { arg } );
569     }
570 
571     private String getMessage( final String key, final Object arg1, final Object arg2 )
572     {
573         return getMessage( key, new Object[] { arg1, arg2 } );
574     }
575 
576     /**
577      * FIXME tchemit-20123-11-13, need to find out how to do this...
578      * TODO remove the part with ToolchainManager lookup once we depend on
579      * 2.0.9 (have it as prerequisite). Define as regular component field then.
580      *
581      * @return Toolchain instance
582      */
583     private Toolchain getToolchain()
584     {
585         Toolchain tc = null;
586         if ( toolchainManager != null )
587         {
588             tc = toolchainManager.getToolchainFromBuildContext( "jdk", session );
589         }
590 
591         return tc;
592     }
593 }