blob: fdccbf30feadb8d1bb5e267b3cfb7e11c161454a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2021 IBM Corporation and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which accompanies this distribution,
* and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors: IBM Corporation - initial API and implementation
******************************************************************************/
package org.eclipse.osgi.internal.module;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.osgi.service.resolver.BundleSpecification;
import org.eclipse.osgi.service.resolver.ExportPackageDescription;
/*
* The GroupingChecker checks the 'uses' directive on exported packages for consistency
*/
public class GroupingChecker {
final PackageRoots nullPackageRoots = new PackageRoots(null);
// a mapping of bundles to their package roots; keyed by
// ResolverBundle -> HashMap of packages; keyed by
// package name -> PackageRoots
private Map<ResolverBundle, Map<String, PackageRoots>> bundles = new HashMap<>();
/*
* This method fully populates a bundles package roots for the purpose of resolving
* a dynamic import. Package roots must be fully populated because we need all the
* roots to do proper uses constraint verification on a dynamic import supplier.
*/
public void populateRoots(ResolverBundle bundle) {
if (bundles.containsKey(bundle))
// only do the full populate the first time (bug 337272)
return;
// process all requires
BundleConstraint[] requires = bundle.getRequires();
for (BundleConstraint require : requires) {
ResolverBundle selectedSupplier = (ResolverBundle) require.getSelectedSupplier();
if (selectedSupplier != null)
isConsistentInternal(bundle, selectedSupplier, new ArrayList<>(1), true, null);
}
// process all imports
// must check resolved imports to get any dynamically resolved imports
ExportPackageDescription[] imports = bundle.getBundleDescription().getResolvedImports();
for (ExportPackageDescription importPkg : imports) {
List<ResolverExport> exports = bundle.getResolver().getResolverExports().get(importPkg.getName());
for (ResolverExport export : exports) {
if (export.getExportPackageDescription() == importPkg)
isConsistentInternal(bundle, export, true, null);
}
}
}
/*
* Re-populates the package roots or an importing bundle with the given export
* This is done after wiring a package from a dynamic import (bug 337272)
*/
public void populateRoots(ResolverBundle importingBundle, ResolverExport export) {
Map<String, PackageRoots> packageRoots = bundles.get(importingBundle);
if (packageRoots != null)
packageRoots.remove(export.getName());
PackageRoots roots = getPackageRoots(export.getExporter(), export.getName(), null);
packageRoots.put(export.getName(), roots);
}
/*
* Verifies the uses constraint consistency for the requiringBundle with the possible matching bundle.
* If an inconsistency is found the export inconsistency is returned; otherwise null is returned
*/
public PackageRoots[][] isConsistent(ResolverBundle requiringBundle, ResolverBundle matchingBundle) {
List<PackageRoots[]> results = isConsistentInternal(requiringBundle, matchingBundle, new ArrayList<>(1), false, null);
return results == null ? null : results.toArray(new PackageRoots[results.size()][]);
}
private List<PackageRoots[]> isConsistentInternal(ResolverBundle requiringBundle, ResolverBundle matchingBundle, List<ResolverBundle> visited, boolean dynamicImport, List<PackageRoots[]> results) {
// needed to prevent endless cycles
if (visited.contains(matchingBundle))
return results;
visited.add(matchingBundle);
// check that the packages exported by the matching bundle are consistent
ResolverExport[] matchingExports = matchingBundle.getExportPackages();
for (ResolverExport matchingExport : matchingExports) {
if (matchingExport.getSubstitute() != null) {
matchingExport = (ResolverExport) matchingExport.getSubstitute();
}
results = isConsistentInternal(requiringBundle, matchingExport, dynamicImport, results);
}
// check that the packages from reexported bundles are consistent
BundleConstraint[] supplierRequires = matchingBundle.getRequires();
for (BundleConstraint supplierRequire : supplierRequires) {
ResolverBundle reexported = (ResolverBundle) supplierRequire.getSelectedSupplier();
if (reexported == null || !((BundleSpecification) supplierRequire.getVersionConstraint()).isExported()) {
continue;
}
results = isConsistentInternal(requiringBundle, reexported, visited, dynamicImport, results);
}
return results;
}
/*
* Verifies the uses constraint consistency for the importingBundle with the possible matching export.
* If an inconsistency is found the export returned; otherwise null is returned
*/
public PackageRoots[][] isConsistent(ResolverBundle importingBundle, ResolverExport matchingExport) {
List<PackageRoots[]> results = isConsistentInternal(importingBundle, matchingExport, false, null);
return results == null ? null : results.toArray(new PackageRoots[results.size()][]);
}
public PackageRoots[][] isConsistent(ResolverBundle requiringBundle, GenericCapability matchingCapability) {
String[] uses = matchingCapability.getUsesDirective();
if (uses == null)
return null;
ArrayList<PackageRoots[]> results = new ArrayList<>(0);
for (String usedPackage : uses) {
PackageRoots providingRoots = getPackageRoots(matchingCapability.getResolverBundle(), usedPackage, null);
providingRoots.addConflicts(requiringBundle, usedPackage, null, results);
}
return results.size() == 0 ? null : results.toArray(new PackageRoots[results.size()][]);
}
/*
* Verifies the uses constraint consistency for the importingBundle with the possible dynamic matching export.
* If an inconsistency is found the export returned; otherwise null is returned.
* Dynamic imports must perform extra checks to ensure that existing wires to package roots are
* consistent with the possible matching dynamic export.
*/
public PackageRoots[][] isDynamicConsistent(ResolverBundle importingBundle, ResolverExport matchingExport) {
List<PackageRoots[]> results = isConsistentInternal(importingBundle, matchingExport, true, null);
return results == null ? null : results.toArray(new PackageRoots[results.size()][]);
}
private List<PackageRoots[]> isConsistentInternal(ResolverBundle importingBundle, ResolverExport matchingExport, boolean dyanamicImport, List<PackageRoots[]> results) {
PackageRoots exportingRoots = getPackageRoots(matchingExport.getExporter(), matchingExport.getName(), null);
// check that the exports uses packages are consistent with existing package roots
results = exportingRoots.isConsistentClassSpace(importingBundle, null, results);
if (!dyanamicImport)
return results;
// for dynamic imports we must check that each existing root is consistent with the possible matching export
PackageRoots importingRoots = getPackageRoots(importingBundle, matchingExport.getName(), null);
Map<String, PackageRoots> importingPackages = bundles.get(importingBundle);
if (importingPackages != null)
for (PackageRoots roots : importingPackages.values()) {
if (roots != importingRoots)
results = roots.isConsistentClassSpace(exportingRoots, matchingExport.getExporter(), null, results);
}
// We also must check any generic capabilities are consistent
GenericConstraint[] genericRequires = importingBundle.getGenericRequires();
for (GenericConstraint constraint : genericRequires) {
VersionSupplier[] suppliers = constraint.getMatchingCapabilities();
if (suppliers != null) {
for (VersionSupplier supplier : suppliers) {
String[] uses = ((GenericCapability) supplier).getUsesDirective();
if (uses != null)
for (String usedPackage : uses) {
if (usedPackage.equals(matchingExport.getName())) {
results = exportingRoots.addConflicts(supplier.getResolverBundle(), usedPackage, null, results);
}
}
}
}
}
return results;
}
/*
* returns package roots for a specific package name for a specific bundle
*/
PackageRoots getPackageRoots(ResolverBundle bundle, String packageName, List<ResolverBundle> visited) {
Map<String, PackageRoots> packages = bundles.get(bundle);
if (packages == null) {
packages = new HashMap<>(5);
bundles.put(bundle, packages);
}
PackageRoots packageRoots = packages.get(packageName);
if (packageRoots == null) {
packageRoots = createPackageRoots(bundle, packageName, visited == null ? new ArrayList<ResolverBundle>(1) : visited);
packages.put(packageName, packageRoots);
}
return packageRoots != null ? packageRoots : nullPackageRoots;
}
private PackageRoots createPackageRoots(ResolverBundle bundle, String packageName, List<ResolverBundle> visited) {
if (visited.contains(bundle))
return null;
visited.add(bundle); // prevent endless cycles
// check imports
if (bundle.getBundleDescription().isResolved()) {
// must check resolved imports to get any dynamically resolved imports
ExportPackageDescription[] imports = bundle.getBundleDescription().getResolvedImports();
for (ExportPackageDescription importPkg : imports) {
if (importPkg.getExporter() == bundle.getBundleDescription() || !importPkg.getName().equals(packageName))
continue;
List<ResolverExport> exports = bundle.getResolver().getResolverExports().get(packageName);
for (ResolverExport export : exports) {
if (export.getExportPackageDescription() == importPkg)
return getPackageRoots(export.getExporter(), packageName, visited);
}
}
} else {
ResolverImport imported = bundle.getImport(packageName);
if (imported != null && imported.getSelectedSupplier() != null) {
// make sure we are not resolved to our own import
ResolverExport selectedExport = (ResolverExport) imported.getSelectedSupplier();
if (selectedExport.getExporter() != bundle) {
// found resolved import; get the roots from the resolved exporter;
// this is all the roots if the package is imported
return getPackageRoots(selectedExport.getExporter(), packageName, visited);
}
}
}
// check if the bundle exports the package
ResolverExport[] exports = bundle.getExports(packageName);
List<PackageRoots> roots = new ArrayList<>(0);
// check roots from required bundles
BundleConstraint[] requires = bundle.getRequires();
for (BundleConstraint require : requires) {
ResolverBundle supplier = (ResolverBundle) require.getSelectedSupplier();
if (supplier == null)
continue; // no supplier, probably optional
if (supplier.getExport(packageName) != null) {
// the required bundle exports the package; get the package roots from it
PackageRoots requiredRoots = getPackageRoots(supplier, packageName, visited);
if (requiredRoots != nullPackageRoots)
roots.add(requiredRoots);
} else {
// the bundle does not export the package; but it may reexport another bundle that does
BundleConstraint[] supplierRequires = supplier.getRequires();
for (BundleConstraint supplierRequire : supplierRequires) {
ResolverBundle reexported = (ResolverBundle) supplierRequire.getSelectedSupplier();
if (reexported == null || !((BundleSpecification) supplierRequire.getVersionConstraint()).isExported()) {
continue;
}
if (reexported.getExport(packageName) != null) {
// the reexported bundle exports the package; get the package roots from it
PackageRoots reExportedRoots = getPackageRoots(reexported, packageName, visited);
if (reExportedRoots != nullPackageRoots)
roots.add(reExportedRoots);
}
}
}
}
if (exports.length > 0 || roots.size() > 1) {
PackageRoots[] requiredRoots = roots.toArray(new PackageRoots[roots.size()]);
if (exports.length == 0) {
PackageRoots superSet = requiredRoots[0];
for (int i = 1; i < requiredRoots.length; i++) {
if (requiredRoots[i].superSet(superSet)) {
superSet = requiredRoots[i];
} else if (!superSet.superSet(requiredRoots[i])) {
superSet = null;
break;
}
}
if (superSet != null)
return superSet;
}
// in this case we cannot share the package roots object; must create one specific for this bundle
PackageRoots result = new PackageRoots(packageName);
// first merge all the roots from required bundles
for (PackageRoots requiredRoot : requiredRoots) {
result.merge(requiredRoot);
}
// always add this bundles exports to the end if it exports the package
for (ResolverExport export : exports) {
result.addRoot(export);
}
return result;
}
return roots.size() == 0 ? nullPackageRoots : roots.get(0);
}
public void clear() {
bundles.clear();
}
public void clear(ResolverBundle rb) {
bundles.remove(rb);
}
class PackageRoots {
private String name;
private ResolverExport[] roots;
PackageRoots(String name) {
this.name = name;
}
public boolean hasRoots() {
return roots != null && roots.length > 0;
}
public void addRoot(ResolverExport export) {
if (roots == null) {
roots = new ResolverExport[] {export};
return;
}
// need to do an extra check to make sure we are not adding the same package name
// from multiple versions of the same bundle
String exportBSN = export.getExporter().getName();
if (exportBSN != null) {
// first one wins
for (ResolverExport root : roots) {
if (export.getExporter() != root.getExporter() && exportBSN.equals(root.getExporter().getName())) {
return;
}
}
}
if (!contains(export, roots)) {
ResolverExport[] newRoots = new ResolverExport[roots.length + 1];
System.arraycopy(roots, 0, newRoots, 0, roots.length);
newRoots[roots.length] = export;
roots = newRoots;
}
}
private boolean contains(ResolverExport export, ResolverExport[] exports) {
for (ResolverExport e : exports) {
if (e == export) {
return true;
}
}
return false;
}
public void merge(PackageRoots packageRoots) {
if (packageRoots == null || packageRoots.roots == null)
return;
int size = packageRoots.roots.length;
for (int i = 0; i < size; i++)
addRoot(packageRoots.roots[i]);
}
public List<PackageRoots[]> isConsistentClassSpace(ResolverBundle importingBundle, List<PackageRoots> visited, List<PackageRoots[]> results) {
if (roots == null)
return results;
if (visited == null)
visited = new ArrayList<>(1);
if (visited.contains(this))
return results;
visited.add(this);
int size = roots.length;
for (int i = 0; i < size; i++) {
ResolverExport root = roots[i];
String[] uses = root.getUsesDirective();
if (uses == null)
continue;
for (String use : uses) {
if (use.equals(root.getName())) {
continue;
}
PackageRoots thisUsedRoots = getPackageRoots(root.getExporter(), use, null);
PackageRoots importingUsedRoots = getPackageRoots(importingBundle, use, null);
if (thisUsedRoots == importingUsedRoots)
continue;
if (thisUsedRoots != nullPackageRoots && importingUsedRoots != nullPackageRoots)
if (!(subSet(thisUsedRoots.roots, importingUsedRoots.roots) || subSet(importingUsedRoots.roots, thisUsedRoots.roots))) {
if (results == null)
results = new ArrayList<>(1);
results.add(new PackageRoots[] {this, importingUsedRoots});
}
// need to check the usedRoots consistency for transitive closure
results = thisUsedRoots.isConsistentClassSpace(importingBundle, visited, results);
}
}
return results;
}
public List<PackageRoots[]> isConsistentClassSpace(PackageRoots exportingRoots, ResolverBundle exporter, List<PackageRoots> visited, List<PackageRoots[]> results) {
if (roots == null)
return results;
for (ResolverExport root : roots) {
String[] uses = root.getUsesDirective();
if (uses == null)
continue;
if (visited == null)
visited = new ArrayList<>(1);
if (visited.contains(this))
return results;
visited.add(this);
for (String use : uses) {
if (use.equals(root.getName()) || !use.equals(exportingRoots.name)) {
continue;
}
PackageRoots thisUsedRoots = getPackageRoots(root.getExporter(), use, null);
PackageRoots exportingUsedRoots = getPackageRoots(exporter, use, null);
if (thisUsedRoots == exportingRoots)
return results;
if (thisUsedRoots != nullPackageRoots && exportingUsedRoots != nullPackageRoots)
if (!(subSet(thisUsedRoots.roots, exportingUsedRoots.roots) || subSet(exportingUsedRoots.roots, thisUsedRoots.roots))) {
if (results == null)
results = new ArrayList<>(1);
results.add(new PackageRoots[] {this, exportingUsedRoots});
}
// need to check the usedRoots consistency for transitive closure
results = thisUsedRoots.isConsistentClassSpace(exportingRoots, exporter, visited, results);
}
}
return results;
}
List<PackageRoots[]> addConflicts(ResolverBundle bundle, String usedPackage, List<PackageRoots> visited, List<PackageRoots[]> results) {
PackageRoots bundleUsedRoots = getPackageRoots(bundle, usedPackage, null);
if (this == bundleUsedRoots)
return results;
if (this != nullPackageRoots && bundleUsedRoots != nullPackageRoots)
if (!(subSet(this.roots, bundleUsedRoots.roots) || subSet(bundleUsedRoots.roots, this.roots))) {
if (results == null)
results = new ArrayList<>(1);
results.add(new PackageRoots[] {this, bundleUsedRoots});
}
// need to check the usedRoots consistency for transitive closure
return this.isConsistentClassSpace(bundleUsedRoots, bundle, visited, results);
}
// TODO this is a behavioral change; before we only required 1 supplier to match; now roots must be subsets
private boolean subSet(ResolverExport[] superSet, ResolverExport[] subSet) {
for (ResolverExport subexport : subSet) {
boolean found = false;
for (ResolverExport superexport : superSet) {
// compare by exporter in case the bundle exports the package multiple times
if (subexport.getExporter() == superexport.getExporter()) {
found = true;
break;
}
}
if (!found)
return false;
}
return true;
}
public boolean superSet(PackageRoots subSet) {
return subSet(roots, subSet.roots);
}
public String getName() {
return name;
}
public ResolverExport[] getRoots() {
return roots;
}
}
}