blob: 2164a863596fc8f237d999d47ce0937585aa105d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sling.feature.apiregions.impl;
import org.osgi.framework.Bundle;
import org.osgi.framework.Version;
import org.osgi.framework.hooks.resolver.ResolverHook;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.BundleRequirement;
import org.osgi.framework.wiring.BundleRevision;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
class ResolverHookImpl implements ResolverHook {
final Map<Map.Entry<String, Version>, List<String>> bsnVerMap;
final Map<String, Set<String>> bundleFeatureMap;
final Map<String, Set<String>> featureRegionMap;
final Map<String, Set<String>> regionPackageMap;
final Set<String> defaultRegions;
ResolverHookImpl(Map<Entry<String, Version>, List<String>> bsnVerMap, Map<String, Set<String>> bundleFeatureMap,
Map<String, Set<String>> featureRegionMap, Map<String, Set<String>> regionPackageMap, Set<String> defaultRegions) {
this.bsnVerMap = bsnVerMap;
this.bundleFeatureMap = bundleFeatureMap;
this.featureRegionMap = featureRegionMap;
this.regionPackageMap = regionPackageMap;
this.defaultRegions = defaultRegions;
}
@Override
public void filterResolvable(Collection<BundleRevision> candidates) {
// Nothing to do
}
@Override
public void filterSingletonCollisions(BundleCapability singleton, Collection<BundleCapability> collisionCandidates) {
// Nothing to do
}
@Override
public void filterMatches(BundleRequirement requirement, Collection<BundleCapability> candidates) {
// Filtering is only on package resolution. Any other kind of resolution is not limited
if (!PackageNamespace.PACKAGE_NAMESPACE.equals(requirement.getNamespace()))
return;
Bundle reqBundle = requirement.getRevision().getBundle();
long reqBundleID = reqBundle.getBundleId();
String reqBundleName = reqBundle.getSymbolicName();
Version reqBundleVersion = reqBundle.getVersion();
Set<String> reqRegions = new HashSet<>(defaultRegions);
List<String> reqFeatures = new ArrayList<>();
List<String> aids = bsnVerMap.get(new AbstractMap.SimpleEntry<String, Version>(reqBundleName, reqBundleVersion));
if (aids != null) {
for (String aid : aids) {
Set<String> fid = bundleFeatureMap.get(aid);
if (fid != null)
reqFeatures.addAll(fid);
}
for (String feature : reqFeatures) {
Set<String> fr = featureRegionMap.get(feature);
if (fr != null) {
reqRegions.addAll(fr);
}
}
} else {
// Bundle is not coming from a feature
}
Map<BundleCapability, String> coveredCaps = new HashMap<>();
Map<BundleCapability, String> bcFeatureMap = new HashMap<>();
String packageName = null;
nextCapability:
for (BundleCapability bc : candidates) {
BundleRevision rev = bc.getRevision();
Bundle capBundle = rev.getBundle();
long capBundleID = capBundle.getBundleId();
if (capBundleID == 0) {
// always allow capability from the system bundle
coveredCaps.put(bc, null); // null value means same bundle, same feature or system bundlee
continue nextCapability;
}
if (capBundleID == reqBundleID) {
// always allow capability from same bundle
coveredCaps.put(bc, null); // null value means same bundle, same feature or system bundle
continue nextCapability;
}
String capBundleName = capBundle.getSymbolicName();
Version capBundleVersion = capBundle.getVersion();
List<String> capBundleArtifacts = bsnVerMap.get(new AbstractMap.SimpleEntry<String, Version>(capBundleName, capBundleVersion));
if (capBundleArtifacts == null) {
// Capability is not in any feature, everyone can access
coveredCaps.put(bc, RegionEnforcer.GLOBAL_REGION);
continue nextCapability;
}
List<String> capFeatures = new ArrayList<>();
for (String ba : capBundleArtifacts) {
Set<String> capfeats = bundleFeatureMap.get(ba);
if (capfeats != null)
capFeatures.addAll(capfeats);
}
if (capFeatures.isEmpty())
capFeatures = Collections.singletonList(null);
for (String capFeat : capFeatures) {
if (capFeat == null) {
// everyone can access capability not coming from a feature
coveredCaps.put(bc, RegionEnforcer.GLOBAL_REGION);
continue nextCapability;
}
if (reqFeatures.contains(capFeat)) {
// Within a single feature everything can wire to everything else
coveredCaps.put(bc, null); // null value means same bundle, same feature or system bundle
continue nextCapability;
}
Set<String> capRegions = featureRegionMap.get(capFeat);
if (capRegions == null || capRegions.size() == 0) {
// If the feature hosting the capability has no regions defined, everyone can access
coveredCaps.put(bc, RegionEnforcer.GLOBAL_REGION);
continue nextCapability;
}
bcFeatureMap.put(bc, capFeat);
List<String> sharedRegions = new ArrayList<>(reqRegions);
sharedRegions.retainAll(capRegions);
Object pkg = bc.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE);
if (pkg instanceof String) {
packageName = (String) pkg;
// Look at specific regions first as they take precedence over the global region
for (String region : sharedRegions) {
Set<String> regionPackages = regionPackageMap.get(region);
if (regionPackages != null && regionPackages.contains(packageName)) {
// If the export is in a region that the feature is also in, then allow
coveredCaps.put(bc, region);
continue nextCapability;
}
}
// Now check the global region
Set<String> globalPackages = regionPackageMap.get(RegionEnforcer.GLOBAL_REGION);
if (globalPackages != null && globalPackages.contains(packageName)) {
// If the export is in the global region everyone can access
coveredCaps.put(bc, RegionEnforcer.GLOBAL_REGION);
continue nextCapability;
}
}
}
}
pruneCoveredCaps(reqRegions, coveredCaps);
List<BundleCapability> removedCandidates = new ArrayList<>(candidates);
// Remove any capabilities that are not covered
candidates.retainAll(coveredCaps.keySet());
if (candidates.isEmpty()) {
removedCandidates.removeAll(candidates);
StringBuilder sb = new StringBuilder();
boolean first = true;
for (BundleCapability bc : removedCandidates) {
if (first)
first = false;
else
sb.append(", ");
String capFeat = bcFeatureMap.get(bc);
sb.append(bc.toString());
sb.append("[Regions: ");
sb.append(getRegionsForPackage(packageName, capFeat));
sb.append(", Feature: ");
sb.append(capFeat);
sb.append("]");
}
RegionEnforcer.LOG.log(Level.WARNING,
"API-Regions removed candidates {0} for requirement {1} as the requirement is in the following regions: {2} and in feature: {3}",
new Object[] {sb, requirement, reqRegions, reqFeatures});
}
}
/*
* If there are multiple choices of capabilities and some of the capabilities are in the global
* region while others are in another named region, take out the capabilities from the global
* region so that the requirement gets wired to the more 'specifc' one than the global one.
* Capabilities from bundle 0 (the system bundle), the same bundle as the requirer and from the
* same feature as the requirer should always be kept. These are marked in the capMap with a
* {@code null} region value.
*/
private void pruneCoveredCaps(Set<String> reqRegions, Map<BundleCapability,String> capMap) {
Set<String> reqNonGlobalRegions = new HashSet<>(reqRegions);
reqNonGlobalRegions.remove(RegionEnforcer.GLOBAL_REGION);
if (capMap.size() <= 1) {
// Shortcut: there is only 0 or 1 capability, nothing to do
return;
}
if (reqRegions.size() == 0
|| Collections.singleton(RegionEnforcer.GLOBAL_REGION).equals(reqRegions)) {
// No regions (other than global) for the requirement: do nothing
return;
}
List<BundleCapability> specificCaps = new ArrayList<>();
for (Iterator<Map.Entry<BundleCapability,String>> it = capMap.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<BundleCapability,String> entry = it.next();
String capRegion = entry.getValue();
if (capRegion == null) {
// This one came from the same bundle, the same feature or bundle 0 -> always allow
specificCaps.add(entry.getKey());
continue;
}
if (reqNonGlobalRegions.contains(capRegion)) {
// the requirement has the region from the capability
specificCaps.add(entry.getKey());
}
}
if (specificCaps.size() == 0) {
// There are no capabilities that are either in the same bundle, same feature or overlapping specific
// feature. We should just allow all, including the global region
return;
}
// There are specific capabilities, therefore we should remove the Global region is any from the capabilities
// We have collected the capabilities we want to keep in specificCaps
for (Iterator<BundleCapability> it = capMap.keySet().iterator(); it.hasNext(); ) {
if (!specificCaps.contains(it.next())) {
it.remove();
}
}
}
List<String> getRegionsForPackage(String packageName, String feature) {
if (packageName == null)
return Collections.emptyList();
Set<String> regions = featureRegionMap.get(feature);
if (regions == null)
return Collections.emptyList();
List<String> res = new ArrayList<>();
for (String region : regions) {
Set<String> packages = regionPackageMap.get(region);
if (packages == null)
continue;
if (packages.contains(packageName)) {
res.add(region);
}
}
return res;
}
@Override
public void end() {
// Nothing to do
}
}