package fr.imag.adele.obrMan.internal; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.felix.bundlerepository.Capability; import org.apache.felix.bundlerepository.Reason; import org.apache.felix.bundlerepository.Repository; import org.apache.felix.bundlerepository.Requirement; import org.apache.felix.bundlerepository.Resolver; import org.apache.felix.bundlerepository.Resource; import org.osgi.framework.Bundle; import org.osgi.framework.BundleException; import org.osgi.framework.Version; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import fr.imag.adele.apam.CST; import fr.imag.adele.apam.Component; import fr.imag.adele.apam.RelToResolve; import fr.imag.adele.apam.declarations.ComponentDeclaration; import fr.imag.adele.apam.declarations.ImplementationDeclaration; import fr.imag.adele.apam.declarations.references.components.ComponentReference; import fr.imag.adele.apam.declarations.references.components.ImplementationReference; import fr.imag.adele.apam.declarations.references.components.InstanceReference; import fr.imag.adele.apam.declarations.references.components.SpecificationReference; import fr.imag.adele.apam.util.ApamFilter; /** * This class represents an APAM component that can be deployed from a bundle repository, * and searched by its associated metadata. */ public class DeployableComponent { private final static Logger logger = LoggerFactory.getLogger(DeployableComponent.class); private final OBRMan manager; private final String repository; private final Resource resource; private final Capability metadata; private final ComponentReference<?> component; private final int hashCode; public DeployableComponent(Repository repository, OBRMan manager, Resource resource, Capability capability) { assert capability.getName().equals(CST.CAPABILITY_COMPONENT); this.manager = manager; this.resource = resource; this.repository = repository.getURI(); this.metadata = capability; this.component = getComponent(metadata); this.hashCode = this.resource.getURI().hashCode()^ this.component.getName().hashCode(); } /** * The component in this deployment unit */ public ComponentReference<?> getReference() { return component; } /** * The list of components within the same deployment unit as this component. * * A deployable component can be packaged in the same deployment unit along with other components. * Installing or updating it then can have some side-effects on other components. */ public Set<String> getDeploymentUnitComponents() { Set<String> resourceComponents = new HashSet<String>(); for (Capability capability : resource.getCapabilities()) { if (capability.getName().equals(CST.CAPABILITY_COMPONENT)) { Object name = capability.getPropertiesAsMap().get(CST.NAME); if (name != null) resourceComponents.add(name.toString()); } } return resourceComponents; } /** * Whether this component satisfies the specified requirement, the requirement must concern the * component metadata in the repository */ public boolean satisfies(Requirement requirement) { assert requirement.getName().equals(CST.CAPABILITY_COMPONENT); return requirement.isSatisfied(metadata); } /** * Whether this component satisfies the specified filter, the filter must concern the component * metadata in the repository * * TODO Currently there seems to be a problem with repository requirements as superset and subset * operators used by APAM doesn't appear to work. Needs further investigation, just using APAM * filter directly by now * */ public boolean satisfies(ApamFilter filter) { return filter.matchCase(metadata.getPropertiesAsMap()); } /** * Whether this component satisfies requirement specified in the given relation. * * TODO NOTE Currently, because of filter substitutions, we can not translate the APAM constraints * into repository requirements, and use a single search/resolution mechanism. * * An alternative implementation of substitution is to evaluate variables in the context of the * source when the RelToResolve object is created, and to translate the constraints and preferences * into normal ldap filters that can be used with standard OSGi services. */ @SuppressWarnings("unchecked") public boolean satisfies(RelToResolve relation) { return relation.matchRelationConstraints(this.getReference().getKind(), (Map<String,Object>) metadata.getPropertiesAsMap()); } /** * The ranking of this component as a candidate to satisfy the given relation */ @SuppressWarnings("unchecked") public int ranking(RelToResolve relation) { return relation.ranking(this.getReference().getKind(), (Map<String,Object>) metadata.getPropertiesAsMap()); } /** * The version of this component */ public Version getVersion() { Object version = this.metadata.getPropertiesAsMap().get(Resource.VERSION); return (version != null) && (version instanceof Version) ? (Version) version : Version.emptyVersion; } /** * Whether the receiver represents a version of the specified component */ public boolean isVersionOf(DeployableComponent that) { return this.component.equals(that.component); } /** * Whether the receiver represents a preferred version of the specified component * * The preferred version has the higher version number, and in case both deployable * units have the same version we prefer the resource providing more capabilities */ public boolean isPreferedVersionThan(DeployableComponent that) { assert this.isVersionOf(that); int deltaVersion = this.getVersion().compareTo(that.getVersion()); return deltaVersion > 0 || (deltaVersion == 0 && this.resource.getCapabilities().length > that.resource.getCapabilities().length); } /** * Installs the component in the platform using the specified context to resolve requirements, and waits * until it is reified in APAM. * * Several cases must be considered as installations can proceed in parallel in several threads, we try * to avoid starting several deployment unnecessarily. */ public Component install(OBRManager context) { /* * If the component exists, maybe arrived in the mean time, or from another bundle or in another version, do nothing */ Component result = CST.componentBroker.getComponent(component.getName()); if (result != null) return result; /* * Check if the bundle is already existing in the platform */ Bundle installed = getBundle(resource); return installed != null ? CST.componentBroker.getWaitComponent(component.getName(), 10*1000 /* milliseconds*/) : deploy(context); } /** * Get the installed bundle corresponding to the specified resource. Activates the bundle if necessary. */ private final Bundle getBundle(Resource resource) { /* * Get the platform bundle with the same symbolic name, if any */ Bundle bundle = manager.getBundle(resource); if (bundle == null) { return null; } /* * the bundle is installed but is not started : try to start it before checking version numbers to be sure that any updates are finished */ if (bundle.getState() == Bundle.INSTALLED || bundle.getState() == Bundle.RESOLVED) { try { bundle.start(); logger.info("The bundle " + bundle.getSymbolicName() + " is installed and has been started!"); } catch (BundleException e) { logger.info("The bundle " + bundle.getSymbolicName() + " is installed but cannot be started!"); } } /* * Verify the version, notice that there can be only a single version of a bundle in the platform, so if we find another installed version, * there may be conflicting updates in progress. * */ boolean matchVersion = bundle.getVersion().equals(resource.getVersion()); boolean active = (bundle.getState() == Bundle.ACTIVE || bundle.getState() == Bundle.STARTING); if (!matchVersion) { logger.error("Bundle " + resource.getSymbolicName() + " is already installed under version " + bundle.getVersion() + " while trying to deploy version " + resource.getVersion()); } return active && matchVersion ? bundle : null; } /** * Deploy this resource on the platform and waits for the component to be reified in APAM. * * Return null if deployment is not possible. * */ private Component deploy(OBRManager context) { Resolver resolver = manager.getResolver(context); /* * Try to perform requirement resolution and deployment. This may need to be retried several times * as other installations can be ongoing in parallel, and the resource requirement resolution must * be recalculated in the new resource context. */ boolean deployed = false; boolean retrying = true; do { /* * calculate the transitive dependencies to satisfy requirements of the resource being deployed */ resolver.add(this.resource); resolver.resolve(); /* * If we can not resolve the resource requirements, just give up */ if (resolver.getUnsatisfiedRequirements().length > 0) { retrying = false; deployed = false; continue; } /* * Perform the actual installation, */ try { resolver.deploy(Resolver.START+Resolver.NO_OPTIONAL_RESOURCES); retrying = false; deployed = true; /* * Verify that all required resources were actually installed. A resource may not be installed if a * concurrent deployment has installed a conflicting version. * * In this case there is not much we can do, we just give up the deployment */ List<Resource> deployedResources = new ArrayList<Resource>(); deployedResources.addAll(Arrays.asList(resolver.getAddedResources())); deployedResources.addAll(Arrays.asList(resolver.getRequiredResources())); for (Resource deployedResource : deployedResources) { if (getBundle(deployedResource) == null) deployed = false; } } /* * Concurrent bundle deployment may interfere with installation and change the state of the local repository, * which produces an IllegalStateException of the resolver. * * We can recover by trying again the resolution and installation process, using the new local bundles. * */ catch (IllegalStateException e) { logger.debug("OBR changed state. Resolving again " + this.resource.getSymbolicName()); retrying = true; deployed = false; } /* * If we can not recover from the error, just give up * */ catch (Exception e) { logger.error ("Deployment of " + component + " from "+resource+" failed.",e) ; retrying = false; deployed = false; } } while (retrying); /* * Log deployment result */ logger.debug("Component: " + component+ (deployed ? " successfully " : " not ") + "deployed"); logger.debug(" repository: " + repository); List<Resource> deployedResources = new ArrayList<Resource>(); deployedResources.addAll(Arrays.asList(resolver.getAddedResources())); deployedResources.addAll(Arrays.asList(resolver.getRequiredResources())); for (Resource deployedResource : deployedResources) { logger.debug(" required resource: " + deployedResource+(getBundle(deployedResource) != null ? " (installed) ":" (not installed) ")); logger.debug(" location: " + deployedResource.getURI()); Reason[] reasons = resolver.getReason(deployedResource); for(Reason reason : reasons != null ? reasons : new Reason[0]) { logger.debug(" satisfies requirement: " + reason.getRequirement()+" of "+reason.getResource()); } } for (Reason missingRequirement : resolver.getUnsatisfiedRequirements()) { logger.error(" unsatisfied requirement: " + missingRequirement.getRequirement()); } return deployed ? CST.componentBroker.getWaitComponent(component.getName(), 10*1000 /* milliseconds*/) : null; } /** * Whether this unit represents the repository version of an installed component in the platform. * * This is useful to determine if the installed component can be updated from this unit, notice that * to be considered versions of each other the component name AND the bundle symbolic name must match. * Otherwise, component packaging has changed, and we can not perform an update. * */ public boolean isRepositoryVersionOf(Component component) { return component.getDeclaration().getReference().equals(this.component) && component.getApformComponent().getBundle().getSymbolicName().equals(this.resource.getSymbolicName()); } /** * Updates the currently deployed component from this repository version */ public void update(OBRManager context, Component component) throws Exception { assert this.isRepositoryVersionOf(component); URL updateLocation = (new URI(resource.getURI())).toURL(); component.getApformComponent().getBundle().update(updateLocation.openStream()); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null) return false; if (! (object instanceof DeployableComponent)) return false; DeployableComponent that = (DeployableComponent) object; return this.resource.getURI().equals(that.resource.getURI()) && this.component.equals(that.component); } @Override public int hashCode() { return hashCode; } @Override public String toString() { return this.component.getName()+"["+getVersion()+"] @ "+resource.getURI(); } /* * Utility methods to manipulate APAM metadata in capabilities */ private final static ComponentReference<?> getComponent(Capability metadata) { String componentName = getAttributeInCapability(metadata, CST.NAME); String componentkind = getAttributeInCapability(metadata, CST.COMPONENT_TYPE); if (CST.SPECIFICATION.equals(componentkind)) { return new SpecificationReference(componentName); } if (CST.IMPLEMENTATION.equals(componentkind)) { return new ImplementationReference<ImplementationDeclaration>(componentName); } if (CST.INSTANCE.equals(componentkind)) { return new InstanceReference(componentName); } return new ComponentReference<ComponentDeclaration>(componentName); } private final static String getAttributeInCapability(Capability aCap, String attr) { return (String) (aCap.getPropertiesAsMap().get(attr)); } }