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