diff --git a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/AbstractJarEntry.java b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/AbstractJarEntry.java
index 2dd771f..8b6a285 100644
--- a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/AbstractJarEntry.java
+++ b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/AbstractJarEntry.java
@@ -32,6 +32,7 @@
import java.io.IOException;
import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
/**
@@ -42,11 +43,44 @@
*/
public abstract class AbstractJarEntry {
+ /**
+ * A {@link #getVersion()} value for jar entries that are at the base
+ * version in the jar.
+ */
+ public static final int UNVERSIONED = -1;
+
+ private static final String META_INF = "META-INF/";
+ private static final String VERSIONS_PREFIX = META_INF + "versions/";
+
protected final String name;
protected final long time;
+ private String unversionedName;
+ private int version = UNVERSIONED;
private String packageName;
private String simpleName;
+ /**
+ * Create a new jar entry for a specific multi-release variant.
+ *
+ *
If {@code unversionedName} starts with {@code META-INF}, it will be
+ * treated as being in the base version no matter what value is provided for
+ * {@code version}, to match the behavior of the JDK's {@link JarFile}.
+ *
+ * @param version the Java version number to associate this entry with
+ * @param unversionedName the name without any versioned prefix
+ * @param time the time the entry was created at
+ */
+ protected AbstractJarEntry(final int version, final String unversionedName, final long time) {
+ if (version == UNVERSIONED || unversionedName.startsWith(META_INF)) {
+ this.name = unversionedName;
+ } else {
+ this.version = version;
+ this.unversionedName = unversionedName;
+ this.name = VERSIONS_PREFIX + version + '/' + unversionedName;
+ }
+ this.time = time;
+ }
+
protected AbstractJarEntry(final String name, final long time) {
this.name = name;
this.time = time;
@@ -55,6 +89,8 @@ protected AbstractJarEntry(final String name, final long time) {
/**
* Gets the fully-qualified name of the jar entry.
*
+ * This method does not have any special handling for multi-release jars.
+ *
* @return The name
*/
public final String getName() {
@@ -70,6 +106,48 @@ public final long getTime() {
return this.time;
}
+ /**
+ * Get the name of this entry, as it will be seen by a multi-release-aware
+ * jar handler.
+ *
+ * When a file path is in the {@code META-INF/versions/} folder but does
+ * not provide a valid multi-release version, it will be treated as if it
+ * were an ordinary, un-versioned resource.
+ *
+ * This will always handle multi-release paths, even when the
+ * {@code Multi-Release} manifest attribute is set to false.
+ *
+ * @return the full entry name, without any version prefix
+ */
+ public final String getUnversionedName() {
+ if (this.unversionedName != null) return this.unversionedName;
+
+ if (!this.name.startsWith(VERSIONS_PREFIX)) {
+ return this.unversionedName = this.name;
+ }
+ // /
+ final String trimmed = this.name.substring(VERSIONS_PREFIX.length());
+ final int divider = trimmed.indexOf('/');
+ if (divider == -1) { // malformed, ignore
+ return this.unversionedName = this.name;
+ }
+
+ final String version = trimmed.substring(0, divider);
+ final String unversioned = trimmed.substring(divider + 1);
+ try {
+ if (!unversioned.startsWith(META_INF)) { // Files already within META-INF cannot be versioned
+ final int parsedVersion = Integer.parseInt(version);
+ if (parsedVersion >= 0) {
+ this.version = parsedVersion;
+ return this.unversionedName = unversioned;
+ }
+ }
+ } catch (final NumberFormatException ignored) { // invalid integer, treat as unversioned
+ // fall through
+ }
+ return this.unversionedName = this.name;
+ }
+
/**
* Gets the package that contains the jar entry, an empty
* string if in the root package.
@@ -78,9 +156,10 @@ public final long getTime() {
*/
public final String getPackage() {
if (this.packageName != null) return this.packageName;
- final int index = this.name.lastIndexOf('/');
+ final String name = this.getUnversionedName();
+ final int index = name.lastIndexOf('/');
if (index == -1) return this.packageName = "";
- return this.packageName = this.name.substring(0, index);
+ return this.packageName = name.substring(0, index);
}
/**
@@ -92,12 +171,28 @@ public final String getSimpleName() {
if (this.simpleName != null) return this.simpleName;
final int packageLength = this.getPackage().isEmpty() ? -1 : this.getPackage().length();
final int extensionLength = this.getExtension().isEmpty() ? -1 : this.getExtension().length();
- return this.simpleName = this.name.substring(
+ final String name = this.getUnversionedName();
+ return this.simpleName = name.substring(
packageLength + 1,
- this.name.length() - (extensionLength + 1)
+ name.length() - (extensionLength + 1)
);
}
+ /**
+ * If this is a multi-release variant of a class file in a multi-release
+ * jar, the version associated with this variant.
+ *
+ * @return the version, or {@link #UNVERSIONED} if this is the base version,
+ * or a file that would not be interpreted as a multi-release variant
+ * within the version folder.
+ * @see #getUnversionedName() for a description of the conditions on multi-release jars
+ */
+ public int getVersion() {
+ if (this.unversionedName != null) return this.version;
+ this.getUnversionedName(); // initialize versions
+ return this.version;
+ }
+
/**
* Gets the extension of the jar entry.
*
@@ -132,9 +227,9 @@ public final void write(final JarOutputStream jos) throws IOException {
/**
* Processes the jar entry with the given transformer.
*
- * @param vistor The transformer
+ * @param visitor The transformer
* @return The jar entry
*/
- public abstract AbstractJarEntry accept(final JarEntryTransformer vistor);
+ public abstract AbstractJarEntry accept(final JarEntryTransformer visitor);
}
diff --git a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarClassEntry.java b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarClassEntry.java
index 51fdbac..3ed0049 100644
--- a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarClassEntry.java
+++ b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarClassEntry.java
@@ -42,6 +42,11 @@ public class JarClassEntry extends AbstractJarEntry {
private final byte[] contents;
+ public JarClassEntry(final int version, final String unversionedName, final long time, final byte[] contents) {
+ super(version, unversionedName, time);
+ this.contents = contents;
+ }
+
public JarClassEntry(final String name, final long time, final byte[] contents) {
super(name, time);
this.contents = contents;
@@ -58,8 +63,8 @@ public final byte[] getContents() {
}
@Override
- public final JarClassEntry accept(final JarEntryTransformer vistor) {
- return vistor.transform(this);
+ public final JarClassEntry accept(final JarEntryTransformer visitor) {
+ return visitor.transform(this);
}
}
diff --git a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarManifestEntry.java b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarManifestEntry.java
index f55f012..4f66817 100644
--- a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarManifestEntry.java
+++ b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarManifestEntry.java
@@ -78,8 +78,8 @@ public final byte[] getContents() {
}
@Override
- public JarManifestEntry accept(final JarEntryTransformer vistor) {
- return vistor.transform(this);
+ public JarManifestEntry accept(final JarEntryTransformer visitor) {
+ return visitor.transform(this);
}
}
diff --git a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarResourceEntry.java b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarResourceEntry.java
index 65f94ae..490bf38 100644
--- a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarResourceEntry.java
+++ b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarResourceEntry.java
@@ -42,6 +42,11 @@ public class JarResourceEntry extends AbstractJarEntry {
private final byte[] contents;
private String extension;
+ public JarResourceEntry(final int version, final String unversionedName, final long time, final byte[] contents) {
+ super(version, unversionedName, time);
+ this.contents = contents;
+ }
+
public JarResourceEntry(final String name, final long time, final byte[] contents) {
super(name, time);
this.contents = contents;
@@ -61,8 +66,8 @@ public final byte[] getContents() {
}
@Override
- public final JarResourceEntry accept(final JarEntryTransformer vistor) {
- return vistor.transform(this);
+ public final JarResourceEntry accept(final JarEntryTransformer visitor) {
+ return visitor.transform(this);
}
}
diff --git a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarServiceProviderConfigurationEntry.java b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarServiceProviderConfigurationEntry.java
index d3ebb96..51fb92c 100644
--- a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarServiceProviderConfigurationEntry.java
+++ b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarServiceProviderConfigurationEntry.java
@@ -79,8 +79,8 @@ public final byte[] getContents() {
}
@Override
- public final JarServiceProviderConfigurationEntry accept(final JarEntryTransformer vistor) {
- return vistor.transform(this);
+ public final JarServiceProviderConfigurationEntry accept(final JarEntryTransformer visitor) {
+ return visitor.transform(this);
}
}
diff --git a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/asm/JarEntryRemappingTransformer.java b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/asm/JarEntryRemappingTransformer.java
index 9b9e96a..777ea45 100644
--- a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/asm/JarEntryRemappingTransformer.java
+++ b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/asm/JarEntryRemappingTransformer.java
@@ -85,9 +85,9 @@ public JarClassEntry transform(final JarClassEntry entry) {
), 0);
// Create the jar entry
- final String originalName = entry.getName().substring(0, entry.getName().length() - ".class".length());
+ final String originalName = entry.getUnversionedName().substring(0, entry.getUnversionedName().length() - ".class".length());
final String name = this.remapper.map(originalName) + ".class";
- return new JarClassEntry(name, entry.getTime(), writer.toByteArray());
+ return new JarClassEntry(entry.getVersion(), name, entry.getTime(), writer.toByteArray());
}
@Override
diff --git a/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/JarEntrySpec.groovy b/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/JarEntrySpec.groovy
index e682c63..e1c7904 100644
--- a/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/JarEntrySpec.groovy
+++ b/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/JarEntrySpec.groovy
@@ -31,6 +31,7 @@
package org.cadixdev.bombe.jar.test
import org.cadixdev.bombe.jar.AbstractJarEntry
+import org.cadixdev.bombe.jar.JarClassEntry
import org.cadixdev.bombe.jar.JarResourceEntry
import spock.lang.Specification
@@ -41,6 +42,10 @@ class JarEntrySpec extends Specification {
private static final AbstractJarEntry PACKAGED_ENTRY = new JarResourceEntry("pack/beep.boop", 0, null)
private static final AbstractJarEntry ROOT_ENTRY = new JarResourceEntry("beep.boop", 0, null)
+ private static final AbstractJarEntry VERSION_BY_PATH = new JarClassEntry("META-INF/versions/9/module-info.class", 0, null)
+ private static final AbstractJarEntry VERSION_EXPLICIT = new JarClassEntry(11, "pack/a/b.class", 0, null)
+ private static final AbstractJarEntry VERSION_MALFORMED = new JarClassEntry("META-INF/versions/ab/module-info.class", 0, null)
+ private static final AbstractJarEntry VERSION_UNVERSIONABLE = new JarClassEntry("META-INF/versions/14/META-INF/services/a.b\$Provider", 0, null)
def "reads name correctly"(final AbstractJarEntry entry,
final String packageName,
@@ -57,4 +62,22 @@ class JarEntrySpec extends Specification {
ROOT_ENTRY | '' | 'beep' | 'boop'
}
+ def "handles multirelease paths correctly"(final AbstractJarEntry entry,
+ final String fullName,
+ final int version,
+ final String name) {
+ expect:
+ entry.name == fullName
+ entry.version == version
+ entry.unversionedName == name
+
+ where:
+ entry | fullName | version | name
+ PACKAGED_ENTRY | "pack/beep.boop" | AbstractJarEntry.UNVERSIONED | "pack/beep.boop"
+ VERSION_BY_PATH | "META-INF/versions/9/module-info.class" | 9 | "module-info.class"
+ VERSION_EXPLICIT | "META-INF/versions/11/pack/a/b.class" | 11 | "pack/a/b.class"
+ VERSION_MALFORMED | "META-INF/versions/ab/module-info.class" | AbstractJarEntry.UNVERSIONED | "META-INF/versions/ab/module-info.class"
+ VERSION_UNVERSIONABLE | "META-INF/versions/14/META-INF/services/a.b\$Provider" | AbstractJarEntry.UNVERSIONED | "META-INF/versions/14/META-INF/services/a.b\$Provider"
+ }
+
}
diff --git a/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/asm/JarEntryRemappingTransformerSpec.groovy b/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/asm/JarEntryRemappingTransformerSpec.groovy
index c7e4298..fa1ebdc 100644
--- a/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/asm/JarEntryRemappingTransformerSpec.groovy
+++ b/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/asm/JarEntryRemappingTransformerSpec.groovy
@@ -80,6 +80,28 @@ class JarEntryRemappingTransformerSpec extends Specification {
node.name == 'pkg/Demo'
}
+ def "remaps multi-release class"() {
+ given:
+ // Create a test class
+ def obf = new ClassWriter(0)
+ obf.visit(Opcodes.V9, Opcodes.ACC_PUBLIC, 'a', null, 'java/lang/Object', null)
+
+ // Run it through the transformer
+ def entry = TRANSFORMER.transform(new JarClassEntry('META-INF/versions/9/a.class', 0, obf.toByteArray()))
+
+ // Use a ClassNode for convenience
+ def node = new ClassNode()
+ def reader = new ClassReader(entry.contents)
+ reader.accept(node, 0)
+
+ expect:
+ entry.name == 'META-INF/versions/9/pkg/Demo.class'
+ entry.version == 9
+ entry.unversionedName == 'pkg/Demo.class'
+ node.name == 'pkg/Demo'
+
+ }
+
def "remaps manifest"() {
given:
// Create a test Manifest