Skip to content

Commit

Permalink
Add support for reading Multi-Release-Jar content
Browse files Browse the repository at this point in the history
This adds methods to the Jar class to read versioned content of a
multi-release jar for the following cases:

- get a resource or a map of resources for a given release version
- get a merged manifest according to the OSGi specification
- get module name/version for a given given release version
- list all contained release alternative versions

Beside this a unit-test is added to cover these new functions.

Signed-off-by: Christoph Läubrich <laeubi@laeubi-soft.de>
  • Loading branch information
laeubi committed Aug 25, 2022
1 parent 09c46ea commit 2d1a4c6
Show file tree
Hide file tree
Showing 2 changed files with 422 additions and 14 deletions.
235 changes: 221 additions & 14 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/Jar.java
Expand Up @@ -24,16 +24,21 @@
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
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;
import java.util.Set;
import java.util.SortedMap;
import java.util.Spliterator;
import java.util.Spliterators.AbstractSpliterator;
import java.util.TreeMap;
Expand All @@ -46,6 +51,8 @@
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import java.util.zip.CRC32;
Expand Down Expand Up @@ -74,6 +81,7 @@
import aQute.libg.glob.PathSet;

public class Jar implements Closeable {

private static final int BUFFER_SIZE = IOConstants.PAGE_SIZE * 16;
/**
* Note that setting the January 1st 1980 (or even worse, "0", as time)
Expand Down Expand Up @@ -104,7 +112,7 @@ public enum Compression {
private final NavigableMap<String, Resource> resources = new TreeMap<>();
private final NavigableMap<String, Map<String, Resource>> directories = new TreeMap<>();
private Optional<Manifest> manifest;
private Optional<ModuleAttribute> moduleAttribute;
private Map<Integer, Optional<ModuleAttribute>> moduleAttributes = new HashMap<>();
private boolean manifestFirst;
private String manifestName = JarFile.MANIFEST_NAME;
private String name;
Expand All @@ -124,6 +132,14 @@ 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 String MULTI_RELEASE_HEADER = "Multi-Release";
static final String SUPPLEMENTAL_MANIFEST_PATH = "OSGI-INF/MANIFEST.MF";
static final int MULTI_RELEASE_MIN_VERSION = 9;
static final int MULTI_RELEASE_DEFAULT_VERSION = 0;
static final int MULTI_RELEASE_VERSION_GROUP = 1;
static final int MULTI_RELEASE_PATH_GROUP = 2;
static final Pattern MULTI_RELEASE_PATTERN = Pattern
.compile("^META-INF/versions/(\\d+)/(.*)$", Pattern.CASE_INSENSITIVE);

public Jar(String name) {
this.name = name;
Expand Down Expand Up @@ -262,6 +278,24 @@ public Jar(String string, File file) throws IOException {
this(string, file, DEFAULT_DO_NOT_COPY);
}

public boolean isMultiRelease() {
return manifest().map(Manifest::getMainAttributes)
.map(attr -> attr.getValue(MULTI_RELEASE_HEADER))
.map(Boolean::parseBoolean)
.orElse(Boolean.FALSE);
}

public void setMultiRelease(boolean multiRelease) {
try {
ensureManifest();
manifest().get()
.getMainAttributes()
.putValue(MULTI_RELEASE_HEADER, String.valueOf(multiRelease));
} catch (Exception e) {
Exceptions.duck(e);
}
}

private Jar buildFromDirectory(final Path baseDir, final Pattern doNotCopy) throws IOException {
Files.walkFileTree(baseDir, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
new SimpleFileVisitor<Path>() {
Expand Down Expand Up @@ -361,13 +395,22 @@ public boolean putResource(String path, Resource resource) {
public boolean putResource(String path, Resource resource, boolean overwrite) {
check();
path = ZipUtil.cleanPath(path);
versionedResources = null;

if (path.equals(manifestName)) {
manifest = null;
if (resources.isEmpty())
manifestFirst = true;
} else if (path.equals(Constants.MODULE_INFO_CLASS)) {
moduleAttribute = null;
} else {
if (path.equals(Constants.MODULE_INFO_CLASS)) {
moduleAttributes.remove(MULTI_RELEASE_DEFAULT_VERSION);
} else {
Matcher matcher = MULTI_RELEASE_PATTERN.matcher(path);
if (matcher.matches() && matcher.group(MULTI_RELEASE_PATH_GROUP)
.equals(Constants.MODULE_INFO_CLASS)) {
moduleAttributes.remove(Integer.parseInt(matcher.group(MULTI_RELEASE_VERSION_GROUP)));
}
}
}
String dir = getParent(path);
Map<String, Resource> s = directories.get(dir);
Expand Down Expand Up @@ -397,6 +440,30 @@ public Resource getResource(String path) {
return resources.get(path);
}

/**
* Returns a resource taking the release version into account as described
* by the {@link JarFile#getJarEntry(String)}.
*
* @param path the path of the resource to read
* @param release the release to use
* @return an optional representing the highest versioned resource for the
* given release or an empty optional if the resource do not exits
*/
public Optional<Resource> getVersionedResource(String path, int release) {
if (isMultiRelease() && release >= MULTI_RELEASE_MIN_VERSION) {
check();
path = ZipUtil.cleanPath(path);
NavigableMap<Integer, Resource> map = getAllVersionMap().getOrDefault(path, Collections.emptyNavigableMap())
.headMap(release, true);
Entry<Integer, Resource> releaseEntry = map.lastEntry();
if (releaseEntry != null) {
return Optional.of(releaseEntry.getValue());
}
return Optional.empty();
}
return Optional.ofNullable(getResource(path));
}

public Stream<String> getResourceNames(Predicate<String> matches) {
return getResources().keySet()
.stream()
Expand Down Expand Up @@ -432,6 +499,74 @@ public Map<String, Resource> getResources() {
return resources;
}

/**
* returns an (unmodifiable) view of resources in this jar according to the
* given release version as described by the
* {@link JarFile#getJarEntry(String)}.
*
* @return a map whose keys are resource names and the value the highest
* available resource for the given release.
*/
public Map<String, Resource> getVersionedResources(int release) {
if (isMultiRelease()) {
check();
Map<String, NavigableMap<Integer, Resource>> versionedResources = getAllVersionMap();
return versionedResources.entrySet()
.stream()
.map(versions -> {
Entry<Integer, Resource> releaseEntry = versions.getValue()
.headMap(release, true)
.lastEntry();
if (releaseEntry != null) {
return new SimpleEntry<>(versions.getKey(), releaseEntry.getValue());
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
}
return Collections.unmodifiableMap(getResources());
}

/**
* provides a stream of all additional releases declared by this jar
*
* @return a stream of additional releases declared by this jar
*/
public IntStream getReleaseVersions() {
if (isMultiRelease()) {
return getAllVersionMap().values()
.stream()
.flatMap(map -> map.keySet()
.stream())
.mapToInt(i -> i)
.distinct()
.sorted();
}
return IntStream.empty();
}

private Map<String, NavigableMap<Integer, Resource>> getAllVersionMap() {
if (versionedResources == null) {
versionedResources = new HashMap<>();
for (Entry<String, Resource> entry : resources.entrySet()) {
Matcher matcher = Jar.MULTI_RELEASE_PATTERN.matcher(entry.getKey());
String path;
int version;
if (matcher.matches()) {
path = matcher.group(Jar.MULTI_RELEASE_PATH_GROUP);
version = Integer.parseInt(matcher.group(Jar.MULTI_RELEASE_VERSION_GROUP));
} else {
path = entry.getKey();
version = Jar.MULTI_RELEASE_DEFAULT_VERSION;
}
SortedMap<Integer, Resource> map = versionedResources.computeIfAbsent(path, nil -> new TreeMap<>());
map.put(version, entry.getValue());
}
}
return versionedResources;
}

public boolean addDirectory(Map<String, Resource> directory, boolean overwrite) {
check();
boolean duplicates = false;
Expand All @@ -448,6 +583,48 @@ public Manifest getManifest() throws Exception {
return manifest().orElse(null);
}

/**
* Creates a <b>copy</b> of the current jars manifest that is enhanced by
* the supplemental manifest data (if any) for the given release.
*
* @param release the release for fetching an enhanced manifest
* @return a <b>copy</b> that is <b>not</b> backed by the original manifest
* but copied and enhanced with the supplemental manifest data (if
* any) for the given release
*/
public Optional<Manifest> getManifest(int release) {
if (isMultiRelease()) {
return manifest().map(original -> {
Manifest copy = new Manifest(original);
if (release >= MULTI_RELEASE_MIN_VERSION) {
Optional<Resource> releaseEntry = getVersionedResource(SUPPLEMENTAL_MANIFEST_PATH, release);
releaseEntry.map(resource -> {
try (InputStream in = resource.openInputStream()) {
return new Manifest(in);
} catch (Exception e) {
throw Exceptions.duck(e);
}
})
.ifPresent(supplemental -> {
enhanceManifestAttribute(supplemental, copy, Constants.REQUIRE_CAPABILITY);
enhanceManifestAttribute(supplemental, copy, Constants.IMPORT_PACKAGE);
});
}
return copy;
});
}
return manifest().map(Manifest::new);
}

private static void enhanceManifestAttribute(Manifest supplemental, Manifest target, String key) {
String value = supplemental.getMainAttributes()
.getValue(key);
if (value != null) {
target.getMainAttributes()
.putValue(key, value);
}
}

Optional<Manifest> manifest() {
check();
Optional<Manifest> optional = manifest;
Expand All @@ -468,15 +645,29 @@ Optional<Manifest> manifest() {
}

Optional<ModuleAttribute> moduleAttribute() throws Exception {
return moduleAttribute(MULTI_RELEASE_DEFAULT_VERSION);
}

Optional<ModuleAttribute> moduleAttribute(int release) throws Exception {
check();
Optional<ModuleAttribute> optional = moduleAttribute;
if (optional != null) {
return optional;
if (release < MULTI_RELEASE_MIN_VERSION) {
release = MULTI_RELEASE_DEFAULT_VERSION;
}
Resource module_info_resource = getResource(Constants.MODULE_INFO_CLASS);
if (module_info_resource == null) {
return moduleAttribute = Optional.empty();
Optional<ModuleAttribute> moduleAttribute = moduleAttributes.get(release);
if (moduleAttribute == null) {
Optional<Resource> resource = getVersionedResource(Constants.MODULE_INFO_CLASS, release);
if (resource.isPresent()) {
moduleAttribute = readModuleAttribute(resource.get());
} else {
moduleAttribute = Optional.empty();
}
moduleAttributes.put(release, moduleAttribute);
}
return moduleAttribute;
}

private static Optional<ModuleAttribute> readModuleAttribute(Resource module_info_resource)
throws Exception {
ClassFile module_info;
ByteBuffer bb = module_info_resource.buffer();
if (bb != null) {
Expand All @@ -486,25 +677,38 @@ Optional<ModuleAttribute> moduleAttribute() throws Exception {
module_info = ClassFile.parseClassFile(din);
}
}
return moduleAttribute = Arrays.stream(module_info.attributes)
return Arrays.stream(module_info.attributes)
.filter(ModuleAttribute.class::isInstance)
.map(ModuleAttribute.class::cast)
.findFirst();
}

public String getModuleName() throws Exception {
return moduleAttribute().map(a -> a.module_name)
.orElseGet(this::automaticModuleName);
return getModuleName(MULTI_RELEASE_DEFAULT_VERSION);
}

public String getModuleName(int release) throws Exception {
return moduleAttribute(release).map(a -> a.module_name)
.orElseGet(() -> automaticModuleName(release));
}

String automaticModuleName() {
return manifest().map(m -> m.getMainAttributes()
return automaticModuleName(MULTI_RELEASE_DEFAULT_VERSION);
}

String automaticModuleName(int release) {
return getManifest(release)
.map(m -> m.getMainAttributes()
.getValue(Constants.AUTOMATIC_MODULE_NAME))
.orElse(null);
}

public String getModuleVersion() throws Exception {
return moduleAttribute().map(a -> a.module_version)
return getModuleVersion(MULTI_RELEASE_DEFAULT_VERSION);
}

public String getModuleVersion(int release) throws Exception {
return moduleAttribute(release).map(a -> a.module_version)
.orElse(null);
}

Expand Down Expand Up @@ -947,6 +1151,8 @@ public void close() {
directories.clear();
manifest = null;
source = null;
versionedResources = null;
moduleAttributes.clear();
}

public long lastModified() {
Expand Down Expand Up @@ -1303,6 +1509,7 @@ public void removeSubDirs(String dir) {
}

private static final Predicate<String> pomXmlFilter = new PathSet("META-INF/maven/*/*/pom.xml").matches();
private Map<String, NavigableMap<Integer, Resource>> versionedResources;

public Stream<Resource> getPomXmlResources() {
return getResources(pomXmlFilter);
Expand Down

0 comments on commit 2d1a4c6

Please sign in to comment.