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