blob: e936e1c11a6b4a5948414ab80f09b0388463cfc8 [file] [log] [blame]
package org.apache.maven.plugin.jar;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.commons.lang.SystemUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.util.cli.StreamConsumer;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
/**
* Signs a JAR using jarsigner.
*
* @author <a href="jerome@coffeebreaks.org">Jerome Lacoste</a>
* @version $Id$
* @goal sign
* @phase package
* @requiresProject
* @todo refactor the common code with javadoc plugin
* @requiresDependencyResolution runtime
*/
public class JarSignMojo
extends AbstractMojo
{
/**
* Set this to <code>true</code> to disable signing.
* Useful to speed up build process in development environment.
*
* @parameter expression="${maven.jar.sign.skip}" default-value="false"
*/
private boolean skip;
/**
* The working directory in which the jarsigner executable will be run.
*
* @parameter expression="${workingdir}" default-value="${basedir}"
* @required
*/
private File workingDirectory;
/**
* Directory containing the generated JAR.
*
* @parameter expression="${project.build.directory}"
* @required
* @readonly
*/
private File basedir;
/**
* Name of the generated JAR (without classifier and extension).
*
* @parameter alias="jarname" expression="${project.build.finalName}"
* @required
*/
private String finalName;
/**
* Path of the jar to sign. When specified, the finalName is ignored.
*
* @parameter alias="jarpath" default-value="${project.build.directory}/${project.build.finalName}.${project.packaging}"
*/
private File jarPath;
/**
* See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
*
* @parameter expression="${keystore}"
*/
private String keystore;
/**
* See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
*
* @parameter expression="${storepass}"
*/
private String storepass;
/**
* See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
*
* @parameter expression="${keypass}"
*/
private String keypass;
/**
* See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
*
* @parameter expression="${sigfile}"
* @todo make a File?
*/
private String sigfile;
/**
* See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
* <p/>
* Not specifying this argument will sign the jar in-place (your original jar is going to be overwritten).
*
* @parameter expression="${signedjar}"
*/
private File signedjar;
/**
* See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
* The corresponding option in the command line is -storetype.
*
* @parameter expression="${type}"
*/
private String type;
/**
* See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
*
* @parameter expression="${alias}"
* @required
*/
private String alias;
/**
* Automatically verify a jar after signing it.
* <p/>
* See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
*
* @parameter expression="${verify}" default-value="false"
*/
private boolean verify;
/**
* Skip attaching the signed artifact. By default the signed artifact is attached.
* This is not a Mojo parameter as we shouldn't need this when using this mojo.
* Just needed when reusing the implementation. See MJAR-84 for discussions.
*/
private boolean skipAttachSignedArtifact;
/**
* Enable verbose.
* See <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/jarsigner.html#Options">options</a>.
*
* @parameter expression="${verbose}" default-value="false"
*/
private boolean verbose;
/**
* @component
*/
private MavenProjectHelper projectHelper;
/**
* The Maven project.
*
* @parameter expression="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* Classifier to use for the generated artifact.
* If not specified, the generated artifact becomes the primary artifact.
*
* @parameter expression="${classifier}"
*/
private String classifier;
public void execute()
throws MojoExecutionException
{
if ( skip )
{
getLog().info( "Skipping JAR signing for file: " + getJarFile().getAbsolutePath() );
return;
}
if ( project != null )
{
ArtifactHandler artifactHandler = project.getArtifact().getArtifactHandler();
if ( artifactHandler != null && !"java".equals( artifactHandler.getLanguage() ) )
{
getLog().debug( "Not executing jar:sign as the project is not a Java module" );
return;
}
}
// we use this mojo to check if there's a need to sign.
// If we sign and if we need to verify, we reuse it to check the signature
JarSignVerifyMojo verifyMojo = createJarSignVerifyMojo();
verifyMojo.setWorkingDir( workingDirectory );
verifyMojo.setBasedir( basedir );
File signedJarFile = signedjar != null ? signedjar : getJarFile();
verifyMojo.setVerbose( verbose );
verifyMojo.setJarPath( signedJarFile );
if ( signedJarFile.exists() )
{
verifyMojo.setErrorWhenNotSigned( false );
verifyMojo.execute();
}
if ( verifyMojo.isSigned() )
{
getLog().info( "JAR " + signedJarFile.getAbsoluteFile() + " is already signed. Skipping." );
return;
}
signJar();
if ( this.verify )
{
verifyMojo.setErrorWhenNotSigned( true );
verifyMojo.execute();
}
}
protected JarSignVerifyMojo createJarSignVerifyMojo()
{
return new JarSignVerifyMojo();
}
File getJarFile()
{
if ( jarPath != null )
{
return jarPath;
}
else
{
return AbstractJarMojo.getJarFile( basedir, finalName, null );
}
}
void signJar()
throws MojoExecutionException
{
List arguments = new ArrayList();
Commandline commandLine = new Commandline();
commandLine.setExecutable( getJarsignerPath() );
addArgIf( arguments, verbose, "-verbose" );
// I believe Commandline to add quotes where appropriate, although I haven't tested it enough.
// FIXME addArgIfNotEmpty will break those parameters containing a space.
// Look at webapp:gen-keystore for a way to fix that
addArgIfNotEmpty( arguments, "-keystore", this.keystore );
addArgIfNotEmpty( arguments, "-storepass", this.storepass );
addArgIfNotEmpty( arguments, "-keypass", this.keypass );
addArgIfNotEmpty( arguments, "-signedjar", this.signedjar );
addArgIfNotEmpty( arguments, "-storetype", this.type );
addArgIfNotEmpty( arguments, "-sigfile", this.sigfile );
arguments.add( getJarFile() );
addArgIf( arguments, alias != null, this.alias );
for ( Iterator it = arguments.iterator(); it.hasNext(); )
{
commandLine.createArgument().setValue( it.next().toString() );
}
commandLine.setWorkingDirectory( workingDirectory.getAbsolutePath() );
createParentDirIfNecessary( signedjar );
if ( signedjar == null )
{
getLog().debug( "Signing JAR in-place (overwritting original JAR)." );
}
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Executing: " + purgePassword( commandLine ) );
}
// jarsigner may ask for some input if the parameters are missing or incorrect.
// This should take care of it and make it fail gracefully
final InputStream inputStream = new InputStream()
{
public int read()
{
return -1;
}
};
StreamConsumer outConsumer = new StreamConsumer()
{
public void consumeLine( String line )
{
getLog().info( line );
}
};
final StringBuffer errBuffer = new StringBuffer();
StreamConsumer errConsumer = new StreamConsumer()
{
public void consumeLine( String line )
{
errBuffer.append( line );
getLog().warn( line );
}
};
try
{
int result = executeCommandLine( commandLine, inputStream, outConsumer, errConsumer );
if ( result != 0 )
{
throw new MojoExecutionException( "Result of " + purgePassword( commandLine ) +
" execution is: \'" + result + "\'." );
}
}
catch ( CommandLineException e )
{
throw new MojoExecutionException( "command execution failed", e );
}
// signed in place, no need to attach
if ( signedjar == null || skipAttachSignedArtifact )
{
return;
}
if ( classifier != null )
{
projectHelper.attachArtifact( project, "jar", classifier, signedjar );
}
else
{
project.getArtifact().setFile( signedjar );
}
}
private String purgePassword( Commandline commandLine )
{
String out = commandLine.toString();
if ( keypass != null && out.indexOf( keypass ) != -1 )
{
out = StringUtils.replace( out, keypass, "******" );
}
return out;
}
private void createParentDirIfNecessary( File file )
{
if ( file != null )
{
File fileDir = file.getParentFile();
if ( fileDir != null )
{ // not a relative path
boolean mkdirs = fileDir.mkdirs();
getLog().debug( "mdkirs: " + mkdirs + " " + fileDir );
}
}
}
// taken from JavadocReport then slightly refactored
// should probably share with other plugins that use $JAVA_HOME/bin tools
/**
* Get the path of jarsigner tool depending the OS.
*
* @return the path of the jarsigner tool
*/
private String getJarsignerPath()
{
return getJDKCommandPath( "jarsigner", getLog() );
}
private static String getJDKCommandPath( String command, Log logger )
{
String path = getJDKCommandExe( command ).getAbsolutePath();
logger.debug( command + " executable=[" + path + "]" );
return path;
}
private static File getJDKCommandExe( String command )
{
String fullCommand = command + ( SystemUtils.IS_OS_WINDOWS ? ".exe" : "" );
File exe;
// For IBM's JDK 1.2
if ( SystemUtils.IS_OS_AIX )
{
exe = new File( SystemUtils.getJavaHome() + "/../sh", fullCommand );
}
else if ( SystemUtils.IS_OS_MAC_OSX )
{
exe = new File( SystemUtils.getJavaHome() + "/bin", fullCommand );
}
else
{
exe = new File( SystemUtils.getJavaHome() + "/../bin", fullCommand );
}
return exe;
}
// Helper methods. Could/should be shared e.g. with JavadocReport
/**
* Convenience method to add an argument to the <code>command line</code>
* conditionally based on the given flag.
*
* @param arguments
* @param b the flag which controls if the argument is added or not.
* @param value the argument value to be added.
*/
private void addArgIf( List arguments, boolean b, String value )
{
if ( b )
{
arguments.add( value );
}
}
/**
* Convenience method to add an argument to the <code>command line</code>
* if the the value is not null or empty.
* <p/>
* Moreover, the value could be comma separated.
*
* @param arguments
* @param key the argument name.
* @param value the argument value to be added.
* @see #addArgIfNotEmpty(java.util.List,String,Object,boolean)
*/
private void addArgIfNotEmpty( List arguments, String key, Object value )
{
addArgIfNotEmpty( arguments, key, value, false );
}
/**
* Convenience method to add an argument to the <code>command line</code>
* if the the value is not null or empty.
* <p/>
* Moreover, the value could be comma separated.
*
* @param arguments
* @param key the argument name.
* @param value the argument value to be added.
* @param repeatKey repeat or not the key in the command line
*/
private void addArgIfNotEmpty( List arguments, String key, Object value, boolean repeatKey )
{
if ( value != null && !StringUtils.isEmpty( value.toString() ) )
{
arguments.add( key );
StringTokenizer token = new StringTokenizer( value.toString(), "," );
while ( token.hasMoreTokens() )
{
String current = token.nextToken().trim();
if ( !StringUtils.isEmpty( current ) )
{
arguments.add( current );
if ( token.hasMoreTokens() && repeatKey )
{
arguments.add( key );
}
}
}
}
}
//
// methods used for tests purposes - allow mocking and simulate automatic setters
//
protected int executeCommandLine( Commandline commandLine, InputStream inputStream, StreamConsumer stream1,
StreamConsumer stream2 )
throws CommandLineException
{
return CommandLineUtils.executeCommandLine( commandLine, inputStream, stream1, stream2 );
}
public void setWorkingDir( File workingDir )
{
this.workingDirectory = workingDir;
}
public void setBasedir( File basedir )
{
this.basedir = basedir;
}
public void setKeystore( String keystore )
{
this.keystore = keystore;
}
public void setKeypass( String keypass )
{
this.keypass = keypass;
}
public void setSignedJar( File signedjar )
{
this.signedjar = signedjar;
}
public void setAlias( String alias )
{
this.alias = alias;
}
// hiding for now - I don't think this is required to be seen
/*
public void setFinalName( String finalName )
{
this.finalName = finalName;
}
*/
public void setJarPath( File jarPath )
{
this.jarPath = jarPath;
}
public void setStorepass( String storepass )
{
this.storepass = storepass;
}
public void setSigFile( String sigfile )
{
this.sigfile = sigfile;
}
public void setType( String type )
{
this.type = type;
}
public void setVerbose( boolean verbose )
{
this.verbose = verbose;
}
public void setSkipAttachSignedArtifact( boolean skipAttachSignedArtifact )
{
this.skipAttachSignedArtifact = skipAttachSignedArtifact;
}
public void setProject( MavenProject project )
{
this.project = project;
}
public void setVerify( boolean verify )
{
this.verify = verify;
}
}