Skip to content

jar: Collect all exceptions thrown during a jar transformation #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,19 @@ dependencies {
compile "org.ow2.asm:asm-commons:$asmVersion"
compile "org.cadixdev:bombe:$bombeVersion"
compile "org.cadixdev:bombe-jar:$bombeVersion"

testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
}

processResources {
from 'LICENSE.txt'
}

test {
useJUnitPlatform()
}

task javadocJar(type: Jar, dependsOn: 'javadoc') {
from javadoc.destinationDir
classifier = 'javadoc'
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ inceptionYear = 2019
javaVersion = 1.8
asmVersion = 7.1
bombeVersion = 0.5.0-SNAPSHOT
junitVersion = 5.7.1
29 changes: 19 additions & 10 deletions src/main/java/org/cadixdev/atlas/Atlas.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package org.cadixdev.atlas;

import org.cadixdev.atlas.jar.JarFile;
import org.cadixdev.atlas.jar.JarTransformFailedException;
import org.cadixdev.atlas.util.CompositeClassProvider;
import org.cadixdev.atlas.util.JarRepacker;
import org.cadixdev.bombe.analysis.InheritanceProvider;
Expand Down Expand Up @@ -111,6 +112,8 @@ public Atlas install(final Function<AtlasTransformerContext, JarEntryTransformer
* @param output The output binary
* @throws IOException Should an issue occur reading the input JAR, or
* reading the output JAR
* @throws JarTransformFailedException if one or more transformers threw
* exceptions while processing a jar entry
* @see #run(JarFile, Path)
*/
public void run(final Path input, final Path output) throws IOException {
Expand All @@ -127,6 +130,8 @@ public void run(final Path input, final Path output) throws IOException {
* @param output The output binary
* @throws IOException Should an issue occur reading the input JAR, or
* reading the output JAR
* @throws JarTransformFailedException if one or more transformers threw
* exceptions while processing a jar entry
*/
public void run(final JarFile jar, final Path output) throws IOException {
// Create a classpath for the current JAR file
Expand All @@ -148,16 +153,20 @@ public void run(final JarFile jar, final Path output) throws IOException {
}

// Transform the JAR, and save to the output path
jar.transform(output, transformers);

JarRepacker.verifyJarManifest(output);

// Close the JarFiles we made earlier
for (final ClassProvider classProvider : classpath) {
if (classProvider == jar) continue;
if (!(classProvider instanceof Closeable)) continue;

((Closeable) classProvider).close();
try {
jar.transform(output, transformers);

JarRepacker.verifyJarManifest(output);
} finally {
// Close the JarFiles we made earlier
for (final ClassProvider classProvider : classpath) {
if (classProvider == jar)
continue;
if (!(classProvider instanceof Closeable))
continue;

((Closeable) classProvider).close();
}
}
}

Expand Down
45 changes: 38 additions & 7 deletions src/main/java/org/cadixdev/atlas/jar/JarFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
Expand Down Expand Up @@ -157,6 +156,8 @@ else if (name.endsWith(".class")) {
* @param export The JAR path to write to
* @param transformers The transformers to use
* @throws IOException Should an issue with reading or writing occur
* @throws JarTransformFailedException if one or more transformers threw
* exceptions while processing a jar entry
*/
public void transform(final Path export, final JarEntryTransformer... transformers) throws IOException {
final ExecutorService executorService = Executors.newWorkStealingPool();
Expand All @@ -176,10 +177,13 @@ public void transform(final Path export, final JarEntryTransformer... transforme
* @param executorService The executor service to use
* @param transformers The transformers to use
* @throws IOException Should an issue with reading or writing occur
* @throws JarTransformFailedException if one or more transformers threw
* exceptions while processing a jar entry
* @since 0.2.1
*/
public void transform(final Path export, final ExecutorService executorService, final JarEntryTransformer... transformers) throws IOException {
Files.deleteIfExists(export);
final Map<JarPath, Exception> failedLocations = new ConcurrentHashMap<>();
try (final FileSystem fs = NIOHelper.openZip(export, true)) {
final CompletableFuture<Void> future = CompletableFuture.allOf(this.walk().map(path -> CompletableFuture.runAsync(() -> {
try {
Expand All @@ -203,12 +207,23 @@ public void transform(final Path export, final ExecutorService executorService,
Files.write(outEntry, entry.getContents());
Files.setLastModifiedTime(outEntry, FileTime.fromMillis(entry.getTime()));
}
catch (final IOException ex) {
throw new CompletionException(ex);
catch (final Exception ex) {
failedLocations.put(path, ex);
}
}, executorService)).toArray(CompletableFuture[]::new));

future.get();
future.handle((value, error) -> {
// Capture errors
if (error != null) {
throw new RuntimeException(error);
}
return value;
}).get();

if (!failedLocations.isEmpty()) {
// Some entries failed to transform
throw new JarTransformFailedException("Some entries in " + this.getName() + " failed to be transformed", failedLocations);
}

// Add additions from transformers
for (final JarEntryTransformer transformer : transformers) {
Expand Down Expand Up @@ -254,6 +269,8 @@ public void transform(final Path export, final ExecutorService executorService,
*
* @param transformers The transformers to use
* @throws IOException Should an issue with reading occur
* @throws JarTransformFailedException if one or more transformers threw
* exceptions while processing a jar entry
* @since 0.2.2
*/
public void process(final JarEntryTransformer... transformers) throws IOException {
Expand All @@ -277,10 +294,13 @@ public void process(final JarEntryTransformer... transformers) throws IOExceptio
* @param executorService The executor service to use
* @param transformers The transformers to use
* @throws IOException Should an issue with reading occur
* @throws JarTransformFailedException if one or more transformers threw
* exceptions while processing a jar entry
* @since 0.2.2
*/
public void process(final ExecutorService executorService, final JarEntryTransformer... transformers)
throws IOException {
final Map<JarPath, Exception> failedLocations = new ConcurrentHashMap<>();
final CompletableFuture<Void> future = CompletableFuture.allOf(this.walk().map(path -> CompletableFuture.runAsync(() -> {
try {
// Get the entry
Expand All @@ -293,13 +313,24 @@ public void process(final ExecutorService executorService, final JarEntryTransfo
if (entry == null) return;
}
}
catch (final IOException ex) {
throw new CompletionException(ex);
catch (final Exception ex) {
failedLocations.put(path, ex);
}
}, executorService)).toArray(CompletableFuture[]::new));

try {
future.get();
future.handle((value, error) -> {
// Capture errors
if (error != null) {
throw new RuntimeException(error);
}
return value;
}).get();

if (!failedLocations.isEmpty()) {
// Some entries failed to transform
throw new JarTransformFailedException("Some entries in " + this.getName() + " failed to be transformed", failedLocations);
}
}
catch (final InterruptedException ex) {
throw new RuntimeException(ex);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

package org.cadixdev.atlas.jar;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
* An exception thrown when one or more entries in a jar fail to transform.
*
* @since 0.3.0
*/
public final class JarTransformFailedException extends IOException {

private static final long serialVersionUID = 3971759325502243118L;

private final Map<JarPath, Exception> failedPaths;

/**
* Create a new exception.
*
* @param message the user-visible message
* @param failedPaths a map from failed entry to
*/
public JarTransformFailedException(final String message, final Map<JarPath, Exception> failedPaths) {
super(message);
this.failedPaths = Collections.unmodifiableMap(new HashMap<>(failedPaths));
if (this.failedPaths.size() == 1) {
this.initCause(this.failedPaths.values().iterator().next());
}
}

/**
* Return an unmodifiable snapshot of a map of jar path to the exception
* thrown at the path.
*
* @return the failed paths
*/
public Map<JarPath, Exception> getFailedPaths() {
return this.failedPaths;
}

@Override
public String getMessage() {
final String superMessage = super.getMessage();
if (this.failedPaths.size() == 1) { // details already included in cause
return superMessage + " (in entry " + this.failedPaths.keySet().iterator().next().getName() + " )";
}

final StringBuilder message = new StringBuilder(superMessage == null ? "Failed to transform jar: " : superMessage);
for (final Map.Entry<JarPath, Exception> failure : this.failedPaths.entrySet()) {
message.append(System.lineSeparator())
.append("- ")
.append(failure.getKey().getName());
final String elementMessage = failure.getValue().getMessage();
if (elementMessage != null) {
message.append(": ")
.append(elementMessage);
}
}
return message.toString();
}
}
Loading