| /* |
| * 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; |
| } |
| |
| } |