The OSGi specification provides APIs for getting various status information and initiating administrative actions. For example, you can find out about all the currently installed bundles, their IDs, states, and locations; you can get a rundown of a bundle's manifest headers; and you can install and start other bundles. The command console is built on these very same APIs, and it allows you to perform administrative tasks interactively. A bundle with sufficient permissions can also assume the same administrative role programmatically.
We do not attempt to address the general issue of management. That job deals with provisioning for and managing numerous gateways running the Java Embedded Server software over the Internet in a distributed fashion and must consider scalability and reliability requirements seriously. This is referred to as macro management. As a contrast, we discuss micro management—local administration of one gateway, and in particular, how to resolve a group of bundles automatically.
We have learned that for a bundle to be resolved, the packages it imports must be exported in the framework by some other bundles, and the exported packages must have compatible versions, meaning an exported package's version must be equal to or greater than the version required for importing that package. So far we have been keeping track of the package dependency manually: Before we start admin.jar, for example, we first install and start servlet.jar, http.jar, and param.jar in the console, knowing that the administrative bundle depends on the parameter bundle for accessing the parameter storage area and the HTTP bundle for registering the servlet that presents the Web user interface. The HTTP bundle in turn relies on the servlet bundle for the Java Servlet library classes. These relationships form a treelike graph, as shown in Figure 9.7. Won't it be convenient to automate this process? That is, when a bundle is installed, other bundles that it needs are automatically found, installed, and activated, so that the bundle in question can be resolved successfully.
To begin, installing and activating the needed bundles cannot be done by the bundle itself, because it cannot be started, let alone get anything done, without them in the first place. A separate facilitator must act as the go-between, which provides the following functionalities:
Encapsulating the knowledge of interbundle package dependency relationships.
Resolving bundles recursively. If bundle A depends on bundle B, we must examine whether B further needs bundle C, C needs D, and so on. In computer science jargon, we need to resolve the transitive closure of bundles starting from A.
If the resolution process fails at some point, reporting the package whose exporter either is not found or exports it at an incompatible version, as well as the unsatisfied bundle that is in need of the package.
Package dependency can be expressed as a map, as shown in Table 9.2. It basically answers the following question: Given a package, what is the location of the bundle that exports the package and at what version? We call this relation the package exporter map.
Package | Version | Location of the Exporting Bundle |
---|---|---|
org.osgi.service.http | 1.0 | file:jes_path/bundles/http.jar |
javax.servlet | 2.1.1 | file:jes_path/bundles/servlet.jar |
javax.servlet.http | 2.1.1 | file:jes_path/bundles/servlet.jar |
org.osgi.service.log | 1.0 | file:jes_path/bundles/log.jar |
com.acme.service.param | 0.0 | file:/home/joe/bundles/param.jar |
The map is implemented as a property file. The property name is the package name, the property value contains the location, a comma, and the version. The version can be omitted, in which case, it is assumed to be zero. For example, the first row in Table 9.2 corresponds to this entry in the property file:
org.osgi.service.http=file:jes_path/bundles/http.jar, 1.0
Next, the facilitator is designed to carry out the following resolution algorithm:
1. |
Install the bundle whose location is given. Examine its Import-Package header by calling the getHeaders method of the Bundle interface. Parse the header and obtain a set of package names. |
2. |
Consult the package exporter map and find the locations of the bundles that export the packages. If the map yields nothing, or if the package version for export is incompatible with the one needed for import, the resolution fails. |
3. |
Install the newly found bundles. |
4. |
Repeat this process recursively for the bundles installed in step 3. |
The termination condition for the recursion is as follows: At step 1, if a bundle's Import-Package header is empty, or the needed packages are available from CLASSPATH, then the bundle does not depend on another and is said to be a leaf in the dependency tree.
Before we look at the facilitator implementation, let's get a sense of how it is typically used. Let's assume we want to resolve admin.jar, whose location is given in url:
Facilitator f = new Facilitator(bundleContext); if ( ! f.resolve(url)) { System.out.println("Failed to resolve bundle from " + url); Enumeration unsatisfiedBundles = f.getUnsatisfiedBundles(); while (unsatisfiedBundles.hasMoreElements()) { Bundle b = (Bundle) unsatisfiedBundles.nextElement(); String[] pkgs = f.getMissingPackages(b); System.out.println("Bundle (" + b.getLocation() + ")" + " needs the following missing packages:"); for (int i=0; i<pkgs.length; i++) System.out.println(" " + pkgs[i]); } } f.reset(); // now f is ready to resolve another bundle
The getMissingPackages and getUnsatisfiedBundles methods return why and where the resolution fails. The following is the main part of the implementation. To highlight the resolution portion of the code, we omit the error-reporting methods. The complete listing is in Appendix A (section A.3.3.1):
class Facilitator { private BundleContext context; private Properties packageExporterMap; // Flag a visited bundle to prevent following circular dependency // during recursion. private Hashtable visited; Facilitator(BundleContext ctxt) throws IOException { this.context = ctxt; this.packageExporterMap = new Properties(); String resourceName = "/com/acme/admin/resolv/package-exporters.map"; InputStream in = this.getClass().getResourceAsStream(resourceName); packageExporterMap.load(in); this.visited = new Hashtable(); } boolean resolve(String urlstr) throws BundleException { if (urlstr == null) return false; if (visited.get(urlstr) != null) { // if a bundle is processed, don't run in circles return true; } Bundle b = context.installBundle(urlstr); // flag that the bundle has been processed visited.put(urlstr, Boolean.TRUE); Dictionary headers = b.getHeaders(); String imports = (String) headers.get("Import-Package"); boolean r = true; if (imports != null) { String[] loc = findExporters(imports, b); for (int i=0; i<loc.length; i++) { // use bitwise & to prevent short-circuit // behavior associated with logical && r = r & resolve(loc[i]); } } if (r) { b.start(); } return r; } private String[] findExporters(String imports, Bundle b) { StringTokenizer st = new StringTokenizer(imports, ","); Vector v = new Vector(st.countTokens()); while (st.hasMoreTokens()) { // enumerate each package String token = st.nextToken().trim(); // separate package name and import version int semicolonPos = token.indexOf(';'), String pkgname = token; String importVersion = ""; if (semicolonPos != -1) { pkgname = token.substring(0, semicolonPos).trim(); int eqPos = token.indexOf('=', semicolonPos); importVersion = token.substring(eqPos + 1); } if (isFromClasspath(pkgname)) continue; String loc = (String) packageExporterMap.get(pkgname); if (loc != null) { // separate bundle location and export version String exportVersion = ""; int commaPos = loc.indexOf(','), if (commaPos != -1) { exportVersion = loc.substring(commaPos + 1); loc = loc.substring(0, commaPos); } if (! areVersionsCompatible(importVersion, exportVersion)) { loc = null; } } if (loc == null) { // save current package 'pkgname' // to be returned by getMissingPackages method. ... } v.addElement(loc); } // while String[] locations = new String[v.size()]; v.copyInto(locations); // save all missing packages for the bundle 'b' ... return locations; } private boolean isFromClasspath(String pkg) { URL u = ClassLoader.getSystemResource( pkg.replace('.','/') + "/"); return u != null; } private boolean areVersionsCompatible(String impVerStr, String expVerStr) { // The package version for export must be equal to or // greater than the package version needed for import ... } }
In the constructor, the package exporter map is read from a static property file included in the facilitator bundle. The resolve method implements the resolution algorithm just presented. If the current bundle and all the bundles on which it depends have been resolved, then the bundle is started. Notice that resolved bundles are activated on return from recursion to ensure that child bundles in the dependency tree are activated before their parents and ancestors.
If bundle A needs to import packages from bundle B, which directly or indirectly also needs to import packages from bundle A, a circular dependency is formed. The resolution routine runs infinitely if this situation arises and is unchecked. The visited hash table is used to break the cycle by not repeating resolution of a bundle if it has been marked as having been processed.
Much of the findExporters method is responsible for parsing the Import-Package header for a set of package names, looking up the locations of exporting bundles from the package exporter map and ensuring that the package's export version is compatible with its import version.
The isFromClasspath method reports whether a package is available from CLASSPATH. If so, there is no need to locate an exporter for that package. ClassLoader.getSystemResource returns non-null URL for system classes on the CLASSPATH, but null for classes loaded from another bundle. This check is necessary because according to the OSGi specification, packages with names that do not begin with java. must be declared in the Import-Package header, even though they are available from CLASSPATH.
What is not shown is how to interact with the facilitator. In our example, we implement a multithread server so that we can connect to the Java Embedded Server software from another host over a TCP/IP channel, install a bundle, and resolve it remotely. We depart from using our favorite servlet solution, because the facilitator, which is charged with the job of resolving other bundles, should be self-sufficient to be effective. The complete source code is shown in Appendix A, section A.3.3.2.
First, let's install and activate the facilitator bundle, which starts a server listening on port 8082:
> start file:/home/joe/bundles/facilitator.jar > bundles ID STATE LOCATION -- ------ ---------------------------- 1 ACTIVE file:/home/joe/bundles/facilitator.jar >
Then start a session using the standard telnet command[1] as the client. Enter a location string for the bundle you'd like to resolve after the prompt. Note that the location string is referring to a location on the host where the Java Embedded Server software is running:
[1] To see your typing in Windows systems, turn on “Local Echo” of the telnet program from Menu Terminal → Preferences → Local Echo.
% telnet mybox 8082 Trying 129.144.175.65... Connected to mybox. Escape character is '^]'. Bundle Resolution Enter the bundle's URL: file:/home/joe/bundles/admin.jar OK Connection closed by foreign host.
From the console we can verify the result by issuing the bundles command:
> bundles ID STATE LOCATION -- ------ ---------------------------- 1 ACTIVE file:/home/joe/bundles/facilitator.jar 2 ACTIVE file:/home/joe/bundles/admin.jar 3 ACTIVE file:jes_path/bundles/http.jar 4 ACTIVE file:jes_path/bundles/servlet.jar 5 ACTIVE file:/home/joe/bundles/param.jar
Just as we expected, the facilitator automatically installed and activated the servlet.jar, param.jar, http.jar, and admin.jar in the right order so that every bundle is resolved and started successfully all at once.
The previous implementation attempts to resolve as many bundles as possible. For instance, suppose the dependency map has the following entry missing, meaning it does not know which bundle exports the com.acme.service.param package:
com.acme.service.param=file:jes_path/bundles/param.jar
The algorithm will install and activate servlet.jar and http.jar in addition to admin.jar before reporting the resolution failure. It allows us to activate all the bundles in a subtree, even though bundles below another branch have resolution problems.
When the resolution fails, a partial set of bundles is left installed or activated in the framework. This is fine because the installation and activation operations are both idempotent: If an installed bundle is to be installed, or an active bundle is to be activated a second time, the framework detects this situation and does nothing. However, if transactional behavior is desired, we can remember the set of installed and activated bundles and undo them by deactivating and uninstalling them if the resolution fails eventually. Another way is to examine the manifest headers directly from the source without installing the bundles at all until the complete set of bundles is resolved.
The package exporter map is at the heart of the resolution algorithm. However, its static nature is limiting for the usefulness of the solution. A tool can be developed to construct the map automatically for a given set of bundles. Assuming the locations of the bundles under inspection are saved in an array locations, the following code fragment shows how to do this:
import java.util.jar.*; ... Properties packageExporterMap = new Properties(); for (int i=0; i<locations.length(); i++) { Attributes headers = null; try { URL bundleURL = new URL(locations[i]); InputStream in = bundleURL.openConnection().getInputStream(); JarInputStream jis = new JarInputStream(in); // get the bundle's manifest Manifest manifest = jis.getManifest(); // get the manifest headers Attributes headers = manifest.getMainAttributes(); } catch (IOException e) { } finally { try { jis.close(); } catch (IOException e) {} } if (headers == null) continue; // get the value for the Export-Package header String exports = headers.getValue("Export-Package"); // parse the header for package names and version spec StringTokenizer st = new StringTokenizer(exports, ","); while (st.hasMoreTokens()) { // enumerate each package for export String token = st.nextToken().trim(); int semicolonPos = token.indexOf(';'), String pkgname = token; String exportVersion = null; if (semicolonPos != -1) { // parse the version spec pkgname = token.substring(0, semicolonPos).trim(); int eqPos = token.indexOf('=', semiconlonPos); if (eqPos != -1) { exportVersion = token.substring(eqPos+1).trim(); } } String val = locations[i]; if (exportVersion != null) val += ", " + exportVersion; packageExporterMap.put(pkgname, val); } }
We directly read and interpret the manifest of a bundle JAR file from its source URL using the classes in the java.util.jar package available since JDK 1.2.
An optimization can be done by partially applying the resolution logic to the previous process, so that we can determine whether any dependency cannot be satisfied early on. For example, we can additionally read and parse the Import-Package header of a bundle. If it imports certain packages not exported by any other bundles in the set, we already know this group of bundles will not resolve when they are deployed to a framework all by themselves.
Additional complications may occur because many bundles can declare to export the same package with the same version. Until the bundles are actually installed and resolved in the framework at run-time, there is no way of determining which bundle will be picked as the exporter. Moreover, because bundles can be started and stopped in various orders, a selected exporter may not be the winner the next time the framework is restarted and the same package is to be exported. The good news is that as long as at least one bundle exists and offers the needed package at the compatible version, the importers will be able to get resolved.
Let's look at this situation with a concrete example. The DA bundle device.jar imports the org.osgi.service.log package, which is offered by two bundles: the Log bundle and the HTTP bundle. The HTTP bundle does this to ensure it can always be resolved even when the Log bundle is absent. If we attempt to set up the dependency among these three bundles, two equally legal outcomes may emerge. The first version of the package exporter map is presented in Table 9.3.
Package | Location of the Exporting Bundle |
---|---|
org.osgi.service.log | file:jes_path/bundles/log.jar |
org.osgi.service.http | file:jes_path/bundles/http.jar |
org.osgi.service.device | file:jes_path/bundles/device.jar |
If you start the Device bundle, the Log bundle is automatically started. This is usually what is expected. However, a second result is also possible (Table 9.4).
Package | Location of the Exporting Bundle |
---|---|
org.osgi.service.log | file:jes_path/bundles/http.jar |
org.osgi.service.http | file:jes_path/bundles/http.jar |
org.osgi.service.device | file:jes_path/bundles/device.jar |
The only difference from the first outcome is that the org.osgi.service.log package is shown to be exported by the HTTP bundle. As a result, if you start the device bundle, the HTTP bundle is automatically started. This is usually not what is intended, because the HTTP bundle does not provide the Log services. Yet as far as bundle resolution is concerned, the Device is resolved and can be started. It is then up to the Device bundle to handle the missing service situation using techniques from Chapter 5. In this case, it may choose simply to display log messages to the display until the Log bundle comes along some time later and registers its services.
Now let's summarize the manifest headers and APIs that allow you to provide and retrieve information about the bundles and services, which is essential to administrative applications.
The Bundle interface represents an installed bundle in the framework. The following APIs return various information about the bundle.
We have grown familiar with the important manifest headers such as Bundle-Activator, Import-Package, Export-Package, Bundle-ClassPath, and Bundle-NativeCode. Here we look at a group of additional headers that serve informational and advisory purposes (Table 9.5).
Manifest Headers | Meaning |
---|---|
Bundle-Name | The name of a bundle |
Bundle-Vendor | The name of the bundle vendor |
Bundle-Version | The version of a bundle |
Bundle-Description | The description of a bundle |
Bundle-DocURL | The URL to a bundle's documentation |
Bundle-ContactAddress | The e-mail address to contact about a bundle |
Export-Service | The services that are to be registered by a bundle |
Import-Service | The services a bundle is to get |
For example, we can add the following to the administrative bundle's manifest definition:
Bundle-Name: The Administrative Bundle Bundle-Description: This bundle can access and modify parameters for other bundles. It requires org.osgi.framework.AdminPermission, and provides a web user interface at http://<host>:8080/admin. Bundle-Vendor: Acme Systems, Inc. Bundle-Version: 1.0 Bundle-DocURL: http://www.acme.com/bundles/admin/index.html Bundle-ContactAddress: [email protected] Import-Service: com.acme.service.param.ParameterAdmin
A management application can retrieve the information as follows, assuming bundle refers to the administrative bundle:
Dictionary headers = bundle.getHeaders(); System.out.println("Bundle: " + headers.get("Bundle-Name") + " " + headers.get("Bundle-Version")); System.out.println("description: " + headers.get("Bundle-Description"));
Unlike Import-Package and Export-Package, the Import-Service and Export-Service manifest headers are advisory. Their presence simply indicates that at some point in time, the bundle needs to get or is going to register the specified service. Conceivably, a “resolution” process (similar to what we have just attempted with package dependency) can be implemented based on the Import-Service and Export-Service headers. However, even activating bundles following the order thus generated still has no guarantee that each bundle will register the service before another tries to get it. Therefore, the usefulness of these headers is quite limited, and they are no substitute for the techniques described in Chapter 5.
This method returns the unique ID of the bundle installed in the framework.
This method returns the location string from which the bundle is installed. If the bundle has been installed directly from InputStream, the location identifier string passed into BundleContext's installBundle method is returned.
This method returns the current state of the bundle. It will be one of Bundle.INSTALLED, Bundle.UNINSTALLED, Bundle.RESOLVED, Bundle.STARTING, Bundle.STOPPING, and Bundle.ACTIVE. To check whether a bundle is in one of multiple states, use the following statement:
if (bundle.getState() & (Bundle.STARTING | Bundle.STOPPING | Bundle.ACTIVE) != 0)
This method returns an array of service references of services registered by this bundle.
This method returns an array of service references of services that are being used by this bundle.
This method checks whether this bundle has the given permission. If the given permission is implied by the collection of permissions granted to the bundle, the method returns true.
If BundleContext is passed to a bundle's activator, calling getBundle returns the Bundle object representing the receiving bundle. Quite a mouthful for a simple operation. It is mostly used by a bundle to find out information about itself. For example,
public class Activator implements BundleActivator { public void start(BundleContext ctxt) { Bundle self = ctxt.getBundle(); System.out.println("I was from " + self.getLocation() + " and assigned an ID of " + self.getBundleId()); System.out.println("Am I being started? " + (self.getState() == Bundle.STARTING)); } ...
reports the following messages on bundle activation:
I was from file:/somewhere/foo.jar and assigned an ID of 5 Am I being started? true
Get the installed bundle with the given bundleID. This is mostly used by an administrative bundle to perform a life cycle operation on another bundle or to obtain its location or state.
This method gets all currently installed bundles in the framework.
This method gets the value of an OSGi framework environment property. We covered the following standard properties in Chapter 4. They reflect various attributes of the underlying native platform:
3.147.85.181