Skip to content

Commit

Permalink
Add multi-release processing support
Browse files Browse the repository at this point in the history
Currently bnd is not a capable of processing multi-release jars, this
adds support of multi-release processing by a new directive -release
where one can select what release version should be processed by bnd.

Fixes bndtools#5346

Signed-off-by: Christoph Läubrich <laeubi@laeubi-soft.de>
  • Loading branch information
laeubi committed Aug 19, 2022
1 parent b545b94 commit 00f2d33
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 4 deletions.
18 changes: 18 additions & 0 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java
Expand Up @@ -200,7 +200,12 @@ public static Properties getManifest(File dirOrJar) throws Exception {
*/
public void analyze() throws Exception {
if (!analyzed) {
int release = getRelease();
analyzed = true;
dot.setRelease(release);
for (Jar current : getClasspath()) {
current.setRelease(release);
}
analyzeContent();

// Execute any plugins
Expand Down Expand Up @@ -423,6 +428,19 @@ public void analyze() throws Exception {
}
}

private int getRelease() {
String property = getProperty(JAVA_RELEASE);
if (property != null) {
try {
return Integer.parseInt(property.trim());
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
JAVA_RELEASE + " must be a valid integer but was " + property + " (" + e.getMessage() + ")", e);
}
}
return -1;
}

private void reset() {
contained.clear();
classspace.clear();
Expand Down
3 changes: 2 additions & 1 deletion biz.aQute.bndlib/src/aQute/bnd/osgi/Constants.java
Expand Up @@ -293,6 +293,7 @@ public interface Constants {
String WORKINGSET = "-workingset";
String WORKINGSET_MEMBER = "member";
String REQUIRE_BND = "-require-bnd";
String JAVA_RELEASE = "-release";

// Deprecated
String CLASSPATH = "-classpath";
Expand All @@ -314,7 +315,7 @@ public interface Constants {
CONNECTION_SETTINGS, RUNPROVIDEDCAPABILITIES, WORKINGSET, RUNSTORAGE, REPRODUCIBLE, INCLUDEPACKAGE,
CDIANNOTATIONS, REMOTEWORKSPACE, MAVEN_DEPENDENCIES, BUILDERIGNORE, STALECHECK, MAVEN_SCOPE, RUNSTARTLEVEL,
RUNOPTIONS, NOCLASSFORNAME, EXPORT_APIGUARDIAN, RESOLVE, DEFINE_CONTRACT, GENERATE, RUNFRAMEWORKRESTART,
NOIMPORTJAVA, VERSIONDEFAULTS, LIBRARY);
NOIMPORTJAVA, VERSIONDEFAULTS, LIBRARY, JAVA_RELEASE);

// Ignore bundle specific headers. These headers do not make a lot of sense
// to inherit
Expand Down
52 changes: 49 additions & 3 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java
Expand Up @@ -30,6 +30,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -124,6 +125,8 @@ public enum Compression {
private long zipEntryConstantTime = ZIP_ENTRY_CONSTANT_TIME;
public static final Pattern METAINF_SIGNING_P = Pattern
.compile("META-INF/([^/]+\\.(?:DSA|RSA|EC|SF)|SIG-[^/]+)", Pattern.CASE_INSENSITIVE);
static final Pattern MULTI_RELEASE_PATH = Pattern
.compile("^META-INF/versions/(\\d+)/(.*)$", Pattern.CASE_INSENSITIVE);

public Jar(String name) {
this.name = name;
Expand Down Expand Up @@ -394,6 +397,9 @@ public boolean putResource(String path, Resource resource, boolean overwrite) {
public Resource getResource(String path) {
check();
path = ZipUtil.cleanPath(path);
if (release > -1) {
return getResources().get(path);
}
return resources.get(path);
}

Expand Down Expand Up @@ -429,6 +435,35 @@ public Map<String, Resource> getDirectory(String path) {

public Map<String, Resource> getResources() {
check();
if (release > -1) {
Set<String> keySet = resources.keySet();
TreeMap<Integer, Map<String, Resource>> releaseMap = new TreeMap<>();
TreeMap<String, Resource> resultMap = new TreeMap<>();
for (String path : keySet) {
Matcher matcher = MULTI_RELEASE_PATH.matcher(path);
if (matcher.matches()) {
// needs special handling
int version = Integer.parseInt(matcher.group(1));
String resourcePath = matcher.group(2);
releaseMap.computeIfAbsent(version, r -> new TreeMap<>())
.put(resourcePath, resources.get(path));
} else {
// can pass through
resultMap.put(path, resources.get(path));
}
}
if (release >= 9) {
NavigableMap<Integer, Map<String, Resource>> map = releaseMap.headMap(release, true);
for (Entry<Integer, Map<String, Resource>> versionEntry : map.entrySet()) {
for (Entry<String, Resource> entry : versionEntry.getValue()
.entrySet()) {
resultMap.put(entry.getKey(), entry.getValue());
}
}
}
return resultMap;
}
// no release option, do as before...
return resources;
}

Expand Down Expand Up @@ -621,7 +656,7 @@ public void write(OutputStream to) throws Exception {
}

// Write all remaining entries
for (Map.Entry<String, Resource> entry : getResources().entrySet()) {
for (Map.Entry<String, Resource> entry : resources.entrySet()) {
// Skip metainf contents
if (!done.contains(entry.getKey()))
writeResource(jout, directories, entry.getKey(), entry.getValue());
Expand Down Expand Up @@ -664,7 +699,7 @@ public void writeFolder(File dir) throws Exception {
}
}

for (Map.Entry<String, Resource> entry : getResources().entrySet()) {
for (Map.Entry<String, Resource> entry : resources.entrySet()) {
String path = entry.getKey();
if (done.contains(path))
continue;
Expand Down Expand Up @@ -1302,7 +1337,8 @@ public void removeSubDirs(String dir) {
subDirs.forEach(subDir -> removePrefix(subDir.concat("/")));
}

private static final Predicate<String> pomXmlFilter = new PathSet("META-INF/maven/*/*/pom.xml").matches();
private static final Predicate<String> pomXmlFilter = new PathSet("META-INF/maven/*/*/pom.xml").matches();
private int release = -1;

public Stream<Resource> getPomXmlResources() {
return getResources(pomXmlFilter);
Expand Down Expand Up @@ -1343,4 +1379,14 @@ public Optional<byte[]> getSHA256() {
public int getLength() {
return fileLength;
}

/**
* Set the release flag that should be used for this jar file to read
* Multi-Release-Content jars
*
* @param release
*/
public void setRelease(int release) {
this.release = release;
}
}
70 changes: 70 additions & 0 deletions biz.aQute.bndlib/test/aQute/bnd/build/JarTest.java
@@ -0,0 +1,70 @@
package aQute.bnd.build;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
import java.util.Collections;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Resource;

public class JarTest {

private static final String TEST_CLASS_PATH = "a/test/package/Test.class";

private static final String VERSIONED_TEST_CLASS_PATH = "META-INF/versions/9/" + TEST_CLASS_PATH;

@TempDir
File tempDir;

@Test
public void testMultiReleaseJar() throws Exception {
File jarfile = new File(tempDir, "packed.jar");
try (Jar jar = new Jar("testme")) {
jar.ensureManifest();
Resource java8Class = resource();
Resource java9Class = resource();
jar.putResource(TEST_CLASS_PATH, java8Class);
jar.putResource(VERSIONED_TEST_CLASS_PATH, java9Class);
// without a release, content must be returned as-is
assertEquals(java8Class, jar.getResource(TEST_CLASS_PATH));
assertEquals(java9Class, jar.getResource(VERSIONED_TEST_CLASS_PATH));
// with a release set below 9 we should only see the default content
jar.setRelease(0);
assertEquals(java8Class, jar.getResource(TEST_CLASS_PATH));
assertNull(jar.getResource(VERSIONED_TEST_CLASS_PATH));
// with release 9 set, we now should see the java 9 content
jar.setRelease(9);
assertEquals(java9Class, jar.getResource(TEST_CLASS_PATH));
// if we write the jar out, all content should be present
jar.writeFolder(tempDir);
File defaultFile = new File(tempDir, TEST_CLASS_PATH);
assertTrue(defaultFile.isFile(), defaultFile.getAbsolutePath() + " is missing");
File versionedFile = new File(tempDir, VERSIONED_TEST_CLASS_PATH);
assertTrue(versionedFile.isFile(), versionedFile.getAbsolutePath() + " is missing");
jar.write(jarfile);
}
try (JarFile jar = new JarFile(jarfile)) {
Set<String> collect = Collections.list(jar.entries())
.stream()
.map(JarEntry::getName)
.collect(Collectors.toSet());
assertTrue(collect.contains(TEST_CLASS_PATH));
assertTrue(collect.contains(VERSIONED_TEST_CLASS_PATH));
}
}

private Resource resource() {
return new aQute.bnd.osgi.EmbeddedResource(new byte[0], System.currentTimeMillis());
}

}
13 changes: 13 additions & 0 deletions docs/_instructions/release.md
@@ -0,0 +1,13 @@
---
layout: default
class: Analyzer
title: -release NUMBER
summary: Specify the java release for which the Analyzer should generate meta-data.
---
The `-release` instruction is very similar to the `--release` option of javac and instructs the Analyser to process Multi-Release-JARs with the specified release as defined in [JEP 238: Multi-Release JAR Files](https://openjdk.org/jeps/238).

If the `-release` is not specified or NUMBER is smaller than 0 then release processing is **disabled** no further processing is done

If the `-release` is specified and NUMBER is smaller than or equal to 8 the **default content** is processed, that means for every jar entries in the META-INF/versions/* directories are effectively ignored by the processor.

If the `-release` is specified and NUMBER is larger or equal than 9 the content is processed as with the rules from JEP 238 possibly hiding some of the default content.

0 comments on commit 00f2d33

Please sign in to comment.