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 1575440 2014-03-07 22:37:29Z tchemit $
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     @Component
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     @Component
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                 jarSigner.setToolchain(toolchain);
265             }
266 
267             int processed = 0;
268 
269             if ( this.archive != null )
270             {
271                 processArchive( this.archive );
272                 processed++;
273             }
274             else
275             {
276                 if ( processMainArtifact )
277                 {
278                     processed += processArtifact( this.project.getArtifact() ) ? 1 : 0;
279                 }
280 
281                 if ( processAttachedArtifacts && !Boolean.FALSE.equals( attachments ) )
282                 {
283                     Collection<String> includes = new HashSet<String>();
284                     if ( includeClassifiers != null )
285                     {
286                         includes.addAll( Arrays.asList( includeClassifiers ) );
287                     }
288 
289                     Collection<String> excludes = new HashSet<String>();
290                     if ( excludeClassifiers != null )
291                     {
292                         excludes.addAll( Arrays.asList( excludeClassifiers ) );
293                     }
294 
295                     for ( Object o : this.project.getAttachedArtifacts() )
296                     {
297                         final Artifact artifact = (Artifact) o;
298 
299                         if ( !includes.isEmpty() && !includes.contains( artifact.getClassifier() ) )
300                         {
301                             continue;
302                         }
303 
304                         if ( excludes.contains( artifact.getClassifier() ) )
305                         {
306                             continue;
307                         }
308 
309                         processed += processArtifact( artifact ) ? 1 : 0;
310                     }
311                 }
312                 else
313                 {
314                     if ( verbose )
315                     {
316                         getLog().info( getMessage( "ignoringAttachments" ) );
317                     }
318                     else
319                     {
320                         getLog().debug( getMessage( "ignoringAttachments" ) );
321                     }
322                 }
323 
324                 if ( archiveDirectory != null )
325                 {
326                     String includeList = ( includes != null ) ? StringUtils.join( includes, "," ) : null;
327                     String excludeList = ( excludes != null ) ? StringUtils.join( excludes, "," ) : null;
328 
329                     List<File> jarFiles;
330                     try
331                     {
332                         jarFiles = FileUtils.getFiles( archiveDirectory, includeList, excludeList );
333                     }
334                     catch ( IOException e )
335                     {
336                         throw new MojoExecutionException(
337                             "Failed to scan archive directory for JARs: " + e.getMessage(), e );
338                     }
339 
340                     for ( File jarFile : jarFiles )
341                     {
342                         processArchive( jarFile );
343                         processed++;
344                     }
345                 }
346             }
347 
348             getLog().info( getMessage( "processed", processed) );
349         }
350         else
351         {
352             getLog().info( getMessage( "disabled", null ) );
353         }
354     }
355 
356     /**
357      * Creates the jar signer request to be executed.
358      *
359      * @param archive the archive file to treat by jarsigner
360      * @return the request
361      * @since 1.3
362      */
363     protected abstract JarSignerRequest createRequest( File archive )
364         throws MojoExecutionException;
365 
366     /**
367      * Gets a string representation of a {@code Commandline}.
368      * <p>This method creates the string representation by calling {@code commandLine.toString()} by default.</p>
369      *
370      * @param commandLine The {@code Commandline} to get a string representation of.
371      * @return The string representation of {@code commandLine}.
372      * @throws NullPointerException if {@code commandLine} is {@code null}.
373      */
374     protected String getCommandlineInfo( final Commandline commandLine )
375     {
376         if ( commandLine == null )
377         {
378             throw new NullPointerException( "commandLine" );
379         }
380 
381         String commandLineInfo = commandLine.toString();
382         commandLineInfo = StringUtils.replace( commandLineInfo, this.storepass, "'*****'" );
383         return commandLineInfo;
384     }
385 
386     public String getStoretype()
387     {
388         return storetype;
389     }
390 
391     public String getStorepass()
392     {
393         return storepass;
394     }
395 
396     /**
397      * Checks whether the specified artifact is a ZIP file.
398      *
399      * @param artifact The artifact to check, may be <code>null</code>.
400      * @return <code>true</code> if the artifact looks like a ZIP file, <code>false</code> otherwise.
401      */
402     private boolean isZipFile( final Artifact artifact )
403     {
404         return artifact != null && artifact.getFile() != null && JarSignerUtil.isZipFile( artifact.getFile() );
405     }
406 
407     /**
408      * Processes a given artifact.
409      *
410      * @param artifact The artifact to process.
411      * @return <code>true</code> if the artifact is a JAR and was processed, <code>false</code> otherwise.
412      * @throws NullPointerException   if {@code artifact} is {@code null}.
413      * @throws MojoExecutionException if processing {@code artifact} fails.
414      */
415     private boolean processArtifact( final Artifact artifact )
416         throws MojoExecutionException
417     {
418         if ( artifact == null )
419         {
420             throw new NullPointerException( "artifact" );
421         }
422 
423         boolean processed = false;
424 
425         if ( isZipFile( artifact ) )
426         {
427             processArchive( artifact.getFile() );
428 
429             processed = true;
430         }
431         else
432         {
433             if ( this.verbose )
434             {
435                 getLog().info( getMessage( "unsupported", artifact ) );
436             }
437             else if ( getLog().isDebugEnabled() )
438             {
439                 getLog().debug( getMessage( "unsupported", artifact ) );
440             }
441         }
442 
443         return processed;
444     }
445 
446     /**
447      * Pre-processes a given archive.
448      *
449      * @param archive The archive to process, must not be <code>null</code>.
450      * @throws MojoExecutionException If pre-processing failed.
451      */
452     protected void preProcessArchive( final File archive )
453         throws MojoExecutionException
454     {
455         // default does nothing
456     }
457 
458     /**
459      * Processes a given archive.
460      *
461      * @param archive The archive to process.
462      * @throws NullPointerException   if {@code archive} is {@code null}.
463      * @throws MojoExecutionException if processing {@code archive} fails.
464      */
465     private void processArchive( final File archive )
466         throws MojoExecutionException
467     {
468         if ( archive == null )
469         {
470             throw new NullPointerException( "archive" );
471         }
472 
473         preProcessArchive( archive );
474 
475         if ( this.verbose )
476         {
477             getLog().info( getMessage( "processing", archive ) );
478         }
479         else if ( getLog().isDebugEnabled() )
480         {
481             getLog().debug( getMessage( "processing", archive ) );
482         }
483 
484         JarSignerRequest request = createRequest( archive );
485         request.setVerbose( verbose );
486         request.setAlias( alias );
487         request.setArchive( archive );
488         request.setKeystore( keystore );
489         request.setStoretype( storetype );
490         request.setProviderArg( providerArg );
491         request.setProviderClass( providerClass );
492         request.setProviderName( providerName );
493         request.setWorkingDirectory( workingDirectory );
494         request.setMaxMemory( maxMemory );
495         request.setArguments( arguments );
496         request.setProtectedAuthenticationPath( protectedAuthenticationPath );
497 
498         // Special handling for passwords through the Maven Security Dispatcher
499         request.setStorepass( decrypt( storepass ) );
500 
501         try
502         {
503             JavaToolResult result = jarSigner.execute( request );
504 
505             Commandline commandLine = result.getCommandline();
506 
507             int resultCode = result.getExitCode();
508 
509             if ( resultCode != 0 )
510             {
511                 throw new MojoExecutionException(
512                     getMessage( "failure", getCommandlineInfo( commandLine ), resultCode) );
513             }
514 
515         }
516         catch ( JavaToolException e )
517         {
518             throw new MojoExecutionException( getMessage( "commandLineException", e.getMessage() ), e );
519         }
520     }
521 
522     protected String decrypt(String encoded)
523         throws MojoExecutionException
524     {
525         try
526         {
527             return securityDispatcher.decrypt( encoded );
528         }
529         catch ( SecDispatcherException e )
530         {
531             getLog().error( "error using security dispatcher: " + e.getMessage(), e );
532             throw new MojoExecutionException( "error using security dispatcher: " + e.getMessage(), e );
533         }
534     }
535 
536     /**
537      * Gets a message for a given key from the resource bundle backing the implementation.
538      *
539      * @param key  The key of the message to return.
540      * @param args Arguments to format the message with or {@code null}.
541      * @return The message with key {@code key} from the resource bundle backing the implementation.
542      * @throws NullPointerException if {@code key} is {@code null}.
543      * @throws java.util.MissingResourceException
544      *                              if there is no message available matching {@code key} or accessing
545      *                              the resource bundle fails.
546      */
547     private String getMessage( final String key, final Object[] args )
548     {
549         if ( key == null )
550         {
551             throw new NullPointerException( "key" );
552         }
553 
554         return new MessageFormat( ResourceBundle.getBundle( "jarsigner" ).getString( key ) ).format( args );
555     }
556 
557     private String getMessage( final String key )
558     {
559         return getMessage( key, null );
560     }
561 
562     String getMessage( final String key, final Object arg )
563     {
564         return getMessage( key, new Object[]{ arg } );
565     }
566 
567     private String getMessage( final String key, final Object arg1, final Object arg2 )
568     {
569         return getMessage( key, new Object[]{ arg1, arg2 } );
570     }
571 
572     /**
573      * FIXME tchemit-20123-11-13, need to find out how to do this...
574      * TODO remove the part with ToolchainManager lookup once we depend on
575      * 2.0.9 (have it as prerequisite). Define as regular component field then.
576      *
577      * @return Toolchain instance
578      */
579     private Toolchain getToolchain()
580     {
581         Toolchain tc = null;
582         if ( toolchainManager != null )
583         {
584             tc = toolchainManager.getToolchainFromBuildContext( "jdk", session );
585         }
586 
587         return tc;
588     }
589 }