From e34ce0c5ff15c389a898a4614d42f34141e02fd6 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Fri, 29 Jan 2021 23:53:15 +0000 Subject: [PATCH 1/8] 0.2.1: Begin dev cycle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b863f28..9881524 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = javaVersion group = 'org.cadixdev' archivesBaseName = project.name.toLowerCase() -version = '0.2.0' +version = '0.2.1-SNAPSHOT' repositories { mavenCentral() From 1c1bf0173002b675b0fea5cb93a68ebc2d7ba0e6 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 4 May 2020 15:05:48 +0100 Subject: [PATCH 2/8] Don't eat exceptions during transform This addresses one of the issues raised in GH-2. --- src/main/java/org/cadixdev/atlas/jar/JarFile.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/cadixdev/atlas/jar/JarFile.java b/src/main/java/org/cadixdev/atlas/jar/JarFile.java index b9b48fd..1286ba2 100644 --- a/src/main/java/org/cadixdev/atlas/jar/JarFile.java +++ b/src/main/java/org/cadixdev/atlas/jar/JarFile.java @@ -197,7 +197,8 @@ public void transform(final Path export, final JarEntryTransformer... transforme catch (final IOException ioe) { throw ioe; } - catch (final Throwable ignored) { + catch (final Throwable cause) { + throw new RuntimeException(cause); } } } From 86628c35a4ca0e641332edd529ecc3a749ff3a64 Mon Sep 17 00:00:00 2001 From: Kyle Wood Date: Fri, 1 Jan 2021 23:23:05 -0800 Subject: [PATCH 3/8] Ensure output jars have META-INF/MANIFEST.MF as the first entry This ensures the output jar is compatible with the JDK's JarInputStream. --- src/main/java/org/cadixdev/atlas/Atlas.java | 3 + .../org/cadixdev/atlas/util/JarRepacker.java | 139 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/main/java/org/cadixdev/atlas/util/JarRepacker.java diff --git a/src/main/java/org/cadixdev/atlas/Atlas.java b/src/main/java/org/cadixdev/atlas/Atlas.java index 67e9428..e7795f7 100644 --- a/src/main/java/org/cadixdev/atlas/Atlas.java +++ b/src/main/java/org/cadixdev/atlas/Atlas.java @@ -8,6 +8,7 @@ import org.cadixdev.atlas.jar.JarFile; import org.cadixdev.atlas.util.CascadingClassProvider; +import org.cadixdev.atlas.util.JarRepacker; import org.cadixdev.bombe.analysis.InheritanceProvider; import org.cadixdev.bombe.asm.analysis.ClassProviderInheritanceProvider; import org.cadixdev.bombe.asm.jar.ClassProvider; @@ -103,6 +104,8 @@ 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); } /** diff --git a/src/main/java/org/cadixdev/atlas/util/JarRepacker.java b/src/main/java/org/cadixdev/atlas/util/JarRepacker.java new file mode 100644 index 0000000..ed74a24 --- /dev/null +++ b/src/main/java/org/cadixdev/atlas/util/JarRepacker.java @@ -0,0 +1,139 @@ +/* + * 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.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + +public final class JarRepacker { + + private JarRepacker() {} + + /** + * {@link JarInputStream} requires that if a {@code META-INF/MANIFEST.MF} record is present in a jar file, it must be + * the first entry. + *

+ * In order to maintain compatibility with the jars Atlas produces, this method will check first if + * the output jar has any manifest file at all, and if it does, if it is retrievable by {@link JarInputStream}. + *

+ * If the output jar does have a manifest file that {@link JarInputStream} can't access, then this method will repack + * the jar to fix the issue. For performance reasons Atlas remapping process remaps jar entries in parallel, and it + * uses the NIO zip file system API, which we have no control of. Since this repacking process is a simple copy it is + * still very fast (compared to the remapping operation). + * + * @param outputJar The jar produced by the atlas transformation. + * @throws IOException If an IO error occurs + */ + public static void verifyJarManifest(final Path outputJar) throws IOException { + final boolean maybeNeedsRepack; + try (final JarInputStream input = new JarInputStream(Files.newInputStream(outputJar))) { + maybeNeedsRepack = input.getManifest() == null; + } + if (maybeNeedsRepack) { + final boolean hasManifest; + try (final JarFile outputJarFile = new JarFile(outputJar.toFile())) { + hasManifest = outputJarFile.getManifest() != null; + } + if (hasManifest) { + fixJarManifest(outputJar); + } + } + } + + /** + * Given that the output jar needs to be fixed, repack the given jar with the {@code META-INF/MANIFEST.MF} file as + * the first entry. + * + * @param outputJar The file to repack. + * @throws IOException If an IO error occurs + * @see #verifyJarManifest(Path) + */ + private static void fixJarManifest(final Path outputJar) throws IOException { + final byte[] buffer = new byte[8192]; + + final Path tempOut = Files.createTempFile(outputJar.getParent(), "atlas", "jar"); + try { + try (final JarOutputStream out = new JarOutputStream(Files.newOutputStream(tempOut)); + final JarFile jarFile = new JarFile(outputJar.toFile())) { + + copyManifest(jarFile, out, buffer); + + final Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + final JarEntry currentEntry = entries.nextElement(); + if (currentEntry.getName().equals("META-INF/") || currentEntry.getName().equals("META-INF/MANIFEST.MF")) { + continue; + } + + out.putNextEntry(new ZipEntry(currentEntry.getName())); + try (final InputStream input = jarFile.getInputStream(currentEntry)) { + copy(input, out, buffer); + } finally { + out.closeEntry(); + } + } + } + + Files.move(tempOut, outputJar, REPLACE_EXISTING, ATOMIC_MOVE); + } finally { + Files.deleteIfExists(tempOut); + } + } + + /** + * Finds the manifest entry in the given {@code jarFile} and copies it into {@code out}. + * + * @param jarFile The input file to read the manifest from. + * @param out The output stream to write the manifest to. + * @param buffer The byte array to use as the copy buffer + * @throws IOException If an IO error occurs + */ + private static void copyManifest(final JarFile jarFile, final JarOutputStream out, final byte[] buffer) throws IOException { + out.putNextEntry(new ZipEntry("META-INF/")); + out.closeEntry(); + + final ZipEntry manifestEntry = jarFile.getEntry("META-INF/MANIFEST.MF"); + if (manifestEntry == null) { + // something weird happened, but don't error + return; + } + + out.putNextEntry(new ZipEntry(manifestEntry.getName())); + try (final InputStream entryInput = jarFile.getInputStream(manifestEntry)) { + copy(entryInput, out, buffer); + } finally { + out.closeEntry(); + } + } + + /** + * Copy all of the data from the {@code from} input to the {@code to} output. + * + * @param from The input to copy from + * @param to The output to copy to + * @param buffer The byte array to use as the copy buffer + * @throws IOException If an IO error occurs + */ + private static void copy(final InputStream from, final OutputStream to, final byte[] buffer) throws IOException { + int read; + while ((read = from.read(buffer)) != -1) { + to.write(buffer, 0, read); + } + } +} From 168b95ff288cc500c39efd53885a0863b14cce5d Mon Sep 17 00:00:00 2001 From: Kyle Wood Date: Sat, 2 Jan 2021 13:59:57 -0800 Subject: [PATCH 4/8] Improve and cleanup the jar repacker code This includes a few doc comment improvements, but also has some logic improvements as well. First, this will handle cases where MANIFEST.MF isn't all-caps, as it technically doesn't have to be. JarFile will find any file in META-INF/ named MANIFEST.MF, ignoring case. By using JarFile directly to get the manifest rather than looking for the entry ourselves we remove the possibility of that issue causing a failure. We also now retrieve the manifest first, so if that fails somehowe we don't create the empty META-INF/ entry. By returning `true` only when the entries have been set we prevent a silent failure in the copyManifest() method from causing an error downstream (that is, the META-INF/ entry getting skipped later even though it wasn't copied). --- .../org/cadixdev/atlas/util/JarRepacker.java | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/cadixdev/atlas/util/JarRepacker.java b/src/main/java/org/cadixdev/atlas/util/JarRepacker.java index ed74a24..b0da52f 100644 --- a/src/main/java/org/cadixdev/atlas/util/JarRepacker.java +++ b/src/main/java/org/cadixdev/atlas/util/JarRepacker.java @@ -16,6 +16,7 @@ import java.util.jar.JarFile; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; import java.util.zip.ZipEntry; import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; @@ -38,7 +39,7 @@ private JarRepacker() {} * still very fast (compared to the remapping operation). * * @param outputJar The jar produced by the atlas transformation. - * @throws IOException If an IO error occurs + * @throws IOException If an IO error occurs. */ public static void verifyJarManifest(final Path outputJar) throws IOException { final boolean maybeNeedsRepack; @@ -61,7 +62,7 @@ public static void verifyJarManifest(final Path outputJar) throws IOException { * the first entry. * * @param outputJar The file to repack. - * @throws IOException If an IO error occurs + * @throws IOException If an IO error occurs. * @see #verifyJarManifest(Path) */ private static void fixJarManifest(final Path outputJar) throws IOException { @@ -72,16 +73,17 @@ private static void fixJarManifest(final Path outputJar) throws IOException { try (final JarOutputStream out = new JarOutputStream(Files.newOutputStream(tempOut)); final JarFile jarFile = new JarFile(outputJar.toFile())) { - copyManifest(jarFile, out, buffer); + final boolean skipManifest = copyManifest(jarFile, out); final Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { final JarEntry currentEntry = entries.nextElement(); - if (currentEntry.getName().equals("META-INF/") || currentEntry.getName().equals("META-INF/MANIFEST.MF")) { + final String name = currentEntry.getName(); + if (skipManifest && (name.equals("META-INF/") || name.equalsIgnoreCase("META-INF/MANIFEST.MF"))) { continue; } - out.putNextEntry(new ZipEntry(currentEntry.getName())); + out.putNextEntry(new ZipEntry(name)); try (final InputStream input = jarFile.getInputStream(currentEntry)) { copy(input, out, buffer); } finally { @@ -101,34 +103,33 @@ private static void fixJarManifest(final Path outputJar) throws IOException { * * @param jarFile The input file to read the manifest from. * @param out The output stream to write the manifest to. - * @param buffer The byte array to use as the copy buffer - * @throws IOException If an IO error occurs + * @return {@code true} if the manifest file was copied successfully. + * @throws IOException If an IO error occurs. */ - private static void copyManifest(final JarFile jarFile, final JarOutputStream out, final byte[] buffer) throws IOException { + private static boolean copyManifest(final JarFile jarFile, final JarOutputStream out) throws IOException { + final Manifest manifest = jarFile.getManifest(); + if (manifest == null) { + // something weird happened, but don't error + return false; + } + out.putNextEntry(new ZipEntry("META-INF/")); out.closeEntry(); - final ZipEntry manifestEntry = jarFile.getEntry("META-INF/MANIFEST.MF"); - if (manifestEntry == null) { - // something weird happened, but don't error - return; - } + out.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); + manifest.write(out); + out.closeEntry(); - out.putNextEntry(new ZipEntry(manifestEntry.getName())); - try (final InputStream entryInput = jarFile.getInputStream(manifestEntry)) { - copy(entryInput, out, buffer); - } finally { - out.closeEntry(); - } + return true; } /** * Copy all of the data from the {@code from} input to the {@code to} output. * - * @param from The input to copy from - * @param to The output to copy to - * @param buffer The byte array to use as the copy buffer - * @throws IOException If an IO error occurs + * @param from The input to copy from. + * @param to The output to copy to. + * @param buffer The byte array to use as the copy buffer. + * @throws IOException If an IO error occurs. */ private static void copy(final InputStream from, final OutputStream to, final byte[] buffer) throws IOException { int read; From e55fc5413ee18d91b4189f903967df5a025d5cdc Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 2 Jan 2021 22:07:39 +0000 Subject: [PATCH 5/8] ocd: Minor codestyle corrections to jar repacker --- .../org/cadixdev/atlas/util/JarRepacker.java | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/cadixdev/atlas/util/JarRepacker.java b/src/main/java/org/cadixdev/atlas/util/JarRepacker.java index b0da52f..fc64302 100644 --- a/src/main/java/org/cadixdev/atlas/util/JarRepacker.java +++ b/src/main/java/org/cadixdev/atlas/util/JarRepacker.java @@ -6,6 +6,9 @@ package org.cadixdev.atlas.util; +import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -19,12 +22,17 @@ import java.util.jar.Manifest; import java.util.zip.ZipEntry; -import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; -import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; - +/** + * A helper class for repackaging jar files so that they're read by Java correctly, see {@link #verifyJarManifest(Path)} + * for more details. + * + * @author Kyle Wood + * @since 0.2.1 + */ public final class JarRepacker { - private JarRepacker() {} + private JarRepacker() { + } /** * {@link JarInputStream} requires that if a {@code META-INF/MANIFEST.MF} record is present in a jar file, it must be @@ -86,14 +94,16 @@ private static void fixJarManifest(final Path outputJar) throws IOException { out.putNextEntry(new ZipEntry(name)); try (final InputStream input = jarFile.getInputStream(currentEntry)) { copy(input, out, buffer); - } finally { + } + finally { out.closeEntry(); } } } Files.move(tempOut, outputJar, REPLACE_EXISTING, ATOMIC_MOVE); - } finally { + } + finally { Files.deleteIfExists(tempOut); } } @@ -137,4 +147,5 @@ private static void copy(final InputStream from, final OutputStream to, final by to.write(buffer, 0, read); } } + } From bb22f810d0db318f6ac9843529b3a198c47efacf Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 30 Jan 2021 00:06:14 +0000 Subject: [PATCH 6/8] Allow transformers to mark files for removal To do this, they should return null rather than an entry. This fixes GH-8. --- src/main/java/org/cadixdev/atlas/jar/JarFile.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/cadixdev/atlas/jar/JarFile.java b/src/main/java/org/cadixdev/atlas/jar/JarFile.java index 1286ba2..ebaf420 100644 --- a/src/main/java/org/cadixdev/atlas/jar/JarFile.java +++ b/src/main/java/org/cadixdev/atlas/jar/JarFile.java @@ -167,6 +167,10 @@ public void transform(final Path export, final JarEntryTransformer... transforme // Transform the entry for (final JarEntryTransformer transformer : transformers) { entry = entry.accept(transformer); + + // If a transformer wants to remove an entry, it should return null. + // TODO: document this in Bombe + if (entry == null) return null; } // Write to jar From f2c16aa65d7c31f3aa4a351921be00ae80fb9043 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 30 Jan 2021 00:50:34 +0000 Subject: [PATCH 7/8] Allow Atlases to use their own ExecutorService --- src/main/java/org/cadixdev/atlas/Atlas.java | 24 +++++++++++++- .../java/org/cadixdev/atlas/jar/JarFile.java | 31 ++++++++++++++----- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/cadixdev/atlas/Atlas.java b/src/main/java/org/cadixdev/atlas/Atlas.java index e7795f7..940a9b8 100644 --- a/src/main/java/org/cadixdev/atlas/Atlas.java +++ b/src/main/java/org/cadixdev/atlas/Atlas.java @@ -19,6 +19,8 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.function.Function; /** @@ -36,6 +38,22 @@ public class Atlas implements Closeable { private final List> transformers = new ArrayList<>(); private final List classpath = new ArrayList<>(); + private final ExecutorService executorService; + private final boolean manageExecutor; + + public Atlas(final ExecutorService executorService, final boolean manageExecutor) { + this.executorService = executorService; + this.manageExecutor = manageExecutor; + } + + public Atlas(final int parallelism) { + this(Executors.newWorkStealingPool(parallelism), true); + } + + public Atlas() { + this(Executors.newWorkStealingPool(), true); + } + /** * Adds the given JAR file to the Atlas classpath, allowing the * {@link InheritanceProvider inheritance provider} to have a more complete view @@ -103,7 +121,7 @@ public void run(final JarFile jar, final Path output) throws IOException { .toArray(JarEntryTransformer[]::new); // Transform the JAR, and save to the output path - jar.transform(output, transformers); + jar.transform(output, this.executorService, transformers); JarRepacker.verifyJarManifest(output); } @@ -115,6 +133,10 @@ public void run(final JarFile jar, final Path output) throws IOException { */ @Override public void close() throws IOException { + if (this.manageExecutor) { + this.executorService.shutdown(); + } + for (final JarFile jar : this.classpath) { jar.close(); } diff --git a/src/main/java/org/cadixdev/atlas/jar/JarFile.java b/src/main/java/org/cadixdev/atlas/jar/JarFile.java index ebaf420..8e17c1f 100644 --- a/src/main/java/org/cadixdev/atlas/jar/JarFile.java +++ b/src/main/java/org/cadixdev/atlas/jar/JarFile.java @@ -23,15 +23,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; -import java.util.HashSet; import java.util.Map; import java.util.Objects; -import java.util.Set; 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; +import java.util.concurrent.Executors; import java.util.jar.Manifest; import java.util.stream.Stream; @@ -156,13 +155,32 @@ else if (name.endsWith(".class")) { * @throws IOException Should an issue with reading or writing occur */ public void transform(final Path export, final JarEntryTransformer... transformers) throws IOException { + final ExecutorService executorService = Executors.newWorkStealingPool(); + try { + this.transform(export, executorService, transformers); + } + finally { + executorService.shutdown(); + } + } + + /** + * Transforms the JAR file, with the given {@link JarEntryTransformer}s, writing + * to the given output JAR path. + * + * @param export The JAR path to write to + * @param executorService The executor service to use + * @param transformers The transformers to use + * @throws IOException Should an issue with reading or writing occur + */ + public void transform(final Path export, final ExecutorService executorService, final JarEntryTransformer... transformers) throws IOException { Files.deleteIfExists(export); try (final FileSystem fs = NIOHelper.openZip(export, true)) { - final CompletableFuture future = CompletableFuture.allOf(this.walk().map(path -> CompletableFuture.supplyAsync(() -> { + final CompletableFuture future = CompletableFuture.allOf(this.walk().map(path -> CompletableFuture.runAsync(() -> { try { // Get the entry AbstractJarEntry entry = this.get(path); - if (entry == null) return null; + if (entry == null) return; // Transform the entry for (final JarEntryTransformer transformer : transformers) { @@ -170,7 +188,7 @@ public void transform(final Path export, final JarEntryTransformer... transforme // If a transformer wants to remove an entry, it should return null. // TODO: document this in Bombe - if (entry == null) return null; + if (entry == null) return; } // Write to jar @@ -186,8 +204,7 @@ public void transform(final Path export, final JarEntryTransformer... transforme catch (final IOException ex) { throw new CompletionException(ex); } - return null; - })).toArray(CompletableFuture[]::new)); + }, executorService)).toArray(CompletableFuture[]::new)); future.get(); } From acaae9ca9670962b557900ba56abb7eaad5c5744 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 30 Jan 2021 02:31:01 +0000 Subject: [PATCH 8/8] 0.2.1: Release Time --- build.gradle | 3 +- src/main/java/org/cadixdev/atlas/Atlas.java | 28 ++++++++++++++++++- .../java/org/cadixdev/atlas/jar/JarFile.java | 1 + 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9881524..b6fe227 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = javaVersion group = 'org.cadixdev' archivesBaseName = project.name.toLowerCase() -version = '0.2.1-SNAPSHOT' +version = '0.2.1' repositories { mavenCentral() @@ -39,6 +39,7 @@ artifacts { if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) { apply plugin: 'signing' signing { + useGpgCmd() required { !version.endsWith('-SNAPSHOT') && gradle.taskGraph.hasTask(tasks.uploadArchives) } sign configurations.archives } diff --git a/src/main/java/org/cadixdev/atlas/Atlas.java b/src/main/java/org/cadixdev/atlas/Atlas.java index 940a9b8..094a91a 100644 --- a/src/main/java/org/cadixdev/atlas/Atlas.java +++ b/src/main/java/org/cadixdev/atlas/Atlas.java @@ -41,15 +41,41 @@ public class Atlas implements Closeable { private final ExecutorService executorService; private final boolean manageExecutor; - public Atlas(final ExecutorService executorService, final boolean manageExecutor) { + /** + * Creates an Atlas with an associated executor service. + * + * @param executorService The executor service + * @param manageExecutor Whether to shutdown the executor service when closing the atlas + * @since 0.2.1 + */ + private Atlas(final ExecutorService executorService, final boolean manageExecutor) { this.executorService = executorService; this.manageExecutor = manageExecutor; } + /** + * Creates an Atlas with an associated executor service. + * + * @param executorService The executor service + * @since 0.2.1 + */ + public Atlas(final ExecutorService executorService) { + this(executorService, false); + } + + /** + * Creates an Atlas with a default executor service (made with {@link Executors#newWorkStealingPool(int)}). + * + * @param parallelism The targeted parallelism level + * @since 0.2.1 + */ public Atlas(final int parallelism) { this(Executors.newWorkStealingPool(parallelism), true); } + /** + * Creates an Atlas with a default executor service, {@link Executors#newWorkStealingPool()}. + */ public Atlas() { this(Executors.newWorkStealingPool(), true); } diff --git a/src/main/java/org/cadixdev/atlas/jar/JarFile.java b/src/main/java/org/cadixdev/atlas/jar/JarFile.java index 8e17c1f..cf45e0b 100644 --- a/src/main/java/org/cadixdev/atlas/jar/JarFile.java +++ b/src/main/java/org/cadixdev/atlas/jar/JarFile.java @@ -172,6 +172,7 @@ 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 + * @since 0.2.1 */ public void transform(final Path export, final ExecutorService executorService, final JarEntryTransformer... transformers) throws IOException { Files.deleteIfExists(export);