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>**/*.?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 }