
aQute.bnd.osgi.JPMSModule Maven / Gradle / Ivy
Show all versions of biz.aQute.bndlib Show documentation
package aQute.bnd.osgi;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import aQute.bnd.build.model.EE;
import aQute.bnd.classfile.ModuleAttribute;
import aQute.bnd.exceptions.Exceptions;
/**
* Multi Release jars are another error magnet extension to Java. Instead of
* having the flat class space of Java and Javac, it allows developers to put
* multiple versions of a class in versioned directory.
*
*
* @see "https://docs.oracle.com/en/java/javase/19/docs/specs/jar/jar.html#multi-release-jar-files"
*
* Multi-release JAR files
*
* A multi-release JAR file allows for a single JAR file to support
* multiple major versions of Java platform releases. For example, a
* multi-release JAR file can depend on both the Java 8 and Java 9 major
* platform releases, where some class files depend on APIs in Java 8 and
* other class files depend on APIs in Java 9. This enables library and
* framework developers to decouple the use of APIs in a specific major
* version of a Java platform release from the requirement that all their
* users migrate to that major version. Library and framework developers
* can gradually migrate to and support new Java features while still
* supporting the old features.
*
* A multi-release JAR file is identified by the main attribute:
*
*
* Multi-Release: true
*
*
* declared in the main section of the JAR Manifest.
*
* Classes and resource files dependent on a major version, 9 or greater,
* of a Java platform release may be located under a versioned directory
* instead of under the top-level (or root) directory. The versioned
* directory is located under the the META-INF directory and is of the
* form:
*
*
* META - INF / versions / N
*
*
* where N is the string representation of the major version number of a
* Java platform release. Specifically N must conform to the specification:
* N: {1-9} {0-9}*
*
* Any versioned directory whose value of N is less than 9 is ignored as is
* a string representation of N that does not conform to the above
* specification.
*
* A class file under a versioned directory, of version N say, in a
* multi-release JAR must have a class file version less than or equal to
* the class file version associated with Nth major version of a Java
* platform release. If the class of the class file is public or protected
* then that class must preside over a class of the same fully qualified
* name and access modifier whose class file is present under the top-level
* directory. By logical extension this applies to a class of a class file,
* if present, under a versioned directory whose version is less than N.
*
* If a multi-release JAR file is deployed on the class path or module path
* (as an automatic module or an explicit multi-release module) of major
* version N of a Java platform release runtime, then a class loader
* loading classes from that JAR file will first search for class files
* under the Nth versioned directory, then prior versioned directories in
* descending order (if present), down to a lower major version bound of 9,
* and finally under the top-level directory.
*
* The public API exported by the classes in a multi-release JAR file must
* be exactly the same across versions, hence at a minimum why versioned
* public or protected classes for class files under a versioned directory
* must preside over classes for class files under the top-level directory.
* It is difficult and costly to perform extensive API verification checks
* as such tooling, such as the jar tool, is not required to perform
* extensive verification and a Java runtime is not required to perform any
* verification. A future release of this specification may relax the exact
* same API constraint to support careful evolution.
*
* Resources under the META-INF directory cannot be versioned (such as for
* service configuration).
*
* A multi-release JAR file can be signed.
*
* Multi-release JAR files are not supported by the boot class loader of a
* Java runtime. If a multi-release JAR file is appended to the boot class
* path (with the -Xbootclasspath/a option) then the JAR is treated as if
* it is an ordinary JAR file. Modular multi-release JAR files
*
* A modular multi-release JAR file is a multi-release JAR file that has a
* module descriptor, module-info.class, in the top-level directory (as for
* a modular JAR file), or directly in a versioned directory.
*
* A public or protected class in a non-exported package (that is not
* declared as exported in the module descriptor) need not preside over a
* class of the same fully qualified name and access modifier whose class
* file is present under the top-level directory.
*
* A module descriptor is generally treated no differently to any other
* class or resource file. A module descriptor may be present under a
* versioned area but not present under the top-level directory. This
* ensures, for example, only Java 8 versioned classes can be present under
* the top-level directory while Java 9 versioned classes (including, or
* perhaps only, the module descriptor) can be present under the 9
* versioned directory.
*
* Any versioned module descriptor that presides over a lesser versioned
* module descriptor or that at the top-level, M say, must be identical to
* M, with two exceptions:
*
* - the presiding versioned descriptor can have different non-transitive
* requires clauses of java.* and jdk.* modules; and
*
- the presiding versioned descriptor can have different uses clauses,
* even of service types defined outside of java.* and jdk.* modules.
*
* Tooling, such as the jar tool, should perform such verification of
* versioned module descriptors but a Java runtime is not required to
* perform any verification.
*/
public class JPMSModule {
final static Pattern VERSIONED_P = Pattern
.compile("META-INF/versions/(?\\d+)/(?.*)");
public static final String MULTI_RELEASE_HEADER = "Multi-Release";
public static final String VERSIONS_PATH = "META-INF/versions/";
public static final String MODULE_INFO_CLASS = "module-info.class";
public static final String OSGI_VERSIONED_MANIFEST_PATH = "OSGI-INF/MANIFEST.MF";
final Jar jar;
Optional moduleAttribute;
/**
* Cosntructor
*
* @param jar the base for this module
*/
public JPMSModule(Jar jar) {
this.jar = jar;
}
/**
* Get the underlying JAR
*/
public Jar getJar() {
return jar;
}
/**
* Get the path to a resource in a version directory
*
* @param release the release version of the VM
* @param path the path in the version directory
* @return a path
*/
public static String getVersionedPath(int release, String path) {
assert release > 8 && release < 100;
StringBuilder sb = new StringBuilder();
sb.append("META-INF/versions/")
.append(release);
if (path != null)
sb.append('/')
.append(path);
return sb.toString();
}
/**
* Return the available releases in this modules. This does not include the
* base, we do not know what version that is
*
*
* @return a map with the release number -> a JAR.
*/
public SortedSet getVersions() {
SortedSet result = new TreeSet<>();
for (int i = 9; i <= EE.MAX_SUPPORTED_RELEASE; i++) {
if (jar.getDirectories()
.containsKey(getVersionedPath(i, null)))
result.add(i);
}
return result;
}
/**
* Return a Jar that contains only the resources for a specific release
* version.
*
* @param release the release number
* @return the Jar will only the release classes & resources
*/
public Jar getReleaseOnly(int release) {
Jar versioned = new Jar(this.jar.getName() + "-" + release) {
@Override
public void close() {}
};
versioned.setSource(this.jar.getSource());
for (Map.Entry e : this.jar.getResources()
.entrySet()) {
String path = e.getKey();
Matcher m = VERSIONED_P.matcher(path);
if (m.matches()) {
int r = Integer.parseInt(m.group("release"));
if (r == release) {
String relative = m.group("path");
versioned.putResource(relative, e.getValue());
}
}
}
return versioned;
}
/**
* Put a resource in a Jar in the META-INF/versions/ subtree for Multi
* Release Jars.
*
* @param release the release number (> 8)
* @param path the relative path of the resource
* @param resource the resource
* @return true if this modified the set of resources
*/
public boolean putResource(int release, String path, Resource resource) {
if (release < 9)
return getJar().putResource(path, resource);
String versionedPath = getVersionedPath(release, path);
return getJar().putResource(versionedPath, resource);
}
/**
* Get a resource from a Jar that is in the versioned area.
*
* @param release the release number we're looking for
* @param path the path
* @return the resource or null if not existent
*/
public Resource getResource(int release, String path) {
if (release < 9)
return getJar().getResource(path);
String versionedPath = getVersionedPath(release, path);
return getJar().getResource(versionedPath);
}
/**
* Find a resource from a Jar. Start in the normal space than look in the
* increasing versioned areas.
*
* @param path the path
* @param release the release to start. If < 0, start from default and go up
* in releases. If <9, use default. Otherwise start there and go
* down, ending in default
* @return the resource or null if not existent
*/
public Optional findResource(String path, int release) {
List order = new ArrayList<>(getVersions());
order.add(0, 0);
if (release <= 0) {
// order is ok
} else {
for (Iterator it = order.iterator(); it.hasNext();) {
int n = it.next();
if (n > release) {
it.remove();
}
}
Collections.reverse(order);
}
for (int n : order) {
Resource resource = getResource(n, path);
if (resource != null)
return Optional.ofNullable(resource);
}
return Optional.empty();
}
/**
* Checks if this JAR has version directories. This does not look at the
* manifest header.
*
* @return if this is a multi release JAR (has versions)
*/
public boolean isMultiRelease() {
return !getVersions().isEmpty();
}
/**
* Returns the name of the module, either from the module descriptor or
* automatically generated from the JAR file manifest.
*
* @return An optional string containing the module name, or an empty
* optional if the module name cannot be determined.
* @throws Exception If an error occurs while parsing the module descriptor
* or manifest.
*/
public Optional getModuleName() throws Exception {
return moduleAttribute().map(a -> a.module_name)
.or(this::automaticModuleName);
}
/**
* Returns the name of the module, automatically generated from the JAR file
* manifest.
*
* @return An optional string containing the automatic module name, or an
* empty optional if the automatic module name cannot be determined.
*/
Optional automaticModuleName() {
return manifest().map(m -> m.getMainAttributes()
.getValue(Constants.AUTOMATIC_MODULE_NAME));
}
/**
* Returns the manifest for the JAR file, or an empty optional if the
* manifest cannot be read.
*
* @return An optional manifest for the JAR file.
*/
Optional manifest() {
try {
return Optional.ofNullable(jar.getManifest());
} catch (Exception e) {
throw Exceptions.duck(e);
}
}
/**
* Returns the version of the module, if present in the module descriptor.
*
* @return An optional string containing the module version, or an empty
* optional if the module version cannot be determined.
* @throws Exception If an error occurs while parsing the module descriptor.
*/
public Optional getModuleVersion() throws Exception {
return moduleAttribute().map(a -> a.module_version);
}
/*
* Returns the module attribute for the module descriptor, if present.
* @return An optional module attribute containing the name and version of
* the module, or an empty optional if the module descriptor cannot be found
* or parsed.
* @throws Exception If an error occurs while parsing the module descriptor.
*/
Optional moduleAttribute() throws Exception {
if (moduleAttribute != null) {
return moduleAttribute;
}
return findResource(Constants.MODULE_INFO_CLASS, -1).map(Clazz::parse)
.flatMap(ci -> ci.getAttribute(ModuleAttribute.class));
}
/**
* Return a manifest for OSGi.
*
* The Framework must first look in the versioned directory for the major
* version of the current Java platform and then prior versioned directories
* in descending order. The first supplemental manifest file found must be
* used and the Framework must replace the values of the following manifest
* headers in the manifest with the values of these headers, if present, in
* the supplemental manifest file.
*
*
Import-Package
Require-Capability
*
*
* Any other headers in the supplemental manifest file must be ignored.
*
* @param release the version of the VM
* @return a Manifest
*/
public Manifest getManifest(int release) {
try {
Manifest defaultManifest = getJar().getManifest();
if (defaultManifest == null)
return new Manifest();
if (release < 9)
return defaultManifest;
Manifest r = findResource(OSGI_VERSIONED_MANIFEST_PATH, release).map(rr -> {
try {
return new Manifest(rr.openInputStream());
} catch (Exception e) {
throw Exceptions.duck(e);
}
})
.orElseGet(Manifest::new);
if (r.equals(defaultManifest))
return defaultManifest;
Manifest result = new Manifest(defaultManifest);
copy(result, r, Constants.IMPORT_PACKAGE, Constants.REQUIRE_CAPABILITY);
return result;
} catch (Exception e) {
throw Exceptions.duck(e);
}
}
/**
* Copies specified headers from one manifest to another.
*
* @param result The manifest to copy the headers to. If null, a new
* manifest will be created.
* @param r The manifest to copy the headers from. If null, no headers will
* be copied.
* @param headers The headers to copy.
* @return The manifest with the copied headers.
*/
public static Manifest copy(Manifest result, Manifest r, String... headers) {
if (result == null)
result = new Manifest();
if (r == null)
return result;
for (String header : headers) {
String value = r.getMainAttributes()
.getValue(header);
if (value != null)
result.getMainAttributes()
.putValue(header, value);
else
result.getMainAttributes()
.putValue(header, "");
}
return result;
}
/**
* Gets a new JAR file that includes all the files from this JAR file and
* all previous versions up to and including the specified release.
*
* @param release The release number to include in the new JAR file.
* @return A new JAR file containing all the files from this JAR file and
* all previous versions up to and including the specified release.
*/
public Jar getRelease(int release) {
Jar target = new Jar(jar.getName());
target.addAll(jar, new Instruction("!META-INF/versions/*"));
for (int r : getVersions()) {
if (release < r)
break;
Jar delta = getReleaseOnly(r);
target.addAll(delta);
}
return target;
}
/**
* Gets the release number of the next version after the specified release.
*
* @param release The release number.
* @return The release number of the next version after the specified
* release, or Integer.MAX_VALUE if there are no more versions.
*/
public int getNextRelease(int release) {
SortedSet versions = getVersions();
SortedSet tailSet = versions.tailSet(release + 1);
if (tailSet.isEmpty())
return Integer.MAX_VALUE;
return tailSet.first();
}
/**
* Cleanup a bsn so that it matches a JPMS module name
*
* @param bsn a symbolic name or null
* @return a name that matches the JPMS specification for a module name or
* null if the input was null
*/
public static String cleanupName(String bsn) {
if (bsn == null)
return null;
if ( bsn.endsWith(".jar"))
bsn= bsn.substring(0, bsn.length()-4);
Pattern STRIP_SUFFIX_P = Pattern.compile("-\\d+\\..*$");
Matcher m = STRIP_SUFFIX_P.matcher(bsn);
if (m.find()) {
bsn = bsn.substring(0, m.start());
}
String[] split = bsn.split("[^A-Za-z0-9]+");
return Stream.of(split)
.filter(str -> !str.isEmpty())
.collect(Collectors.joining("."));
}
}