/*
* Copyright (C) 2013 Google Inc.
*
* Licensed 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 interactivespaces.system.internal.osgi;
import interactivespaces.InteractiveSpacesException;
import interactivespaces.SimpleInteractiveSpacesException;
import interactivespaces.logging.ExtendedLog;
import interactivespaces.resource.Version;
import interactivespaces.resource.io.ResourceSource;
import interactivespaces.system.InteractiveSpacesFilesystem;
import interactivespaces.system.core.container.ContainerFilesystemLayout;
import interactivespaces.system.resources.ContainerResource;
import interactivespaces.system.resources.ContainerResourceCollection;
import interactivespaces.system.resources.ContainerResourceLocation;
import interactivespaces.system.resources.ContainerResourceManager;
import interactivespaces.system.resources.ContainerResourceType;
import interactivespaces.util.data.resource.MessageDigestResourceSignatureCalculator;
import interactivespaces.util.data.resource.ResourceSignatureCalculator;
import interactivespaces.util.io.FileSupport;
import interactivespaces.util.io.FileSupportImpl;
import interactivespaces.util.resource.ManagedResource;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.wiring.FrameworkWiring;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A container resource manager using OSGi.
*
* @author Keith M. Hughes
*/
public class OsgiContainerResourceManager implements ContainerResourceManager, ManagedResource {
/**
* Time to wait for a bundle update to take place. In milliseconds.
*/
public static final int BUNDLE_UPDATER_TIMEOUT = 4000;
/**
* What to search for in an OSGi bundle location for being in the system bootstrap folder.
*/
private static final String SYSTEM_BOOTSTRAP_COMPONENT = "/" + ContainerFilesystemLayout.FOLDER_SYSTEM_BOOTSTRAP
+ "/";
/**
* What to search for in an OSGi bundle location for being in the user bootstrap folder.
*/
private static final String USER_BOOTSTRAP_COMPONENT = "/" + ContainerFilesystemLayout.FOLDER_USER_BOOTSTRAP + "/";
/**
* The bundle context the manager is installed in.
*/
private final BundleContext bundleContext;
/**
* The OSGi service for wiring together bundles.
*/
private final FrameworkWiring frameworkWiring;
/**
* The file system for the container.
*/
private final InteractiveSpacesFilesystem filesystem;
/**
* Folder for configs. Can be {@code null}.
*/
private final File configFolder;
/**
* Logger for this manager.
*/
private final ExtendedLog log;
/**
* A map from bundle IDs to bundle updaters.
*/
private final Map<Long, BundleUpdater> bundleUpdaters = Maps.newConcurrentMap();
/**
* The resource signature calculator.
*/
private ResourceSignatureCalculator resourceSignatureCalculator = new MessageDigestResourceSignatureCalculator();
/**
* The resources in the container.
*/
private Map<String, ContainerResource> cachedResources;
/**
* The file support to use.
*/
private FileSupport fileSupport = FileSupportImpl.INSTANCE;
/**
* Construct a new resource manager.
*
* @param bundleContext
* the OSGi bundle context
* @param frameworkWiring
* the OSGi service for wiring together bundles
* @param filesystem
* the Interactive Spaces container filesystem
* @param configFolder
* the folder for configurations, can be {@code null}
* @param log
* the log to use
*/
public OsgiContainerResourceManager(BundleContext bundleContext, FrameworkWiring frameworkWiring,
InteractiveSpacesFilesystem filesystem, File configFolder, ExtendedLog log) {
this.bundleContext = bundleContext;
this.frameworkWiring = frameworkWiring;
this.filesystem = filesystem;
this.configFolder = configFolder;
this.log = log;
}
@Override
public void startup() {
// Can't cache resources in here as cannot know if all bundles have been loaded.
}
@Override
public void shutdown() {
// Nothing to do.
}
@Override
public synchronized ContainerResourceCollection getResources() {
loadContainerResources();
return new ContainerResourceCollection(cachedResources.values());
}
@Override
public synchronized void addResource(ContainerResource incomingResource, ResourceSource resourceSource) {
log.formatInfo("Adding container resource %s from URI %s", incomingResource, resourceSource.getLocation());
loadContainerResources();
ContainerResourceLocation location = incomingResource.getLocation();
File resourceDestinationFolder = getResourceDestinationFolder(location);
File resourceDestinationFile =
fileSupport.newFile(resourceDestinationFolder, incomingResource.getName() + "-"
+ incomingResource.getVersion().toString() + ".jar");
String resourceDestinationFileUri = resourceDestinationFile.toURI().toString();
if (location.isImmediateLoad()) {
Bundle installedBundle = locateBundleForResource(incomingResource);
if (installedBundle != null) {
if (installedBundle.getLocation().equals(resourceDestinationFileUri)) {
updateExactInstalledBundle(incomingResource, resourceSource, resourceDestinationFile, installedBundle);
} else {
swapInstalledBundle(incomingResource, resourceSource, resourceDestinationFile, resourceDestinationFileUri,
installedBundle);
}
} else {
loadNewBundle(incomingResource, resourceSource, resourceDestinationFile, resourceDestinationFileUri);
}
} else {
// TODO(keith): need to copy content
}
}
@Override
public synchronized Bundle loadAndStartBundle(File bundleFile, ContainerResourceType type)
throws InteractiveSpacesException {
if (!fileSupport.isFile(bundleFile)) {
throw SimpleInteractiveSpacesException.newFormattedException("Could not find bundle file %s of type %s",
fileSupport.getAbsolutePath(bundleFile), type);
}
loadContainerResources();
String bundleUri = bundleFile.toURI().toString();
Bundle bundle = null;
try {
bundle = bundleContext.installBundle(bundleUri);
bundle.start();
addBundleToResources(bundle, type, null);
return bundle;
} catch (Throwable e) {
// if managed to install the bundle then it should be uninstalled since we are in an error.
if (bundle != null) {
try {
bundle.uninstall();
} catch (BundleException e1) {
log.formatError(e, "Could not uninstall an OSGi bundle that could not be started: %s of type",
fileSupport.getAbsolutePath(bundleFile), type);
}
}
throw InteractiveSpacesException.newFormattedException(e, "Cannot load bundle file %s of type %s", type,
fileSupport.getAbsoluteFile(bundleFile), type);
}
}
/**
* Add in a bundle to the known resources.
*
* @param bundle
* the bundle being added
* @param type
* the type of the bundle
* @param resourceLocation
* the location of the bundle in the container, can be {@code null}
*
* @throws Exception
* was unable to add bundle in
*/
private void addBundleToResources(Bundle bundle, ContainerResourceType type,
ContainerResourceLocation resourceLocation) throws Exception {
org.osgi.framework.Version version = bundle.getVersion();
String bundleLocation = bundle.getLocation();
ContainerResource containerResource =
new ContainerResource(bundle.getSymbolicName(), new Version(version.getMajor(), version.getMinor(),
version.getMicro(), version.getQualifier()), type, resourceLocation,
resourceSignatureCalculator.getResourceSignature(getBundleFile(bundle)));
cachedResources.put(bundleLocation, containerResource);
}
@Override
public synchronized void uninstallBundle(Bundle bundle) {
loadContainerResources();
String bundleUri = bundle.getLocation();
ContainerResource containerResource = cachedResources.remove(bundleUri);
if (containerResource == null) {
throw SimpleInteractiveSpacesException.newFormattedException(
"Attempting to uninstall a bundle that is not installed: %s", bundleUri);
}
try {
bundle.uninstall();
} catch (BundleException e) {
throw InteractiveSpacesException.newFormattedException(e, "Cannot unload bundle %s", bundleUri, e);
}
}
/**
* Load in a bundle into the container for the first time.
*
* @param resource
* the resource to be loaded
* @param resourceSource
* the resource source
* @param resourceDestinationFile
* the file where the resource will be copied
* @param resourceDestinationFileUri
* the URI of the destination file
*/
private void loadNewBundle(ContainerResource resource, ResourceSource resourceSource, File resourceDestinationFile,
String resourceDestinationFileUri) {
log.formatDebug("New Resource %s being loaded into the container from %s to %s", resource,
resourceSource.getLocation(), resourceDestinationFileUri);
// Always copy, it isn't there.
resourceSource.copyTo(resourceDestinationFile);
try {
Bundle installedBundle = bundleContext.installBundle(resourceDestinationFileUri);
installedBundle.start();
addNewContainerResource(installedBundle);
} catch (Throwable e) {
throw InteractiveSpacesException.newFormattedException(e, "Could not load a new bundle %s from source %s",
resourceDestinationFileUri, resourceSource.getLocation());
}
}
/**
* Update the bundle that has the exact same filename as the resource coming in.
*
* @param incomingResource
* the container resource for the replacement
* @param resourceSource
* the source of the resource
* @param resourceDestinationFile
* the file where the resource will ultimately end up
* @param installedBundle
* the bundle of the exact file that will be replaced
*/
private void updateExactInstalledBundle(ContainerResource incomingResource, ResourceSource resourceSource,
File resourceDestinationFile, Bundle installedBundle) {
log.formatDebug("Resource %s already was loaded. Attempting update.", incomingResource);
ContainerResource existingResource = getContainerResource(getBundleUri(installedBundle).toString());
String signatureNew = incomingResource.getSignature();
if (existingResource.getSignature().equals(signatureNew)) {
log.formatDebug("Resource %s was not updated, signatures identical.", incomingResource);
return;
}
Collection<Bundle> activitiesDependentOnBundle = getLoadedActivitiesDependentOnBundle(installedBundle);
if (!activitiesDependentOnBundle.isEmpty()) {
throw SimpleInteractiveSpacesException.newFormattedException(
"Cannot update dependency %s, an activity depends on it and the activity is running",
installedBundle.getLocation());
}
resourceSource.copyTo(resourceDestinationFile);
log.formatDebug("Resource %s update beginning.", incomingResource);
newBundleUpdater(installedBundle, existingResource, signatureNew).updateBundle();
}
/**
* Get a list of all activity bundles that are dependent on the dependency bundle.
*
* @param dependencyBundle
* the dependency bundle
*
* @return {@code true} if an activity is dependent
*/
private Collection<Bundle> getLoadedActivitiesDependentOnBundle(Bundle dependencyBundle) {
List<Bundle> dependentActivities = Lists.newArrayList();
Collection<Bundle> dependencyClosure = frameworkWiring.getDependencyClosure(Lists.newArrayList(dependencyBundle));
for (Bundle bundle : dependencyClosure) {
String bundleUri = bundle.getLocation();
ContainerResource containerResource = cachedResources.get(bundleUri);
if (containerResource != null) {
if (containerResource.getType() == ContainerResourceType.ACTIVITY) {
dependentActivities.add(bundle);
}
} else {
log.formatWarn("The OSGi container has an untracked bundle at %s", bundleUri);
}
}
return dependentActivities;
}
/**
* Get the bundle URI.
*
* @param bundle
* the bundle
*
* @return the URI
*/
private URI getBundleUri(Bundle bundle) {
try {
return new URI(bundle.getLocation());
} catch (URISyntaxException e) {
throw SimpleInteractiveSpacesException.newFormattedException(e, "Could not parse URI for %s",
bundle.getLocation());
}
}
/**
* Swap a bundle that was named one way with a new bundle named another way.
*
* @param resource
* the container resource that is being installed
* @param resourceSource
* the source of the resource
* @param resourceDestinationFile
* the file for the new resource
* @param resourceDestinationFileUri
* the destination URI for the new resource
* @param installedBundle
* the bundle that is being swapped with a new file
*/
private void swapInstalledBundle(ContainerResource resource, ResourceSource resourceSource,
File resourceDestinationFile, String resourceDestinationFileUri, Bundle installedBundle) {
log.formatDebug("Resource %s already was loaded with another name. Uninstalling old and loading new.", resource);
// No matter what, copy the new bundle without checking the signature since we want it with its new name.
resourceSource.copyTo(resourceDestinationFile);
try {
installedBundle.uninstall();
fileSupport.delete(getBundleFile(installedBundle));
installedBundle = bundleContext.installBundle(resourceDestinationFileUri);
installedBundle.start();
} catch (Throwable e) {
throw InteractiveSpacesException.newFormattedException(e, "Could not swap a resource %s", resource);
}
}
/**
* Get the destination for a resource.
*
* @param location
* the final location for the resource
*
* @return the corresponding file for the location
*
* @throws InteractiveSpacesException
* no file location was available for the specified location
*/
private File getResourceDestinationFolder(ContainerResourceLocation location) throws InteractiveSpacesException {
switch (location) {
case SYSTEM_BOOTSTRAP:
return filesystem.getSystemBootstrapDirectory();
case CONFIG:
if (configFolder == null) {
throw new SimpleInteractiveSpacesException("Configurations are not modifiable");
}
return fileSupport.newFile(configFolder, ContainerFilesystemLayout.FOLDER_CONFIG_INTERACTIVESPACES);
case LIB_SYSTEM:
return fileSupport.newFile(filesystem.getInstallDirectory(),
ContainerFilesystemLayout.FOLDER_INTERACTIVESPACES_SYSTEM);
case USER_BOOTSTRAP:
return fileSupport.newFile(filesystem.getInstallDirectory(), ContainerFilesystemLayout.FOLDER_USER_BOOTSTRAP);
case ROOT:
return filesystem.getInstallDirectory();
default:
throw SimpleInteractiveSpacesException.newFormattedException("Unsupported container location %s", location);
}
}
/**
* Locate the bundle for a resource if it exists in the OSGi container.
*
* @param resource
* the resource
*
* @return the bundle for the resource, or {@code null} if no bundles are available that provide the resource
*/
private Bundle locateBundleForResource(ContainerResource resource) {
Version version = resource.getVersion();
org.osgi.framework.Version osgiVersion =
new org.osgi.framework.Version(version.getMajor(), version.getMinor(), version.getMicro(),
version.getQualifier());
for (Bundle bundle : bundleContext.getBundles()) {
if (bundle.getSymbolicName().equals(resource.getName()) && bundle.getVersion().equals(osgiVersion)) {
return bundle;
}
}
return null;
}
/**
* Load the container resource cache.
*/
private void loadContainerResources() {
if (cachedResources != null) {
return;
}
cachedResources = Maps.newHashMap();
for (Bundle bundle : bundleContext.getBundles()) {
addNewContainerResource(bundle);
}
}
/**
* Add in the container resource for a bundle into the cache.
*
* @param bundle
* the bundle
*/
private void addNewContainerResource(Bundle bundle) {
String bundleLocation = bundle.getLocation();
ContainerResourceLocation resourceLocation = null;
if (bundleLocation.contains(SYSTEM_BOOTSTRAP_COMPONENT)) {
resourceLocation = ContainerResourceLocation.SYSTEM_BOOTSTRAP;
} else if (bundleLocation.contains(USER_BOOTSTRAP_COMPONENT)) {
resourceLocation = ContainerResourceLocation.USER_BOOTSTRAP;
}
// Only add if in a monitored area.
if (resourceLocation != null) {
try {
org.osgi.framework.Version osgiVersion = bundle.getVersion();
ContainerResource resource =
new ContainerResource(bundle.getSymbolicName(), new Version(osgiVersion.getMajor(),
osgiVersion.getMinor(), osgiVersion.getMicro(), osgiVersion.getQualifier()),
ContainerResourceType.LIBRARY, resourceLocation,
resourceSignatureCalculator.getResourceSignature(getBundleFile(bundle)));
cachedResources.put(bundleLocation, resource);
} catch (Throwable e) {
log.formatInfo(e, "Could not create container resource information for %s", bundleLocation);
}
}
}
/**
* Get the container resource for the given URI.
*
* @param bundleUri
* the bundle URI
*
* @return the container resource, or {@code null} if no such bundle being tracked
*/
@VisibleForTesting
ContainerResource getContainerResource(String bundleUri) {
return cachedResources.get(bundleUri);
}
/**
* Get the file associated with a bundle.
*
* @param installedBundle
* the installed bundle
*
* @return the file associated with the bundle
*
* @throws Exception
* something happened while getting the bundle file
*/
private File getBundleFile(Bundle installedBundle) throws Exception {
return fileSupport.newFile(getBundleUri(installedBundle));
}
/**
* Set the file support to use.
*
* @param fileSupport
* the file support
*/
@VisibleForTesting
void setFileSupport(FileSupport fileSupport) {
this.fileSupport = fileSupport;
}
/**
* Set the source signature calculator to use.
*
* @param resourceSignatureCalculator
* the calculator
*/
@VisibleForTesting
void setResourceSignatureCalculator(ResourceSignatureCalculator resourceSignatureCalculator) {
this.resourceSignatureCalculator = resourceSignatureCalculator;
}
/**
* Create a bundle updater for a bundle.
*
* @param bundle
* the bundle whose updater should be gotten
* @param existingResource
* the existing resource
* @param signatureNew
* the signature of the incoming bundle
*
* @return the updater
*/
private BundleUpdater newBundleUpdater(Bundle bundle, ContainerResource existingResource, String signatureNew) {
BundleUpdater updater = getBundleUpdater(bundle);
if (updater == null) {
updater = new BundleUpdater(bundle, existingResource, signatureNew);
bundleUpdaters.put(bundle.getBundleId(), updater);
}
return updater;
}
/**
* Get the bundle updater for a bundle.
*
* @param bundle
* the bundle
*
* @return the updater for the bundle, or {@code null} if none
*/
@VisibleForTesting
BundleUpdater getBundleUpdater(Bundle bundle) {
return bundleUpdaters.get(bundle.getBundleId());
}
/**
* Complete a bundle update.
*
* @param bundle
* the bundle whose update is complete
* @param existingResource
* the resource that has just been updated
* @param newSignature
* the new signature for the bundle
*/
private synchronized void
completeBundleUpdate(Bundle bundle, ContainerResource existingResource, String newSignature) {
bundleUpdaters.remove(bundle.getBundleId());
existingResource.setSignature(newSignature);
}
/**
* An updater for bundle updates. This is for bundles which are already loaded and being used and that very bundle is
* being updated.
*
* @author Keith M. Hughes
*/
public class BundleUpdater implements FrameworkListener {
/**
* The bundle being updated.
*/
private final Bundle bundle;
/**
* The existing resource being updated.
*/
private final ContainerResource existingResource;
/**
* The new signature for the bundle.
*/
private final String newSignature;
/**
* A latch for declaring the update is done.
*/
private final CountDownLatch doneUpdateLatch = new CountDownLatch(1);
/**
* An exception found during updating.
*/
private Throwable throwable;
/**
* {@code true} if update() has been called.
*/
private AtomicBoolean updateStarted = new AtomicBoolean();
/**
* Construct a new updater.
*
* @param bundle
* the bundle to be updated
* @param existingResource
* the existing resource being updated
* @param newSignature
* the new signature for the bundle
*/
public BundleUpdater(Bundle bundle, ContainerResource existingResource, String newSignature) {
this.bundle = bundle;
this.existingResource = existingResource;
this.newSignature = newSignature;
}
/**
* Start the updating of the bundle.
*/
public void updateBundle() {
// If was true already, just return
if (updateStarted.getAndSet(true)) {
return;
}
try {
bundle.update();
// The refresh happens in another thread.
frameworkWiring.refreshBundles(Lists.newArrayList(bundle), this);
try {
if (!doneUpdateLatch.await(BUNDLE_UPDATER_TIMEOUT, TimeUnit.MILLISECONDS)) {
throw new SimpleInteractiveSpacesException("Could not update bundle in time");
}
} catch (InterruptedException e) {
// Don't care
}
} catch (BundleException e) {
throwable = e;
}
if (throwable != null) {
throw new InteractiveSpacesException("Could not update resource", throwable);
} else {
completeBundleUpdate(bundle, existingResource, newSignature);
}
}
@Override
public void frameworkEvent(FrameworkEvent event) {
if (event.getType() == FrameworkEvent.ERROR) {
throwable = event.getThrowable();
}
doneUpdateLatch.countDown();
}
}
}