blob: 071c13d5d3ce6f0ee121326ac67e7c1b829c27f4 [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.felix.bundlerepository.impl;
import java.net.URL;
import java.util.*;
import org.apache.felix.bundlerepository.*;
import org.apache.felix.utils.log.Logger;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
public class ResolverImpl implements Resolver
{
private final BundleContext m_context;
private final Logger m_logger;
private final Repository[] m_repositories;
private final Set<Resource> m_addedSet = new HashSet<Resource>();
private final Set<Requirement> m_addedRequirementSet = new HashSet<Requirement>();
private final Set<Capability> m_globalCapabilities = new HashSet<Capability>();
private final Set<Resource> m_failedSet = new HashSet<Resource>();
private final Set<Resource> m_resolveSet = new HashSet<Resource>();
private final Set<Resource> m_requiredSet = new HashSet<Resource>();
private final Set<Resource> m_optionalSet = new HashSet<Resource>();
private final Map<Resource, List<Reason>> m_reasonMap = new HashMap<Resource, List<Reason>>();
private final Set<Reason> m_unsatisfiedSet = new HashSet<Reason>();
private boolean m_resolved = false;
private long m_resolveTimeStamp;
private int m_resolutionFlags;
public ResolverImpl(BundleContext context, Repository[] repositories, Logger logger)
{
m_context = context;
m_logger = logger;
m_repositories = repositories;
}
public synchronized void add(Resource resource)
{
m_resolved = false;
m_addedSet.add(resource);
}
public synchronized Resource[] getAddedResources()
{
return m_addedSet.toArray(new Resource[m_addedSet.size()]);
}
public synchronized void add(Requirement requirement)
{
m_resolved = false;
m_addedRequirementSet.add(requirement);
}
public synchronized Requirement[] getAddedRequirements()
{
return m_addedRequirementSet.toArray(new Requirement[m_addedRequirementSet.size()]);
}
public void addGlobalCapability(Capability capability)
{
m_globalCapabilities.add(capability);
}
public Capability[] getGlobalCapabilities()
{
return m_globalCapabilities.toArray(new Capability[m_globalCapabilities.size()]);
}
public synchronized Resource[] getRequiredResources()
{
if (m_resolved)
{
return m_requiredSet.toArray(new Resource[m_requiredSet.size()]);
}
throw new IllegalStateException("The resources have not been resolved.");
}
public synchronized Resource[] getOptionalResources()
{
if (m_resolved)
{
return m_optionalSet.toArray(new Resource[m_optionalSet.size()]);
}
throw new IllegalStateException("The resources have not been resolved.");
}
public synchronized Reason[] getReason(Resource resource)
{
if (m_resolved)
{
List<Reason> l = m_reasonMap.get(resource);
return l != null ? l.toArray(new Reason[l.size()]) : null;
}
throw new IllegalStateException("The resources have not been resolved.");
}
public synchronized Reason[] getUnsatisfiedRequirements()
{
if (m_resolved)
{
return m_unsatisfiedSet.toArray(new Reason[m_unsatisfiedSet.size()]);
}
throw new IllegalStateException("The resources have not been resolved.");
}
protected LocalResource[] getLocalResources()
{
List<LocalResource> resources = new ArrayList<LocalResource>();
for (Resource resource : getResources())
{
if (resource != null && resource.isLocal())
{
resources.add((LocalResource) resource);
}
}
return resources.toArray(new LocalResource[resources.size()]);
}
private Resource[] getRemoteResources()
{
List<Resource> resources = new ArrayList<Resource>();
for (Resource resource : getResources())
{
if (resource != null && !resource.isLocal())
{
resources.add(resource);
}
}
return resources.toArray(new Resource[resources.size()]);
}
private Resource[] getResources()
{
List<Resource> resources = new ArrayList<Resource>();
for (int repoIdx = 0; (m_repositories != null) && (repoIdx < m_repositories.length); repoIdx++)
{
boolean isLocal = m_repositories[repoIdx].getURI().equals(Repository.LOCAL);
boolean isSystem = m_repositories[repoIdx].getURI().equals(Repository.SYSTEM);
if (isLocal && (m_resolutionFlags & NO_LOCAL_RESOURCES) != 0) {
continue;
}
if (isSystem && (m_resolutionFlags & NO_SYSTEM_BUNDLE) != 0) {
continue;
}
Collections.addAll(resources, m_repositories[repoIdx].getResources());
}
return resources.toArray(new Resource[resources.size()]);
}
public synchronized boolean resolve()
{
return resolve(0);
}
public synchronized boolean resolve(int flags)
{
// Find resources
Resource[] locals = getLocalResources();
Resource[] remotes = getRemoteResources();
// time of the resolution process start
m_resolveTimeStamp = 0;
for (int repoIdx = 0; (m_repositories != null) && (repoIdx < m_repositories.length); repoIdx++)
{
m_resolveTimeStamp = Math.max(m_resolveTimeStamp, m_repositories[repoIdx].getLastModified());
}
// Reset instance values.
m_failedSet.clear();
m_resolveSet.clear();
m_requiredSet.clear();
m_optionalSet.clear();
m_reasonMap.clear();
m_unsatisfiedSet.clear();
m_resolved = true;
m_resolutionFlags = flags;
boolean result = true;
// Add a fake resource if needed
if (!m_addedRequirementSet.isEmpty() || !m_globalCapabilities.isEmpty())
{
ResourceImpl fake = new ResourceImpl();
for (Capability cap : m_globalCapabilities) {
fake.addCapability(cap);
}
for (Requirement req : m_addedRequirementSet) {
fake.addRequire(req);
}
if (!resolve(fake, locals, remotes, false))
{
result = false;
}
}
// Loop through each resource in added list and resolve.
for (Resource aM_addedSet : m_addedSet) {
if (!resolve(aM_addedSet, locals, remotes, false)) {
// If any resource does not resolve, then the
// entire result will be false.
result = false;
}
}
// Clean up the resulting data structures.
m_requiredSet.removeAll(m_addedSet);
if ((flags & NO_LOCAL_RESOURCES) == 0)
{
m_requiredSet.removeAll(Arrays.asList(locals));
}
m_optionalSet.removeAll(m_addedSet);
m_optionalSet.removeAll(m_requiredSet);
if ((flags & NO_LOCAL_RESOURCES) == 0)
{
m_optionalSet.removeAll(Arrays.asList(locals));
}
// Return final result.
return result;
}
private boolean resolve(Resource resource, Resource[] locals, Resource[] remotes, boolean optional)
{
boolean result = true;
// Check for cycles.
if (m_resolveSet.contains(resource) || m_requiredSet.contains(resource) || m_optionalSet.contains(resource))
{
return true;
}
else if (m_failedSet.contains(resource))
{
return false;
}
// Add to resolve map to avoid cycles.
m_resolveSet.add(resource);
// Resolve the requirements for the resource according to the
// search order of: added, resolving, local and finally remote
// resources.
Requirement[] reqs = resource.getRequirements();
if (reqs != null)
{
Resource candidate;
for (Requirement req : reqs) {
// Do not resolve optional requirements
if ((m_resolutionFlags & NO_OPTIONAL_RESOURCES) != 0 && req.isOptional()) {
continue;
}
candidate = searchResources(req, m_addedSet);
if (candidate == null) {
candidate = searchResources(req, m_requiredSet);
}
if (candidate == null) {
candidate = searchResources(req, m_optionalSet);
}
if (candidate == null) {
candidate = searchResources(req, m_resolveSet);
}
if (candidate == null) {
List<ResourceCapability> candidateCapabilities = searchResources(req, locals);
candidateCapabilities.addAll(searchResources(req, remotes));
// Determine the best candidate available that
// can resolve.
while ((candidate == null) && !candidateCapabilities.isEmpty()) {
ResourceCapability bestCapability = getBestCandidate(candidateCapabilities);
// Try to resolve the best resource.
if (resolve(bestCapability.getResource(), locals, remotes, optional || req.isOptional())) {
candidate = bestCapability.getResource();
} else {
candidateCapabilities.remove(bestCapability);
}
}
}
if ((candidate == null) && !req.isOptional()) {
// The resolve failed.
result = false;
// Associated the current resource to the requirement
// in the unsatisfied requirement set.
m_unsatisfiedSet.add(new ReasonImpl(resource, req));
} else if (candidate != null) {
// Try to resolve the candidate.
if (resolve(candidate, locals, remotes, optional || req.isOptional())) {
// The resolved succeeded; record the candidate
// as either optional or required.
if (optional || req.isOptional()) {
m_optionalSet.add(candidate);
m_resolveSet.remove(candidate);
} else {
m_requiredSet.add(candidate);
m_optionalSet.remove(candidate);
m_resolveSet.remove(candidate);
}
// Add the reason why the candidate was selected.
List<Reason> reasons = m_reasonMap.get(candidate);
if (reasons == null) {
reasons = new ArrayList<Reason>();
m_reasonMap.put(candidate, reasons);
}
reasons.add(new ReasonImpl(resource, req));
} else {
result = false;
}
}
}
}
// If the resolve failed, remove the resource from the resolve set and
// add it to the failed set to avoid trying to resolve it again.
if (!result)
{
m_resolveSet.remove(resource);
m_failedSet.add(resource);
}
return result;
}
private Resource searchResources(Requirement req, Set<Resource> resourceSet)
{
for (Resource aResourceSet : resourceSet) {
checkInterrupt();
Capability[] caps = aResourceSet.getCapabilities();
if (caps != null) {
for (Capability cap : caps) {
if (req.isSatisfied(cap)) {
// The requirement is already satisfied an existing
// resource, return the resource.
return aResourceSet;
}
}
}
}
return null;
}
/**
* Searches for resources that do meet the given requirement
* @param req the the requirement that must be satisfied by resources
* @param resources list of resources to look at
* @return all resources meeting the given requirement
*/
private List<ResourceCapability> searchResources(Requirement req, Resource[] resources)
{
List<ResourceCapability> matchingCapabilities = new ArrayList<ResourceCapability>();
if (resources != null) {
for (Resource resource : resources) {
checkInterrupt();
// We don't need to look at resources we've already looked at.
if (!m_failedSet.contains(resource)) {
Capability[] caps = resource.getCapabilities();
if (caps != null) {
for (Capability cap : caps) {
if (req.isSatisfied(cap))
matchingCapabilities.add(new ResourceCapabilityImpl(resource, cap));
}
}
}
}
}
return matchingCapabilities;
}
/**
* Determines which resource is preferred to deliver the required capability.
* This method selects the resource providing the highest version of the capability.
* If two resources provide the same version of the capability, the resource with
* the largest number of cabailities be preferred
* @param caps
* @return
*/
private ResourceCapability getBestCandidate(List<ResourceCapability> caps)
{
Version bestVersion = null;
ResourceCapability best = null;
boolean bestLocal = false;
for (ResourceCapability cap : caps) {
boolean isCurrentLocal = cap.getResource().isLocal();
if (best == null) {
best = cap;
bestLocal = isCurrentLocal;
Object v = cap.getCapability().getPropertiesAsMap().get(Resource.VERSION);
if ((v != null) && (v instanceof Version)) {
bestVersion = (Version) v;
}
} else if ((m_resolutionFlags & DO_NOT_PREFER_LOCAL) != 0 || !bestLocal || isCurrentLocal) {
Object v = cap.getCapability().getPropertiesAsMap().get(Resource.VERSION);
// If there is no version, then select the resource
// with the greatest number of capabilities.
if ((v == null) && (bestVersion == null)
&& (best.getResource().getCapabilities().length
< cap.getResource().getCapabilities().length)) {
best = cap;
bestLocal = isCurrentLocal;
bestVersion = null;
} else if ((v != null) && (v instanceof Version)) {
// If there is no best version or if the current
// resource's version is lower, then select it.
if ((bestVersion == null) || (bestVersion.compareTo((Version) v) < 0)) {
best = cap;
bestLocal = isCurrentLocal;
bestVersion = (Version) v;
}
// If the current resource version is equal to the
// best
else if ((bestVersion.compareTo((Version) v) == 0)) {
// If the symbolic name is the same, use the highest
// bundle version.
if ((best.getResource().getSymbolicName() != null)
&& best.getResource().getSymbolicName().equals(
cap.getResource().getSymbolicName())) {
if (best.getResource().getVersion().compareTo(
cap.getResource().getVersion()) < 0) {
best = cap;
bestLocal = isCurrentLocal;
bestVersion = (Version) v;
}
}
// Otherwise select the one with the greatest
// number of capabilities.
else if (best.getResource().getCapabilities().length
< cap.getResource().getCapabilities().length) {
best = cap;
bestLocal = isCurrentLocal;
bestVersion = (Version) v;
}
}
}
}
}
return (best == null) ? null : best;
}
private void checkInterrupt()
{
if (Thread.interrupted())
{
throw new org.apache.felix.bundlerepository.InterruptedResolutionException();
}
}
public synchronized void deploy(int flags)
{
// Must resolve if not already resolved.
if (!m_resolved && !resolve(flags))
{
m_logger.log(Logger.LOG_ERROR, "Resolver: Cannot resolve target resources.");
return;
}
// Check to make sure that our local state cache is up-to-date
// and error if it is not. This is not completely safe, because
// the state can still change during the operation, but we will
// be optimistic. This could also be made smarter so that it checks
// to see if the local state changes overlap with the resolver.
for (int repoIdx = 0; (m_repositories != null) && (repoIdx < m_repositories.length); repoIdx++)
{
if (m_repositories[repoIdx].getLastModified() > m_resolveTimeStamp)
{
throw new IllegalStateException("Framework state has changed, must resolve again.");
}
}
// Eliminate duplicates from target, required, optional resources.
Set<Resource> resourceSet = new HashSet<Resource>();
Resource[] resources = getAddedResources();
for (int i = 0; (resources != null) && (i < resources.length); i++)
{
resourceSet.add(resources[i]);
}
resources = getRequiredResources();
for (int i = 0; (resources != null) && (i < resources.length); i++)
{
resourceSet.add(resources[i]);
}
if ((flags & NO_OPTIONAL_RESOURCES) == 0)
{
resources = getOptionalResources();
for (int i = 0; (resources != null) && (i < resources.length); i++)
{
resourceSet.add(resources[i]);
}
}
Resource[] deployResources = resourceSet.toArray(new Resource[resourceSet.size()]);
// List to hold all resources to be started.
List<Bundle> startList = new ArrayList<Bundle>();
// Deploy each resource, which will involve either finding a locally
// installed resource to update or the installation of a new version
// of the resource to be deployed.
for (Resource deployResource : deployResources) {
// For the resource being deployed, see if there is an older
// version of the resource already installed that can potentially
// be updated.
LocalResource localResource = findUpdatableLocalResource(deployResource);
// If a potentially updatable older version was found,
// then verify that updating the local resource will not
// break any of the requirements of any of the other
// resources being deployed.
if ((localResource != null) &&
isResourceUpdatable(localResource, deployResource, deployResources)) {
// Only update if it is a different version.
if (!localResource.equals(deployResource)) {
// Update the installed bundle.
try {
// stop the bundle before updating to prevent
// the bundle update from throwing due to not yet
// resolved dependencies
boolean doStartBundle = (flags & START) != 0;
if (localResource.getBundle().getState() == Bundle.ACTIVE) {
doStartBundle = true;
localResource.getBundle().stop();
}
localResource.getBundle().update(FileUtil.openURL(new URL(deployResource.getURI())));
// If necessary, save the updated bundle to be
// started later.
if (doStartBundle) {
Bundle bundle = localResource.getBundle();
if (!isFragmentBundle(bundle)) {
startList.add(bundle);
}
}
} catch (Exception ex) {
m_logger.log(
Logger.LOG_ERROR,
"Resolver: Update error - " + getBundleName(localResource.getBundle()),
ex);
return;
}
}
} else {
// Install the bundle.
try {
// Perform the install, but do not use the actual
// bundle JAR URL for the bundle location, since this will
// limit OBR's ability to manipulate bundle versions. Instead,
// use a unique timestamp as the bundle location.
URL url = new URL(deployResource.getURI());
Bundle bundle = m_context.installBundle(
"obr://"
+ deployResource.getSymbolicName()
+ "/-" + System.currentTimeMillis(),
FileUtil.openURL(url));
// If necessary, save the installed bundle to be
// started later.
if ((flags & START) != 0) {
if (!isFragmentBundle(bundle)) {
startList.add(bundle);
}
}
} catch (Exception ex) {
m_logger.log(
Logger.LOG_ERROR,
"Resolver: Install error - " + deployResource.getSymbolicName(),
ex);
return;
}
}
}
for (Bundle aStartList : startList) {
try {
aStartList.start();
} catch (BundleException ex) {
m_logger.log(
Logger.LOG_ERROR,
"Resolver: Start error - " + aStartList.getSymbolicName(),
ex);
}
}
}
/**
* Determines if the given bundle is a fragement bundle.
*
* @param bundle bundle to check
* @return flag indicating if the given bundle is a fragement
*/
private boolean isFragmentBundle(Bundle bundle)
{
return bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null;
}
// TODO: OBR - Think about this again and make sure that deployment ordering
// won't impact it...we need to update the local state too.
private LocalResource findUpdatableLocalResource(Resource resource)
{
// Determine if any other versions of the specified resource
// already installed.
LocalResource[] localResources = findLocalResources(resource.getSymbolicName());
// Since there are local resources with the same symbolic
// name installed, then we must determine if we can
// update an existing resource or if we must install
// another one. Loop through all local resources with same
// symbolic name and find the first one that can be updated
// without breaking constraints of existing local resources.
for (LocalResource localResource : localResources) {
if (isResourceUpdatable(localResource, resource, localResources)) {
return localResource;
}
}
return null;
}
/**
* Returns all local resources with the given symbolic name.
* @param symName The symbolic name of the wanted local resources.
* @return The local resources with the specified symbolic name.
*/
private LocalResource[] findLocalResources(String symName)
{
LocalResource[] localResources = getLocalResources();
List<LocalResource> matchList = new ArrayList<LocalResource>();
for (LocalResource localResource : localResources) {
String localSymName = localResource.getSymbolicName();
if ((localSymName != null) && localSymName.equals(symName)) {
matchList.add(localResource);
}
}
return matchList.toArray(new LocalResource[matchList.size()]);
}
private boolean isResourceUpdatable(
Resource oldVersion, Resource newVersion, Resource[] resources)
{
// Get all of the local resolvable requirements for the old
// version of the resource from the specified resource array.
Requirement[] reqs = getResolvableRequirements(oldVersion, resources);
if (reqs == null)
{
return true;
}
// Now make sure that all of the requirements resolved by the
// old version of the resource can also be resolved by the new
// version of the resource.
Capability[] caps = newVersion.getCapabilities();
if (caps == null)
{
return false;
}
for (Requirement req : reqs) {
boolean satisfied = false;
for (int capIdx = 0; !satisfied && (capIdx < caps.length); capIdx++) {
if (req.isSatisfied(caps[capIdx])) {
satisfied = true;
}
}
// If any of the previously resolved requirements cannot
// be resolved, then the resource is not updatable.
if (!satisfied) {
return false;
}
}
return true;
}
private Requirement[] getResolvableRequirements(Resource resource, Resource[] resources)
{
// For the specified resource, find all requirements that are
// satisfied by any of its capabilities in the specified resource
// array.
Capability[] caps = resource.getCapabilities();
if ((caps != null) && (caps.length > 0))
{
List<Requirement> reqList = new ArrayList<Requirement>();
for (Capability cap : caps) {
boolean added = false;
for (Resource aResource : resources) {
Requirement[] reqs = aResource.getRequirements();
if (reqs != null) {
for (Requirement req : reqs) {
if (req.isSatisfied(cap)) {
added = true;
reqList.add(req);
}
}
}
if (added) break;
}
}
return reqList.toArray(new Requirement[reqList.size()]);
}
return null;
}
public static String getBundleName(Bundle bundle)
{
String name = bundle.getHeaders().get(Constants.BUNDLE_NAME);
return (name == null)
? "Bundle " + Long.toString(bundle.getBundleId())
: name;
}
}