Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions biz.aQute.bndlib.tests/test/test/AnalyzerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1504,4 +1504,80 @@ static <K, V> V get(Map<K, ? extends Map<String, V>> headers, K key, String attr
return null;
return clauses.get(attr);
}

/**
* Test Import-Packages marked with resolution:=conditional.
* When package is from an OSGi bundle, it should be imported normally.
*/
@Test
public void testConditionalImportWithOSGiBundle() throws Exception {
Builder a = new Builder();
try {
Properties p = new Properties();
// Use resolution:=conditional for OSGi packages
p.put("Import-Package", "*;resolution:=conditional");
p.put("Private-Package", "test.conditionalimport");

a.addClasspath(new File("bin_test"));
a.addClasspath(IO.getFile("jar/osgi.jar")); // OSGi bundle

a.setProperties(p);
Jar jar = a.build();
assertTrue(a.check());

String imports = jar.getManifest()
.getMainAttributes()
.getValue("Import-Package");

// org.osgi.framework should be imported normally (from OSGi bundle)
assertNotNull(imports);
assertTrue(imports.contains("org.osgi.framework"));
// Should have version from OSGi bundle
assertTrue(imports.contains("version="));
assertFalse(imports.contains("resolution:=optional"));
assertFalse(imports.contains("resolution:=conditional"));

} finally {
a.close();
}
}

