blob: 1981fd49aebea11835de1a1256d58ce4c07f46b0 [file] [log] [blame]
package org.apache.maven.plugins.jarsigner;
/*
* 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 java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.zip.ZipInputStream;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.Os;
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;
/**
* Maven Jarsigner Plugin base class.
*
* @author <a href="cs@schulte.it">Christian Schulte</a>
* @version $Id$
*/
public abstract class AbstractJarsignerMojo
extends AbstractMojo
{
/**
* See <a href="http://java.sun.com/javase/6/docs/technotes/tools/windows/jarsigner.html#Options">options</a>.
*
* @parameter expression="${jarsigner.verbose}" default-value="false"
*/
private boolean verbose;
/**
* The maximum memory available to the JAR signer, e.g. <code>256M</code>. See <a
* href="http://java.sun.com/javase/6/docs/technotes/tools/windows/java.html#Xms">-Xmx</a> for more details.
*
* @parameter expression="${jarsigner.maxMemory}"
*/
private String maxMemory;
/**
* Archive to process. If set, neither the project artifact nor any attachments or archive sets are processed.
*
* @parameter expression="${jarsigner.archive}"
*/
private File archive;
/**
* The base directory to scan for JAR files using Ant-like inclusion/exclusion patterns.
*
* @parameter expression="${jarsigner.archiveDirectory}"
* @since 1.1
*/
private File archiveDirectory;
/**
* The Ant-like inclusion patterns used to select JAR files to process. The patterns must be relative to the
* directory given by the parameter {@link #archiveDirectory}. By default, the pattern
* <code>&#42;&#42;/&#42;.?ar</code> is used.
*
* @parameter
* @since 1.1
*/
private String[] includes = { "**/*.?ar" };
/**
* The Ant-like exclusion patterns used to exclude JAR files from processing. The patterns must be relative to the
* directory given by the parameter {@link #archiveDirectory}.
*
* @parameter
* @since 1.1
*/
private String[] excludes = {};
/**
* List of additional arguments to append to the jarsigner command line.
*
* @parameter expression="${jarsigner.arguments}"
*/
private String[] arguments;
/**
* Set to {@code true} to disable the plugin.
*
* @parameter expression="${jarsigner.skip}" default-value="false"
*/
private boolean skip;
/**
* Controls processing of the main artifact produced by the project.
*
* @parameter expression="${jarsigner.processMainArtifact}" default-value="true"
* @since 1.1
*/
private boolean processMainArtifact;
/**
* Controls processing of project attachments. If enabled, attached artifacts that are no JAR/ZIP files will be
* automatically excluded from processing.
*
* @parameter expression="${jarsigner.processAttachedArtifacts}" default-value="true"
* @since 1.1
*/
private boolean processAttachedArtifacts;
/**
* Controls processing of project attachments.
*
* @parameter expression="${jarsigner.attachments}"
* @deprecated As of version 1.1 in favor of the new parameter <code>processAttachedArtifacts</code>.
*/
private Boolean attachments;
/**
* A set of artifact classifiers describing the project attachments that should be processed. This parameter is only
* relevant if {@link #processAttachedArtifacts} is <code>true</code>. If empty, all attachments are included.
*
* @parameter
* @since 1.2
*/
private String[] includeClassifiers;
/**
* A set of artifact classifiers describing the project attachments that should not be processed. This parameter is
* only relevant if {@link #processAttachedArtifacts} is <code>true</code>. If empty, no attachments are excluded.
*
* @parameter
* @since 1.2
*/
private String[] excludeClassifiers;
/**
* The Maven project.
*
* @parameter default-value="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* The path to the jarsigner we are going to use.
*/
private String executable;
public final void execute()
throws MojoExecutionException
{
if ( !this.skip )
{
this.executable = getExecutable();
int processed = 0;
if ( this.archive != null )
{
processArchive( this.archive );
processed++;
}
else
{
if ( processMainArtifact )
{
processed += processArtifact( this.project.getArtifact() ) ? 1 : 0;
}
if ( processAttachedArtifacts && !Boolean.FALSE.equals( attachments ) )
{
Collection includes = new HashSet();
if ( includeClassifiers != null )
{
includes.addAll( Arrays.asList( includeClassifiers ) );
}
Collection excludes = new HashSet();
if ( excludeClassifiers != null )
{
excludes.addAll( Arrays.asList( excludeClassifiers ) );
}
for ( Iterator it = this.project.getAttachedArtifacts().iterator(); it.hasNext(); )
{
final Artifact artifact = (Artifact) it.next();
if ( !includes.isEmpty() && !includes.contains( artifact.getClassifier() ) )
{
continue;
}
if ( excludes.contains( artifact.getClassifier() ) )
{
continue;
}
processed += processArtifact( artifact ) ? 1 : 0;
}
}
else
{
if ( verbose )
{
getLog().info( getMessage( "ignoringAttachments" ) );
}
else
{
getLog().debug( getMessage( "ignoringAttachments" ) );
}
}
if ( archiveDirectory != null )
{
String includeList = ( includes != null ) ? StringUtils.join( includes, "," ) : null;
String excludeList = ( excludes != null ) ? StringUtils.join( excludes, "," ) : null;
List jarFiles;
try
{
jarFiles = FileUtils.getFiles( archiveDirectory, includeList, excludeList );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Failed to scan archive directory for JARs: "
+ e.getMessage(), e );
}
for ( Iterator it = jarFiles.iterator(); it.hasNext(); )
{
File jarFile = (File) it.next();
processArchive( jarFile );
processed++;
}
}
}
getLog().info( getMessage( "processed", new Integer( processed ) ) );
}
else
{
getLog().info( getMessage( "disabled", null ) );
}
}
/**
* Gets the {@code Commandline} to execute for a given Java archive taking a command line prepared for executing
* jarsigner.
*
* @param archive The Java archive to get a {@code Commandline} to execute for.
* @param commandLine A {@code Commandline} prepared for executing jarsigner without any arguments.
*
* @return A {@code Commandline} for executing jarsigner with {@code archive}.
*
* @throws NullPointerException if {@code archive} or {@code commandLine} is {@code null}.
*/
protected abstract Commandline getCommandline( final File archive, final Commandline commandLine );
/**
* Gets a string representation of a {@code Commandline}.
* <p>This method creates the string representation by calling {@code commandLine.toString()} by default.</p>
*
* @param commandLine The {@code Commandline} to get a string representation of.
*
* @return The string representation of {@code commandLine}.
*
* @throws NullPointerException if {@code commandLine} is {@code null}.
*/
protected String getCommandlineInfo( final Commandline commandLine )
{
if ( commandLine == null )
{
throw new NullPointerException( "commandLine" );
}
return commandLine.toString();
}
/**
* Checks whether the specified artifact is a ZIP file.
*
* @param artifact The artifact to check, may be <code>null</code>.
*
* @return <code>true</code> if the artifact looks like a ZIP file, <code>false</code> otherwise.
*/
private boolean isZipFile( final Artifact artifact )
{
return artifact != null && artifact.getFile() != null && isZipFile( artifact.getFile() );
}
/**
* Checks whether the specified file is a JAR file. For our purposes, a ZIP file is a ZIP stream with at least one
* entry.
*
* @param file The file to check, must not be <code>null</code>.
* @return <code>true</code> if the file looks like a ZIP file, <code>false</code> otherwise.
*/
private boolean isZipFile( final File file )
{
try
{
ZipInputStream zis = new ZipInputStream( new FileInputStream( file ) );
try
{
return zis.getNextEntry() != null;
}
finally
{
zis.close();
}
}
catch ( Exception e )
{
// ignore, will fail below
}
return false;
}
/**
* Processes a given artifact.
*
* @param artifact The artifact to process.
* @return <code>true</code> if the artifact is a JAR and was processed, <code>false</code> otherwise.
*
* @throws NullPointerException if {@code artifact} is {@code null}.
* @throws MojoExecutionException if processing {@code artifact} fails.
*/
private boolean processArtifact( final Artifact artifact )
throws MojoExecutionException
{
if ( artifact == null )
{
throw new NullPointerException( "artifact" );
}
boolean processed = false;
if ( isZipFile( artifact ) )
{
processArchive( artifact.getFile() );
processed = true;
}
else
{
if ( this.verbose )
{
getLog().info( getMessage( "unsupported", artifact ) );
}
else if ( getLog().isDebugEnabled() )
{
getLog().debug( getMessage( "unsupported", artifact ) );
}
}
return processed;
}
/**
* Pre-processes a given archive.
*
* @param archive The archive to process, must not be <code>null</code>.
* @throws MojoExecutionException If pre-processing failed.
*/
protected void preProcessArchive( final File archive )
throws MojoExecutionException
{
// default does nothing
}
/**
* Processes a given archive.
*
* @param archive The archive to process.
* @throws NullPointerException if {@code archive} is {@code null}.
* @throws MojoExecutionException if processing {@code archive} fails.
*/
private void processArchive( final File archive )
throws MojoExecutionException
{
if ( archive == null )
{
throw new NullPointerException( "archive" );
}
preProcessArchive( archive );
if ( this.verbose )
{
getLog().info( getMessage( "processing", archive ) );
}
else if ( getLog().isDebugEnabled() )
{
getLog().debug( getMessage( "processing", archive ) );
}
Commandline commandLine = new Commandline();
commandLine.setExecutable( this.executable );
commandLine.setWorkingDirectory( this.project.getBasedir() );
if ( this.verbose )
{
commandLine.createArg().setValue( "-verbose" );
}
if ( StringUtils.isNotEmpty( maxMemory ) )
{
commandLine.createArg().setValue( "-J-Xmx" + maxMemory );
}
if ( this.arguments != null )
{
commandLine.addArguments( this.arguments );
}
commandLine = getCommandline( archive, commandLine );
try
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( getMessage( "command", getCommandlineInfo( commandLine ) ) );
}
final int result = CommandLineUtils.executeCommandLine( commandLine,
new InputStream()
{
public int read()
{
return -1;
}
}, new StreamConsumer()
{
public void consumeLine( final String line )
{
if ( verbose )
{
getLog().info( line );
}
else
{
getLog().debug( line );
}
}
}, new StreamConsumer()
{
public void consumeLine( final String line )
{
getLog().warn( line );
}
} );
if ( result != 0 )
{
throw new MojoExecutionException( getMessage( "failure", getCommandlineInfo( commandLine ),
new Integer( result ) ) );
}
}
catch ( CommandLineException e )
{
throw new MojoExecutionException( getMessage( "commandLineException", getCommandlineInfo( commandLine ) ),
e );
}
}
/**
* Locates the executable for the jarsigner tool.
*
* @return The executable of the jarsigner tool, never <code>null<code>.
*/
private String getExecutable()
{
String command = "jarsigner" + ( Os.isFamily( Os.FAMILY_WINDOWS ) ? ".exe" : "" );
String executable =
findExecutable( command, System.getProperty( "java.home" ), new String[] { "../bin", "bin", "../sh" } );
if ( executable == null )
{
try
{
Properties env = CommandLineUtils.getSystemEnvVars();
String[] variables = { "JDK_HOME", "JAVA_HOME" };
for ( int i = 0; i < variables.length && executable == null; i++ )
{
executable =
findExecutable( command, env.getProperty( variables[i] ), new String[] { "bin", "sh" } );
}
}
catch ( IOException e )
{
if ( getLog().isDebugEnabled() )
{
getLog().warn( "Failed to retrieve environment variables, cannot search for " + command, e );
}
else
{
getLog().warn( "Failed to retrieve environment variables, cannot search for " + command );
}
}
}
if ( executable == null )
{
executable = command;
}
return executable;
}
/**
* Finds the specified command in any of the given sub directories of the specified JDK/JRE home directory.
*
* @param command The command to find, must not be <code>null</code>.
* @param homeDir The home directory to search in, may be <code>null</code>.
* @param subDirs The sub directories of the home directory to search in, must not be <code>null</code>.
* @return The (absolute) path to the command if found, <code>null</code> otherwise.
*/
private String findExecutable( String command, String homeDir, String[] subDirs )
{
if ( StringUtils.isNotEmpty( homeDir ) )
{
for ( int i = 0; i < subDirs.length; i++ )
{
File file = new File( new File( homeDir, subDirs[i] ), command );
if ( file.isFile() )
{
return file.getAbsolutePath();
}
}
}
return null;
}
/**
* Gets a message for a given key from the resource bundle backing the implementation.
*
* @param key The key of the message to return.
* @param args Arguments to format the message with or {@code null}.
*
* @return The message with key {@code key} from the resource bundle backing the implementation.
*
* @throws NullPointerException if {@code key} is {@code null}.
* @throws java.util.MissingResourceException if there is no message available matching {@code key} or accessing
* the resource bundle fails.
*/
private String getMessage( final String key, final Object[] args )
{
if ( key == null )
{
throw new NullPointerException( "key" );
}
return new MessageFormat( ResourceBundle.getBundle( "jarsigner" ).getString( key ) ).format( args );
}
private String getMessage( final String key )
{
return getMessage( key, null );
}
private String getMessage( final String key, final Object arg )
{
return getMessage( key, new Object[] { arg } );
}
private String getMessage( final String key, final Object arg1, final Object arg2 )
{
return getMessage( key, new Object[] { arg1, arg2 } );
}
}