Skip to content

Commit

Permalink
Merge branch 'release-0.2.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
jamierocks committed Jan 30, 2021
2 parents 135c254 + acaae9c commit 7af0aad
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 9 deletions.
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ targetCompatibility = javaVersion

group = 'org.cadixdev'
archivesBaseName = project.name.toLowerCase()
version = '0.2.0'
version = '0.2.1'

repositories {
mavenCentral()
Expand Down Expand Up @@ -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
}
Expand Down
53 changes: 52 additions & 1 deletion src/main/java/org/cadixdev/atlas/Atlas.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,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;

/**
Expand All @@ -35,6 +38,48 @@ public class Atlas implements Closeable {
private final List<Function<AtlasTransformerContext, JarEntryTransformer>> transformers = new ArrayList<>();
private final List<JarFile> classpath = new ArrayList<>();

private final ExecutorService executorService;
private 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);
}

/**
* Adds the given JAR file to the Atlas classpath, allowing the
* {@link InheritanceProvider inheritance provider} to have a more complete view
Expand Down Expand Up @@ -102,7 +147,9 @@ 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);
}

/**
Expand All @@ -112,6 +159,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();
}
Expand Down
37 changes: 30 additions & 7 deletions src/main/java/org/cadixdev/atlas/jar/JarFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -156,17 +155,41 @@ 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
* @since 0.2.1
*/
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<Void> future = CompletableFuture.allOf(this.walk().map(path -> CompletableFuture.supplyAsync(() -> {
final CompletableFuture<Void> 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) {
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;
}

// Write to jar
Expand All @@ -182,8 +205,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();
}
Expand All @@ -197,7 +219,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);
}
}
}
Expand Down
151 changes: 151 additions & 0 deletions src/main/java/org/cadixdev/atlas/util/JarRepacker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* 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 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;
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.jar.Manifest;
import java.util.zip.ZipEntry;

/**
* 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() {
}

/**
* {@link JarInputStream} requires that if a {@code META-INF/MANIFEST.MF} record is present in a jar file, it must be
* the first entry.
* <p>
* 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}.
* <p>
* 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())) {

final boolean skipManifest = copyManifest(jarFile, out);

final Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
final JarEntry currentEntry = entries.nextElement();
final String name = currentEntry.getName();
if (skipManifest && (name.equals("META-INF/") || name.equalsIgnoreCase("META-INF/MANIFEST.MF"))) {
continue;
}

out.putNextEntry(new ZipEntry(name));
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.
* @return {@code true} if the manifest file was copied successfully.
* @throws IOException If an IO error occurs.
*/
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();

out.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
manifest.write(out);
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.
*/
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);
}
}

}

0 comments on commit 7af0aad

Please sign in to comment.