/**
* Test Import-Packages marked with resolution:=conditional for non-OSGi jar.
* The package should be embedded, not imported.
*/
@Test
public void testConditionalImportWithNonOSGiJar() throws Exception {
Builder a = new Builder();
try {
Properties p = new Properties();
// Use resolution:=conditional which should embed packages from non-OSGi jars
p.put("Import-Package", "*;resolution:=conditional");
p.put("Private-Package", "test.dynamicimport");

a.addClasspath(new File("bin_test"));
a.addClasspath(IO.getFile("jar/asm.jar")); // Non-OSGi jar
a.addClasspath(IO.getFile("jar/osgi.jar")); // OSGi bundle

a.setProperties(p);
Jar jar = a.build();
assertTrue(a.check());

String imports = jar.getManifest()
.getMainAttributes()
.getValue("Import-Package");

// org.osgi.framework should be imported (from OSGi bundle)
assertNotNull(imports);
assertTrue(imports.contains("org.osgi.framework"));

// If test.dynamicimport references asm classes, they should be embedded
// For now, just verify build succeeds

} finally {
a.close();
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package test.conditionalimport;

import org.osgi.framework.BundleContext;

/**
* Test class that imports:
* 1. org.osgi.framework (from OSGi bundle)
*/
public class ConditionalImport {

public ConditionalImport(BundleContext bc) {
// Use the imports
}
}
86 changes: 86 additions & 0 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,9 @@ Pair<Packages, Parameters> divideRegularAndDynamicImports() {
Packages regularImports = new Packages(imports);
Parameters dynamicImports = getDynamicImportPackage();

// First, handle conditional imports before processing dynamic imports
processConditionalImports(regularImports);

Iterator<Entry<PackageRef, Attrs>> regularImportsIterator = regularImports.entrySet()
.iterator();
while (regularImportsIterator.hasNext()) {
Expand All @@ -2195,6 +2198,89 @@ Pair<Packages, Parameters> divideRegularAndDynamicImports() {
return new Pair<>(regularImports, dynamicImports);
}

/**
* Process imports with resolution:=conditional directive.
* For each conditional import:
* 1. If package is from an OSGi bundle (in classpathExports with INTERNAL_EXPORTED_DIRECTIVE),
* keep it as a regular import (remove the resolution directive)
* 2. If package is from a non-OSGi jar (in classpathExports but without INTERNAL_EXPORTED_DIRECTIVE),
* remove from imports and add to conditional packages to be embedded
* 3. If package is not on classpath at all, change resolution to optional
*/
private void processConditionalImports(Packages regularImports) {
Packages packagesToEmbed = new Packages();

Iterator<Entry<PackageRef, Attrs>> importsIterator = regularImports.entrySet()
.iterator();
while (importsIterator.hasNext()) {
Entry<PackageRef, Attrs> packageEntry = importsIterator.next();
PackageRef packageRef = packageEntry.getKey();
Attrs attrs = packageEntry.getValue();
String resolution = attrs.get(Constants.RESOLUTION_DIRECTIVE);

if (Constants.RESOLUTION_CONDITIONAL.equals(resolution)) {
attrs.remove(Constants.RESOLUTION_DIRECTIVE);

Attrs classpathAttrs = classpathExports.get(packageRef);
if (classpathAttrs != null) {
// Package is on the classpath
if (classpathAttrs.containsKey(Constants.INTERNAL_EXPORTED_DIRECTIVE)) {
// Package is from an OSGi bundle - keep as regular import
// Directive already removed, nothing else to do
} else {
// Package is from a non-OSGi jar - mark for embedding
packagesToEmbed.put(packageRef, attrs);
importsIterator.remove();
}
} else {
// Package not found on classpath - make it optional
attrs.put(Constants.RESOLUTION_DIRECTIVE, Constants.RESOLUTION_OPTIONAL);
}
}
}

// Add packages to embed to conditional packages
if (!packagesToEmbed.isEmpty()) {
embedConditionalPackages(packagesToEmbed);
}
}

/**
* Add packages that need to be embedded to the jar.
* This is called for conditional imports from non-OSGi jars.
*/
private void embedConditionalPackages(Packages packagesToEmbed) {
try {
for (PackageRef packageRef : packagesToEmbed.keySet()) {
// Find the package in the classpath and copy it to the dot jar
for (Jar cpe : getClasspath()) {
Map<String, Resource> packageDir = cpe.getDirectory(packageRef.getPath());
if (packageDir != null && !packageDir.isEmpty()) {
// Copy all resources from this package
for (Map.Entry<String, Resource> entry : packageDir.entrySet()) {
String path = entry.getKey();
Resource resource = entry.getValue();
// Only copy if not already present
if (dot.getResource(path) == null) {
dot.putResource(path, resource);
}
}
// Mark package as contained (embedded)
Attrs attrs = packagesToEmbed.get(packageRef);
if (attrs == null) {
attrs = new Attrs();
}
contained.put(packageRef, attrs);
// Package found and copied, no need to check other jars
break;
}
}
}
} catch (Exception e) {
exception(e, "Failed to embed conditional packages: %s", e.getMessage());
}
}

String applyVersionPolicy(String exportVersion, String importRange, boolean provider) {
try {
setProperty(CURRENT_VERSION, exportVersion);
Expand Down
2 changes: 2 additions & 0 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,8 @@ public interface Constants {
String CLASS_ATTRIBUTE = "class";
String NAME_ATTRIBUTE = "name";
String RESOLUTION_DYNAMIC = "dynamic";
String RESOLUTION_OPTIONAL = "optional";
String RESOLUTION_CONDITIONAL = "conditional";
String DESCRIPTION_ATTRIBUTE = "description";
String OSNAME_ATTRIBUTE = "osname";
String OSVERSION_ATTRIBUTE = "osversion";
Expand Down
24 changes: 24 additions & 0 deletions docs/_heads/_ext/import_package.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,30 @@ Packages with directive `resolution:=dynamic` will be removed from `Import-Packa

Import-Package: org.slf4j.*;resolution:=dynamic, *

## Conditional Resolution

Packages with directive `resolution:=conditional` provide a flexible way to handle imports based on the classpath configuration. This is particularly useful when wrapping third-party libraries where you want automatic handling of dependencies:

Import-Package: *;resolution:=conditional

When a package is marked with `resolution:=conditional`, bnd will:

1. **Import normally** if the package is found on the classpath and comes from an OSGi bundle (has Export-Package metadata)
2. **Embed the package** if it's found on the classpath but comes from a non-OSGi jar (no OSGi metadata)
3. **Mark as optional** (`resolution:=optional`) if the package is not found on the classpath at all

This behavior is especially useful when:
- Wrapping existing JARs that mix OSGi and non-OSGi dependencies
- Creating flexible bundles that can adapt to different deployment scenarios
- Avoiding the need to manually specify which packages should be imported vs. embedded

Example use case: Wrapping a third-party library that depends on both OSGi frameworks (like org.osgi.framework) and plain Java libraries (like Apache Commons):

Import-Package: *;resolution:=conditional
Private-Package: com.thirdparty.*

In this case, OSGi packages will be imported with proper version ranges, while non-OSGi dependencies will be embedded directly into the bundle, and any optional dependencies not on the classpath will be marked as optional imports.

If an imported package uses mandatory attributes, then bnd will attempt to add those attributes to the import statement. However, in certain (bizarre!) cases this is not wanted. It is therefore possible to remove an attribute from the import clause. This is done with the `-remove-attribute` directive or by setting the value of an attribute to `!`. The parameter of the `-remove-attribute` directive is an instruction and can use the standard options with `!`, `*`, `?`, etc.

Import-Package: org.eclipse.core.runtime;-remove-attribute:="common",*
Expand Down