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