From 930b7e495674a5a9fbff290425ce964526605ba8 Mon Sep 17 00:00:00 2001 From: schedin Date: Fri, 15 Dec 2023 21:42:00 +0100 Subject: [PATCH] [MJARSIGNER-72] Parallel signing for increased speed (#18) Adding support for threadCount when signing jar files --- .../jarsigner/AbstractJarsignerMojo.java | 148 ++++++------ .../plugins/jarsigner/JarsignerSignMojo.java | 55 ++++- src/main/resources/jarsigner.properties | 1 + .../JarsignerSignMojoParallelTest.java | 218 ++++++++++++++++++ .../jarsigner/JarsignerSignMojoRetryTest.java | 10 +- .../jarsigner/JarsignerSignMojoTest.java | 61 +++++ 6 files changed, 419 insertions(+), 74 deletions(-) create mode 100644 src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoParallelTest.java diff --git a/src/main/java/org/apache/maven/plugins/jarsigner/AbstractJarsignerMojo.java b/src/main/java/org/apache/maven/plugins/jarsigner/AbstractJarsignerMojo.java index 41f6678..30b9b7a 100644 --- a/src/main/java/org/apache/maven/plugins/jarsigner/AbstractJarsignerMojo.java +++ b/src/main/java/org/apache/maven/plugins/jarsigner/AbstractJarsignerMojo.java @@ -26,6 +26,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.ResourceBundle; import org.apache.maven.artifact.Artifact; @@ -279,65 +280,70 @@ public final void execute() throws MojoExecutionException { jarSigner.setToolchain(toolchain); } - int processed = 0; + List archives = findJarfiles(); + processArchives(archives); + getLog().info(getMessage("processed", archives.size())); + } + /** + * Finds all jar files, by looking at the Maven project and user configuration. + * + * @return a List of File objects + * @throws MojoExecutionException if it was not possible to build a list of jar files + */ + private List findJarfiles() throws MojoExecutionException { if (this.archive != null) { - processArchive(this.archive); - processed++; - } else { - if (processMainArtifact) { - processed += processArtifact(this.project.getArtifact()) ? 1 : 0; - } + // Only process this, but nothing more + return Arrays.asList(this.archive); + } - if (processAttachedArtifacts) { - Collection includes = new HashSet<>(); - if (includeClassifiers != null) { - includes.addAll(Arrays.asList(includeClassifiers)); - } + List archives = new ArrayList<>(); + if (processMainArtifact) { + getFileFromArtifact(this.project.getArtifact()).ifPresent(archives::add); + } - Collection excludes = new HashSet<>(); - if (excludeClassifiers != null) { - excludes.addAll(Arrays.asList(excludeClassifiers)); - } + if (processAttachedArtifacts) { + Collection includes = new HashSet<>(); + if (includeClassifiers != null) { + includes.addAll(Arrays.asList(includeClassifiers)); + } - for (Artifact artifact : this.project.getAttachedArtifacts()) { - if (!includes.isEmpty() && !includes.contains(artifact.getClassifier())) { - continue; - } + Collection excludes = new HashSet<>(); + if (excludeClassifiers != null) { + excludes.addAll(Arrays.asList(excludeClassifiers)); + } - if (excludes.contains(artifact.getClassifier())) { - continue; - } + for (Artifact artifact : this.project.getAttachedArtifacts()) { + if (!includes.isEmpty() && !includes.contains(artifact.getClassifier())) { + continue; + } - processed += processArtifact(artifact) ? 1 : 0; + if (excludes.contains(artifact.getClassifier())) { + continue; } + + getFileFromArtifact(artifact).ifPresent(archives::add); + } + } else { + if (verbose) { + getLog().info(getMessage("ignoringAttachments")); } else { - if (verbose) { - getLog().info(getMessage("ignoringAttachments")); - } else { - getLog().debug(getMessage("ignoringAttachments")); - } + 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); - } + if (archiveDirectory != null) { + String includeList = (includes != null) ? StringUtils.join(includes, ",") : null; + String excludeList = (excludes != null) ? StringUtils.join(excludes, ",") : null; - for (File jarFile : jarFiles) { - processArchive(jarFile); - processed++; - } + try { + archives.addAll(FileUtils.getFiles(archiveDirectory, includeList, excludeList)); + } catch (IOException e) { + throw new MojoExecutionException("Failed to scan archive directory for JARs: " + e.getMessage(), e); } } - getLog().info(getMessage("processed", processed)); + return archives; } /** @@ -358,7 +364,7 @@ public final void execute() throws MojoExecutionException { * * @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}. + * @throws NullPointerException if {@code commandLine} is {@code null} */ protected String getCommandlineInfo(final Commandline commandLine) { if (commandLine == null) { @@ -384,45 +390,39 @@ public String getStorepass() { * @param artifact The artifact to check, may be null. * @return true if the artifact looks like a ZIP file, false otherwise. */ - private boolean isZipFile(final Artifact artifact) { + private static boolean isZipFile(final Artifact artifact) { return artifact != null && artifact.getFile() != null && JarSignerUtil.isZipFile(artifact.getFile()); } /** - * Processes a given artifact. + * Examines an Artifact and extract the File object pointing to the Artifact jar file. * - * @param artifact The artifact to process. - * @return true if the artifact is a JAR and was processed, false otherwise. - * @throws NullPointerException if {@code artifact} is {@code null}. - * @throws MojoExecutionException if processing {@code artifact} fails. + * @param artifact the artifact to examine + * @return An Optional containing the File, or Optional.empty() if the File is not a jar file. + * @throws NullPointerException if {@code artifact} is {@code null} */ - private boolean processArtifact(final Artifact artifact) throws MojoExecutionException { + private Optional getFileFromArtifact(final Artifact artifact) { 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 Optional.of(artifact.getFile()); } - return processed; + if (this.verbose) { + getLog().info(getMessage("unsupported", artifact)); + } else if (getLog().isDebugEnabled()) { + getLog().debug(getMessage("unsupported", artifact)); + } + return Optional.empty(); } /** * Pre-processes a given archive. * * @param archive The archive to process, must not be null. - * @throws MojoExecutionException If pre-processing failed. + * @throws MojoExecutionException if pre-processing failed */ protected void preProcessArchive(final File archive) throws MojoExecutionException { // Default implementation does nothing @@ -437,14 +437,26 @@ protected void validateParameters() throws MojoExecutionException { // Default implementation does nothing } + /** + * Process (sign/verify) a list of archives. + * + * @param archives list of jar files to process + * @throws MojoExecutionException if an error occurs during the processing of archives + */ + protected void processArchives(List archives) throws MojoExecutionException { + for (File file : archives) { + processArchive(file); + } + } + /** * 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. + * @throws NullPointerException if {@code archive} is {@code null} + * @throws MojoExecutionException if processing {@code archive} fails */ - private void processArchive(final File archive) throws MojoExecutionException { + protected final void processArchive(final File archive) throws MojoExecutionException { if (archive == null) { throw new NullPointerException("archive"); } diff --git a/src/main/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojo.java b/src/main/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojo.java index 2b82ec4..7ebf498 100644 --- a/src/main/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojo.java +++ b/src/main/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojo.java @@ -21,6 +21,13 @@ import java.io.File; import java.io.IOException; import java.time.Duration; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.stream.Collectors; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; @@ -115,6 +122,17 @@ public class JarsignerSignMojo extends AbstractJarsignerMojo { @Parameter(property = "jarsigner.maxRetryDelaySeconds", defaultValue = "0") private int maxRetryDelaySeconds; + /** + * Maximum number of parallel threads to use when signing jar files. Increases performance when signing multiple jar + * files, especially when network operations are used during signing, for example when using a Time Stamp Authority + * or network based PKCS11 HSM solution for storing code signing keys. Note: the logging from the signing process + * will be interleaved, and harder to read, when using many threads. + * + * @since 3.1.0 + */ + @Parameter(property = "jarsigner.threadCount", defaultValue = "1") + private int threadCount; + /** Current WaitStrategy, to allow for sleeping after a signing failure. */ private WaitStrategy waitStrategy = this::defaultWaitStrategy; @@ -156,6 +174,11 @@ protected void validateParameters() throws MojoExecutionException { getLog().warn(getMessage("invalidMaxRetryDelaySeconds", maxRetryDelaySeconds)); maxRetryDelaySeconds = 0; } + + if (threadCount < 1) { + getLog().warn(getMessage("invalidThreadCount", threadCount)); + threadCount = 1; + } } /** @@ -174,12 +197,42 @@ protected JarSignerRequest createRequest(File archive) throws MojoExecutionExcep return request; } + /** + * {@inheritDoc} Processing of files may be parallelized for increased performance. + */ + @Override + protected void processArchives(List archives) throws MojoExecutionException { + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + List> futures = archives.stream() + .map(file -> executor.submit((Callable) () -> { + processArchive(file); + return null; + })) + .collect(Collectors.toList()); + try { + for (Future future : futures) { + future.get(); // Wait for completion. Result ignored, but may raise any Exception + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new MojoExecutionException("Thread interrupted while waiting for jarsigner to complete", e); + } catch (ExecutionException e) { + if (e.getCause() instanceof MojoExecutionException) { + throw (MojoExecutionException) e.getCause(); + } + throw new MojoExecutionException("Error processing archives", e); + } finally { + // Shutdown of thread pool. If an Exception occurred, remaining threads will be aborted "best effort" + executor.shutdownNow(); + } + } + /** * {@inheritDoc} * * Will retry signing up to maxTries times if it fails. * - * @throws MojoExecutionException If all signing attempts fail. + * @throws MojoExecutionException if all signing attempts fail */ @Override protected void executeJarSigner(JarSigner jarSigner, JarSignerRequest request) diff --git a/src/main/resources/jarsigner.properties b/src/main/resources/jarsigner.properties index 1100958..7146b16 100644 --- a/src/main/resources/jarsigner.properties +++ b/src/main/resources/jarsigner.properties @@ -26,3 +26,4 @@ failure = Failed executing ''{0}'' - exitcode {1,number} archiveNotSigned = Archive ''{0}'' is not signed invalidMaxTries = Invalid maxTries value. Was ''{0}'' but should be >= 1 invalidMaxRetryDelaySeconds = Invalid maxRetryDelaySeconds value. Was ''{0}'' but should be >= 0 +invalidThreadCount = Invalid threadCount value. Was ''{0}'' but should be >= 1 diff --git a/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoParallelTest.java b/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoParallelTest.java new file mode 100644 index 0000000..47aff1c --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoParallelTest.java @@ -0,0 +1,218 @@ +/* + * 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.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; +import org.apache.maven.shared.jarsigner.JarSigner; +import org.apache.maven.shared.jarsigner.JarSignerSignRequest; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.apache.maven.plugins.jarsigner.TestJavaToolResults.RESULT_ERROR; +import static org.apache.maven.plugins.jarsigner.TestJavaToolResults.RESULT_OK; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.*; + +public class JarsignerSignMojoParallelTest { + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + private MavenProject project = mock(MavenProject.class); + private JarSigner jarSigner = mock(JarSigner.class); + private File projectDir; + private Map configuration = new LinkedHashMap<>(); + private MojoTestCreator mojoTestCreator; + private ExecutorService executor; + private Log log; + + @Before + public void setUp() throws Exception { + projectDir = folder.newFolder("dummy-project"); + configuration.put("processMainArtifact", "false"); + mojoTestCreator = + new MojoTestCreator(JarsignerSignMojo.class, project, projectDir, jarSigner); + log = mock(Log.class); + mojoTestCreator.setLog(log); + executor = + Executors.newSingleThreadExecutor(namedThreadFactory(getClass().getSimpleName())); + } + + @After + public void tearDown() { + executor.shutdown(); + } + + @Test(timeout = 30000) + public void test10Files2Parallel() throws Exception { + configuration.put("archiveDirectory", createArchives(10).getPath()); + configuration.put("threadCount", "2"); + + // Make one jar file wait until some external event happens and let nine pass + Semaphore semaphore = new Semaphore(9); + when(jarSigner.execute(isA(JarSignerSignRequest.class))).then(invocation -> { + semaphore.acquire(); + return RESULT_OK; + }); + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + Future future = executor.submit(() -> { + mojo.execute(); + return null; + }); + + // Wait until 10 invocation of execute() has happened (nine files are done and one are hanging) + verify(jarSigner, timeout(Duration.ofSeconds(10).toMillis()).times(10)).execute(any()); + // Even though 10 invocations of execute() have happened, mojo is not yet done executing (it is waiting for one) + assertFalse(future.isDone()); + + semaphore.release(); // Release the one waiting jar file + future.get(10, TimeUnit.SECONDS); // Wait for entire Mojo to finish + assertTrue(future.isDone()); + } + + @Test(timeout = 30000) + public void test10Files2Parallel3Hanging() throws Exception { + configuration.put("archiveDirectory", createArchives(10).getPath()); + configuration.put("threadCount", "2"); + + // Make three jar files wait until some external event happens and let seven pass + Semaphore semaphore = new Semaphore(7); + when(jarSigner.execute(isA(JarSignerSignRequest.class))).then(invocation -> { + semaphore.acquire(); + return RESULT_OK; + }); + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + Future future = executor.submit(() -> { + mojo.execute(); + return null; + }); + + // Wait until 9 invocations to execute has happened (2 is ongoing and 1 has not yet happened) + verify(jarSigner, timeout(Duration.ofSeconds(10).toMillis()).times(9)).execute(any()); + assertFalse(future.isDone()); + + semaphore.release(); // Release one waiting jar file + + // Wait until 10 invocation to execute has happened (8 are done and 2 are hanging) + verify(jarSigner, timeout(Duration.ofSeconds(10).toMillis()).times(10)).execute(any()); + + semaphore.release(2); // Release last two jar files + future.get(10, TimeUnit.SECONDS); // Wait for entire Mojo to finish + assertTrue(future.isDone()); + } + + @Test(timeout = 30000) + public void test10Files1Parallel() throws Exception { + configuration.put("archiveDirectory", createArchives(10).getPath()); + configuration.put("threadCount", "1"); + + // Make one jar file wait until some external event happens and let nine pass + Semaphore semaphore = new Semaphore(9); + when(jarSigner.execute(isA(JarSignerSignRequest.class))).then(invocation -> { + semaphore.acquire(); + return RESULT_OK; + }); + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + Future future = executor.submit(() -> { + mojo.execute(); + return null; + }); + + // Wait until 10 invocation to execute has happened (nine has finished and one is hanging). + verify(jarSigner, timeout(Duration.ofSeconds(10).toMillis()).times(10)).execute(any()); + assertFalse(future.isDone()); + + semaphore.release(); // Release the one waiting jar file + future.get(10, TimeUnit.SECONDS); // Wait for entire Mojo to finish + assertTrue(future.isDone()); + } + + @Test(timeout = 30000) + public void test10Files2ParallelOneFail() throws Exception { + configuration.put("archiveDirectory", createArchives(10).getPath()); + configuration.put("threadCount", "2"); + + when(jarSigner.execute(isA(JarSignerSignRequest.class))) + .thenReturn(RESULT_OK) + .thenReturn(RESULT_OK) + .thenReturn(RESULT_ERROR) + .thenReturn(RESULT_OK); + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + MojoExecutionException mojoException = assertThrows(MojoExecutionException.class, () -> { + mojo.execute(); + }); + + assertThat(mojoException.getMessage(), containsString(String.valueOf("Failed executing 'jarsigner "))); + } + + @Test + public void testInvalidThreadCount() throws Exception { + Artifact mainArtifact = TestArtifacts.createJarArtifact(projectDir, "my-project.jar"); + when(project.getArtifact()).thenReturn(mainArtifact); + when(jarSigner.execute(any(JarSignerSignRequest.class))).thenReturn(RESULT_OK); + configuration.put("processMainArtifact", "true"); + configuration.put("threadCount", "0"); // Setting an "invalid" value + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + mojo.execute(); + + verify(jarSigner, times(1)).execute(any()); + verify(log).warn(contains("Invalid threadCount value")); + verify(log).warn(contains("Was '0'")); + } + + private File createArchives(int numberOfArchives) throws IOException { + File archiveDirectory = new File(projectDir, "my_archive_dir"); + archiveDirectory.mkdir(); + for (int i = 0; i < numberOfArchives; i++) { + TestArtifacts.createDummyZipFile(new File(archiveDirectory, "archive" + i + ".jar")); + } + return archiveDirectory; + } + + private static ThreadFactory namedThreadFactory(String threadNamePrefix) { + return r -> new Thread(r, threadNamePrefix + "-Thread"); + } +} diff --git a/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoRetryTest.java b/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoRetryTest.java index 54929f8..feba11c 100644 --- a/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoRetryTest.java +++ b/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoRetryTest.java @@ -62,7 +62,7 @@ public class JarsignerSignMojoRetryTest { private MavenProject project = mock(MavenProject.class); private JarSigner jarSigner = mock(JarSigner.class); private WaitStrategy waitStrategy = mock(WaitStrategy.class); - private File dummyMavenProjectDir; + private File projectDir; private Map configuration = new LinkedHashMap<>(); private Log log; private MojoTestCreator mojoTestCreator; @@ -71,12 +71,12 @@ public class JarsignerSignMojoRetryTest { public void setUp() throws Exception { originalLocale = Locale.getDefault(); Locale.setDefault(Locale.ENGLISH); // For English ResourceBundle to test log messages - dummyMavenProjectDir = folder.newFolder("dummy-project"); - mojoTestCreator = new MojoTestCreator( - JarsignerSignMojo.class, project, dummyMavenProjectDir, jarSigner); + projectDir = folder.newFolder("dummy-project"); + mojoTestCreator = + new MojoTestCreator(JarsignerSignMojo.class, project, projectDir, jarSigner); log = mock(Log.class); mojoTestCreator.setLog(log); - Artifact mainArtifact = TestArtifacts.createJarArtifact(dummyMavenProjectDir, "my-project.jar"); + Artifact mainArtifact = TestArtifacts.createJarArtifact(projectDir, "my-project.jar"); when(project.getArtifact()).thenReturn(mainArtifact); } diff --git a/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoTest.java b/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoTest.java index f2122d5..0c62e11 100644 --- a/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoTest.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import org.apache.maven.artifact.Artifact; @@ -34,6 +35,7 @@ import org.apache.maven.shared.utils.cli.javatool.JavaToolException; import org.apache.maven.toolchain.Toolchain; import org.apache.maven.toolchain.ToolchainManager; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -50,6 +52,7 @@ import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.contains; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -61,6 +64,7 @@ public class JarsignerSignMojoTest { @Rule public TemporaryFolder folder = new TemporaryFolder(); + private Locale originalLocale; private MavenProject project = mock(MavenProject.class); private JarSigner jarSigner = mock(JarSigner.class); private File projectDir; @@ -70,6 +74,8 @@ public class JarsignerSignMojoTest { @Before public void setUp() throws Exception { + originalLocale = Locale.getDefault(); + Locale.setDefault(Locale.ENGLISH); // For English ResourceBundle to test log messages projectDir = folder.newFolder("dummy-project"); mojoTestCreator = new MojoTestCreator(JarsignerSignMojo.class, project, projectDir, jarSigner); @@ -77,6 +83,11 @@ public void setUp() throws Exception { mojoTestCreator.setLog(log); } + @After + public void tearDown() { + Locale.setDefault(originalLocale); + } + /** Standard Java project with nothing special configured */ @Test public void testStandardJavaProject() throws Exception { @@ -401,4 +412,54 @@ public void testSetCustomFileEncoding() throws Exception { .execute(MockitoHamcrest.argThat( RequestMatchers.hasArguments(new String[] {"-J-Dfile.encoding=ISO-8859-1", "argument2"}))); } + + /** + * Test what is logged when verbose=true. The sign-mojo.html documentation indicates that the verbose flag should + * be sent in to the jarsigner command. That is true, but in addition to this it is also (undocumented) used to + * control the level of some logging events. + */ + @Test + public void testLoggingVerboseTrue() throws Exception { + when(log.isDebugEnabled()).thenReturn(true); + Artifact mainArtifact = TestArtifacts.createPomArtifact(projectDir, "pom.xml"); + when(project.getArtifact()).thenReturn(mainArtifact); + configuration.put("processAttachedArtifacts", "false"); + File archiveDirectory = new File(projectDir, "my_archive_dir"); + archiveDirectory.mkdir(); + TestArtifacts.createDummyZipFile(new File(archiveDirectory, "archive1.jar")); + configuration.put("archiveDirectory", archiveDirectory.getPath()); + configuration.put("verbose", "true"); + when(jarSigner.execute(any(JarSignerSignRequest.class))).thenReturn(RESULT_OK); + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + mojo.execute(); + + verify(log, times(1)).info(contains("Unsupported artifact ")); + verify(log, times(1)).info(contains("Forcibly ignoring attached artifacts")); + verify(log, times(1)).info(contains("Processing ")); + verify(log, times(1)).info(contains("1 archive(s) processed")); + } + + /** Test what is logged when verbose=false */ + @Test + public void testLoggingVerboseFalse() throws Exception { + when(log.isDebugEnabled()).thenReturn(true); + Artifact mainArtifact = TestArtifacts.createPomArtifact(projectDir, "pom.xml"); + when(project.getArtifact()).thenReturn(mainArtifact); + configuration.put("processAttachedArtifacts", "false"); + File archiveDirectory = new File(projectDir, "my_archive_dir"); + archiveDirectory.mkdir(); + TestArtifacts.createDummyZipFile(new File(archiveDirectory, "archive1.jar")); + configuration.put("archiveDirectory", archiveDirectory.getPath()); + configuration.put("verbose", "false"); + when(jarSigner.execute(any(JarSignerSignRequest.class))).thenReturn(RESULT_OK); + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + mojo.execute(); + + verify(log, times(1)).debug(contains("Unsupported artifact ")); + verify(log, times(1)).debug(contains("Forcibly ignoring attached artifacts")); + verify(log, times(1)).debug(contains("Processing ")); + verify(log, times(1)).info(contains("1 archive(s) processed")); + } }