blob: 45ef6dc5db37ec1317e211440aa417a15e39435c [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.bundlerepository.Resolver;
import org.apache.felix.bundlerepository.impl.ResourceImpl;
import org.apache.felix.utils.log.Logger;
import org.osgi.framework.*;
public class ResolverImpl implements Resolver
{
private final BundleContext m_context;
private final Logger m_logger;
private final Repository[] m_repositories;
private final Set m_addedSet = new HashSet();
private final Set m_addedRequirementSet = new HashSet();
private final Set m_globalCapabilities = new HashSet();
private final Set m_failedSet = new HashSet();
private final Set m_resolveSet = new HashSet();
private final Set m_requiredSet = new HashSet();
private final Set m_optionalSet = new HashSet();
private final Map m_reasonMap = new HashMap();
private final Set m_unsatisfiedSet = new HashSet();
private boolean m_resolved = false;
private long m_resolveTimeStamp;
private int m_resolutionFlags;
private int m_deployFlags;
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 (Resource[]) 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 (Requirement[]) m_addedRequirementSet.toArray(new Requirement[m_addedRequirementSet.size()]);
}
public void addGlobalCapability(Capability capability)
{
m_globalCapabilities.add(capability);
}
public Capability[] getGlobalCapabilities()
{
return (Capability[]) m_globalCapabilities.toArray(new Capability[m_globalCapabilities.size()]);
}
public synchronized Resource[] getRequiredResources()
{
if (m_resolved)
{
return (Resource[]) 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 (Resource[]) 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 l = (List) m_reasonMap.get(resource);
return l != null ? (Reason[]) l.toArray(new Reason[l.size()]) : null;
}
throw new IllegalStateException("The resources have not been resolved.");
}
public synchronized Reason[] getUnsatisfiedRequirements()
{
if (m_resolved)
{
return (Reason[]) m_unsatisfiedSet.toArray(new Reason[m_unsatisfiedSet.size()]);
}
throw new IllegalStateException("The resources have not been resolved.");
}
private Resource[] getResources(boolean local)
{
List resources = new ArrayList();
for (int repoIdx = 0; (m_repositories != null) && (repoIdx < m_repositories.length); repoIdx++)
{
boolean isLocal = m_repositories[repoIdx] instanceof LocalRepositoryImpl;
boolean isSystem = m_repositories[repoIdx] instanceof SystemRepositoryImpl;
if (isLocal && (m_resolutionFlags & NO_LOCAL_RESOURCES) != 0) {
continue;
}
if (isSystem && (m_resolutionFlags & NO_SYSTEM_BUNDLE) != 0) {
continue;
}
Resource[] res = m_repositories[repoIdx].getResources();
for (int resIdx = 0; (res != null) && (resIdx < res.length); resIdx++)
{
if (res[resIdx].isLocal() == local)
{
resources.add(res[resIdx]);
}
}
}
return (Resource[]) resources.toArray(new Resource[resources.size()]);
}
public synchronized boolean resolve()
{
return resolve(0);
}
public synchronized boolean resolve(int flags)
{
// Find resources
Resource[] locals = getResources(true);
Resource[] remotes = getResources(false);
// 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 (Iterator iter = m_globalCapabilities.iterator(); iter.hasNext();)
{
Capability cap = (Capability) iter.next();
fake.addCapability(cap);
}
for (Iterator iter = m_addedRequirementSet.iterator(); iter.hasNext();)
{
Requirement req = (Requirement) iter.next();
fake.addRequire(req);
}
if (!resolve(fake, locals, remotes, false))
{
result = false;
}
}
// Loop through each resource in added list and resolve.
for (Iterator iter = m_addedSet.iterator(); iter.hasNext(); )
{
if (!resolve((Resource) iter.next(), 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 (int reqIdx = 0; reqIdx < reqs.length; reqIdx++)
{
// Do not resolve optional requirements
if ((m_resolutionFlags & NO_OPTIONAL_RESOURCES) != 0 && reqs[reqIdx].isOptional())
{
continue;
}
candidate = searchResources(reqs[reqIdx], m_addedSet);
if (candidate == null)
{
candidate = searchResources(reqs[reqIdx], m_requiredSet);
}
if (candidate == null)
{
candidate = searchResources(reqs[reqIdx], m_optionalSet);
}
if (candidate == null)
{
candidate = searchResources(reqs[reqIdx], m_resolveSet);
}
if (candidate == null)
{
List candidateCapabilities = searchResources(reqs[reqIdx], locals);
candidateCapabilities.addAll(searchResources(reqs[reqIdx], 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 || reqs[reqIdx].isOptional()))
{
candidate = bestCapability.getResource();
}
else
{
candidateCapabilities.remove(bestCapability);
}
}
}
if ((candidate == null) && !reqs[reqIdx].isOptional())
{
// The resolve failed.
result = false;
// Associated the current resource to the requirement
// in the unsatisfied requirement set.
m_unsatisfiedSet.add(new ReasonImpl(resource, reqs[reqIdx]));
}
else if (candidate != null)
{
// Try to resolve the candidate.
if (resolve(candidate, locals, remotes, optional || reqs[reqIdx].isOptional()))
{
// The resolved succeeded; record the candidate
// as either optional or required.
if (optional || reqs[reqIdx].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 reasons = (List) m_reasonMap.get(candidate);
if (reasons == null)
{
reasons = new ArrayList();
m_reasonMap.put(candidate, reasons);
}
reasons.add(new ReasonImpl(resource, reqs[reqIdx]));
}
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 resourceSet)
{
for (Iterator iter = resourceSet.iterator(); iter.hasNext(); )
{
checkInterrupt();
Resource resource = (Resource) iter.next();
Capability[] caps = resource.getCapabilities();
for (int capIdx = 0; (caps != null) && (capIdx < caps.length); capIdx++)
{
if (req.isSatisfied(caps[capIdx]))
{
// The requirement is already satisfied an existing
// resource, return the resource.
return resource;
}
}
}
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 searchResources(Requirement req, Resource[] resources)
{
List matchingCapabilities = new ArrayList();
for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++)
{
checkInterrupt();
// We don't need to look at resources we've already looked at.
if (!m_failedSet.contains(resources[resIdx]))
{
Capability[] caps = resources[resIdx].getCapabilities();
for (int capIdx = 0; (caps != null) && (capIdx < caps.length); capIdx++)
{
if (req.isSatisfied(caps[capIdx]))
{
matchingCapabilities.add(new ResourceCapabilityImpl(resources[resIdx], caps[capIdx]));
}
}
}
}
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 caps)
{
Version bestVersion = null;
ResourceCapability best = null;
boolean bestLocal = false;
for(int capIdx = 0; capIdx < caps.size(); capIdx++)
{
ResourceCapability current = (ResourceCapability) caps.get(capIdx);
boolean isCurrentLocal = current.getResource().isLocal();
if (best == null)
{
best = current;
bestLocal = isCurrentLocal;
Object v = current.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 = current.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
< current.getResource().getCapabilities().length))
{
best = current;
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(v) < 0))
{
best = current;
bestLocal = isCurrentLocal;
bestVersion = (Version) v;
}
// If the current resource version is equal to the
// best, then select the one with the greatest
// number of capabilities.
else if ((bestVersion != null) && (bestVersion.compareTo(v) == 0)
&& (best.getResource().getCapabilities().length
< current.getResource().getCapabilities().length))
{
best = current;
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)
{
m_deployFlags = 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.
Map deployMap = new HashMap();
Resource[] resources = getAddedResources();
for (int i = 0; (resources != null) && (i < resources.length); i++)
{
deployMap.put(resources[i], resources[i]);
}
resources = getRequiredResources();
for (int i = 0; (resources != null) && (i < resources.length); i++)
{
deployMap.put(resources[i], resources[i]);
}
if ((flags & NO_OPTIONAL_RESOURCES) == 0)
{
resources = getOptionalResources();
for (int i = 0; (resources != null) && (i < resources.length); i++)
{
deployMap.put(resources[i], resources[i]);
}
}
Resource[] deployResources = (Resource[])
deployMap.keySet().toArray(new Resource[deployMap.size()]);
// List to hold all resources to be started.
List startList = new ArrayList();
// 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 (int i = 0; i < deployResources.length; i++)
{
// For the resource being deployed, see if there is an older
// version of the resource already installed that can potentially
// be updated.
LocalResourceImpl localResource =
findUpdatableLocalResource(deployResources[i]);
// 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, deployResources[i], deployResources))
{
// Only update if it is a different version.
if (!localResource.equals(deployResources[i]))
{
// 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(deployResources[i].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(deployResources[i].getURI());
if (url != null)
{
Bundle bundle = m_context.installBundle(
"obr://"
+ deployResources[i].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 - " + deployResources[i].getSymbolicName(),
ex);
return;
}
}
}
for (int i = 0; i < startList.size(); i++)
{
try
{
((Bundle) startList.get(i)).start();
}
catch (BundleException ex)
{
m_logger.log(
Logger.LOG_ERROR,
"Resolver: Start error - " + ((Bundle) startList.get(i)).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 LocalResourceImpl findUpdatableLocalResource(Resource resource)
{
// Determine if any other versions of the specified resource
// already installed.
Resource[] localResources = findLocalResources(resource.getSymbolicName());
if (localResources != null)
{
// 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 (int i = 0; i < localResources.length; i++)
{
if (isResourceUpdatable(localResources[i], resource, localResources))
{
return (LocalResourceImpl) localResources[i];
}
}
}
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 Resource[] findLocalResources(String symName)
{
Resource[] localResources = getResources(true);
List matchList = new ArrayList();
for (int i = 0; i < localResources.length; i++)
{
String localSymName = localResources[i].getSymbolicName();
if ((localSymName != null) && localSymName.equals(symName))
{
matchList.add(localResources[i]);
}
}
return (Resource[]) matchList.toArray(new Resource[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 (int reqIdx = 0; reqIdx < reqs.length; reqIdx++)
{
boolean satisfied = false;
for (int capIdx = 0; !satisfied && (capIdx < caps.length); capIdx++)
{
if (reqs[reqIdx].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 reqList = new ArrayList();
for (int capIdx = 0; capIdx < caps.length; capIdx++)
{
boolean added = false;
for (int resIdx = 0; !added && (resIdx < resources.length); resIdx++)
{
Requirement[] reqs = resources[resIdx].getRequirements();
for (int reqIdx = 0;
(reqs != null) && (reqIdx < reqs.length);
reqIdx++)
{
if (reqs[reqIdx].isSatisfied(caps[capIdx]))
{
added = true;
reqList.add(reqs[reqIdx]);
}
}
}
}
return (Requirement[]) reqList.toArray(new Requirement[reqList.size()]);
}
return null;
}
public static String getBundleName(Bundle bundle)
{
String name = (String) bundle.getHeaders().get(Constants.BUNDLE_NAME);
return (name == null)
? "Bundle " + Long.toString(bundle.getBundleId())
: name;
}
}