| 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 org.apache.maven.artifact.Artifact; |
| import org.apache.maven.execution.MavenSession; |
| import org.apache.maven.plugin.AbstractMojo; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.plugins.annotations.Component; |
| import org.apache.maven.plugins.annotations.Parameter; |
| import org.apache.maven.project.MavenProject; |
| import org.apache.maven.settings.Settings; |
| import org.apache.maven.shared.jarsigner.JarSigner; |
| import org.apache.maven.shared.jarsigner.JarSignerRequest; |
| import org.apache.maven.shared.jarsigner.JarSignerUtil; |
| import org.apache.maven.shared.utils.StringUtils; |
| import org.apache.maven.shared.utils.cli.Commandline; |
| import org.apache.maven.shared.utils.cli.javatool.JavaToolException; |
| import org.apache.maven.shared.utils.cli.javatool.JavaToolResult; |
| import org.apache.maven.shared.utils.io.FileUtils; |
| import org.apache.maven.toolchain.Toolchain; |
| import org.apache.maven.toolchain.ToolchainManager; |
| import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher; |
| import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.text.MessageFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.ResourceBundle; |
| import org.apache.maven.shared.utils.ReaderFactory; |
| |
| /** |
| * Maven Jarsigner Plugin base class. |
| * |
| * @author <a href="cs@schulte.it">Christian Schulte</a> |
| */ |
| public abstract class AbstractJarsignerMojo |
| extends AbstractMojo |
| { |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter( property = "jarsigner.verbose", defaultValue = "false" ) |
| private boolean verbose; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter( property = "jarsigner.keystore" ) |
| private String keystore; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter( property = "jarsigner.storetype" ) |
| private String storetype; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter( property = "jarsigner.storepass" ) |
| private String storepass; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter( property = "jarsigner.providerName" ) |
| private String providerName; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter( property = "jarsigner.providerClass" ) |
| private String providerClass; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter( property = "jarsigner.providerArg" ) |
| private String providerArg; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter( property = "jarsigner.alias" ) |
| private String alias; |
| |
| /** |
| * The maximum memory available to the JAR signer, e.g. <code>256M</code>. See <a |
| * href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/java.html#Xms">-Xmx</a> for more details. |
| */ |
| @Parameter( property = "jarsigner.maxMemory" ) |
| private String maxMemory; |
| |
| /** |
| * Archive to process. If set, neither the project artifact nor any attachments or archive sets are processed. |
| */ |
| @Parameter( property = "jarsigner.archive" ) |
| private File archive; |
| |
| /** |
| * The base directory to scan for JAR files using Ant-like inclusion/exclusion patterns. |
| * |
| * @since 1.1 |
| */ |
| @Parameter( property = "jarsigner.archiveDirectory" ) |
| 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>**/*.?ar</code> is used. |
| * |
| * @since 1.1 |
| */ |
| @Parameter |
| 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}. |
| * |
| * @since 1.1 |
| */ |
| @Parameter |
| private String[] excludes = {}; |
| |
| /** |
| * List of additional arguments to append to the jarsigner command line. |
| */ |
| @Parameter( property = "jarsigner.arguments" ) |
| private String[] arguments; |
| |
| /** |
| * Set to {@code true} to disable the plugin. |
| */ |
| @Parameter( property = "jarsigner.skip", defaultValue = "false" ) |
| private boolean skip; |
| |
| /** |
| * Controls processing of the main artifact produced by the project. |
| * |
| * @since 1.1 |
| */ |
| @Parameter( property = "jarsigner.processMainArtifact", defaultValue = "true" ) |
| private boolean processMainArtifact; |
| |
| /** |
| * Controls processing of project attachments. If enabled, attached artifacts that are no JAR/ZIP files will be |
| * automatically excluded from processing. |
| * |
| * @since 1.1 |
| */ |
| @Parameter( property = "jarsigner.processAttachedArtifacts", defaultValue = "true" ) |
| private boolean processAttachedArtifacts; |
| |
| /** |
| * Must be set to true if the password must be given via a protected |
| * authentication path such as a dedicated PIN reader. |
| * |
| * @since 1.3 |
| */ |
| @Parameter( property = "jarsigner.protectedAuthenticationPath", defaultValue = "false" ) |
| private boolean protectedAuthenticationPath; |
| |
| /** |
| * 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. |
| * |
| * @since 1.2 |
| */ |
| @Parameter |
| 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. |
| * |
| * @since 1.2 |
| */ |
| @Parameter |
| private String[] excludeClassifiers; |
| |
| /** |
| * The Maven project. |
| */ |
| @Parameter( defaultValue = "${project}", readonly = true, required = true ) |
| private MavenProject project; |
| |
| /** |
| * The Maven settings. |
| * |
| * @since 1.5 |
| */ |
| @Parameter( defaultValue = "${settings}", readonly = true, required = true ) |
| private Settings settings; |
| |
| /** |
| * Location of the working directory. |
| * |
| * @since 1.3 |
| */ |
| @Parameter( defaultValue = "${project.basedir}" ) |
| private File workingDirectory; |
| |
| /** |
| */ |
| @Component |
| private JarSigner jarSigner; |
| |
| /** |
| * The current build session instance. This is used for |
| * toolchain manager API calls. |
| * |
| * @since 1.3 |
| */ |
| @Parameter( defaultValue = "${session}", readonly = true, required = true ) |
| private MavenSession session; |
| |
| /** |
| * To obtain a toolchain if possible. |
| * |
| * @since 1.3 |
| */ |
| @Component |
| private ToolchainManager toolchainManager; |
| |
| /** |
| * @since 1.3.2 |
| */ |
| @Component( hint = "mng-4384" ) |
| private SecDispatcher securityDispatcher; |
| |
| public final void execute() |
| throws MojoExecutionException |
| { |
| if ( !this.skip ) |
| { |
| Toolchain toolchain = getToolchain(); |
| |
| if ( toolchain != null ) |
| { |
| getLog().info( "Toolchain in maven-jarsigner-plugin: " + toolchain ); |
| jarSigner.setToolchain( toolchain ); |
| } |
| |
| int processed = 0; |
| |
| if ( this.archive != null ) |
| { |
| processArchive( this.archive ); |
| processed++; |
| } |
| else |
| { |
| if ( processMainArtifact ) |
| { |
| processed += processArtifact( this.project.getArtifact() ) ? 1 : 0; |
| } |
| |
| if ( processAttachedArtifacts ) |
| { |
| Collection<String> includes = new HashSet<>(); |
| if ( includeClassifiers != null ) |
| { |
| includes.addAll( Arrays.asList( includeClassifiers ) ); |
| } |
| |
| Collection<String> excludes = new HashSet<>(); |
| if ( excludeClassifiers != null ) |
| { |
| excludes.addAll( Arrays.asList( excludeClassifiers ) ); |
| } |
| |
| for ( Artifact artifact : this.project.getAttachedArtifacts() ) |
| { |
| 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<File> 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 ( File jarFile : jarFiles ) |
| { |
| processArchive( jarFile ); |
| processed++; |
| } |
| } |
| } |
| |
| getLog().info( getMessage( "processed", processed ) ); |
| } |
| else |
| { |
| getLog().info( getMessage( "disabled", null ) ); |
| } |
| } |
| |
| /** |
| * Creates the jar signer request to be executed. |
| * |
| * @param archive the archive file to treat by jarsigner |
| * @return the request |
| * @throws MojoExecutionException if an exception occurs |
| * @since 1.3 |
| */ |
| protected abstract JarSignerRequest createRequest( File archive ) |
| throws MojoExecutionException; |
| |
| /** |
| * 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" ); |
| } |
| |
| String commandLineInfo = commandLine.toString(); |
| commandLineInfo = StringUtils.replace( commandLineInfo, this.storepass, "'*****'" ); |
| return commandLineInfo; |
| } |
| |
| public String getStoretype() |
| { |
| return storetype; |
| } |
| |
| public String getStorepass() |
| { |
| return storepass; |
| } |
| |
| /** |
| * 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 && JarSignerUtil.isZipFile( artifact.getFile() ); |
| } |
| |
| /** |
| * 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 ) ); |
| } |
| |
| JarSignerRequest request = createRequest( archive ); |
| request.setVerbose( verbose ); |
| request.setAlias( alias ); |
| request.setArchive( archive ); |
| request.setKeystore( keystore ); |
| request.setStoretype( storetype ); |
| request.setProviderArg( providerArg ); |
| request.setProviderClass( providerClass ); |
| request.setProviderName( providerName ); |
| request.setWorkingDirectory( workingDirectory ); |
| request.setMaxMemory( maxMemory ); |
| request.setProtectedAuthenticationPath( protectedAuthenticationPath ); |
| |
| // Preserves 'file.encoding' the plugin is executed with. |
| final List<String> additionalArguments = new ArrayList<>(); |
| |
| boolean fileEncodingSeen = false; |
| |
| if ( this.arguments != null ) |
| { |
| for ( final String argument : this.arguments ) |
| { |
| if ( argument.trim().startsWith( "-J-Dfile.encoding=" ) ) |
| { |
| fileEncodingSeen = true; |
| } |
| |
| additionalArguments.add( argument ); |
| } |
| } |
| |
| if ( !fileEncodingSeen ) |
| { |
| additionalArguments.add( "-J-Dfile.encoding=" + ReaderFactory.FILE_ENCODING ); |
| } |
| |
| // Adds proxy information. |
| if ( this.settings != null && this.settings.getActiveProxy() != null |
| && StringUtils.isNotEmpty( this.settings.getActiveProxy().getHost() ) ) |
| { |
| additionalArguments.add( "-J-Dhttp.proxyHost=" + this.settings.getActiveProxy().getHost() ); |
| additionalArguments.add( "-J-Dhttps.proxyHost=" + this.settings.getActiveProxy().getHost() ); |
| additionalArguments.add( "-J-Dftp.proxyHost=" + this.settings.getActiveProxy().getHost() ); |
| |
| if ( this.settings.getActiveProxy().getPort() > 0 ) |
| { |
| additionalArguments.add( "-J-Dhttp.proxyPort=" + this.settings.getActiveProxy().getPort() ); |
| additionalArguments.add( "-J-Dhttps.proxyPort=" + this.settings.getActiveProxy().getPort() ); |
| additionalArguments.add( "-J-Dftp.proxyPort=" + this.settings.getActiveProxy().getPort() ); |
| } |
| |
| if ( StringUtils.isNotEmpty( this.settings.getActiveProxy().getNonProxyHosts() ) ) |
| { |
| additionalArguments.add( "-J-Dhttp.nonProxyHosts=\"" |
| + this.settings.getActiveProxy().getNonProxyHosts() + "\"" ); |
| |
| additionalArguments.add( "-J-Dftp.nonProxyHosts=\"" |
| + this.settings.getActiveProxy().getNonProxyHosts() + "\"" ); |
| |
| } |
| } |
| |
| request.setArguments( !additionalArguments.isEmpty() |
| ? additionalArguments.toArray( new String[ additionalArguments.size() ] ) |
| : null ); |
| |
| // Special handling for passwords through the Maven Security Dispatcher |
| request.setStorepass( decrypt( storepass ) ); |
| |
| try |
| { |
| JavaToolResult result = jarSigner.execute( request ); |
| |
| Commandline commandLine = result.getCommandline(); |
| |
| int resultCode = result.getExitCode(); |
| |
| if ( resultCode != 0 ) |
| { |
| // CHECKSTYLE_OFF: LineLength |
| throw new MojoExecutionException( getMessage( "failure", getCommandlineInfo( commandLine ), resultCode ) ); |
| // CHECKSTYLE_ON: LineLength |
| } |
| |
| } |
| catch ( JavaToolException e ) |
| { |
| throw new MojoExecutionException( getMessage( "commandLineException", e.getMessage() ), e ); |
| } |
| } |
| |
| protected String decrypt( String encoded ) |
| throws MojoExecutionException |
| { |
| try |
| { |
| return securityDispatcher.decrypt( encoded ); |
| } |
| catch ( SecDispatcherException e ) |
| { |
| getLog().error( "error using security dispatcher: " + e.getMessage(), e ); |
| throw new MojoExecutionException( "error using security dispatcher: " + e.getMessage(), e ); |
| } |
| } |
| |
| /** |
| * 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 ); |
| } |
| |
| 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 } ); |
| } |
| |
| /** |
| * the part with ToolchainManager lookup once we depend on |
| * 2.0.9 (have it as prerequisite). Define as regular component field then. |
| * hint: check maven-compiler-plugin code |
| * |
| * @return Toolchain instance |
| */ |
| private Toolchain getToolchain() |
| { |
| Toolchain tc = null; |
| if ( toolchainManager != null ) |
| { |
| tc = toolchainManager.getToolchainFromBuildContext( "jdk", session ); |
| } |
| |
| return tc; |
| } |
| } |