blob: f8bf4d5cb7afbd24d8a4e4e4f79430d94d29f89e [file] [log] [blame]
/*
* 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.
*/
package org.apache.maven.plugins.jarsigner;
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.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.ReaderFactory;
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;
/**
* 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>&#42;&#42;/&#42;.?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;
}
}