Skip to content

Commit 930b7e4

Browse files
authored
[MJARSIGNER-72] Parallel signing for increased speed (#18)
Adding support for threadCount when signing jar files
1 parent 7e47f46 commit 930b7e4

File tree

6 files changed

+419
-74
lines changed

6 files changed

+419
-74
lines changed

src/main/java/org/apache/maven/plugins/jarsigner/AbstractJarsignerMojo.java

Lines changed: 80 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Collection;
2727
import java.util.HashSet;
2828
import java.util.List;
29+
import java.util.Optional;
2930
import java.util.ResourceBundle;
3031

3132
import org.apache.maven.artifact.Artifact;
@@ -279,65 +280,70 @@ public final void execute() throws MojoExecutionException {
279280
jarSigner.setToolchain(toolchain);
280281
}
281282

282-
int processed = 0;
283+
List<File> archives = findJarfiles();
284+
processArchives(archives);
285+
getLog().info(getMessage("processed", archives.size()));
286+
}
283287

288+
/**
289+
* Finds all jar files, by looking at the Maven project and user configuration.
290+
*
291+
* @return a List of File objects
292+
* @throws MojoExecutionException if it was not possible to build a list of jar files
293+
*/
294+
private List<File> findJarfiles() throws MojoExecutionException {
284295
if (this.archive != null) {
285-
processArchive(this.archive);
286-
processed++;
287-
} else {
288-
if (processMainArtifact) {
289-
processed += processArtifact(this.project.getArtifact()) ? 1 : 0;
290-
}
296+
// Only process this, but nothing more
297+
return Arrays.asList(this.archive);
298+
}
291299

292-
if (processAttachedArtifacts) {
293-
Collection<String> includes = new HashSet<>();
294-
if (includeClassifiers != null) {
295-
includes.addAll(Arrays.asList(includeClassifiers));
296-
}
300+
List<File> archives = new ArrayList<>();
301+
if (processMainArtifact) {
302+
getFileFromArtifact(this.project.getArtifact()).ifPresent(archives::add);
303+
}
297304

298-
Collection<String> excludes = new HashSet<>();
299-
if (excludeClassifiers != null) {
300-
excludes.addAll(Arrays.asList(excludeClassifiers));
301-
}
305+
if (processAttachedArtifacts) {
306+
Collection<String> includes = new HashSet<>();
307+
if (includeClassifiers != null) {
308+
includes.addAll(Arrays.asList(includeClassifiers));
309+
}
302310

303-
for (Artifact artifact : this.project.getAttachedArtifacts()) {
304-
if (!includes.isEmpty() && !includes.contains(artifact.getClassifier())) {
305-
continue;
306-
}
311+
Collection<String> excludes = new HashSet<>();
312+
if (excludeClassifiers != null) {
313+
excludes.addAll(Arrays.asList(excludeClassifiers));
314+
}
307315

308-
if (excludes.contains(artifact.getClassifier())) {
309-
continue;
310-
}
316+
for (Artifact artifact : this.project.getAttachedArtifacts()) {
317+
if (!includes.isEmpty() && !includes.contains(artifact.getClassifier())) {
318+
continue;
319+
}
311320

312-
processed += processArtifact(artifact) ? 1 : 0;
321+
if (excludes.contains(artifact.getClassifier())) {
322+
continue;
313323
}
324+
325+
getFileFromArtifact(artifact).ifPresent(archives::add);
326+
}
327+
} else {
328+
if (verbose) {
329+
getLog().info(getMessage("ignoringAttachments"));
314330
} else {
315-
if (verbose) {
316-
getLog().info(getMessage("ignoringAttachments"));
317-
} else {
318-
getLog().debug(getMessage("ignoringAttachments"));
319-
}
331+
getLog().debug(getMessage("ignoringAttachments"));
320332
}
333+
}
321334

322-
if (archiveDirectory != null) {
323-
String includeList = (includes != null) ? StringUtils.join(includes, ",") : null;
324-
String excludeList = (excludes != null) ? StringUtils.join(excludes, ",") : null;
325-
326-
List<File> jarFiles;
327-
try {
328-
jarFiles = FileUtils.getFiles(archiveDirectory, includeList, excludeList);
329-
} catch (IOException e) {
330-
throw new MojoExecutionException("Failed to scan archive directory for JARs: " + e.getMessage(), e);
331-
}
335+
if (archiveDirectory != null) {
336+
String includeList = (includes != null) ? StringUtils.join(includes, ",") : null;
337+
String excludeList = (excludes != null) ? StringUtils.join(excludes, ",") : null;
332338

333-
for (File jarFile : jarFiles) {
334-
processArchive(jarFile);
335-
processed++;
336-
}
339+
try {
340+
archives.addAll(FileUtils.getFiles(archiveDirectory, includeList, excludeList));
341+
} catch (IOException e) {
342+
throw new MojoExecutionException("Failed to scan archive directory for JARs: " + e.getMessage(), e);
337343
}
338344
}
339345

340-
getLog().info(getMessage("processed", processed));
346+
return archives;
341347
}
342348

343349
/**
@@ -358,7 +364,7 @@ public final void execute() throws MojoExecutionException {
358364
*
359365
* @param commandLine The {@code Commandline} to get a string representation of.
360366
* @return The string representation of {@code commandLine}.
361-
* @throws NullPointerException if {@code commandLine} is {@code null}.
367+
* @throws NullPointerException if {@code commandLine} is {@code null}
362368
*/
363369
protected String getCommandlineInfo(final Commandline commandLine) {
364370
if (commandLine == null) {
@@ -384,45 +390,39 @@ public String getStorepass() {
384390
* @param artifact The artifact to check, may be <code>null</code>.
385391
* @return <code>true</code> if the artifact looks like a ZIP file, <code>false</code> otherwise.
386392
*/
387-
private boolean isZipFile(final Artifact artifact) {
393+
private static boolean isZipFile(final Artifact artifact) {
388394
return artifact != null && artifact.getFile() != null && JarSignerUtil.isZipFile(artifact.getFile());
389395
}
390396

391397
/**
392-
* Processes a given artifact.
398+
* Examines an Artifact and extract the File object pointing to the Artifact jar file.
393399
*
394-
* @param artifact The artifact to process.
395-
* @return <code>true</code> if the artifact is a JAR and was processed, <code>false</code> otherwise.
396-
* @throws NullPointerException if {@code artifact} is {@code null}.
397-
* @throws MojoExecutionException if processing {@code artifact} fails.
400+
* @param artifact the artifact to examine
401+
* @return An Optional containing the File, or Optional.empty() if the File is not a jar file.
402+
* @throws NullPointerException if {@code artifact} is {@code null}
398403
*/
399-
private boolean processArtifact(final Artifact artifact) throws MojoExecutionException {
404+
private Optional<File> getFileFromArtifact(final Artifact artifact) {
400405
if (artifact == null) {
401406
throw new NullPointerException("artifact");
402407
}
403408

404-
boolean processed = false;
405-
406409
if (isZipFile(artifact)) {
407-
processArchive(artifact.getFile());
408-
409-
processed = true;
410-
} else {
411-
if (this.verbose) {
412-
getLog().info(getMessage("unsupported", artifact));
413-
} else if (getLog().isDebugEnabled()) {
414-
getLog().debug(getMessage("unsupported", artifact));
415-
}
410+
return Optional.of(artifact.getFile());
416411
}
417412

418-
return processed;
413+
if (this.verbose) {
414+
getLog().info(getMessage("unsupported", artifact));
415+
} else if (getLog().isDebugEnabled()) {
416+
getLog().debug(getMessage("unsupported", artifact));
417+
}
418+
return Optional.empty();
419419
}
420420

421421
/**
422422
* Pre-processes a given archive.
423423
*
424424
* @param archive The archive to process, must not be <code>null</code>.
425-
* @throws MojoExecutionException If pre-processing failed.
425+
* @throws MojoExecutionException if pre-processing failed
426426
*/
427427
protected void preProcessArchive(final File archive) throws MojoExecutionException {
428428
// Default implementation does nothing
@@ -437,14 +437,26 @@ protected void validateParameters() throws MojoExecutionException {
437437
// Default implementation does nothing
438438
}
439439

440+
/**
441+
* Process (sign/verify) a list of archives.
442+
*
443+
* @param archives list of jar files to process
444+
* @throws MojoExecutionException if an error occurs during the processing of archives
445+
*/
446+
protected void processArchives(List<File> archives) throws MojoExecutionException {
447+
for (File file : archives) {
448+
processArchive(file);
449+
}
450+
}
451+
440452
/**
441453
* Processes a given archive.
442454
*
443455
* @param archive The archive to process.
444-
* @throws NullPointerException if {@code archive} is {@code null}.
445-
* @throws MojoExecutionException if processing {@code archive} fails.
456+
* @throws NullPointerException if {@code archive} is {@code null}
457+
* @throws MojoExecutionException if processing {@code archive} fails
446458
*/
447-
private void processArchive(final File archive) throws MojoExecutionException {
459+
protected final void processArchive(final File archive) throws MojoExecutionException {
448460
if (archive == null) {
449461
throw new NullPointerException("archive");
450462
}

src/main/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojo.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
import java.io.File;
2222
import java.io.IOException;
2323
import java.time.Duration;
24+
import java.util.List;
25+
import java.util.concurrent.Callable;
26+
import java.util.concurrent.ExecutionException;
27+
import java.util.concurrent.ExecutorService;
28+
import java.util.concurrent.Executors;
29+
import java.util.concurrent.Future;
30+
import java.util.stream.Collectors;
2431

2532
import org.apache.maven.plugin.MojoExecutionException;
2633
import org.apache.maven.plugins.annotations.LifecyclePhase;
@@ -115,6 +122,17 @@ public class JarsignerSignMojo extends AbstractJarsignerMojo {
115122
@Parameter(property = "jarsigner.maxRetryDelaySeconds", defaultValue = "0")
116123
private int maxRetryDelaySeconds;
117124

125+
/**
126+
* Maximum number of parallel threads to use when signing jar files. Increases performance when signing multiple jar
127+
* files, especially when network operations are used during signing, for example when using a Time Stamp Authority
128+
* or network based PKCS11 HSM solution for storing code signing keys. Note: the logging from the signing process
129+
* will be interleaved, and harder to read, when using many threads.
130+
*
131+
* @since 3.1.0
132+
*/
133+
@Parameter(property = "jarsigner.threadCount", defaultValue = "1")
134+
private int threadCount;
135+
118136
/** Current WaitStrategy, to allow for sleeping after a signing failure. */
119137
private WaitStrategy waitStrategy = this::defaultWaitStrategy;
120138

@@ -156,6 +174,11 @@ protected void validateParameters() throws MojoExecutionException {
156174
getLog().warn(getMessage("invalidMaxRetryDelaySeconds", maxRetryDelaySeconds));
157175
maxRetryDelaySeconds = 0;
158176
}
177+
178+
if (threadCount < 1) {
179+
getLog().warn(getMessage("invalidThreadCount", threadCount));
180+
threadCount = 1;
181+
}
159182
}
160183

161184
/**
@@ -174,12 +197,42 @@ protected JarSignerRequest createRequest(File archive) throws MojoExecutionExcep
174197
return request;
175198
}
176199

200+
/**
201+
* {@inheritDoc} Processing of files may be parallelized for increased performance.
202+
*/
203+
@Override
204+
protected void processArchives(List<File> archives) throws MojoExecutionException {
205+
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
206+
List<Future<Void>> futures = archives.stream()
207+
.map(file -> executor.submit((Callable<Void>) () -> {
208+
processArchive(file);
209+
return null;
210+
}))
211+
.collect(Collectors.toList());
212+
try {
213+
for (Future<Void> future : futures) {
214+
future.get(); // Wait for completion. Result ignored, but may raise any Exception
215+
}
216+
} catch (InterruptedException e) {
217+
Thread.currentThread().interrupt();
218+
throw new MojoExecutionException("Thread interrupted while waiting for jarsigner to complete", e);
219+
} catch (ExecutionException e) {
220+
if (e.getCause() instanceof MojoExecutionException) {
221+
throw (MojoExecutionException) e.getCause();
222+
}
223+
throw new MojoExecutionException("Error processing archives", e);
224+
} finally {
225+
// Shutdown of thread pool. If an Exception occurred, remaining threads will be aborted "best effort"
226+
executor.shutdownNow();
227+
}
228+
}
229+
177230
/**
178231
* {@inheritDoc}
179232
*
180233
* Will retry signing up to maxTries times if it fails.
181234
*
182-
* @throws MojoExecutionException If all signing attempts fail.
235+
* @throws MojoExecutionException if all signing attempts fail
183236
*/
184237
@Override
185238
protected void executeJarSigner(JarSigner jarSigner, JarSignerRequest request)

src/main/resources/jarsigner.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ failure = Failed executing ''{0}'' - exitcode {1,number}
2626
archiveNotSigned = Archive ''{0}'' is not signed
2727
invalidMaxTries = Invalid maxTries value. Was ''{0}'' but should be >= 1
2828
invalidMaxRetryDelaySeconds = Invalid maxRetryDelaySeconds value. Was ''{0}'' but should be >= 0
29+
invalidThreadCount = Invalid threadCount value. Was ''{0}'' but should be >= 1

0 commit comments

Comments
 (0)