blob: 91eebcf11999b06b1780f0de5bf64f5cc724ef19 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 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.container;
import static org.eclipse.osgi.internal.container.NamespaceList.CAPABILITY;
import static org.eclipse.osgi.internal.container.NamespaceList.REQUIREMENT;
import static org.eclipse.osgi.internal.container.NamespaceList.WIRE;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.felix.resolver.Logger;
import org.apache.felix.resolver.ResolutionError;
import org.apache.felix.resolver.ResolverImpl;
import org.eclipse.osgi.container.ModuleRequirement.DynamicModuleRequirement;
import org.eclipse.osgi.container.namespaces.EquinoxFragmentNamespace;
import org.eclipse.osgi.internal.container.InternalUtils;
import org.eclipse.osgi.internal.container.NamespaceList;
import org.eclipse.osgi.internal.container.NamespaceList.Builder;
import org.eclipse.osgi.internal.debug.Debug;
import org.eclipse.osgi.internal.framework.EquinoxConfiguration;
import org.eclipse.osgi.internal.framework.EquinoxContainer;
import org.eclipse.osgi.internal.messages.Msg;
import org.eclipse.osgi.report.resolution.ResolutionReport;
import org.eclipse.osgi.report.resolution.ResolutionReport.Entry;
import org.eclipse.osgi.report.resolution.ResolutionReport.Entry.Type;
import org.eclipse.osgi.service.debug.DebugOptions;
import org.osgi.framework.BundleException;
import org.osgi.framework.Version;
import org.osgi.framework.hooks.resolver.ResolverHook;
import org.osgi.framework.namespace.BundleNamespace;
import org.osgi.framework.namespace.ExecutionEnvironmentNamespace;
import org.osgi.framework.namespace.HostNamespace;
import org.osgi.framework.namespace.IdentityNamespace;
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 org.osgi.resource.Capability;
import org.osgi.resource.Namespace;
import org.osgi.resource.Requirement;
import org.osgi.resource.Resource;
import org.osgi.resource.Wire;
import org.osgi.resource.Wiring;
import org.osgi.service.resolver.HostedCapability;
import org.osgi.service.resolver.ResolutionException;
import org.osgi.service.resolver.ResolveContext;
import org.osgi.service.resolver.Resolver;
/**
* The module resolver handles calls to the {@link Resolver} service for resolving modules
* in a module {@link ModuleContainer container}.
*/
final class ModuleResolver {
static final String SEPARATOR = System.lineSeparator();
static final char TAB = '\t';
private static final String OPTION_RESOLVER = EquinoxContainer.NAME + "/resolver"; //$NON-NLS-1$
private static final String OPTION_ROOTS = OPTION_RESOLVER + "/roots"; //$NON-NLS-1$
private static final String OPTION_PROVIDERS = OPTION_RESOLVER + "/providers"; //$NON-NLS-1$
private static final String OPTION_HOOKS = OPTION_RESOLVER + "/hooks"; //$NON-NLS-1$
private static final String OPTION_USES = OPTION_RESOLVER + "/uses"; //$NON-NLS-1$
private static final String OPTION_WIRING = OPTION_RESOLVER + "/wiring"; //$NON-NLS-1$
private static final String OPTION_REPORT = OPTION_RESOLVER + "/report"; //$NON-NLS-1$
boolean DEBUG_ROOTS = false;
boolean DEBUG_PROVIDERS = false;
boolean DEBUG_HOOKS = false;
boolean DEBUG_USES = false;
boolean DEBUG_WIRING = false;
boolean DEBUG_REPORT = false;
private static final int DEFAULT_BATCH_SIZE = Integer.MAX_VALUE;
private static final int BATCH_MIN_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(5);
private static final int DEFAULT_BATCH_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(2);
final int resolverRevisionBatchSize;
final int resolverBatchTimeout;
void setDebugOptions() {
DebugOptions options = adaptor.getDebugOptions();
// may be null if debugging is not enabled
if (options == null)
return;
boolean debugAll = options.getBooleanOption(OPTION_RESOLVER, false);
DEBUG_ROOTS = debugAll || options.getBooleanOption(OPTION_ROOTS, false);
DEBUG_PROVIDERS = debugAll || options.getBooleanOption(OPTION_PROVIDERS, false);
DEBUG_HOOKS = debugAll || options.getBooleanOption(OPTION_HOOKS, false);
DEBUG_USES = debugAll || options.getBooleanOption(OPTION_USES, false);
DEBUG_WIRING = debugAll || options.getBooleanOption(OPTION_WIRING, false);
DEBUG_REPORT = debugAll || options.getBooleanOption(OPTION_REPORT, false);
}
static final Collection<String> NON_PAYLOAD_CAPABILITIES = Arrays.asList(IdentityNamespace.IDENTITY_NAMESPACE);
static final Collection<String> NON_PAYLOAD_REQUIREMENTS = Arrays.asList(HostNamespace.HOST_NAMESPACE, ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE);
static final Collection<String> NON_SUBSTITUTED_REQUIREMENTS = Arrays.asList(PackageNamespace.PACKAGE_NAMESPACE, BundleNamespace.BUNDLE_NAMESPACE);
final ThreadLocal<Boolean> threadResolving = new ThreadLocal<>();
final ModuleContainerAdaptor adaptor;
/**
* Constructs the module resolver with the specified resolver hook factory
* and resolver.
* @param adaptor the container adaptor
*/
ModuleResolver(final ModuleContainerAdaptor adaptor) {
this.adaptor = adaptor;
setDebugOptions();
String batchSizeConfig = this.adaptor.getProperty(EquinoxConfiguration.PROP_RESOLVER_REVISION_BATCH_SIZE);
this.resolverRevisionBatchSize = parseInteger(batchSizeConfig, DEFAULT_BATCH_SIZE, 1);
String batchTimeoutConfig = this.adaptor.getProperty(EquinoxConfiguration.PROP_RESOLVER_BATCH_TIMEOUT);
this.resolverBatchTimeout = parseInteger(batchTimeoutConfig, DEFAULT_BATCH_TIMEOUT, BATCH_MIN_TIMEOUT);
}
private static int parseInteger(String sInteger, int defaultValue, int minValue) {
try {
int result = sInteger == null ? defaultValue : Integer.parseInt(sInteger);
return result < minValue ? minValue : result;
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* Attempts to resolve all unresolved modules installed in the specified module database.
* returns a delta containing the new wirings or modified wirings that should be
* merged into the specified moduleDatabase.
* <p>
* This method only does read operations on the database no wirings are modified
* directly by this method. The returned wirings need to be merged into
* the database.
* @param triggers the triggers that caused the resolver operation to occur
* @param triggersMandatory true if the triggers must be resolved by the resolve process
* @param unresolved a snapshot of unresolved revisions
* @param wiringCopy the wirings snapshot of the currently resolved revisions
* @param moduleDatabase the module database.
* @return a delta container the new wirings or modified wirings that should be
* merged into the moduleDatabase
* @throws ResolutionException
*/
ModuleResolutionReport resolveDelta(Collection<ModuleRevision> triggers, boolean triggersMandatory, Collection<ModuleRevision> unresolved, Map<ModuleRevision, ModuleWiring> wiringCopy, ModuleDatabase moduleDatabase) {
ResolveProcess resolveProcess = new ResolveProcess(unresolved, triggers, triggersMandatory, wiringCopy, moduleDatabase);
return resolveProcess.resolve();
}
ModuleResolutionReport resolveDynamicDelta(DynamicModuleRequirement dynamicReq, Collection<ModuleRevision> unresolved, Map<ModuleRevision, ModuleWiring> wiringCopy, ModuleDatabase moduleDatabase) {
ResolveProcess resolveProcess = new ResolveProcess(unresolved, dynamicReq, wiringCopy, moduleDatabase);
return resolveProcess.resolve();
}
Map<ModuleRevision, ModuleWiring> generateDelta(Map<Resource, List<Wire>> result, Map<ModuleRevision, ModuleWiring> wiringCopy) {
Map<ModuleRevision, Map<ModuleCapability, List<ModuleWire>>> provided = new HashMap<>();
Map<ModuleRevision, NamespaceList<ModuleWire>> required = new HashMap<>(result.size() * 4 / 3 + 1);
// First populate the list of provided and required wires for revision
// This is done this way to share the wire object between both the provider and requirer
for (Map.Entry<Resource, List<Wire>> resultEntry : result.entrySet()) {
ModuleRevision revision = (ModuleRevision) resultEntry.getKey();
NamespaceList.Builder<ModuleWire> requiredWires = NamespaceList.Builder.create(WIRE);
for (Wire wire : resultEntry.getValue()) {
ModuleWire moduleWire = new ModuleWire((ModuleCapability) wire.getCapability(), (ModuleRevision) wire.getProvider(), (ModuleRequirement) wire.getRequirement(), (ModuleRevision) wire.getRequirer());
requiredWires.add(moduleWire);
Map<ModuleCapability, List<ModuleWire>> providedWiresMap = provided.get(moduleWire.getProvider());
if (providedWiresMap == null) {
providedWiresMap = new HashMap<>();
provided.put(moduleWire.getProvider(), providedWiresMap);
}
List<ModuleWire> providedWires = providedWiresMap.get(moduleWire.getCapability());
if (providedWires == null) {
providedWires = new ArrayList<>();
providedWiresMap.put(moduleWire.getCapability(), providedWires);
}
providedWires.add(moduleWire);
}
required.put(revision, requiredWires.build());
}
Map<ModuleRevision, ModuleWiring> delta = new HashMap<>();
// now create the ModuleWiring for the newly resolved revisions
for (ModuleRevision revision : required.keySet()) {
ModuleWiring existingWiring = wiringCopy.get(revision);
if (existingWiring == null) {
delta.put(revision, createNewWiring(revision, provided, required));
} else {
// this is to handle dynamic imports
delta.put(revision, createWiringDelta(revision, existingWiring, provided.get(revision), required.get(revision)));
}
}
// Also need to create the wiring deltas for already resolved bundles
// This should only include updating provided wires and
// for fragments it may include new hosts
for (ModuleRevision revision : provided.keySet()) {
ModuleWiring existingWiring = wiringCopy.get(revision);
if (existingWiring != null && !delta.containsKey(revision)) {
delta.put(revision, createWiringDelta(revision, existingWiring, provided.get(revision), required.get(revision)));
}
}
return delta;
}
private ModuleWiring createNewWiring(ModuleRevision revision, Map<ModuleRevision, Map<ModuleCapability, List<ModuleWire>>> provided, Map<ModuleRevision, NamespaceList<ModuleWire>> required) {
Map<ModuleCapability, List<ModuleWire>> providedWireMap = provided.getOrDefault(revision, Collections.emptyMap());
NamespaceList<ModuleWire> requiredWires = required.getOrDefault(revision, NamespaceList.empty(WIRE));
NamespaceList.Builder<ModuleCapability> capabilities = Builder.create(CAPABILITY);
capabilities.addAll(revision.getModuleCapabilities(null));
NamespaceList.Builder<ModuleRequirement> requirements = Builder.create(REQUIREMENT);
requirements.addAll(revision.getModuleRequirements(null));
// if revision is a fragment remove payload requirements and capabilities
if ((BundleRevision.TYPE_FRAGMENT & revision.getTypes()) != 0) {
removePayloadContent(capabilities, requirements);
} else {
// add fragment capabilities and requirements
List<ModuleCapability> hostCapabilities = revision.getModuleCapabilities(HostNamespace.HOST_NAMESPACE);
ModuleCapability hostCapability = hostCapabilities.isEmpty() ? null : hostCapabilities.get(0);
if (hostCapability != null) {
addPayloadContent(providedWireMap.get(hostCapability), capabilities, requirements);
}
}
removeNonEffectiveCapabilities(capabilities);
removeNonEffectiveRequirements(requirements, requiredWires);
Collection<String> substituted = removeSubstitutedCapabilities(capabilities, requiredWires);
NamespaceList.Builder<ModuleWire> providedWires = NamespaceList.Builder.create(WIRE);
addProvidedWires(providedWireMap, providedWires, capabilities);
InternalUtils.filterCapabilityPermissions(capabilities);
return new ModuleWiring(revision, capabilities.build(), requirements.build(), providedWires.build(),
requiredWires, substituted);
}
private static void removePayloadContent(NamespaceList.Builder<ModuleCapability> capabilities,
NamespaceList.Builder<ModuleRequirement> requirements) {
capabilities.removeNamespaceIf(namespace -> !NON_PAYLOAD_CAPABILITIES.contains(namespace));
requirements.removeNamespaceIf(namespace -> !NON_PAYLOAD_REQUIREMENTS.contains(namespace));
}
private static Collection<String> removeSubstitutedCapabilities(NamespaceList.Builder<ModuleCapability> capabilities, NamespaceList<ModuleWire> requiredWires) {
Collection<String> substituted = new ArrayList<>();
for (ModuleWire moduleWire : requiredWires.getList(PackageNamespace.PACKAGE_NAMESPACE)) {
String packageName = (String) moduleWire.getCapability().getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE);
capabilities.removeElementsOfNamespaceIf(PackageNamespace.PACKAGE_NAMESPACE, capability -> {
if (packageName.equals(capability.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE))) {
// found a package capability with the same name as a package that got imported
// this indicates a substitution
if (!substituted.contains(packageName)) {
substituted.add(packageName);
}
return true;
}
return false;
});
}
return substituted.isEmpty() ? Collections.emptyList() : substituted;
}
private static void removeNonEffectiveRequirements(NamespaceList.Builder<ModuleRequirement> requirements, NamespaceList<ModuleWire> requiredWires) {
Set<ModuleRequirement> wireRequirements = new HashSet<>();
for (ModuleWire mw : requiredWires.getList(null)) {
wireRequirements.add(mw.getRequirement());
}
requirements.removeIf(requirement -> {
// check the effective directive;
Object effective = requirement.getDirectives().get(Namespace.REQUIREMENT_EFFECTIVE_DIRECTIVE);
if (effective != null && !Namespace.EFFECTIVE_RESOLVE.equals(effective)) {
return true;
}
if (!wireRequirements.contains(requirement)) {
if (!PackageNamespace.PACKAGE_NAMESPACE.equals(requirement.getNamespace())) {
return true;
}
Object resolution = requirement.getDirectives().get(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE);
if (!PackageNamespace.RESOLUTION_DYNAMIC.equals(resolution)) {
return true;
}
}
return false;
});
}
void removeNonEffectiveCapabilities(Collection<ModuleCapability> capabilities) {
capabilities.removeIf(capability -> {
Object effective = capability.getDirectives().get(Namespace.CAPABILITY_EFFECTIVE_DIRECTIVE);
if (effective != null && !Namespace.EFFECTIVE_RESOLVE.equals(effective)) {
if (DEBUG_PROVIDERS) {
Debug.println(new StringBuilder("RESOLVER: Capability filtered because it was not effective") //$NON-NLS-1$
.append(SEPARATOR).append(TAB) //
.append(capability) //
.append(SEPARATOR).append(TAB).append(TAB) //
.append("of resource") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB).append(TAB) //
.append(capability.getResource()) //
.toString());
}
return true;
}
return false;
});
}
private static void addPayloadContent(List<ModuleWire> hostWires, NamespaceList.Builder<ModuleCapability> capabilities, NamespaceList.Builder<ModuleRequirement> requirements) {
if (hostWires == null)
return;
for (ModuleWire hostWire : hostWires) {
// add fragment capabilities
List<ModuleCapability> fragmentCapabilities = hostWire.getRequirer().getModuleCapabilities(null);
for (ModuleCapability fragmentCapability : fragmentCapabilities) {
if (NON_PAYLOAD_CAPABILITIES.contains(fragmentCapability.getNamespace())) {
continue; // don't include, not a payload capability
}
Object effective = fragmentCapability.getDirectives().get(Namespace.CAPABILITY_EFFECTIVE_DIRECTIVE);
if (effective != null && !Namespace.EFFECTIVE_RESOLVE.equals(effective)) {
continue; // don't include, not effective
}
capabilities.add(fragmentCapability);
}
// add fragment requirements
List<ModuleRequirement> fragmentRequriements = hostWire.getRequirer().getModuleRequirements(null);
for (ModuleRequirement fragmentRequirement : fragmentRequriements) {
if (NON_PAYLOAD_REQUIREMENTS.contains(fragmentRequirement.getNamespace())) {
continue; // don't include, not a payload requirement
}
Object effective = fragmentRequirement.getDirectives().get(Namespace.REQUIREMENT_EFFECTIVE_DIRECTIVE);
if (effective != null && !Namespace.EFFECTIVE_RESOLVE.equals(effective)) {
continue; // don't include, not effective
}
if (!PackageNamespace.PACKAGE_NAMESPACE.equals(fragmentRequirement.getNamespace())
|| isDynamic(fragmentRequirement)) {
requirements.add(fragmentRequirement);
} else {
requirements.addAfterLastMatch(fragmentRequirement, r -> !isDynamic(r));
}
}
}
}
static boolean isDynamic(Requirement requirement) {
return PackageNamespace.PACKAGE_NAMESPACE.equals(requirement.getNamespace()) && PackageNamespace.RESOLUTION_DYNAMIC.equals(requirement.getDirectives().get(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE));
}
private static void addProvidedWires(Map<ModuleCapability, List<ModuleWire>> toAdd,
NamespaceList.Builder<ModuleWire> existing, NamespaceList.Builder<ModuleCapability> capabilities) {
if (toAdd == null)
return;
for (ModuleCapability capability : capabilities) {
List<ModuleWire> newWires = toAdd.get(capability);
if (newWires != null) {
existing.addAll(newWires);
}
}
}
private static ModuleWiring createWiringDelta(ModuleRevision revision, ModuleWiring existingWiring, Map<ModuleCapability, List<ModuleWire>> providedWireMap, NamespaceList<ModuleWire> requiredWires) {
// No null checks are done here on the wires since this is a copy.
NamespaceList.Builder<ModuleWire> existingProvidedWires = existingWiring.getProvidedWires().createBuilder();
NamespaceList.Builder<ModuleCapability> existingCapabilities = existingWiring.getCapabilities().createBuilder();
NamespaceList.Builder<ModuleWire> existingRequiredWires = existingWiring.getRequiredWires().createBuilder();
NamespaceList.Builder<ModuleRequirement> existingRequirements = existingWiring.getRequirements().createBuilder();
// First, add newly resolved fragment capabilities and requirements
if (providedWireMap != null) {
List<ModuleCapability> hostCapabilities = revision.getModuleCapabilities(HostNamespace.HOST_NAMESPACE);
ModuleCapability hostCapability = hostCapabilities.isEmpty() ? null : hostCapabilities.get(0);
List<ModuleWire> newHostWires = hostCapability == null ? null : providedWireMap.get(hostCapability);
if (newHostWires != null) {
addPayloadContent(newHostWires, existingCapabilities, existingRequirements);
}
}
// Create a ModuleWiring that only contains the new ordered list of provided wires
addProvidedWires(providedWireMap, existingProvidedWires, existingCapabilities);
// Also need to include any new required wires that may have be added for fragment hosts
// Also will be needed for dynamic imports
if (requiredWires != null) {
existingRequiredWires.addAll(requiredWires);
}
InternalUtils.filterCapabilityPermissions(existingCapabilities);
return new ModuleWiring(revision, existingCapabilities.build(), existingRequirements.build(),
existingProvidedWires.build(), existingRequiredWires.build(), existingWiring.getSubstitutedNames());
}
static boolean isSingleton(ModuleRevision revision) {
List<Capability> identities = revision.getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE);
if (identities.isEmpty())
return false;
return "true".equals(identities.get(0).getDirectives().get(IdentityNamespace.CAPABILITY_SINGLETON_DIRECTIVE)); //$NON-NLS-1$
}
static Version getVersion(Capability c) {
String versionAttr = null;
String namespace = c.getNamespace();
if (IdentityNamespace.IDENTITY_NAMESPACE.equals(namespace)) {
versionAttr = IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE;
} else if (PackageNamespace.PACKAGE_NAMESPACE.equals(namespace)) {
versionAttr = PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE;
} else if (BundleNamespace.BUNDLE_NAMESPACE.equals(namespace)) {
versionAttr = BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE;
} else if (HostNamespace.HOST_NAMESPACE.equals(namespace)) {
versionAttr = HostNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE;
} else {
// Just default to version attribute
versionAttr = IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE;
}
Object version = c.getAttributes().get(versionAttr);
return version instanceof Version ? (Version) version : Version.emptyVersion;
}
class ResolveProcess extends ResolveContext implements Comparator<Capability>, Executor {
class ResolveLogger extends Logger {
private Map<Resource, ResolutionException> errors = null;
public ResolveLogger() {
super(DEBUG_USES ? Logger.LOG_DEBUG : 0);
}
@Override
public void logUsesConstraintViolation(Resource resource, ResolutionError error) {
if (errors == null) {
errors = new HashMap<>();
}
errors.put(resource, error.toException());
if (DEBUG_USES) {
Debug.println(new StringBuilder("RESOLVER: Uses constraint violation") //$NON-NLS-1$
.append(SEPARATOR).append(TAB) //
.append("Resource") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB) //
.append(resource) //
.append(SEPARATOR).append(TAB) //
.append("Error") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB) //
.append(error.getMessage()) //
.toString());
}
}
Map<Resource, ResolutionException> getUsesConstraintViolations() {
return errors == null ? Collections.<Resource, ResolutionException> emptyMap() : errors;
}
@Override
public boolean isDebugEnabled() {
return DEBUG_USES;
}
@Override
protected void doLog(int level, String msg, Throwable throwable) {
Debug.println("RESOLVER: " + msg + SEPARATOR + (throwable != null ? (TAB + TAB + throwable.getMessage()) : "")); //$NON-NLS-1$ //$NON-NLS-2$
}
}
private final ModuleResolutionReport.Builder reportBuilder = new ModuleResolutionReport.Builder();
/*
* Contains the revisions that were requested to be resolved and is not
* modified post instantiation.
*/
private final Collection<ModuleRevision> unresolved;
/*
* Contains unresolved revisions that should not be resolved as part of
* this process. The reasons they should not be resolved will vary. For
* example, some might have been filtered out by the resolver hook while
* others represent singleton collisions. It is assumed that all
* unresolved revisions are disabled at the start of the resolve
* process (see initialization in constructors). Any not filtered out
* by ResolverHook.filterResolvable are then removed but may be added
* back later for other reasons.
*/
private final Collection<ModuleRevision> disabled;
private final Collection<ModuleRevision> triggers;
private final Collection<ModuleRevision> optionals;
private final boolean triggersMandatory;
final ModuleDatabase moduleDatabase;
final Map<ModuleRevision, ModuleWiring> wirings;
private final Set<ModuleRevision> previouslyResolved;
private final DynamicModuleRequirement dynamicReq;
private volatile ResolverHook hook = null;
private volatile Map<String, Collection<ModuleRevision>> byName = null;
private volatile List<Resource> currentlyResolving = null;
private volatile boolean currentlyResolvingMandatory = false;
private final Set<Resource> transitivelyResolveFailures = new LinkedHashSet<>();
private final Set<Resource> failedToResolve = new HashSet<>();
private AtomicBoolean scheduleTimeout = new AtomicBoolean(true);
private AtomicReference<ScheduledFuture<?>> timoutFuture = new AtomicReference<>();
/*
* Used to generate the UNRESOLVED_PROVIDER resolution report entries.
*
* The inner map associates a requirement to the set of all matching
* capabilities that were found. The outer map associates the requiring
* resource to the inner map so that its contents may easily be looked
* up from the set of unresolved resources, if any, after the resolution
* has occurred.
*/
private final Map<Resource, Map<Requirement, Set<Capability>>> unresolvedProviders = new HashMap<>();
ResolveProcess(Collection<ModuleRevision> unresolved, Collection<ModuleRevision> triggers, boolean triggersMandatory, Map<ModuleRevision, ModuleWiring> wirings, ModuleDatabase moduleDatabase) {
this.unresolved = unresolved;
this.disabled = new HashSet<>(unresolved);
this.triggers = new ArrayList<>(triggers);
this.triggersMandatory = triggersMandatory;
this.optionals = new LinkedHashSet<>(unresolved);
if (this.triggersMandatory) {
// do this the hard way because the 'optimization' in removeAll hurts us
for (ModuleRevision triggerRevision : triggers) {
this.optionals.remove(triggerRevision);
}
}
this.wirings = new HashMap<>(wirings);
this.previouslyResolved = new HashSet<>(wirings.keySet());
this.moduleDatabase = moduleDatabase;
this.dynamicReq = null;
}
ResolveProcess(Collection<ModuleRevision> unresolved, DynamicModuleRequirement dynamicReq, Map<ModuleRevision, ModuleWiring> wirings, ModuleDatabase moduleDatabase) {
this.unresolved = unresolved;
this.disabled = new HashSet<>(unresolved);
ModuleRevision revision = dynamicReq.getRevision();
this.triggers = new ArrayList<>(1);
this.triggers.add(revision);
this.triggersMandatory = false;
this.optionals = new ArrayList<>(unresolved);
this.wirings = wirings;
this.previouslyResolved = new HashSet<>(wirings.keySet());
this.moduleDatabase = moduleDatabase;
this.dynamicReq = dynamicReq;
}
@Override
public List<Capability> findProviders(Requirement requirement) {
Requirement origReq = requirement;
Requirement lookupReq = dynamicReq == null || dynamicReq.getOriginal() != requirement ? requirement : dynamicReq;
return findProviders0(origReq, lookupReq);
}
private List<Capability> findProviders0(Requirement origReq, Requirement lookupReq) {
if (DEBUG_PROVIDERS) {
Debug.println(new StringBuilder("RESOLVER: Finding capabilities for requirement") //$NON-NLS-1$
.append(SEPARATOR).append(TAB) //
.append(origReq) //
.append(SEPARATOR).append(TAB).append(TAB) //
.append("of resource") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB).append(TAB) //
.append(origReq.getResource()) //
.toString());
}
List<ModuleCapability> candidates = moduleDatabase.findCapabilities(lookupReq);
List<Capability> result = filterProviders(origReq, candidates);
if (DEBUG_PROVIDERS) {
StringBuilder builder = new StringBuilder("RESOLVER: Capabilities being returned to the resolver"); //$NON-NLS-1$
int i = 0;
for (Capability capability : result) {
builder.append(SEPARATOR).append(TAB) //
.append("[").append(++i).append("] ") //$NON-NLS-1$ //$NON-NLS-2$
.append(capability) //
.append(SEPARATOR).append(TAB).append(TAB) //
.append("of resource") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB).append(TAB) //
.append(capability.getResource());
}
Debug.println(builder.toString());
}
return result;
}
private List<Capability> filterProviders(Requirement requirement, List<ModuleCapability> candidates) {
return filterProviders(requirement, candidates, true);
}
List<Capability> filterProviders(Requirement requirement, List<ModuleCapability> candidates, boolean filterResolvedHosts) {
filterDisabled(candidates);
removeNonEffectiveCapabilities(candidates);
removeSubstituted(candidates);
filterPermissions((BundleRequirement) requirement, candidates);
List<ModuleCapability> filteredMatches = null;
if (DEBUG_PROVIDERS || DEBUG_HOOKS) {
filteredMatches = new ArrayList<>(candidates);
}
hook.filterMatches((BundleRequirement) requirement, InternalUtils.asListBundleCapability(candidates));
if (DEBUG_PROVIDERS || DEBUG_HOOKS) {
filteredMatches.removeAll(candidates);
if (!filteredMatches.isEmpty()) {
StringBuilder builder = new StringBuilder("RESOLVER: Capabilities filtered by ResolverHook.filterMatches"); //$NON-NLS-1$
int i = 0;
for (Capability capability : filteredMatches) {
builder.append(SEPARATOR).append(TAB) //
.append("[").append(++i).append("] ") //$NON-NLS-1$ //$NON-NLS-2$
.append(capability) //
.append(SEPARATOR).append(TAB).append(TAB) //
.append("of resource") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB).append(TAB) //
.append(capability.getResource());
}
Debug.println(builder.toString());
}
}
// filter resolved hosts after calling hooks to allow hooks to see the host capability
filterResolvedHosts(requirement, candidates, filterResolvedHosts);
if (candidates.isEmpty()) {
if (!wirings.containsKey(requirement.getResource()) || isDynamic(requirement)) {
reportBuilder.addEntry(requirement.getResource(), Entry.Type.MISSING_CAPABILITY, requirement);
String resolution = requirement.getDirectives().get(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE);
if ((resolution == null || Namespace.RESOLUTION_MANDATORY.equals(resolution))) {
transitivelyResolveFailures.add(requirement.getResource());
}
}
} else {
computeUnresolvedProviders(requirement, candidates);
}
filterFailedToResolve(candidates);
Collections.sort(candidates, this);
return InternalUtils.asListCapability(candidates);
}
private void filterFailedToResolve(List<ModuleCapability> candidates) {
for (Iterator<ModuleCapability> iCandidates = candidates.iterator(); iCandidates.hasNext();) {
ModuleCapability capability = iCandidates.next();
if (failedToResolve.contains(capability.getRevision())) {
iCandidates.remove();
if (DEBUG_PROVIDERS) {
Debug.println(new StringBuilder("RESOLVER: Capability filtered because its resource was not resolved") //$NON-NLS-1$
.append(SEPARATOR).append(TAB) //
.append(capability) //
.append(SEPARATOR).append(TAB).append(TAB) //
.append("of resource") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB).append(TAB) //
.append(capability.getResource()) //
.toString());
}
}
}
}
private void filterResolvedHosts(Requirement requirement, List<ModuleCapability> candidates, boolean filterResolvedHosts) {
if (filterResolvedHosts && HostNamespace.HOST_NAMESPACE.equals(requirement.getNamespace())) {
for (Iterator<ModuleCapability> iCandidates = candidates.iterator(); iCandidates.hasNext();) {
if (wirings.containsKey(iCandidates.next().getRevision())) {
iCandidates.remove();
}
}
}
}
private void filterPermissions(BundleRequirement requirement, List<ModuleCapability> candidates) {
if (System.getSecurityManager() == null) {
return;
}
if (requirement.getRevision().getBundle() == null) {
// this container is not modeling a real framework, no permission check is done
return;
}
candidates.removeIf(candidate -> {
// TODO this is a hack for when a bundle imports and exports the same package
if (PackageNamespace.PACKAGE_NAMESPACE.equals(requirement.getNamespace())) {
if (requirement.getRevision().equals(candidate.getRevision())) {
return false;
}
}
Permission requirePermission = InternalUtils.getRequirePermission(candidate);
Permission providePermission = InternalUtils.getProvidePermission(candidate);
if (!requirement.getRevision().getBundle().hasPermission(requirePermission)) {
if (DEBUG_PROVIDERS) {
Debug.println(new StringBuilder("RESOLVER: Capability filtered because requirer did not have permission") //$NON-NLS-1$
.append(SEPARATOR).append(TAB) //
.append(candidate) //
.append(SEPARATOR).append(TAB).append(TAB) //
.append("of resource") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB).append(TAB) //
.append(candidate.getResource()) //
.toString());
}
return true;
} else if (!candidate.getRevision().getBundle().hasPermission(providePermission)) {
if (DEBUG_PROVIDERS) {
Debug.println(new StringBuilder("RESOLVER: Capability filtered because provider did not have permission") //$NON-NLS-1$
.append(SEPARATOR).append(TAB) //
.append(candidate) //
.append(SEPARATOR).append(TAB).append(TAB) //
.append("of resource") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB).append(TAB) //
.append(candidate.getResource()) //
.toString());
}
return true;
}
return false;
});
}
private void filterDisabled(List<ModuleCapability> candidates) {
candidates.removeIf(capability -> {
if (disabled.contains(capability.getResource())) {
if (DEBUG_PROVIDERS) {
Debug.println(new StringBuilder("RESOLVER: Capability filtered because it was disabled") //$NON-NLS-1$
.append(SEPARATOR).append(TAB) //
.append(capability) //
.append(SEPARATOR).append(TAB).append(TAB) //
.append("of resource") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB).append(TAB) //
.append(capability.getResource()) //
.toString());
}
return true;
}
return false;
});
}
private void removeSubstituted(List<ModuleCapability> capabilities) {
capabilities.removeIf(capability -> {
ModuleWiring wiring = wirings.get(capability.getRevision());
if (wiring != null && wiring.isSubtituted(capability)) {
if (DEBUG_PROVIDERS) {
Debug.println(new StringBuilder("RESOLVER: Capability filtered because it was substituted") //$NON-NLS-1$
.append(SEPARATOR).append(TAB) //
.append(capability) //
.append(SEPARATOR).append(TAB).append(TAB) //
.append("of resource") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB).append(TAB) //
.append(capability.getResource()) //
.toString());
}
return true;
}
return false;
});
}
@Override
public int insertHostedCapability(List<Capability> capabilities, HostedCapability hostedCapability) {
int index = Collections.binarySearch(capabilities, hostedCapability, this);
if (index < 0)
index = -index - 1;
capabilities.add(index, hostedCapability);
return index;
}
@Override
public boolean isEffective(Requirement requirement) {
String effective = requirement.getDirectives().get(Namespace.REQUIREMENT_EFFECTIVE_DIRECTIVE);
return effective == null || Namespace.EFFECTIVE_RESOLVE.equals(effective);
}
@SuppressWarnings("unchecked")
@Override
public Map<Resource, Wiring> getWirings() {
Map<?, ?> raw = wirings;
return Collections.unmodifiableMap((Map<Resource, Wiring>) raw);
}
@Override
public Collection<Resource> getMandatoryResources() {
if (currentlyResolvingMandatory) {
return Collections.unmodifiableList(currentlyResolving);
}
return Collections.emptyList();
}
@Override
public Collection<Resource> getOptionalResources() {
if (!currentlyResolvingMandatory) {
return Collections.unmodifiableList(currentlyResolving);
}
return Collections.emptyList();
}
@Override
public Collection<Resource> findRelatedResources(Resource host) {
// for the container we only care about fragments for related resources
List<ModuleCapability> hostCaps = ((ModuleRevision) host).getModuleCapabilities(HostNamespace.HOST_NAMESPACE);
if (hostCaps.isEmpty()) {
return Collections.emptyList();
}
Collection<Resource> relatedFragments = new ArrayList<>();
for (String hostBSN : getHostBSNs(hostCaps)) {
String matchFilter = "(" + EquinoxFragmentNamespace.FRAGMENT_NAMESPACE + "=" + hostBSN + ")"; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
Requirement fragmentRequirement = ModuleContainer.createRequirement(EquinoxFragmentNamespace.FRAGMENT_NAMESPACE, Collections.<String, String> singletonMap(Namespace.REQUIREMENT_FILTER_DIRECTIVE, matchFilter), Collections.<String, Object> emptyMap());
List<ModuleCapability> candidates = moduleDatabase.findCapabilities(fragmentRequirement);
// filter out disabled fragments and singletons
filterDisabled(candidates);
for (ModuleCapability candidate : candidates) {
ModuleRequirement hostReq = candidate.getRevision().getModuleRequirements(HostNamespace.HOST_NAMESPACE).get(0);
for (ModuleCapability hostCap : hostCaps) {
if (hostReq.matches(hostCap)) {
relatedFragments.add(candidate.getResource());
break;
}
}
}
}
return relatedFragments;
}
private Collection<String> getHostBSNs(List<ModuleCapability> hostCaps) {
if (hostCaps.size() == 1) {
// optimization and likely the only case since you are not supposed to have multiple host caps
return getHostBSNs(hostCaps.get(0));
}
Set<String> result = new HashSet<>();
for (ModuleCapability hostCap : hostCaps) {
result.addAll(getHostBSNs(hostCap));
}
return result;
}
@SuppressWarnings("unchecked")
private Collection<String> getHostBSNs(ModuleCapability moduleCapability) {
Object namesAttr = moduleCapability.getAttributes().get(HostNamespace.HOST_NAMESPACE);
if (namesAttr instanceof String) {
return Collections.singletonList((String) namesAttr);
}
if (namesAttr instanceof String[]) {
return Arrays.asList((String[]) namesAttr);
}
if (namesAttr instanceof Collection) {
return (Collection<String>) namesAttr;
}
return Collections.emptyList();
}
ModuleResolutionReport resolve() {
if (threadResolving()) {
// throw up a runtime exception, if this is caused by a resolver hook
// then it will get caught at the call to the resolver hook and a proper exception is thrown
throw new IllegalStateException(Msg.ModuleResolver_RecursiveError);
}
threadResolving.set(Boolean.TRUE);
try {
try {
hook = adaptor.getResolverHookFactory().begin(InternalUtils.asListBundleRevision((List<? extends BundleRevision>) triggers));
} catch (RuntimeException e) {
if (e.getCause() instanceof BundleException) {
BundleException be = (BundleException) e.getCause();
if (be.getType() == BundleException.REJECTED_BY_HOOK) {
return new ModuleResolutionReport(null, Collections.<Resource, List<Entry>> emptyMap(), new ResolutionException(be));
}
}
throw e;
}
Map<Resource, List<Wire>> result = null;
ResolutionException re = null;
ModuleResolutionReport report;
ResolveLogger logger = new ResolveLogger();
try {
filterResolvable();
selectSingletons();
// remove disabled from optional and triggers to prevent the resolver from resolving them
optionals.removeAll(disabled);
if (triggers.removeAll(disabled) && triggersMandatory) {
throw new ResolutionException(Msg.ModuleResolver_SingletonDisabledError + disabled);
}
if (dynamicReq != null) {
result = resolveDynamic();
} else {
result = new HashMap<>();
Map<Resource, List<Wire>> dynamicAttachWirings = resolveNonPayLoadFragments();
applyInterimResultToWiringCopy(dynamicAttachWirings);
if (!dynamicAttachWirings.isEmpty()) {
// be sure to remove the revisions from the optional and triggers
// so they no longer attempt to be resolved
Set<Resource> fragmentResources = dynamicAttachWirings.keySet();
triggers.removeAll(fragmentResources);
optionals.removeAll(fragmentResources);
result.putAll(dynamicAttachWirings);
}
if (triggersMandatory) {
resolveRevisionsInBatch(triggers, true, logger, result);
}
resolveRevisionsInBatch(optionals, false, logger, result);
}
} catch (ResolutionException e) {
re = e;
} finally {
ScheduledFuture<?> f = timoutFuture.getAndSet(null);
if (f != null) {
f.cancel(true);
}
computeUnresolvedProviderResolutionReportEntries(result);
computeUsesConstraintViolations(logger.getUsesConstraintViolations());
if (DEBUG_WIRING) {
printWirings(result);
}
report = reportBuilder.build(result, re);
if (DEBUG_REPORT) {
if (report.getResolutionException() != null) {
Debug.printStackTrace(report.getResolutionException());
}
Set<Resource> resources = report.getEntries().keySet();
if (!resources.isEmpty()) {
Debug.println("RESOLVER: Resolution report"); //$NON-NLS-1$
for (Resource resource : report.getEntries().keySet()) {
Debug.println(report.getResolutionReportMessage(resource));
}
}
}
if (hook instanceof ResolutionReport.Listener)
((ResolutionReport.Listener) hook).handleResolutionReport(report);
hook.end();
}
return report;
} finally {
threadResolving.set(Boolean.FALSE);
}
}
private void printWirings(Map<Resource, List<Wire>> wires) {
StringBuilder builder = new StringBuilder("RESOLVER: Wirings for resolved bundles:"); //$NON-NLS-1$
if (wires == null) {
Debug.println(" null wires!"); //$NON-NLS-1$
return;
}
for (Map.Entry<Resource, List<Wire>> entry : wires.entrySet()) {
builder.append(SEPARATOR).append(TAB) //
.append("Resource") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB) //
.append(entry.getKey()) //
.append(SEPARATOR).append(TAB) //
.append("Wiring"); //$NON-NLS-1$
int i = 0;
for (Wire wire : entry.getValue()) {
builder.append(SEPARATOR).append(TAB).append(TAB) //
.append('[').append(++i).append("] ") //$NON-NLS-1$
.append(wire);
}
}
Debug.println(builder);
}
private void resolveRevisionsInBatch(Collection<ModuleRevision> revisions, boolean isMandatory, ResolveLogger logger, Map<Resource, List<Wire>> result) throws ResolutionException {
long startTime = System.currentTimeMillis();
long initialFreeMemory = Runtime.getRuntime().freeMemory();
long maxUsedMemory = 0;
// make a copy so we do not modify the input
revisions = new LinkedList<>(revisions);
List<Resource> toResolve = new ArrayList<>();
try {
for (Iterator<ModuleRevision> iResources = revisions.iterator(); iResources.hasNext();) {
ModuleRevision single = iResources.next();
iResources.remove();
if (!wirings.containsKey(single) && !failedToResolve.contains(single)) {
toResolve.add(single);
}
if (toResolve.size() == resolverRevisionBatchSize || !iResources.hasNext()) {
if (DEBUG_ROOTS) {
Debug.println("Resolver: resolving " + toResolve.size() + " in batch."); //$NON-NLS-1$ //$NON-NLS-2$
for (Resource root : toResolve) {
Debug.println(" Resolving root bundle: " + root); //$NON-NLS-1$
}
}
resolveRevisions(toResolve, isMandatory, logger, result);
toResolve.clear();
}
maxUsedMemory = Math.max(maxUsedMemory, Runtime.getRuntime().freeMemory() - initialFreeMemory);
}
} catch (ResolutionException resolutionException) {
if (resolutionException.getCause() instanceof CancellationException) {
// revert back to single bundle resolves
resolveRevisionsIndividually(isMandatory, logger, result, toResolve, revisions);
} else {
throw resolutionException;
}
} catch (OutOfMemoryError memoryError) {
// revert back to single bundle resolves
resolveRevisionsIndividually(isMandatory, logger, result, toResolve, revisions);
}
if (DEBUG_ROOTS) {
Debug.println("Resolver: resolve batch size: " + resolverRevisionBatchSize); //$NON-NLS-1$
Debug.println("Resolver: time to resolve: " + (System.currentTimeMillis() - startTime) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
Debug.println("Resolver: max used memory: " + maxUsedMemory / (1024 * 1024) + "Mo"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
private void resolveRevisionsIndividually(boolean isMandatory, ResolveLogger logger, Map<Resource, List<Wire>> result, Collection<Resource> toResolve, Collection<ModuleRevision> revisions) throws ResolutionException {
scheduleTimeout.set(false);
for (Resource resource : toResolve) {
if (!wirings.containsKey(resource) && !failedToResolve.contains(resource)) {
resolveRevisions(Collections.singletonList(resource), isMandatory, logger, result);
}
}
for (Resource resource : revisions) {
if (!wirings.containsKey(resource) && !failedToResolve.contains(resource)) {
resolveRevisions(Collections.singletonList(resource), isMandatory, logger, result);
}
}
}
private void resolveRevisions(List<Resource> revisions, boolean isMandatory, ResolveLogger logger, Map<Resource, List<Wire>> result) throws ResolutionException {
boolean applyTransitiveFailures = true;
currentlyResolving = revisions;
currentlyResolvingMandatory = isMandatory;
transitivelyResolveFailures.clear();
Map<Resource, List<Wire>> interimResults = null;
try {
transitivelyResolveFailures.addAll(revisions);
interimResults = new ResolverImpl(logger, this).resolve(this);
applyInterimResultToWiringCopy(interimResults);
if (DEBUG_ROOTS) {
Debug.println("Resolver: resolved " + interimResults.size() + " bundles."); //$NON-NLS-1$ //$NON-NLS-2$
}
// now apply the simple wires to the results
for (Map.Entry<Resource, List<Wire>> interimResultEntry : interimResults.entrySet()) {
if (DEBUG_ROOTS) {
Debug.println(" Resolved bundle: " + interimResultEntry.getKey()); //$NON-NLS-1$
}
List<Wire> existingWires = result.get(interimResultEntry.getKey());
if (existingWires != null) {
existingWires.addAll(interimResultEntry.getValue());
} else {
result.put(interimResultEntry.getKey(), interimResultEntry.getValue());
}
}
} catch (ResolutionException resolutionException) {
if (resolutionException.getCause() instanceof CancellationException) {
applyTransitiveFailures = false;
}
throw resolutionException;
} catch (OutOfMemoryError memoryError) {
applyTransitiveFailures = false;
throw memoryError;
} finally {
if (applyTransitiveFailures) {
transitivelyResolveFailures.addAll(logger.getUsesConstraintViolations().keySet());
if (interimResults != null) {
transitivelyResolveFailures.removeAll(interimResults.keySet());
}
// what is left did not resolve
if (!transitivelyResolveFailures.isEmpty()) {
failedToResolve.addAll(transitivelyResolveFailures);
}
}
currentlyResolving = null;
currentlyResolvingMandatory = false;
}
}
private void applyInterimResultToWiringCopy(Map<Resource, List<Wire>> interimResult) {
if (!interimResult.isEmpty()) {
// update the copy of wirings to include interim results
Map<ModuleRevision, ModuleWiring> updatedWirings = generateDelta(interimResult, wirings);
for (Map.Entry<ModuleRevision, ModuleWiring> updatedWiring : updatedWirings.entrySet()) {
wirings.put(updatedWiring.getKey(), updatedWiring.getValue());
}
}
}
private void computeUsesConstraintViolations(Map<Resource, ResolutionException> usesConstraintViolations) {
for (Map.Entry<Resource, ResolutionException> usesConstraintViolation : usesConstraintViolations.entrySet()) {
reportBuilder.addEntry(usesConstraintViolation.getKey(), Type.USES_CONSTRAINT_VIOLATION, usesConstraintViolation.getValue());
}
}
/*
* Given the results of a resolution, compute which, if any, of the
* enabled, resolving resources are still unresolved. For those that are
* unresolved, generate resolution report entries for unresolved
* providers, if necessary.
*/
private void computeUnresolvedProviderResolutionReportEntries(Map<Resource, List<Wire>> resolution) {
// Create a collection representing the resources asked to be
// resolved.
Collection<Resource> shouldHaveResolvedResources = new ArrayList<>(unresolved);
// Remove disabled resources.
shouldHaveResolvedResources.removeAll(disabled);
// Remove resolved resources, if necessary. The resolution will be
// null if the resolver threw an exception because the triggers
// were mandatory but didn't resolve.
if (resolution != null)
shouldHaveResolvedResources.removeAll(resolution.keySet());
// What remains are resources that should have resolved but didn't.
// For each resource, add report entries for any unresolved
// providers.
for (Resource shouldHaveResolvedResource : shouldHaveResolvedResources) {
Map<Requirement, Set<Capability>> requirementToCapabilities = unresolvedProviders.get(shouldHaveResolvedResource);
if (requirementToCapabilities == null)
continue;
// If nothing resolved then there are no resolved resources to
// filter out.
if (resolution != null) {
// Filter out capability providers that resolved.
for (Iterator<Set<Capability>> values = requirementToCapabilities.values().iterator(); values.hasNext();) {
Set<Capability> value = values.next();
for (Iterator<Capability> capabilities = value.iterator(); capabilities.hasNext();)
if (resolution.containsKey(capabilities.next().getResource()))
// Remove the resolved capability provider.
capabilities.remove();
if (value.isEmpty())
// Remove the requirement that has no unresolved
// capability providers.
values.remove();
}
}
// Add a report entry if there are any remaining requirements
// pointing to unresolved capability providers.
if (!requirementToCapabilities.isEmpty())
reportBuilder.addEntry(shouldHaveResolvedResource, Entry.Type.UNRESOLVED_PROVIDER, requirementToCapabilities);
}
}
/*
* Given a requirement and its matching capabilities, map the
* requirement's resource to the requirement and matching capabilities.
* This data is used to compute report entries for resources that did
* not resolve because a provider did not resolve.
*/
private void computeUnresolvedProviders(Requirement requirement, Collection<? extends Capability> capabilities) {
Resource requirer = requirement.getResource();
Map<Requirement, Set<Capability>> requirementToCapabilities = unresolvedProviders.get(requirer);
if (requirementToCapabilities == null) {
requirementToCapabilities = new HashMap<>();
unresolvedProviders.put(requirer, requirementToCapabilities);
}
Set<Capability> value = requirementToCapabilities.get(requirement);
if (value == null) {
value = new HashSet<>(capabilities.size());
requirementToCapabilities.put(requirement, value);
}
for (Capability capability : capabilities)
if (!wirings.containsKey(capability.getResource()))
value.add(capability);
}
class DynamicFragments {
private final ModuleCapability hostCapability;
private final Map<String, ModuleRevision> fragments = new HashMap<>();
private final Set<ModuleRevision> validProviders = new HashSet<>();
boolean fragmentAdded = false;
DynamicFragments(ModuleWiring hostWiring, ModuleCapability hostCapability) {
this.hostCapability = hostCapability;
validProviders.add(hostWiring.getRevision());
for (ModuleWire hostWire : hostWiring.getProvidedModuleWires(HostNamespace.HOST_NAMESPACE)) {
validProviders.add(hostWire.getRequirer());
fragments.put(hostWire.getRequirer().getSymbolicName(), hostWire.getRequirer());
}
}
void addFragment(ModuleRevision fragment) {
ModuleRevision existing = fragments.get(fragment.getSymbolicName());
if (existing == null) {
fragments.put(fragment.getSymbolicName(), fragment);
validProviders.add(fragment);
fragmentAdded = true;
}
}
Map<Resource, List<Wire>> getNewWires() {
if (!fragmentAdded) {
return Collections.emptyMap();
}
Map<Resource, List<Wire>> result = new HashMap<>();
boolean retry;
do {
retry = false;
result.clear();
fragmentsLoop: for (Iterator<Map.Entry<String, ModuleRevision>> iFragments = fragments.entrySet().iterator(); iFragments.hasNext();) {
Map.Entry<String, ModuleRevision> fragmentEntry = iFragments.next();
if (wirings.get(fragmentEntry.getValue()) == null) {
for (ModuleRequirement req : fragmentEntry.getValue().getModuleRequirements(null)) {
ModuleRevision requirer = NON_PAYLOAD_REQUIREMENTS.contains(req.getNamespace()) ? req.getRevision() : hostCapability.getRevision();
List<Wire> newWires = result.get(requirer);
if (newWires == null) {
newWires = new ArrayList<>();
result.put(requirer, newWires);
}
if (HostNamespace.HOST_NAMESPACE.equals(req.getNamespace())) {
newWires.add(new ModuleWire(hostCapability, hostCapability.getRevision(), req, requirer));
} else {
if (failToWire(req, requirer, newWires)) {
iFragments.remove();
validProviders.remove(req.getRevision());
retry = true;
break fragmentsLoop;
}
}
}
}
}
} while (retry);
return result;
}
private boolean failToWire(ModuleRequirement requirement, ModuleRevision requirer, List<Wire> wires) {
if (!isEffective(requirement)) {
return false;
}
List<ModuleCapability> matching = moduleDatabase.findCapabilities(requirement);
List<Wire> newWires = new ArrayList<>(0);
filterProviders(requirement, matching, false);
for (ModuleCapability candidate : matching) {
// If the requirer equals the requirement revision then this is a non-payload requirement.
// We let non-payload requirements come from anywhere.
// Payload requirements must come from the host or one of the fragments attached to the host
if (requirer.equals(requirement.getRevision()) || validProviders.contains(candidate.getRevision())) {
ModuleRevision provider = NON_PAYLOAD_CAPABILITIES.contains(candidate.getNamespace()) ? candidate.getRevision() : hostCapability.getRevision();
// if there are multiple candidates; then check for cardinality
if (newWires.isEmpty() || Namespace.CARDINALITY_MULTIPLE.equals(requirement.getDirectives().get(Namespace.REQUIREMENT_CARDINALITY_DIRECTIVE))) {
newWires.add(new ModuleWire(candidate, provider, requirement, requirer));
}
}
}
if (newWires.isEmpty()) {
if (!Namespace.RESOLUTION_OPTIONAL.equals(requirement.getDirectives().get(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE))) {
// could not resolve mandatory requirement;
return true;
}
}
// only create the wire if the namespace is a non-substituted namespace (e.g. NOT package)
if (!NON_SUBSTITUTED_REQUIREMENTS.contains(requirement.getNamespace())) {
wires.addAll(newWires);
}
return false;
}
}
private Map<Resource, List<Wire>> resolveNonPayLoadFragments() {
// This is to support dynamic attachment of fragments that do not
// add any payload requirements to their host.
// This is needed for framework extensions since the system bundle
// host is always resolved.
// It is also useful for things like NLS fragments that are installed later
// without the need to refresh the host.
List<ModuleRevision> dynamicAttachableFrags = new ArrayList<>();
if (triggersMandatory) {
for (ModuleRevision moduleRevision : triggers) {
if ((moduleRevision.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0) {
dynamicAttachableFrags.add(moduleRevision);
}
}
}
for (ModuleRevision moduleRevision : optionals) {
if ((moduleRevision.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0) {
dynamicAttachableFrags.add(moduleRevision);
}
}
if (dynamicAttachableFrags.isEmpty()) {
return Collections.emptyMap();
}
Collections.sort(dynamicAttachableFrags, new Comparator<ModuleRevision>() {
@Override
public int compare(ModuleRevision r1, ModuleRevision r2) {
// we only care about versions here
return -(r1.getVersion().compareTo(r2.getVersion()));
}
});
Map<ModuleCapability, DynamicFragments> hostDynamicFragments = new HashMap<>();
// first find the hosts to dynamically attach to
for (ModuleRevision dynamicAttachableFragment : dynamicAttachableFrags) {
List<ModuleRequirement> requirements = dynamicAttachableFragment.getModuleRequirements(null);
for (ModuleRequirement requirement : requirements) {
if (HostNamespace.HOST_NAMESPACE.equals(requirement.getNamespace())) {
List<ModuleCapability> matchingHosts = moduleDatabase.findCapabilities(requirement);
filterProviders(requirement, matchingHosts, false);
for (ModuleCapability hostCandidate : matchingHosts) {
ModuleWiring hostWiring = wirings.get(hostCandidate.getRevision());
String attachDirective = hostCandidate.getDirectives().get(HostNamespace.CAPABILITY_FRAGMENT_ATTACHMENT_DIRECTIVE);
boolean attachAlways = attachDirective == null || HostNamespace.FRAGMENT_ATTACHMENT_ALWAYS.equals(attachDirective);
// only do this if the candidate host is already resolved and it allows dynamic attachment
if (!attachAlways || hostWiring == null) {
continue;
}
DynamicFragments dynamicFragments = hostDynamicFragments.get(hostCandidate);
if (dynamicFragments == null) {
dynamicFragments = new DynamicFragments(hostWiring, hostCandidate);
hostDynamicFragments.put(hostCandidate, dynamicFragments);
}
dynamicFragments.addFragment(requirement.getRevision());
}
}
}
}
// now try to get the new wires for each host
Map<Resource, List<Wire>> dynamicWires = new HashMap<>();
for (DynamicFragments dynamicFragments : hostDynamicFragments.values()) {
dynamicWires.putAll(dynamicFragments.getNewWires());
}
return dynamicWires;
}
private Map<Resource, List<Wire>> resolveDynamic() throws ResolutionException {
return new ResolverImpl(new Logger(0), null).resolveDynamic(this, dynamicReq.getRevision().getWiring(), dynamicReq.getOriginal());
}
private void filterResolvable() {
Collection<ModuleRevision> enabledCandidates = new ArrayList<>(unresolved);
hook.filterResolvable(InternalUtils.asListBundleRevision((List<? extends BundleRevision>) enabledCandidates));
// do this the hard way because the 'optimization' in removeAll hurts us
for (ModuleRevision enabledRevision : enabledCandidates) {
disabled.remove(enabledRevision);
}
for (ModuleRevision revision : disabled) {
reportBuilder.addEntry(revision, Entry.Type.FILTERED_BY_RESOLVER_HOOK, null);
if (DEBUG_HOOKS) {
Debug.println("RESOLVER: Resource filtered by ResolverHook.filterResolvable: " + revision); //$NON-NLS-1$
}
}
}
private void selectSingletons() {
Map<String, Collection<ModuleRevision>> selectedSingletons = new HashMap<>();
for (ModuleRevision revision : unresolved) {
if (!isSingleton(revision) || disabled.contains(revision))
continue;
String bsn = revision.getSymbolicName();
Collection<ModuleRevision> selected = selectedSingletons.get(bsn);
if (selected != null)
continue; // already processed the bsn
selected = new ArrayList<>(1);
selectedSingletons.put(bsn, selected);
Collection<ModuleRevision> sameBSN = getRevisions(bsn);
if (sameBSN.size() < 2) {
selected.add(revision);
continue;
}
// prime selected with resolved singleton bundles
for (ModuleRevision singleton : sameBSN) {
if (isSingleton(singleton) && wirings.containsKey(singleton))
selected.add(singleton);
}
// get the collision map for the BSN
Map<ModuleRevision, Collection<ModuleRevision>> collisionMap = getCollisionMap(sameBSN);
// process the collision map
for (ModuleRevision singleton : sameBSN) {
if (selected.contains(singleton))
continue; // no need to process resolved bundles
Collection<ModuleRevision> collisions = collisionMap.get(singleton);
if (collisions == null || disabled.contains(singleton))
continue; // not a singleton or not resolvable
Collection<ModuleRevision> pickOneToResolve = new ArrayList<>();
for (ModuleRevision collision : collisions) {
if (selected.contains(collision)) {
// Must fail since there is already a selected bundle which is a collision of the singleton bundle
disabled.add(singleton);
reportBuilder.addEntry(singleton, Type.SINGLETON_SELECTION, collision);
break;
}
if (!pickOneToResolve.contains(collision))
pickOneToResolve.add(collision);
}
if (!disabled.contains(singleton)) {
// need to make sure the bundle does not collide from the POV of another entry
for (Map.Entry<ModuleRevision, Collection<ModuleRevision>> collisionEntry : collisionMap.entrySet()) {
if (collisionEntry.getKey() != singleton && collisionEntry.getValue().contains(singleton)) {
if (selected.contains(collisionEntry.getKey())) {
// Must fail since there is already a selected bundle for which the singleton bundle is a collision
disabled.add(singleton);
reportBuilder.addEntry(singleton, Type.SINGLETON_SELECTION, collisionEntry.getKey());
break;
}
if (!pickOneToResolve.contains(collisionEntry.getKey()))
pickOneToResolve.add(collisionEntry.getKey());
}
}
}
if (!disabled.contains(singleton)) {
pickOneToResolve.add(singleton);
selected.add(pickOneToResolve(pickOneToResolve));
}
}
}
}
private Collection<ModuleRevision> getRevisions(String name) {
Map<String, Collection<ModuleRevision>> current = byName;
if (current == null) {
// generate the map using unresolved collection and wiring snap shot
// this is to avoid interacting with the module database
Set<ModuleRevision> revisions = new HashSet<>();
revisions.addAll(unresolved);
revisions.addAll(previouslyResolved);
current = new HashMap<>();
for (ModuleRevision revision : revisions) {
Collection<ModuleRevision> sameName = current.get(revision.getSymbolicName());
if (sameName == null) {
sameName = new ArrayList<>();
current.put(revision.getSymbolicName(), sameName);
}
sameName.add(revision);
}
byName = current;
}
Collection<ModuleRevision> result = current.get(name);
if (result == null) {
return Collections.emptyList();
}
return result;
}
private ModuleRevision pickOneToResolve(Collection<ModuleRevision> pickOneToResolve) {
ModuleRevision selectedVersion = null;
for (ModuleRevision singleton : pickOneToResolve) {
if (selectedVersion == null)
selectedVersion = singleton;
boolean higherVersion = selectedVersion.getVersion().compareTo(singleton.getVersion()) < 0;
if (higherVersion)
selectedVersion = singleton;
}
for (ModuleRevision singleton : pickOneToResolve) {
if (singleton != selectedVersion) {
disabled.add(singleton);
reportBuilder.addEntry(singleton, Type.SINGLETON_SELECTION, selectedVersion);
}
}
return selectedVersion;
}
private Map<ModuleRevision, Collection<ModuleRevision>> getCollisionMap(Collection<ModuleRevision> sameBSN) {
Map<ModuleRevision, Collection<ModuleRevision>> result = new HashMap<>();
for (ModuleRevision singleton : sameBSN) {
if (!isSingleton(singleton) || disabled.contains(singleton))
continue; // ignore non-singleton and non-resolvable
List<BundleCapability> capabilities = new ArrayList<>(sameBSN.size() - 1);
for (ModuleRevision collision : sameBSN) {
if (collision == singleton || !isSingleton(collision) || disabled.contains(collision))
continue; // Ignore the bundle we are checking and non-singletons and non-resolvable
capabilities.add(getIdentity(collision));
}
hook.filterSingletonCollisions(getIdentity(singleton), capabilities);
Collection<ModuleRevision> collisionCandidates = new ArrayList<>(capabilities.size());
for (BundleCapability identity : capabilities) {
collisionCandidates.add((ModuleRevision) identity.getRevision());
}
if (DEBUG_HOOKS) {
Collection<ModuleRevision> filteredSingletons = new ArrayList<>(sameBSN);
filteredSingletons.removeAll(collisionCandidates);
filteredSingletons.remove(singleton);
if (!filteredSingletons.isEmpty()) {
StringBuilder builder = new StringBuilder("RESOLVER: Resources filtered by ResolverHook.filterSingletonCollisions") //$NON-NLS-1$
.append(SEPARATOR).append(TAB) //
.append("Singleton") //$NON-NLS-1$
.append(SEPARATOR).append(TAB).append(TAB) //
.append(singleton) //
.append(" [id=") //$NON-NLS-1$
.append(singleton.getRevisions().getModule().getId()) //
.append(", location=") //$NON-NLS-1$
.append(singleton.getRevisions().getModule().getLocation()) //
.append(']') //
.append(SEPARATOR).append(TAB) //
.append("Collisions"); //$NON-NLS-1$
int i = 0;
for (ModuleRevision revision : filteredSingletons) {
builder.append(SEPARATOR).append(TAB).append(TAB) //
.append("[").append(++i).append("] ") //$NON-NLS-1$ //$NON-NLS-2$
.append(revision) //
.append(" [id=") //$NON-NLS-1$
.append(revision.getRevisions().getModule().getId()) //
.append(", location=") //$NON-NLS-1$
.append(revision.getRevisions().getModule().getLocation()) //
.append(']');
}
Debug.println(builder.toString());
}
}
result.put(singleton, collisionCandidates);
}
return result;
}
private BundleCapability getIdentity(ModuleRevision bundle) {
List<BundleCapability> identities = bundle.getDeclaredCapabilities(IdentityNamespace.IDENTITY_NAMESPACE);
return identities.isEmpty() ? null : identities.get(0);
}
@Override
public int compare(Capability c1, Capability c2) {
// TODO Ideally this policy should be handled by the ModuleDatabase.
// To do that the wirings would have to be provided since the wirings may
// be a subset of the current wirings provided by the ModuleDatabase
boolean resolved1 = previouslyResolved.contains(c1.getResource());
boolean resolved2 = previouslyResolved.contains(c2.getResource());
if (resolved1 != resolved2)
return resolved1 ? -1 : 1;
Version v1 = getVersion(c1);
Version v2 = getVersion(c2);
int versionCompare = -(v1.compareTo(v2));
if (versionCompare != 0)
return versionCompare;
ModuleRevision m1 = getModuleRevision(c1);
ModuleRevision m2 = getModuleRevision(c2);
Long id1 = m1.getRevisions().getModule().getId();
Long id2 = m2.getRevisions().getModule().getId();
if (id1.equals(id2) && !m1.equals(m2)) {
// sort based on revision ordering
List<ModuleRevision> revisions = m1.getRevisions().getModuleRevisions();
int index1 = revisions.indexOf(m1);
int index2 = revisions.indexOf(m2);
// we want to sort the indexes from highest to lowest
return index2 - index1;
}
return id1.compareTo(id2);
}
ModuleRevision getModuleRevision(Capability c) {
// We assume all capabilities here either come from us and have ModuleRevision resources or
// they are HostedCapabilities which have ModuleRevision resources as the host revision
if (c instanceof HostedCapability) {
c = ((HostedCapability) c).getDeclaredCapability();
}
if (c instanceof ModuleCapability) {
return ((ModuleCapability) c).getRevision();
}
// TODO is there some bug in the resolver?
return null;
}
@Override
public void execute(Runnable command) {
adaptor.getResolverExecutor().execute(command);
}
@Override
public void onCancel(Runnable callback) {
// Note that for each resolve Process we only want timeout the initial batch resolve
if (scheduleTimeout.compareAndSet(true, false)) {
ScheduledExecutorService scheduledExecutor = adaptor.getScheduledExecutor();
if (scheduledExecutor != null) {
try {
timoutFuture.set(scheduledExecutor.schedule(callback, resolverBatchTimeout, TimeUnit.MILLISECONDS));
} catch (RejectedExecutionException e) {
// ignore may have been shutdown, it is ok we will not be able to timeout
}
}
}
}
@Override
public List<Wire> getSubstitutionWires(Wiring wiring) {
return ((ModuleWiring) wiring).getSubstitutionWires();
}
}
protected boolean threadResolving() {
Boolean resolvingValue = this.threadResolving.get();
if (resolvingValue == null) {
return false;
}
return resolvingValue.booleanValue();
}
}